diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f1760a2..48a3a41 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -31,7 +31,7 @@ By checking the box below, I agree and represent on behalf of the protocol/entit - the plugin and the underlying protocol comply with all applicable laws, and my API enforces the same geofencing and eligibility restrictions as my own user-facing app; - the plugin is in full compliance with our protocol's/entity's then-current terms and conditions, and if not, submission and use thereafter is considered a formal written exemption to such terms and conditions; - the plugin accurately describes its behavior and contains no hidden, deceptive, or malicious instructions, and my API will not return malicious or unexpected calldata; -- the protocol/entity is solely responsible for the plugin and is submitting a plugin is subject to the [Base Account and Base App Terms of Service](https://wallet.coinbase.com/terms-of-service) as using a "Service"; and +- the protocol/entity is solely responsible for the plugin, and submitting a plugin is subject to the [applicable terms of service](https://wallet.coinbase.com/terms-of-service) as using a "Service"; and - Base may modify, decline to list, or remove the plugin at any time, for any reason. - [ ] I have read and agree to the Base MCP Plugin Submission Agreement above. diff --git a/README.md b/README.md index df43619..f25ff9e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Base](logo.webp) -[Agent Skills](https://agentskills.io) for building on [Base](https://base.org). These skills enable AI agents to connect to Base, deploy contracts, integrate wallets, run nodes, and more. +[Agent Skills](https://agentskills.io) for building on [Base](https://base.org). These skills enable AI agents to connect to Base, deploy contracts, register agents, run nodes, and more. @@ -29,7 +29,7 @@ Consolidated skills that cover the most common use cases. Each uses progressive | Skill | Install | Description | | ----- | ------- | ----------- | -| [build-on-base](./skills/build-on-base/SKILL.md) | `npx skills add base/skills --skill build-on-base` | Complete Base development playbook: network, contracts, wallet auth, payments, attribution, and migrations. Consolidates all individual skills into one. | +| [build-on-base](./skills/build-on-base/SKILL.md) | `npx skills add base/skills --skill build-on-base` | Complete Base development playbook: network, contracts, attribution, agent registration, and nodes. Consolidates all individual skills into one. | | [base-mcp](./skills/base-mcp/SKILL.md) | `npx skills add base/skills --skill base-mcp` | Base MCP server — gives your AI assistant a wallet via mcp.base.org. Sending, swapping, signing, batched calls, balances, and partner plugins for lending, swaps, and more. | | [vibenet](./skills/vibenet/SKILL.md) | `npx skills add base/skills --skill vibenet` | Build on [vibenet](https://chain.base.org/vibenet), the Base Vibes devnet for native account abstraction (EIP-8130) and Cobalt 200ms native blocks, with viem: smart accounts, batched calls, session keys and policies, ERC-8168 payer gas sponsorship, and millisecond block timestamps. | @@ -55,14 +55,6 @@ Deploy my contract to Base Sepolia How do I connect to Base mainnet? ``` -```text -Add Sign in with Base to my app -``` - -```text -Convert my existing Farcaster miniapp to a standard app on Base -``` - ```text Register my trading bot and add builder code attribution to its transactions ``` diff --git a/skills/build-on-base/SKILL.md b/skills/build-on-base/SKILL.md index 526a191..bfd22de 100644 --- a/skills/build-on-base/SKILL.md +++ b/skills/build-on-base/SKILL.md @@ -6,20 +6,15 @@ description: > forge create, BaseScan verification, CDP faucet, testnet ETH, deploy contract to Base; (3) Builder Codes — ERC-8021 attribution suffix, referral fees, dataSuffix for Wagmi/Viem/Privy/ ethers.js/window.ethereum, transaction attribution, earn referral fees, append builder code; - (4) Base Account SDK — Sign in with Base (SIWB), Base Pay, USDC payments, paymasters, gas - sponsorship, sub-accounts, spend permissions, prolinks, batch transactions, smart wallet, - payment link, recurring subscription; (5) Agent registration — trading bots, AI agents, automated + (4) Agent registration — trading bots, AI agents, automated senders, ERC-8021 attribution wiring, base.dev API, register agent, builder code registration; - (6) Node operation — run Base node, Reth setup, hardware requirements, self-hosted RPC, sync; - (7) Migrations — migrate OnchainKit, OnchainKitProvider to WagmiProvider, wagmi migration, - remove onchainkit dependency, MiniKit to Farcaster SDK, convert miniapp, Farcaster miniapp to - regular app, convert Farcaster miniapp. + (5) Node operation — run Base node, Reth setup, hardware requirements, self-hosted RPC, sync. --- # Base Development -Complete playbook for building on Base L2 — network setup, smart contracts, wallet auth, payments, -developer tool attribution, and framework migrations. +Complete playbook for building on Base L2 — network setup, smart contracts, developer tool +attribution, agent registration, and node operation. ## Default Stack @@ -27,8 +22,6 @@ developer tool attribution, and framework migrations. |-------|---------| | Network | Base Mainnet (8453) / Base Sepolia testnet (84532) | | Contracts | Foundry (`forge create` + BaseScan verification) | -| Wallet auth | Base Account SDK (`@base-org/account`) | -| Payments | Base Pay — USDC, gasless, settles in <2s | | Transactions | wagmi + viem | | Attribution | Builder Codes — ERC-8021 via `ox/erc8021` | | RPC (prod) | Dedicated node provider or self-hosted Reth | @@ -37,10 +30,8 @@ developer tool attribution, and framework migrations. - **Never commit private keys** — use `cast wallet import` for Foundry keystores - **Never expose RPC API keys or CDP credentials client-side** — proxy through backend -- **Never skip server-side payment verification** — always call `getPaymentStatus()` server-side and verify `sender`, `amount`, `recipient`; track processed tx IDs to prevent replay attacks - **Never send transactions without Builder Code attribution** — silent data loss, no errors, no warnings - **Validate all user-provided shell inputs** before constructing forge/cast commands (no spaces, semicolons, pipes) -- **COOP headers for Base Account popups** — use `same-origin-allow-popups`, not `same-origin` ## Task Routing @@ -52,11 +43,7 @@ Read the reference for your task: | **Deploy contracts** | Foundry deployment, BaseScan verification, faucet | [references/deploy-contracts.md](references/deploy-contracts.md) | | **Run a Base node** | Self-hosted RPC, Reth, hardware requirements | [references/run-node.md](references/run-node.md) | | **Builder Codes** | Add ERC-8021 attribution to transactions | [references/builder-codes/overview.md](references/builder-codes/overview.md) | -| **Base Account SDK** | SIWB, Base Pay, subscriptions, sub-accounts | [references/base-account/overview.md](references/base-account/overview.md) | | **Register AI agent/bot** | Register wallet, get builder code, wire attribution | [references/agents/register.md](references/agents/register.md) | -| **Migrate from OnchainKit** | OnchainKitProvider → wagmi, wallet/tx components | [references/migrations/onchainkit/overview.md](references/migrations/onchainkit/overview.md) | -| **MiniKit → Farcaster SDK** | `@coinbase/onchainkit/minikit` → `@farcaster/miniapp-sdk` | [references/migrations/minikit-to-farcaster/overview.md](references/migrations/minikit-to-farcaster/overview.md) | -| **Farcaster miniapp → regular app** | Remove Mini App host coupling, convert to Base/web app | [references/migrations/farcaster-miniapp-to-app.md](references/migrations/farcaster-miniapp-to-app.md) | ## Operating Procedure @@ -69,7 +56,6 @@ Read the reference for your task: ## For Edge Cases and Latest API Changes - **AI-optimized docs**: [docs.base.org/llms.txt](https://docs.base.org/llms.txt) -- **Base Account reference**: [docs.base.org/base-account](https://docs.base.org/base-account) - **Base chain docs**: [docs.base.org](https://docs.base.org) ## Installation diff --git a/skills/build-on-base/references/base-account/authentication.md b/skills/build-on-base/references/base-account/authentication.md deleted file mode 100644 index b111d3a..0000000 --- a/skills/build-on-base/references/base-account/authentication.md +++ /dev/null @@ -1,234 +0,0 @@ -# Authentication (Sign in with Base) - -## Table of Contents - -- [Overview](#overview) -- [How It Works](#how-it-works) -- [SDK Setup](#sdk-setup) -- [Sign-In Flow](#sign-in-flow) -- [Backend Verification](#backend-verification) -- [SignInWithBaseButton Component](#signinwithbasebutton-component) -- [Framework Integration: Wagmi](#framework-integration-wagmi) -- [Framework Integration: Privy](#framework-integration-privy) -- [Smart Wallet Signatures (ERC-6492)](#smart-wallet-signatures-erc-6492) -- [Security Checklist](#security-checklist) - -## Overview - -Sign in with Base (SIWB) provides passwordless authentication using wallet signatures. It builds on Sign-In with Ethereum (SIWE, EIP-4361) — the user signs a message with their wallet key, and the backend verifies it. No passwords, no seed phrases. - -Base Accounts are ERC-4337 smart wallets. Unlike traditional wallets (EOAs), the user's key is a passkey — the wallet contract verifies signatures via `isValidSignature` (EIP-1271). Viem handles this automatically. - -## How It Works - -1. Generate a nonce **before** the user clicks sign-in (avoids popup blockers) -2. Call `wallet_connect` with the `signInWithEthereum` capability -3. User approves in the Base Account popup (`keys.coinbase.com`) -4. SDK returns `{ address, message, signature }` -5. Send `message` + `signature` to your backend -6. Backend verifies with viem and creates a session/JWT - -## SDK Setup - -```bash -npm install @base-org/account @base-org/account-ui -``` - -```typescript -import { createBaseAccountSDK } from '@base-org/account'; - -const sdk = createBaseAccountSDK({ - appName: 'My App', - appLogoUrl: 'https://example.com/logo.png', - appChainIds: [8453], -}); - -const provider = sdk.getProvider(); -``` - -`createBaseAccountSDK` parameters: - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `appName` | `string` | No | App name shown in wallet UI (default: `"App"`) | -| `appLogoUrl` | `string` | No | Logo URL for wallet UI | -| `appChainIds` | `number[]` | No | Supported chain IDs | -| `paymasterUrls` | `Record` | No | Chain ID to paymaster URL mapping | - -## Sign-In Flow - -```typescript -const nonce = crypto.randomUUID().replace(/-/g, ''); - -const { accounts } = await provider.request({ - method: 'wallet_connect', - params: [{ - version: '1', - capabilities: { - signInWithEthereum: { - nonce, - chainId: '0x2105', // Base Mainnet (8453) - }, - }, - }], -}); - -const { address } = accounts[0]; -const { message, signature } = accounts[0].capabilities.signInWithEthereum; -``` - -`signInWithEthereum` capability parameters: - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `nonce` | `string` | Yes | Unique random string per auth attempt | -| `chainId` | `string` | Yes | Hex chain ID (`"0x2105"` = Base Mainnet 8453) | - -Response shape: - -| Field | Type | Description | -|-------|------|-------------| -| `accounts[0].address` | `string` | User's wallet address | -| `accounts[0].capabilities.signInWithEthereum.message` | `string` | SIWE-formatted message | -| `accounts[0].capabilities.signInWithEthereum.signature` | `string` | Cryptographic signature | - -### Fallback for Non-Base Wallets - -Not every wallet supports `wallet_connect`. Fall back to `eth_requestAccounts` + `personal_sign`: - -```typescript -try { - const { accounts } = await provider.request({ - method: 'wallet_connect', - params: [{ version: '1', capabilities: { signInWithEthereum: { nonce, chainId: '0x2105' } } }], - }); - // use accounts[0].capabilities.signInWithEthereum -} catch (err) { - if (err.code === 4100) { - const [address] = await provider.request({ method: 'eth_requestAccounts' }); - const signature = await provider.request({ - method: 'personal_sign', - params: [siweMessage, address], - }); - } -} -``` - -## Backend Verification - -Use viem to verify the signature. It handles both EOA and smart wallet (EIP-1271/ERC-6492) signatures automatically. - -```typescript -import { createPublicClient, http } from 'viem'; -import { base } from 'viem/chains'; - -const client = createPublicClient({ chain: base, transport: http() }); - -const valid = await client.verifyMessage({ - address, - message, - signature, -}); -``` - -### Full Express Server Example - -```typescript -import express from 'express'; -import { createPublicClient, http } from 'viem'; -import { base } from 'viem/chains'; - -const app = express(); -const client = createPublicClient({ chain: base, transport: http() }); -const usedNonces = new Set(); - -app.get('/auth/nonce', (req, res) => { - const nonce = crypto.randomUUID().replace(/-/g, ''); - res.json({ nonce }); -}); - -app.post('/auth/verify', async (req, res) => { - const { address, message, signature } = req.body; - const nonceMatch = message.match(/Nonce: (\w+)/); - if (!nonceMatch || usedNonces.has(nonceMatch[1])) { - return res.status(401).json({ error: 'Invalid or reused nonce' }); - } - - const valid = await client.verifyMessage({ address, message, signature }); - if (!valid) return res.status(401).json({ error: 'Invalid signature' }); - - usedNonces.add(nonceMatch[1]); - // Create session/JWT here - res.json({ success: true, address }); -}); -``` - -## SignInWithBaseButton Component - -Pre-built React button from `@base-org/account-ui`. - -```tsx -import { SignInWithBaseButton } from '@base-org/account-ui/react'; - - -``` - -| Prop | Type | Values | Default | -|------|------|--------|---------| -| `align` | `string` | `'left'`, `'center'`, `'right'` | `'center'` | -| `variant` | `string` | `'solid'`, `'transparent'` | `'solid'` | -| `colorScheme` | `string` | `'light'`, `'dark'`, `'system'` | `'light'` | -| `size` | `string` | `'small'`, `'medium'`, `'large'` | `'medium'` | -| `disabled` | `boolean` | — | `false` | -| `onClick` | `() => void` | — | — | -| `onSignInResult` | `(result) => void` | — | — | - -Follow the [Brand Guidelines](https://docs.base.org/base-account/reference/ui-elements/brand-guidelines): use Base blue (`#0000FF`) on light backgrounds, all-white lockup on dark backgrounds. Do not modify the Base Square color or corner radius. - -## Framework Integration: Wagmi - -```typescript -import { createConfig, http } from 'wagmi'; -import { base } from 'wagmi/chains'; -import { createBaseAccountSDK } from '@base-org/account'; -import { custom } from 'viem'; - -const sdk = createBaseAccountSDK({ - appName: 'My App', - appLogoUrl: 'https://example.com/logo.png', - appChainIds: [8453], -}); - -const config = createConfig({ - chains: [base], - transports: { - [base.id]: custom(sdk.getProvider()), - }, -}); -``` - -Then use wagmi hooks (`useConnect`, `useAccount`, `useSignMessage`) as usual. - -## Framework Integration: Privy - -Privy has day-1 Base Account support. Configure it as a wallet connector — see [Privy docs](https://docs.privy.io/) for the latest integration guide. Base Account appears as a wallet option in the Privy modal. - -## Smart Wallet Signatures (ERC-6492) - -Base Accounts may not be deployed onchain until the user's first transaction. Signatures from undeployed wallets include an ERC-6492 wrapper that lets verifiers deploy the contract in a simulation to validate the signature. - -**You don't need to do anything special** — viem's `verifyMessage` and `verifyTypedData` handle ERC-6492 automatically. Just make sure you're using viem for verification. - -## Security Checklist - -- Generate nonces **before** the user clicks sign-in (avoids popup blockers) -- Track used nonces server-side — reject any reused nonce -- Verify signatures on your backend, never trust the frontend alone -- Use `Cross-Origin-Opener-Policy: same-origin-allow-popups` (NOT `same-origin`, which breaks the popup) -- Set appropriate session/JWT expiry times -- Include `chainId` in verification to prevent cross-chain replay diff --git a/skills/build-on-base/references/base-account/capabilities.md b/skills/build-on-base/references/base-account/capabilities.md deleted file mode 100644 index 0b26b21..0000000 --- a/skills/build-on-base/references/base-account/capabilities.md +++ /dev/null @@ -1,263 +0,0 @@ -# Capabilities & Batch Transactions - -## Table of Contents - -- [Overview](#overview) -- [Discovering Capabilities](#discovering-capabilities) -- [wallet_sendCalls](#wallet_sendcalls) -- [wallet_getCallsStatus](#wallet_getcallsstatus) -- [Capability: paymasterService](#capability-paymasterservice) -- [Capability: auxiliaryFunds](#capability-auxiliaryfunds) -- [Capability: atomic](#capability-atomic) -- [Capability: flowControl](#capability-flowcontrol) -- [Capability: dataCallback](#capability-datacallback) -- [Capability: dataSuffix (Attribution)](#capability-datasuffix-attribution) - -## Overview - -Capabilities are chain-specific feature flags that describe what a wallet supports. They're discovered via `wallet_getCapabilities` and used in `wallet_connect` and `wallet_sendCalls` calls. - -Base Account (a smart wallet) supports capabilities that traditional wallets (EOAs) cannot: atomic batching, gas sponsorship, auxiliary funds, etc. - -## Discovering Capabilities - -```typescript -const capabilities = await provider.request({ - method: 'wallet_getCapabilities', - params: [userAddress], -}); - -const baseCapabilities = capabilities['0x2105']; // Base Mainnet -``` - -Response structure (keyed by hex chain ID): - -```typescript -{ - "0x2105": { - auxiliaryFunds: { supported: true }, - atomic: { supported: "supported" }, - paymasterService: { supported: true }, - flowControl: { supported: false }, - datacallback: { supported: false }, - } -} -``` - -Use this to conditionally enable features: - -```typescript -const hasPaymaster = !!baseCapabilities.paymasterService?.supported; -const hasAuxFunds = baseCapabilities.auxiliaryFunds?.supported || false; -const hasAtomicBatch = baseCapabilities.atomic?.supported === 'supported'; -``` - -## wallet_sendCalls - -**Spec: EIP-5792.** Submits a batch of calls to the wallet for execution. - -```typescript -const { batchId } = await provider.request({ - method: 'wallet_sendCalls', - params: [{ - version: '2.0.0', - from: userAddress, - chainId: '0x2105', - atomicRequired: true, - calls: [ - { to: '0xTokenAddress', data: '0xapproveCalldata', value: '0x0' }, - { to: '0xDexAddress', data: '0xswapCalldata', value: '0x0' }, - ], - capabilities: { - paymasterService: { url: 'https://your-paymaster.xyz' }, - }, - }], -}); -``` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `version` | `string` | Yes | Must be `"2.0.0"` | -| `from` | `string` | Yes | Sender address | -| `chainId` | `string` | Yes | Hex chain ID | -| `atomicRequired` | `boolean` | Yes | Require all-or-nothing execution | -| `calls` | `Call[]` | Yes | Array of `{ to, value, data? }` | -| `capabilities` | `object` | No | Capability config | - -Returns: `{ batchId, status }` - -Error codes: - -| Code | Meaning | -|------|---------| -| `4001` | User rejected | -| `5700` | Missing required capability | -| `5720` | Duplicate batch ID | -| `5740` | Batch too large | - -## wallet_getCallsStatus - -Check the status of a batch submitted via `wallet_sendCalls`. - -```typescript -const result = await provider.request({ - method: 'wallet_getCallsStatus', - params: [batchId], -}); -``` - -Status codes: - -| Code | Meaning | -|------|---------| -| `100` | Pending — received, not yet onchain | -| `200` | Success — included onchain, no reverts | -| `400` | Offchain failure — wallet will not retry | -| `500` | Chain failure — batch reverted | -| `600` | Partial failure — some changes may be onchain | - -Returns: `{ version, chainId, id, status, atomic, receipts, capabilities }` - -Polling pattern: - -```typescript -async function waitForBatch(batchId: string) { - while (true) { - const { status, receipts } = await provider.request({ - method: 'wallet_getCallsStatus', - params: [batchId], - }); - if (status !== 100) return { status, receipts }; - await new Promise(r => setTimeout(r, 1000)); - } -} -``` - -## Capability: paymasterService - -**Spec: ERC-7677.** Sponsors gas fees so users transact for free. - -```typescript -capabilities: { - paymasterService: { - url: 'https://your-paymaster-service.xyz', - }, -} -``` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `url` | `string` | Yes | HTTPS URL of an ERC-7677-compliant paymaster | - -The paymaster service must implement: -- `pm_getPaymasterStubData` — for gas estimation -- `pm_getPaymasterData` — for actual UserOp paymaster data - -Get a paymaster URL from [Coinbase Developer Platform](https://portal.cdp.coinbase.com). See also the [Base Gasless Campaign](https://docs.base.org/base-account/more/base-gasless-campaign) for gas credits. - -Best practice: handle failures gracefully with a fallback to regular (user-pays-gas) transactions. - -## Capability: auxiliaryFunds - -**Spec: EIP-5792.** Indicates the wallet has access to funds beyond the visible onchain balance (MagicSpend — use Coinbase balances onchain). - -No configuration parameters — it's a support flag only. - -When `auxiliaryFunds.supported === true`: -- **Do not** block transactions based on visible onchain balance -- **Do not** show "insufficient funds" warnings based on balance checks -- Let the wallet handle funding — it can pull from the user's Coinbase account - -```typescript -if (baseCapabilities.auxiliaryFunds?.supported) { - // Skip balance check, let wallet handle it -} else { - // Traditional balance check - const balance = await client.getBalance({ address: userAddress }); - if (balance < requiredAmount) showInsufficientFundsWarning(); -} -``` - -## Capability: atomic - -**Spec: EIP-5792.** Ensures batched calls execute atomically — all succeed or all revert. - -Support values (string, not boolean): - -| Value | Meaning | -|-------|---------| -| `"supported"` | Wallet executes atomically | -| `"ready"` | Wallet can upgrade to atomic via EIP-7702 | -| `"unsupported"` | No atomicity guarantees | - -Set `atomicRequired: true` in `wallet_sendCalls` to enforce atomic execution. If the wallet doesn't support it, the call fails with error `5700`. - -Use cases: approve + swap, mint + pay, any multi-step flow requiring all-or-nothing. - -## Capability: flowControl - -**Spec: ERC-7867 (proposed, not finalized).** Controls behavior when individual calls in a batch fail. - -```typescript -calls: [{ - to: '0x...', - data: '0x...', - flowControl: { - onFailure: 'continue', - fallbackCall: { to: '0xFallback', data: '0x...' }, - }, -}] -``` - -| Parameter | Type | Values | Description | -|-----------|------|--------|-------------| -| `onFailure` | `string` | `'continue'`, `'stop'`, `'retry'` | What to do when this call reverts | -| `fallbackCall` | `object` | `{ to, value?, data? }` | Optional alternative call to execute on failure | - -**Note:** This spec is actively being developed. Check the latest docs before using. - -## Capability: dataCallback - -Collects user profile information (email, phone, address) during transaction flows. Same mechanism as `payerInfo` in `pay()` but for `wallet_sendCalls`. - -```typescript -capabilities: { - dataCallback: { - requests: [ - { type: 'email' }, - { type: 'name', optional: true }, - ], - callbackURL: 'https://your-api.com/validate', - }, -} -``` - -Request types: `'email'`, `'phoneNumber'`, `'physicalAddress'`, `'name'` - -The `callbackURL` receives a POST with user data before the transaction. Respond with `{ request: requestData }` to accept or `{ errors: { email: 'Invalid' } }` to reject. - -## Capability: dataSuffix (Attribution) - -**Spec: ERC-8021.** Appends arbitrary bytes to transaction calldata for attribution tracking. Used primarily with **Builder Codes** for tracking which app generated a transaction. - -```typescript -import { Attribution } from 'ox/erc8021'; - -const builderCodeSuffix = Attribution.toDataSuffix({ - codes: ['bc_foobar'], // Register at base.dev -}); - -capabilities: { - dataSuffix: { - value: builderCodeSuffix, - optional: true, - }, -} -``` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `value` | `0x${string}` | Yes | Hex bytes to append to calldata | -| `optional` | `boolean` | No | If `true`, wallet may ignore if unsupported | - -Best practice: use `optional: true` if your app functions without attribution. Register for a Builder Code at [base.dev](https://base.dev). Keep suffixes small — larger means more gas. diff --git a/skills/build-on-base/references/base-account/overview.md b/skills/build-on-base/references/base-account/overview.md deleted file mode 100644 index fceca8a..0000000 --- a/skills/build-on-base/references/base-account/overview.md +++ /dev/null @@ -1,73 +0,0 @@ -# Building with Base Account - -Base Account is an ERC-4337 smart wallet providing universal sign-on, one-tap USDC payments, and multi-chain support (Base, Arbitrum, Optimism, Zora, Polygon, BNB, Avalanche, Lordchain, Ethereum Mainnet). - -## Quick Start - -```bash -npm install @base-org/account @base-org/account-ui -``` - -```typescript -import { createBaseAccountSDK } from '@base-org/account'; - -const sdk = createBaseAccountSDK({ - appName: 'My App', - appLogoUrl: 'https://example.com/logo.png', - appChainIds: [8453], // Base Mainnet -}); - -const provider = sdk.getProvider(); -``` - -## Feature References - -Read the reference for the feature you're implementing: - -| Feature | Reference | When to Read | -|---------|-----------|-------------| -| Sign in with Base | [authentication.md](authentication.md) | Wallet auth, SIWE, backend verification, SignInWithBaseButton, Wagmi/Privy setup | -| Base Pay | [payments.md](payments.md) | One-tap USDC payments, payerInfo, server-side verification, BasePayButton | -| Subscriptions | [subscriptions.md](subscriptions.md) | Recurring charges, spend permissions, CDP wallet setup, charge/revoke lifecycle | -| Sub Accounts | [sub-accounts.md](sub-accounts.md) | App-specific embedded wallets, key generation, funding | -| Capabilities | [capabilities.md](capabilities.md) | Batch transactions, gas sponsorship (paymasters), atomic execution, auxiliaryFunds, attribution | -| Prolinks | [prolinks.md](prolinks.md) | Shareable payment links, QR codes, encoded transaction URLs | -| Troubleshooting | [troubleshooting.md](troubleshooting.md) | Popup issues, gas usage, unsupported calls, migration, doc links | - -## Critical Requirements - -### Security - -- **Track transaction IDs** to prevent replay attacks -- **Verify sender matches authenticated user** to prevent impersonation -- **Use a proxy** to protect Paymaster URLs from frontend exposure -- **Paymaster providers must be ERC-7677-compliant** -- **Never expose CDP credentials client-side** (subscription backend only) - -### Popup Handling - -- Generate nonces **before** user clicks "Sign in" to avoid popup blockers -- Use `Cross-Origin-Opener-Policy: same-origin-allow-popups` -- `same-origin` breaks the Base Account popup - -### Base Pay - -- Base Pay works independently from SIWB — no auth required for `pay()` -- `testnet` param in `getPaymentStatus()` must match `pay()` call -- Never disable actions based on onchain balance alone — check `auxiliaryFunds` capability - -### Sub Accounts - -- Call `wallet_addSubAccount` each session before use -- Ownership changes expected on new devices/browsers -- Only Coinbase Smart Wallet contracts supported for import - -### Smart Wallets - -- ERC-6492 wrapper enables signature verification before wallet deployment -- Viem's `verifyMessage`/`verifyTypedData` handle this automatically - -## For Edge Cases and Latest API Changes - -- **AI-optimized docs**: [docs.base.org/llms.txt](https://docs.base.org/llms.txt) -- **Full reference**: [docs.base.org/base-account](https://docs.base.org/base-account) diff --git a/skills/build-on-base/references/base-account/payments.md b/skills/build-on-base/references/base-account/payments.md deleted file mode 100644 index afd9287..0000000 --- a/skills/build-on-base/references/base-account/payments.md +++ /dev/null @@ -1,225 +0,0 @@ -# Payments (Base Pay) - -## Table of Contents - -- [Overview](#overview) -- [One-Time Payments](#one-time-payments) -- [Checking Payment Status](#checking-payment-status) -- [Collecting User Info (payerInfo)](#collecting-user-info-payerinfo) -- [Server-Side Verification](#server-side-verification) -- [Server-Side User Info Validation](#server-side-user-info-validation) -- [BasePayButton Component](#basepaybutton-component) -- [Framework Integration: Wagmi](#framework-integration-wagmi) -- [Testing](#testing) -- [Security Checklist](#security-checklist) - -## Overview - -Base Pay enables one-tap USDC payments on Base. Key facts: - -- Currency is USDC (a digital dollar stablecoin), not ETH -- Gas is sponsored automatically — users don't pay gas fees -- Settles in under 2 seconds on Base -- No chargebacks, no FX fees, no merchant fees -- **Base Pay works independently from Sign in with Base** — no authentication required to call `pay()` -- Users can pay from their Base Account or Coinbase account - -## One-Time Payments - -### `pay()` - -```typescript -import { pay } from '@base-org/account'; - -const payment = await pay({ - amount: '10.50', - to: '0xRecipientAddress', - testnet: false, -}); -``` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `amount` | `string` | Yes | USDC amount (e.g., `"10.50"`) | -| `to` | `string` | Yes | Recipient Ethereum address (`0x...`) | -| `testnet` | `boolean` | No | Use Base Sepolia testnet (default: `false`) | -| `payerInfo` | `object` | No | Collect user info during payment — see [payerInfo section](#collecting-user-info-payerinfo) | - -Returns `PayResult`: - -| Field | Type | Description | -|-------|------|-------------| -| `id` | `string` | Transaction hash | -| `amount` | `string` | Amount sent | -| `to` | `string` | Recipient address | -| `payerInfoResponses` | `object` | Collected user info (if `payerInfo` was provided) | - -## Checking Payment Status - -### `getPaymentStatus()` - -```typescript -import { getPaymentStatus } from '@base-org/account'; - -const status = await getPaymentStatus({ - id: payment.id, - testnet: false, -}); -``` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `id` | `string` | Yes | Transaction hash from `pay()` | -| `testnet` | `boolean` | No | **Must match** the `testnet` value used in `pay()` | - -Returns `PaymentStatus`: - -| Field | Type | Present When | -|-------|------|-------------| -| `status` | `"completed" \| "pending" \| "failed" \| "not_found"` | Always | -| `id` | `string` | Always | -| `message` | `string` | Always | -| `sender` | `string` | `pending`, `completed`, `failed` | -| `amount` | `string` | `completed` | -| `recipient` | `string` | `completed` | -| `error` | `object` | `failed` | - -## Collecting User Info (payerInfo) - -Request user information (email, name, phone, address) during the payment flow. - -```typescript -const payment = await pay({ - amount: '25.00', - to: '0xRecipient', - payerInfo: { - requests: [ - { type: 'email' }, - { type: 'phoneNumber', optional: true }, - { type: 'physicalAddress', optional: true }, - ], - callbackURL: 'https://your-api.com/validate', - }, -}); -``` - -Supported `payerInfo` request types: - -| Type | Response Shape | -|------|---------------| -| `email` | `string` | -| `name` | `{ firstName: string, familyName: string }` | -| `phoneNumber` | `{ number: string, country: string }` | -| `physicalAddress` | `{ address1, address2?, city, state, postalCode, country, name: { firstName, familyName } }` | -| `onchainAddress` | `string` | - -Fields are **required by default**. Set `optional: true` to avoid aborting the payment if the user declines to share. - -## Server-Side Verification - -Never trust frontend payment confirmations alone. Always verify on your backend. - -```typescript -import { getPaymentStatus } from '@base-org/account'; - -async function verifyPayment(txId: string, expectedAmount: string, expectedRecipient: string, authenticatedUser: string) { - // 1. Check if already processed (dedup by txId) - if (await isProcessed(txId)) throw new Error('Already processed'); - - // 2. Verify payment status - const { status, sender, amount, recipient } = await getPaymentStatus({ id: txId }); - if (status !== 'completed') throw new Error(`Payment not completed: ${status}`); - - // 3. Verify sender matches authenticated user (prevents impersonation) - if (sender.toLowerCase() !== authenticatedUser.toLowerCase()) { - throw new Error('Sender mismatch'); - } - - // 4. Validate amount and recipient - if (amount !== expectedAmount || recipient.toLowerCase() !== expectedRecipient.toLowerCase()) { - throw new Error('Payment details mismatch'); - } - - // 5. Mark processed BEFORE fulfilling - await markProcessed(txId); - await fulfillOrder(txId); -} -``` - -Key threats this prevents: -- **Replay attacks**: Track processed transaction IDs with unique constraints -- **Impersonation**: Verify `sender` matches the authenticated user -- **Amount tampering**: Validate `amount` and `recipient` server-side - -## Server-Side User Info Validation - -When you provide a `callbackURL` in `payerInfo`, your endpoint receives the user's data **before** the transaction is submitted. You can validate and accept or reject. - -```typescript -// POST handler at your callbackURL -app.post('/validate', (req, res) => { - const { requestData } = req.body; - const info = requestData.capabilities.dataCallback.requestedInfo; - - // Reject with errors (shown to user) - if (!isValidEmail(info.email)) { - return res.json({ errors: { email: 'Invalid email address' } }); - } - - // Accept — return the original request data - return res.json({ request: requestData }); -}); -``` - -## BasePayButton Component - -Pre-built React button from `@base-org/account-ui`. - -```tsx -import { BasePayButton } from '@base-org/account-ui/react'; - - -``` - -| Prop | Type | Values | Default | -|------|------|--------|---------| -| `colorScheme` | `string` | `'light'`, `'dark'`, `'system'` | `'light'` | -| `size` | `string` | `'small'`, `'medium'`, `'large'` | `'medium'` | -| `variant` | `string` | `'solid'`, `'outline'` | `'solid'` | -| `disabled` | `boolean` | — | `false` | -| `onClick` | `() => void` | — | — | -| `onPaymentResult` | `(result) => void` | — | — | - -Follow the [Brand Guidelines](https://docs.base.org/base-account/reference/ui-elements/brand-guidelines): always use the combination mark (never plain text "Base Pay"), pad the button with at least 1x height on all sides. - -## Framework Integration: Wagmi - -`pay()` and `getPaymentStatus()` are standalone functions — they don't require a provider or wagmi config. Call them directly: - -```typescript -import { pay, getPaymentStatus } from '@base-org/account'; - -const { id } = await pay({ amount: '5.00', to: '0x...', testnet: true }); -const status = await getPaymentStatus({ id, testnet: true }); -``` - -If you're also using SIWB with wagmi, the `pay()` function still works independently alongside the wagmi provider setup. - -## Testing - -- Use `testnet: true` in both `pay()` and `getPaymentStatus()` -- Test on Base Sepolia (chain ID 84532) -- Get test USDC from the [Circle Faucet](https://faucet.circle.com/) on Base Sepolia - -## Security Checklist - -- Always verify payments server-side with `getPaymentStatus()` -- Track processed transaction IDs in a database with unique constraints -- Verify `sender` matches your authenticated user -- Validate `amount` and `recipient` match the expected order -- `testnet` param must match between `pay()` and `getPaymentStatus()` -- Never disable payment buttons based on onchain balance alone — check `auxiliaryFunds` capability (users may have Coinbase balances available via MagicSpend) diff --git a/skills/build-on-base/references/base-account/prolinks.md b/skills/build-on-base/references/base-account/prolinks.md deleted file mode 100644 index 0b74411..0000000 --- a/skills/build-on-base/references/base-account/prolinks.md +++ /dev/null @@ -1,192 +0,0 @@ -# Prolinks (Shareable Payment Links) - -## Table of Contents - -- [Overview](#overview) -- [encodeProlink](#encodeprolink) -- [decodeProlink](#decodeprolink) -- [createProlinkUrl](#createprolinkurl) -- [Common Patterns](#common-patterns) - -## Overview - -Prolinks encode transaction requests (JSON-RPC) into compressed, URL-safe strings that can be shared as links. When a user opens a prolink URL, their Base Account app decodes and executes the request. - -Use cases: shareable payment requests, pre-filled transaction links, QR codes for onchain actions. - -The encoding is optimized per method type (`wallet_sendCalls`, `wallet_sign`, generic JSON-RPC) and uses gzip compression for payloads >= 1KB (50-80% size reduction). - -## encodeProlink - -Encodes a JSON-RPC request into a compressed, base64url-encoded prolink payload. - -```typescript -import { encodeProlink } from '@base-org/account'; - -const prolink = await encodeProlink({ - method: 'wallet_sendCalls', - params: { - version: '2.0.0', - chainId: '0x2105', - calls: [{ - to: '0xUSDCAddress', - data: '0xtransferCalldata', - value: '0x0', - }], - }, -}); -``` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `method` | `string` | Yes | JSON-RPC method (`wallet_sendCalls`, `wallet_sign`, or any) | -| `params` | `unknown` | Yes | Method parameters | -| `chainId` | `number` | No | Required for generic methods; auto-extracted for `wallet_sendCalls`/`wallet_sign` | -| `capabilities` | `Record` | No | Wallet capabilities (e.g., `dataCallback`) | - -Returns: `Promise` — base64url-encoded prolink payload. - -### Examples - -**ERC-20 Transfer (USDC):** - -```typescript -const prolink = await encodeProlink({ - method: 'wallet_sendCalls', - params: { - version: '2.0.0', - chainId: '0x2105', - calls: [{ - to: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base - data: '0xa9059cbb000000000000000000000000RECIPIENT0000000000000000000000000000000000000000000000000000000000989680', // transfer(address,uint256) - value: '0x0', - }], - }, -}); -``` - -**With Capabilities (dataCallback):** - -```typescript -const prolink = await encodeProlink({ - method: 'wallet_sendCalls', - params: { /* ... */ }, - capabilities: { - dataCallback: { - callbackURL: 'https://your-api.com/callback', - events: ['initiated', 'postSign'], - }, - }, -}); -``` - -**Batch Calls (approve + swap):** - -```typescript -const prolink = await encodeProlink({ - method: 'wallet_sendCalls', - params: { - version: '2.0.0', - chainId: '0x2105', - calls: [ - { to: '0xToken', data: '0xapproveData', value: '0x0' }, - { to: '0xDex', data: '0xswapData', value: '0x0' }, - ], - }, -}); -``` - -## decodeProlink - -Decodes a prolink payload back into a JSON-RPC request. - -```typescript -import { decodeProlink } from '@base-org/account'; - -const decoded = await decodeProlink(payload); -// decoded.method → 'wallet_sendCalls' -// decoded.params → { version, chainId, calls } -// decoded.chainId → number | undefined -// decoded.capabilities → Record | undefined -``` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `payload` | `string` | Yes | Base64url-encoded prolink payload | - -Returns `ProlinkDecoded`: - -| Field | Type | Description | -|-------|------|-------------| -| `method` | `string` | JSON-RPC method name | -| `params` | `unknown` | Method parameters | -| `chainId` | `number \| undefined` | Target chain ID | -| `capabilities` | `Record \| undefined` | Wallet capabilities | - -### Validation Before Execution - -Always validate decoded prolinks before executing: - -```typescript -const decoded = await decodeProlink(payload); - -if (decoded.chainId !== 8453) throw new Error('Wrong chain'); -if (decoded.method !== 'wallet_sendCalls') throw new Error('Unexpected method'); - -const { calls } = decoded.params; -const allowedContracts = ['0xUSDC...', '0xDex...']; -for (const call of calls) { - if (!allowedContracts.includes(call.to)) { - throw new Error(`Untrusted contract: ${call.to}`); - } -} - -await provider.request({ method: decoded.method, params: [decoded.params] }); -``` - -## createProlinkUrl - -Creates a complete URL with the prolink as a query parameter. - -```typescript -import { createProlinkUrl } from '@base-org/account'; - -const url = createProlinkUrl(prolink, 'https://yourapp.com/pay'); -// https://yourapp.com/pay?prolink= -``` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `prolink` | `string` | Yes | Base64url-encoded prolink from `encodeProlink` | -| `url` | `string` | Yes | Base URL (default: `https://base.app/base-pay`) | -| `additionalQueryParams` | `Record` | No | Extra query parameters | - -Returns: Complete URL string. - -## Common Patterns - -### Payment Request Link - -```typescript -const prolink = await encodeProlink({ - method: 'wallet_sendCalls', - params: { - version: '2.0.0', - chainId: '0x2105', - calls: [{ to: recipientAddress, data: transferCalldata, value: '0x0' }], - }, -}); -const paymentUrl = createProlinkUrl(prolink); -// Share this URL or render as QR code -``` - -### Extract and Display Transaction Preview - -```typescript -const decoded = await decodeProlink(payload); -const { calls } = decoded.params; - -const preview = calls.map((call, i) => - `Call ${i + 1}: to=${call.to}, value=${call.value}` -).join('\n'); -``` diff --git a/skills/build-on-base/references/base-account/sub-accounts.md b/skills/build-on-base/references/base-account/sub-accounts.md deleted file mode 100644 index 9e52765..0000000 --- a/skills/build-on-base/references/base-account/sub-accounts.md +++ /dev/null @@ -1,250 +0,0 @@ -# Sub Accounts - -## Table of Contents - -- [Overview](#overview) -- [Key Concepts](#key-concepts) -- [SDK Configuration](#sdk-configuration) -- [Key Management](#key-management) -- [Creating Sub Accounts](#creating-sub-accounts) -- [Retrieving Sub Accounts](#retrieving-sub-accounts) -- [Adding Owners](#adding-owners) -- [wallet_addSubAccount RPC](#wallet_addsubaccount-rpc) -- [wallet_getSubAccounts RPC](#wallet_getsubaccounts-rpc) -- [Funding Sub Accounts](#funding-sub-accounts) -- [Session Management](#session-management) - -## Overview - -Sub accounts are app-specific embedded wallets created under a user's Base Account. They let your app perform transactions on behalf of the user without requiring approval popups for every action — useful for gaming, DeFi automation, or any UX that needs low-friction transactions. - -Each sub account is a separate smart wallet owned by the parent Base Account. - -## Key Concepts - -- Sub accounts are **app-scoped** — each app gets its own sub account(s) -- The parent Base Account is the **owner** of each sub account -- Sub accounts can be funded via **spend permissions** or **manual transfers** -- Ownership may change across devices/browsers — always call `wallet_addSubAccount` each session -- Only **Coinbase Smart Wallet** contracts are supported for importing existing sub accounts - -## SDK Configuration - -Configure sub accounts when creating the SDK: - -```typescript -import { createBaseAccountSDK, getCryptoKeyAccount } from '@base-org/account'; - -const sdk = createBaseAccountSDK({ - appName: 'My App', - appLogoUrl: 'https://example.com/logo.png', - appChainIds: [8453], - subAccounts: { - creation: 'on-connect', - defaultAccount: 'sub', - funding: 'spend-permissions', - toOwnerAccount: async () => { - const { account } = await getCryptoKeyAccount(); - return { account }; - }, - }, -}); -``` - -`SubAccountOptions`: - -| Property | Type | Values | Description | -|----------|------|--------|-------------| -| `creation` | `string` | `'on-connect'`, `'manual'` | When to create sub accounts | -| `defaultAccount` | `string` | `'sub'`, `'universal'` | Which account is default (first in accounts array) | -| `funding` | `string` | `'spend-permissions'`, `'manual'` | How sub accounts are funded | -| `toOwnerAccount` | `function` | — | Returns `{ account: LocalAccount \| WebAuthnAccount \| null }` | - -## Key Management - -Sub accounts require a key pair for signing. The SDK provides utilities for P256 key management. - -### `generateKeyPair()` - -```typescript -import { generateKeyPair } from '@base-org/account'; - -const keyPair = await generateKeyPair(); -// keyPair.publicKey → hex string -// keyPair.privateKey → hex string -``` - -### `getKeypair()` - -Retrieves an existing key pair from secure storage (returns `null` if none). - -```typescript -import { getKeypair } from '@base-org/account'; - -let keyPair = await getKeypair(); -if (!keyPair) { - keyPair = await generateKeyPair(); -} -``` - -### `getCryptoKeyAccount()` - -Gets the current crypto key account info. - -```typescript -import { getCryptoKeyAccount } from '@base-org/account'; - -const { account } = await getCryptoKeyAccount(); -// account.publicKey → hex string -// account.type → 'webauthn' | 'local' -// account.address → (for LocalAccount only) -``` - -Returns `{ account }` where `account` is one of: -- `WebAuthnAccount`: `{ publicKey, type: 'webauthn' }` -- `LocalAccount`: `{ address, publicKey, type: 'local' }` -- `null`: No account available - -## Creating Sub Accounts - -### Via SDK Helper - -```typescript -const subAccount = await sdk.subAccount.create({ - type: 'webauthn-p256', - publicKey: keyPair.publicKey, -}); -// subAccount.address → the sub account address -``` - -### Via RPC (wallet_addSubAccount) - -```typescript -const result = await provider.request({ - method: 'wallet_addSubAccount', - params: [{ - account: { - type: 'create', - keys: [{ - type: 'webauthn-p256', - publicKey: keyPair.publicKey, - }], - }, - }], -}); -// result.address → the sub account address -``` - -Key types for the `keys` array: - -| Type | Description | -|------|-------------| -| `'address'` | Raw Ethereum address | -| `'p256'` | P256 public key | -| `'webcrypto-p256'` | WebCrypto P256 key | -| `'webauthn-p256'` | WebAuthn P256 key (recommended) | - -## Retrieving Sub Accounts - -```typescript -const subAccount = await sdk.subAccount.get(); -// Returns the current sub account or null -``` - -Or via RPC: - -```typescript -const subAccounts = await provider.request({ - method: 'wallet_getSubAccounts', -}); -// Array of { address, factory?, factoryData? } -``` - -## Adding Owners - -```typescript -await sdk.subAccount.addOwner({ - address: newOwnerAddress, - chainId: 8453, -}); -``` - -## wallet_addSubAccount RPC - -Two modes of operation: - -### Create a New Sub Account - -```typescript -await provider.request({ - method: 'wallet_addSubAccount', - params: [{ - account: { - type: 'create', - keys: [{ type: 'webauthn-p256', publicKey: '0x...' }], - }, - }], -}); -``` - -### Import an Existing Deployed Account - -```typescript -await provider.request({ - method: 'wallet_addSubAccount', - params: [{ - account: { - type: 'deployed', - address: '0xExistingSubAccount', - chainId: 8453, - }, - }], -}); -``` - -Returns: `{ address, factory?, factoryData? }` - -## wallet_getSubAccounts RPC - -```typescript -const accounts = await provider.request({ - method: 'wallet_getSubAccounts', -}); -``` - -Returns an array of sub account objects. - -## Funding Sub Accounts - -Two strategies: - -### Spend Permissions (Recommended) - -Set `funding: 'spend-permissions'` in SDK config. The parent Base Account grants a spend permission to the sub account, which can then spend tokens within the allowed limit. - -### Manual - -Set `funding: 'manual'`. You transfer tokens directly to the sub account address. - -## Session Management - -**Call `wallet_addSubAccount` at the start of each session** before using the sub account. This is necessary because: - -- Ownership may change when users switch devices or browsers -- The sub account needs to be re-registered with the current session -- Without this call, sub account operations may fail silently - -```typescript -async function initSession() { - const keyPair = await getKeypair() || await generateKeyPair(); - await provider.request({ - method: 'wallet_addSubAccount', - params: [{ - account: { - type: 'create', - keys: [{ type: 'webauthn-p256', publicKey: keyPair.publicKey }], - }, - }], - }); -} -``` diff --git a/skills/build-on-base/references/base-account/subscriptions.md b/skills/build-on-base/references/base-account/subscriptions.md deleted file mode 100644 index 4d56c9f..0000000 --- a/skills/build-on-base/references/base-account/subscriptions.md +++ /dev/null @@ -1,238 +0,0 @@ -# Subscriptions (Recurring Payments) - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Backend Setup: CDP Owner Wallet](#backend-setup-cdp-owner-wallet) -- [Frontend: Create a Subscription](#frontend-create-a-subscription) -- [Backend: Check Subscription Status](#backend-check-subscription-status) -- [Backend: Charge a Subscription](#backend-charge-a-subscription) -- [Backend: Cancel a Subscription](#backend-cancel-a-subscription) -- [Advanced: Manual Execution](#advanced-manual-execution) -- [Fund Routing Patterns](#fund-routing-patterns) -- [Testing](#testing) - -## Overview - -Recurring payments use **Spend Permissions** — an onchain primitive that lets a user grant revocable spending rights to your app. The user approves once, and your backend charges periodically without further user interaction. - -Key properties: -- Spending limit auto-resets each period (no rollover between periods) -- User can cancel anytime from their wallet -- USDC only (on Base Mainnet and Base Sepolia) -- Requires both client-side (subscribe) and server-side (charge/revoke) code - -## Architecture - -``` -Client (browser) Server (Node.js) -───────────────── ──────────────── -subscribe() ──────────────────────> Store subscription ID - ↓ - getStatus() → check if chargeable - ↓ - charge() → execute periodic charge - ↓ - revoke() → cancel when needed -``` - -The server uses a **CDP (Coinbase Developer Platform) smart wallet** to act as the subscription owner (the entity authorized to spend). - -## Backend Setup: CDP Owner Wallet - -### Environment Variables - -```bash -CDP_API_KEY_ID=your-api-key-id -CDP_API_KEY_SECRET=your-api-key-secret -CDP_WALLET_SECRET=your-wallet-secret -PAYMASTER_URL=https://your-paymaster.xyz # optional, for gasless transactions -``` - -Get these from [Coinbase Developer Platform](https://portal.cdp.coinbase.com). - -### Create or Retrieve the Owner Wallet - -```typescript -import { base } from '@base-org/account/node'; - -const wallet = await base.subscription.getOrCreateSubscriptionOwnerWallet({ - walletName: 'my-app-subscriptions', -}); -// wallet.address → share this with the frontend as subscriptionOwner -// wallet.walletName → must match across charge() and revoke() calls -``` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `walletName` | `string` | No | Wallet identifier (default: `"subscription owner"`) | -| `cdpApiKeyId` | `string` | No | Falls back to `CDP_API_KEY_ID` env var | -| `cdpApiKeySecret` | `string` | No | Falls back to `CDP_API_KEY_SECRET` env var | -| `cdpWalletSecret` | `string` | No | Falls back to `CDP_WALLET_SECRET` env var | - -Returns: `{ address, walletName, eoaAddress }` - -This is **idempotent** — the same `walletName` always returns the same wallet. The `address` is the CDP smart wallet address (safe to share publicly as `subscriptionOwner`). - -**Never expose CDP credentials client-side.** Only the wallet `address` is public. - -## Frontend: Create a Subscription - -```typescript -import { base } from '@base-org/account'; - -const subscription = await base.subscription.subscribe({ - recurringCharge: '9.99', - subscriptionOwner: '0xYourCDPWalletAddress', - periodInDays: 30, - testnet: false, -}); -// subscription.id → store this as the subscription identifier -``` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `recurringCharge` | `string` | Yes | USDC amount per period (max 6 decimals) | -| `subscriptionOwner` | `string` | Yes | Your CDP wallet address | -| `periodInDays` | `number` | No | Charge period in days (default: `30`) | -| `testnet` | `boolean` | No | Use testnet (default: `false`) | -| `requireBalance` | `boolean` | No | Check payer balance first (default: `true`) | - -Returns `SubscriptionResult`: - -| Field | Type | Description | -|-------|------|-------------| -| `id` | `string` | Permission hash (subscription identifier) | -| `subscriptionOwner` | `string` | Your app's wallet address | -| `subscriptionPayer` | `string` | The user's wallet address | -| `recurringCharge` | `string` | Amount in USD | -| `periodInDays` | `number` | Period length | - -## Backend: Check Subscription Status - -```typescript -import { base } from '@base-org/account'; - -const status = await base.subscription.getStatus({ - id: subscriptionId, - testnet: false, -}); -``` - -| Parameter | Type | Required | -|-----------|------|----------| -| `id` | `string` | Yes | -| `testnet` | `boolean` | No | - -Returns `SubscriptionStatus`: - -| Field | Type | Description | -|-------|------|-------------| -| `isSubscribed` | `boolean` | Whether subscription is active | -| `recurringCharge` | `string` | Charge amount | -| `remainingChargeInPeriod` | `string` | How much can still be charged this period | -| `currentPeriodStart` | `Date` | — | -| `nextPeriodStart` | `Date` | — | -| `periodInDays` | `number` | — | - -Check before charging: - -```typescript -const status = await base.subscription.getStatus({ id: subscriptionId }); -if (status.isSubscribed && parseFloat(status.remainingChargeInPeriod!) > 0) { - // safe to charge -} -``` - -## Backend: Charge a Subscription - -```typescript -import { base } from '@base-org/account/node'; - -const result = await base.subscription.charge({ - id: subscriptionId, - amount: 'max-remaining-charge', - paymasterUrl: process.env.PAYMASTER_URL, - testnet: false, -}); -``` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `id` | `string` | Yes | Subscription ID | -| `amount` | `string \| 'max-remaining-charge'` | Yes | USDC amount or `'max-remaining-charge'` | -| `paymasterUrl` | `string` | No | For gasless transactions | -| `recipient` | `string` | No | Send USDC to a different address (default: stays in CDP wallet) | -| `testnet` | `boolean` | No | Default: `false` | -| `walletName` | `string` | No | Must match the wallet used in setup | - -Returns: `{ success, id, subscriptionId, amount, subscriptionOwner, recipient }` - -`charge()` handles all transaction details: gas estimation, nonce management, and signing. - -## Backend: Cancel a Subscription - -```typescript -import { base } from '@base-org/account/node'; - -const result = await base.subscription.revoke({ - id: subscriptionId, - paymasterUrl: process.env.PAYMASTER_URL, - testnet: false, -}); -``` - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `id` | `string` | Yes | Subscription ID | -| `paymasterUrl` | `string` | No | For gasless transactions | -| `testnet` | `boolean` | No | Default: `false` | -| `walletName` | `string` | No | Must match the wallet used in setup | - -Returns: `{ success, id, subscriptionId, subscriptionOwner }` - -Revoking is **permanent**. The user would need to create a new subscription. - -## Advanced: Manual Execution - -For custom wallet infrastructure (not using CDP wallets), use `prepareCharge` and `prepareRevoke` to get raw call data. - -### `prepareCharge()` - -```typescript -import { base } from '@base-org/account'; - -const calls = await base.subscription.prepareCharge({ - id: subscriptionId, - amount: 'max-remaining-charge', - testnet: false, -}); -// calls → Array<{ to, data, value: '0x0' }> -// Execute via wallet_sendCalls or eth_sendTransaction -``` - -### `prepareRevoke()` - -```typescript -const call = await base.subscription.prepareRevoke({ - id: subscriptionId, - testnet: false, -}); -// call → { to, data, value: '0x0' } -``` - -## Fund Routing Patterns - -| Pattern | How | When | -|---------|-----|------| -| Default | Omit `recipient` | USDC stays in CDP wallet | -| Treasury | `recipient: '0xTreasury'` | Auto-transfer to treasury | -| Dynamic | Set `recipient` per charge | Route to different addresses based on plan type | - -## Testing - -- Use `testnet: true` in all calls (`subscribe`, `getStatus`, `charge`, `revoke`) -- Use `periodInDays: 1` for faster testing cycles -- Test on Base Sepolia (chain ID 84532) -- Get test USDC from the [Circle Faucet](https://faucet.circle.com/) diff --git a/skills/build-on-base/references/base-account/troubleshooting.md b/skills/build-on-base/references/base-account/troubleshooting.md deleted file mode 100644 index db090e8..0000000 --- a/skills/build-on-base/references/base-account/troubleshooting.md +++ /dev/null @@ -1,146 +0,0 @@ -# Troubleshooting - -## Table of Contents - -- [Quick Fixes](#quick-fixes) -- [Popup Issues](#popup-issues) -- [Gas Usage](#gas-usage) -- [Unsupported Operations](#unsupported-operations) -- [Wallet Library Compatibility](#wallet-library-compatibility) -- [Migration from Coinbase Wallet SDK](#migration-from-coinbase-wallet-sdk) -- [Transaction Simulation Debugging](#transaction-simulation-debugging) -- [When to Consult the Docs](#when-to-consult-the-docs) - -## Quick Fixes - -| Issue | Solution | -|-------|----------| -| Peer dependency error during install | Use `--legacy-peer-deps` flag | -| Popup shows infinite spinner | Set COOP header to `same-origin-allow-popups` (not `same-origin`) | -| Signature verification fails pre-deploy | Use viem — it handles ERC-6492 automatically | -| `wallet_connect` throws `4100` | Wallet doesn't support it; fall back to `eth_requestAccounts` + `personal_sign` | -| Payment status returns `not_found` | Ensure `testnet` param in `getPaymentStatus()` matches `pay()` | -| Sub account operations fail | Call `wallet_addSubAccount` at the start of each session | -| Balance appears insufficient | Check `auxiliaryFunds` capability — user may have Coinbase balances available | - -## Popup Issues - -Base Account uses a popup window at `keys.coinbase.com` for user approvals. - -### Cross-Origin-Opener-Policy (COOP) - -| COOP Value | Works? | -|------------|--------| -| `unsafe-none` (browser default) | Yes | -| `same-origin-allow-popups` | Yes (recommended) | -| `same-origin` | **No** — breaks the popup entirely | - -If using `same-origin`, the popup either errors or shows an infinite spinner. Switch to `same-origin-allow-popups`. - -### Popup Blockers - -Browsers block popups unless triggered by a direct user click. To avoid blocking: - -- Generate nonces and do any async work **before** the user clicks the sign-in button -- Keep zero or minimal logic between the button click handler and the SDK call -- Test across all target browsers — popup blocking behavior varies - -### Popup "Linger" Behavior - -After responding to a request, the popup stays open for **200ms** before closing. If a second SDK request arrives within that window, it's handled in the same popup (no new popup needed). - -If the second request arrives **after** 200ms (popup already closed), the browser will block the new programmatic popup. Design flows to either: -- Chain requests quickly (< 200ms gap) -- Require a new user click for the second request - -## Gas Usage - -Base Accounts use more gas than traditional Ethereum accounts (EOAs) because they're smart contracts processed through ERC-4337 bundling. - -| Operation | EOA | Base Account | -|-----------|-----|-------------| -| Native token transfer | ~21,000 gas | ~100,000 gas | -| ERC-20 token transfer | ~65,000 gas | ~150,000 gas | -| First-time deployment | N/A | ~300,000+ gas (one-time) | - -On L2 networks like Base, the cost difference is typically just a few cents. Use a paymaster to sponsor gas entirely (see [capabilities reference](capabilities.md#capability-paymasterservice)). - -## Unsupported Operations - -Base Account is an ERC-4337 smart wallet. Some operations behave differently: - -### Self-Calls - -Apps **cannot** make calls to the user's own Base Account address. This is a security measure to prevent changing owners, upgrading the account, or other admin operations. - -### CREATE Opcode - -Not supported due to ERC-4337 limitations. Workarounds: -- Use a **factory contract** that deploys on behalf of the user -- Use the `CREATE2` opcode instead - -### Solidity `transfer` Function - -Base Account wallets **cannot receive ETH** via Solidity's built-in `transfer` function because it only forwards 2,300 gas — insufficient for smart contract `receive`/`fallback` functions. - -Use `call` instead: - -```solidity -// Won't work with Base Account -payable(baseAccountAddress).transfer(amount); - -// Use this instead -(bool success, ) = payable(baseAccountAddress).call{value: amount}(""); -require(success, "Transfer failed"); -``` - -Known affected contract: **WETH9 on Base** (`0x4200000000000000000000000000000000000006`) — Base Accounts cannot directly unwrap ETH from it. - -## Wallet Library Compatibility - -These wallet aggregation libraries have day-1 Base Account support: - -| Library | Supported | -|---------|-----------| -| Dynamic | Yes | -| Privy | Yes | -| ThirdWeb | Yes | -| ConnectKit | Yes | -| Web3Modal / Reown | Yes | -| Web3-Onboard | Yes | -| RainbowKit | Yes | - -## Migration from Coinbase Wallet SDK - -The Coinbase Wallet app is transitioning to the Base app. To migrate: - -1. **Don't immediately replace** the existing "Coinbase Wallet" button -2. **Add** a "Sign in with Base" button as a new option alongside it -3. Over time, existing Coinbase Wallet users will be migrated to Base Accounts - -Code change: - -```typescript -// New: add Base Account SDK -import { createBaseAccountSDK } from '@base-org/account'; -const baseAccount = createBaseAccountSDK({ appName: 'My App' }); -``` - -As of Coinbase Wallet SDK v4.0, users without the extension see a popup with options (mobile WalletLink or passkey-powered Smart Wallet). To avoid any popup window, use Coinbase Wallet SDK version < 4.0. - -## Transaction Simulation Debugging - -Hidden feature in the Base Account popup: click the transaction simulation area **five times** to copy the simulation request/response data to your clipboard. Paste into a text editor to inspect. - -## When to Consult the Docs - -This skill covers the most common patterns. For edge cases, advanced configurations, or the latest API changes, consult: - -- **AI-optimized docs**: [docs.base.org/llms.txt](https://docs.base.org/llms.txt) — feed this to your agent for comprehensive context -- **Base Account reference**: [docs.base.org/base-account](https://docs.base.org/base-account) — full API reference, all RPC methods, all capabilities -- **Base Account SDK source**: [github.com/base/account-sdk](https://github.com/base/account-sdk) -- **Smart Wallet contracts**: [github.com/coinbase/smart-wallet](https://github.com/coinbase/smart-wallet) -- **Spend Permissions contracts**: [github.com/coinbase/spend-permissions](https://github.com/coinbase/spend-permissions) -- **Coinbase Developer Platform**: [portal.cdp.coinbase.com](https://portal.cdp.coinbase.com) — paymaster setup, API keys, wallet management - -For standard Ethereum RPC methods (`eth_getBalance`, `eth_sendTransaction`, `eth_getTransactionReceipt`, etc.), Base Account's provider supports all standard methods. See the [provider RPC methods reference](https://docs.base.org/base-account/reference/core/provider-rpc-methods/sdk-overview) for the full list. diff --git a/skills/build-on-base/references/migrations/farcaster-miniapp-to-app.md b/skills/build-on-base/references/migrations/farcaster-miniapp-to-app.md deleted file mode 100644 index 9aeb2ac..0000000 --- a/skills/build-on-base/references/migrations/farcaster-miniapp-to-app.md +++ /dev/null @@ -1,790 +0,0 @@ -# Convert Farcaster Miniapp to Base App - -Convert a Farcaster miniapp into a normal app on Base. The default outcome is a regular web app that works in the Base app browser and on the open web, with Farcaster Mini App host coupling removed. - -If some Farcaster functionality truly needs to survive, keep it separate from the main app surface. Prefer read-only data first. Only preserve Mini App-specific behavior when the user explicitly insists, and isolate it behind a dedicated route or page rather than carrying it through the whole app. - -## Core Principle - -Always separate these decisions: - -1. Remove Mini App host/runtime coupling. -2. Decide whether any Farcaster-facing functionality should remain. -3. If something remains, keep it isolated from the main app and avoid introducing new vendor dependencies by default. - -Do **not** automatically turn "keep some Farcaster functionality" into "migrate to Neynar." If a project already uses Neynar and the user wants to keep an isolated Farcaster-only area, you may preserve that existing integration. Do not introduce new Neynar adoption as the default recommendation. - -## Quick Start - -Follow these five phases sequentially: - -0. **Discovery** — Quick scan + quiz to choose a path -1. **Analysis** — Detailed read-only analysis scoped to the chosen path -2. **Conversion** — Remove Mini App SDK patterns and isolate any intentionally preserved Farcaster surface -3. **Cleanup** — Remove dead code, env vars, and dependencies -4. **Verification** — Type check, build, and summarize - -## Conversion Paths - -The quiz should route the user into one of two paths: - -| Path | Name | Who it's for | What happens | -|------|------|-------------|-------------| -| **A** | Regular App Default | Most projects | Strip Farcaster Mini App coupling and become a normal Base/web app | -| **B** | Isolated Farcaster Surface | The app still needs a small Farcaster-specific area | Convert the main app into a normal app, then keep only a separate Farcaster route/page for the remaining functionality | - -`Path B` is still biased toward removing complexity: -- Prefer **read-only** Farcaster data. -- Avoid preserving Mini App host/runtime behavior unless the user explicitly asks for it. -- Keep any preserved Farcaster logic out of the main app shell, shared providers, and primary auth flow. - ---- - -## Phase 0: Discovery - -### 0A. Quick Scan (automated, no user interaction) - -Run a lightweight scan before asking questions. Produce an internal tally: - -1. **Detect framework** from `package.json` (`next`, `vite`, `react-scripts`, `@remix-run/*`) -2. **Count Farcaster packages** in `dependencies` and `devDependencies` -3. **Grep source files** (`.ts`, `.tsx`, `.js`, `.jsx`) for: - - `sdk.actions.*` calls (count total) - - `sdk.quickAuth` usage (yes/no) - - `sdk.context` usage (yes/no) - - `.well-known/farcaster.json` (yes/no) - - `farcasterMiniApp` / `miniAppConnector` connector (yes/no) - - Total files with any `@farcaster/` import - - `@neynar/` imports or `api.neynar.com` fetch calls (yes/no) -4. **Identify the blast radius**: - - Are Farcaster references spread across the main app shell? - - Are they already mostly confined to a route like `app/farcaster/`, `pages/farcaster/`, or a small set of components? - - Are there obvious host-only features such as haptics, notifications, `openMiniApp`, or `sdk.context.client`? - -Use this tally to inform quiz suggestions. Do **not** dump raw scan output to the user before asking the quiz. - -### 0B. Interactive Quiz - -Ask these questions one at a time. Use the quick scan results to suggest the most likely answer. - -**Q1** (always ask): - -> Based on my scan, your project has [X] files using the Farcaster SDK with [summary of what is used]. -> -> Which outcome do you want? -> - **(a) Regular app everywhere** — remove Farcaster-specific behavior and just keep a normal Base/web app -> - **(b) Regular app first, plus a separate Farcaster area** — keep the main app clean, but preserve a small isolated route/page if really needed - -**Q2** (always ask): - -> How deeply is the Mini App SDK used today? -> - **(a) Minimal** — mostly `sdk.actions.ready()` and a few helpers -> - **(b) Moderate** — some `context`, `openUrl`, profile links, or conditional `isInMiniApp` logic -> - **(c) Heavy** — auth, wallet connector, notifications, compose flows, or host-specific behavior - -**Q3** (ask if Q1 = b): - -> What is the smallest Farcaster feature set you actually need to preserve? -> - **(a) Read-only only** — profile or cast display, links out to Farcaster profiles, maybe a small social page -> - **(b) Some Farcaster-specific interactions** — there is a separate page/path that still needs more than read-only behavior -> - **(c) Not sure** — analyze what is isolated already and recommend the smallest keep-surface possible - -**Q4** (ask if Q1 = b and there is existing isolated Farcaster code or existing Neynar usage): - -> Does the project already have an isolated Farcaster-only route/page or integration that you want to keep as-is if possible? -> - **(a) Yes** — preserve only that isolated surface -> - **(b) No** — prefer removing it unless there is a very strong reason to keep it - -**Q5** (ask if quick auth or other Mini App auth is present): - -> After conversion, what should the main app use for authentication? -> - **(a) SIWE** — wallet-based auth for the regular app -> - **(b) Existing non-Farcaster auth** — keep whatever normal web auth already exists -> - **(c) No auth** — remove auth entirely - -### 0C. Path Selection - -Map answers to a path: - -| Desired outcome | Typical result | -|-----------------|----------------| -| `Q1 = regular app everywhere` | **Path A** — Regular App Default | -| `Q1 = regular app first, plus separate Farcaster area` | **Path B** — Isolated Farcaster Surface | - -Then tighten the recommendation: - -- If the user chose `Path B`, prefer **read-only preservation** unless they explicitly require something else. -- If the scan shows heavy host/runtime coupling but the user wants `Path A`, warn them that some features will be deleted rather than recreated. -- If the project already uses Neynar, only keep it if it remains inside the isolated Farcaster surface. Do not expand it into the main app. - -Announce the chosen path: - -> Based on your answers, I'll use **Path [X]: [Name]**. This will [one-sentence description]. I'll now do a detailed analysis of your project. - -Record the quiz answers internally. They guide whether the agent should: -- fully remove Farcaster features -- preserve only a read-only isolated surface -- quarantine any unavoidable Farcaster-specific logic to a dedicated route/page - -**Proceed to Phase 1.** - ---- - -## Phase 1: Analysis (Read-Only) - -### 1A. Detect Framework - -Read `package.json`: -- `next` → Next.js -- `vite` → Vite -- `react-scripts` → Create React App -- `@remix-run/*` → Remix - -### 1B. Scan for Farcaster Dependencies - -List all packages matching: -- `@farcaster/miniapp-sdk`, `@farcaster/miniapp-core`, `@farcaster/miniapp-wagmi-connector` -- `@farcaster/frame-sdk`, `@farcaster/frame-wagmi-connector` -- `@farcaster/quick-auth`, `@farcaster/auth-kit` -- `@neynar/*` (compatibility only; do not assume it stays) - -### 1C. Grep for Farcaster Code - -Search source files (`.ts`, `.tsx`, `.js`, `.jsx`) for: - -**SDK imports:** -``` -@farcaster/miniapp-sdk -@farcaster/miniapp-core -@farcaster/miniapp-wagmi-connector -@farcaster/frame-sdk -@farcaster/frame-wagmi-connector -@farcaster/quick-auth -@farcaster/auth-kit -@neynar/ -``` - -**SDK calls:** -``` -sdk.actions.ready -sdk.actions.openUrl -sdk.actions.close -sdk.actions.composeCast -sdk.actions.addMiniApp -sdk.actions.requestWalletAddress -sdk.actions.viewProfile -sdk.actions.viewToken -sdk.actions.sendToken -sdk.actions.swapToken -sdk.actions.signIn -sdk.actions.setPrimaryButton -sdk.actions.openMiniApp -sdk.quickAuth -sdk.context -sdk.isInMiniApp -sdk.getCapabilities -sdk.haptics -sdk.back -sdk.wallet -``` - -**Connectors & providers:** -``` -farcasterMiniApp() -miniAppConnector() -farcasterFrame() -MiniAppProvider -MiniAppContext -useMiniApp -useMiniAppContext -``` - -**Manifest & meta:** -``` -.well-known/farcaster.json -fc:miniapp -fc:frame -``` - -**Environment variables:** -``` -NEYNAR_API_KEY -NEXT_PUBLIC_NEYNAR_CLIENT_ID -FARCASTER_ -FC_ -``` - -### 1D. Check Existing Web3 Setup - -Look for: -- `coinbaseWallet` connector in wagmi config -- SIWE / `siwe` package usage -- `connectkit`, `rainbowkit`, or `@coinbase/onchainkit` -- Existing wallet connection UI - -### 1E. Check Separation Boundaries - -Map where Farcaster logic currently lives: - -- Root providers or app shell -- Shared hooks or auth middleware -- One-off components -- Dedicated routes/pages like `app/farcaster/*` -- Server routes used only by Farcaster functionality - -Mark each location with one action: -- **remove** -- **stub** -- **move behind isolated route/page** -- **keep only as read-only** - -### 1F. Report Findings - -Create a path-scoped summary. - -**All paths include:** -``` -## Conversion Analysis — Path [X]: [Name] - -**Framework:** [detected] -**Farcaster packages:** [list] -**Files with Farcaster code:** [count] - -### Wagmi Connector -- File: [path] -- Current connector: [farcasterMiniApp / miniAppConnector / farcasterFrame / none] -- Other connectors: [list] -- Action: [replace with coinbaseWallet / leave existing wallet setup / remove only] - -### MiniApp Provider -- File: [path] -- Pattern: [simple / complex] -- Consumers: [files importing from this] -- Action: [stub / remove / isolate] - -### SDK Action Calls -[list each: file, what it does, action] - -### Manifest & Meta -- Manifest route: [path or N/A] -- Meta tags: [file or N/A] -``` - -**Path A additionally includes:** -``` -### Main App Outcome -- Action: remove Farcaster-specific UI and flows from the main app entirely - -### Authentication -- Quick Auth used: [yes/no, file] -- Action: replace with SIWE / keep existing non-Farcaster auth / remove - -### Existing Neynar Usage -- Package or files: [list or N/A] -- Action: remove entirely unless the user later re-scopes to Path B - -### Environment Variables -[list all FC/Neynar vars that will be removed] -``` - -**Path B additionally includes:** -``` -### Main App Outcome -- Action: convert the main app into a normal web app first - -### Isolated Farcaster Surface -- Route/page to keep: [path or proposed path] -- Scope: [read-only / mixed / host-specific] -- Recommended target scope: [prefer read-only / quarantine existing behavior / remove] - -### Authentication -- Quick Auth used: [yes/no, file] -- Main app action: replace with SIWE / keep existing non-Farcaster auth / remove -- Isolated Farcaster surface action: [remove auth coupling / preserve existing isolated flow only if explicitly requested] - -### Existing Neynar Usage -- Package or files: [list or N/A] -- Action: [remove / keep only inside isolated surface] - -### Environment Variables -- Remove from main app: [FC_*, FARCASTER_*, etc.] -- Keep only if isolated surface truly still needs them: [NEYNAR_API_KEY, etc. or N/A] -``` - -**All paths end with:** -``` -### Potential Issues -- [ ] FID used as database primary key -- [ ] Farcaster colors in tailwind config -- [ ] `isInMiniApp` branches with unique else logic -- [ ] Components only meaningful inside Farcaster -- [ ] Farcaster code mixed into shared providers or root layout -``` - -Ask: - -> Does this analysis look correct? Ready to proceed with conversion? - -**STOP and wait for user confirmation before Phase 2.** - ---- - -## Phase 2: Conversion - -Steps are organized by feature area. Each step notes which paths it applies to and what to do differently for `Path B`. - -### 2A. Wagmi Connector (All Paths) - -Find the wagmi config file (`lib/wagmi.ts`, `config/wagmi.ts`, `providers/wagmi-provider.tsx`, etc.): - -1. Remove import of `farcasterMiniApp` or `miniAppConnector` from `@farcaster/miniapp-wagmi-connector` -2. Remove the `farcasterMiniApp()` / `miniAppConnector()` call from the `connectors` array -3. If no wallet connector remains, add: - ```typescript - import { coinbaseWallet } from "wagmi/connectors"; - - coinbaseWallet({ appName: "" }) - ``` -4. If `coinbaseWallet` already exists, leave it as-is -5. Clean up empty lines and stale imports - -Skip this step if wagmi is not in the project. - -### 2B. MiniApp Provider / Context (All Paths) - -If the app has a shared Mini App provider, remove host/runtime assumptions from the main app. - -**Pattern A: simple provider** - -Replace with a safe stub: - -```tsx -'use client' - -import React, { createContext, useContext, useMemo } from "react"; - -interface MiniAppContextType { - context: undefined; - ready: boolean; - isInMiniApp: boolean; -} - -const MiniAppContext = createContext(undefined); - -export function useMiniAppContext() { - const context = useContext(MiniAppContext); - if (context === undefined) { - throw new Error("useMiniAppContext must be used within a MiniAppProvider"); - } - return context; -} - -export default function MiniAppProvider({ children }: { children: React.ReactNode }) { - const value = useMemo( - () => ({ - context: undefined, - ready: true, - isInMiniApp: false, - }), - [] - ); - - return {children}; -} -``` - -Preserve export style and hook names so consumers do not break. - -**Pattern B: complex provider** - -- If many consumers depend on it, stub it first. -- If only a few files use it, remove it and update those imports directly. -- In `Path B`, do not let the isolated Farcaster surface keep this provider wired through the root app shell. If needed, make it local to the isolated route only. - -### 2C. Authentication - -The main app should use normal web auth, not Mini App auth. - -**Default rule for both paths:** -- If `sdk.quickAuth.getToken()` is used, replace it with SIWE or remove it. -- If a normal non-Farcaster auth system already exists, prefer that over adding new auth. -- Do not introduce new Farcaster or Neynar auth as the default conversion target. - -#### SIWE Replacement Pattern - -**Client-side** (e.g. `useSignIn.ts`): -- Remove `import sdk from "@farcaster/miniapp-sdk"` -- Remove `sdk.quickAuth.getToken()` -- Replace with: - 1. Get wallet address from `useAccount()` (wagmi) - 2. Create a SIWE message with `siwe` - 3. Sign with `useSignMessage()` (wagmi) - 4. Send signature + message to the backend for verification - -**Server-side** (e.g. `app/api/auth/sign-in/route.ts`): -- Remove `@farcaster/quick-auth` verification -- Replace with SIWE verification: - 1. Parse the SIWE message - 2. Verify the signature with `siwe` or `viem` - 3. Use recovered wallet address as the normal app identity - -**If FID is used as a database primary key:** -- Do not auto-change schema -- Add a TODO comment for later migration -- Warn in Phase 4 summary - -**Path B note:** -- If the isolated Farcaster surface already has its own auth or integration flow and the user explicitly wants to keep it, quarantine it there only. -- Do not let that flow remain the default app-wide auth system. - -### 2D. SDK Action Calls - -#### 2D-1. Main replacements for both paths - -| Original | Replacement | -|----------|-------------| -| `sdk.actions.ready()` | Remove entirely | -| `sdk.actions.openUrl(url)` | `window.open(url, "_blank")` | -| `sdk.actions.close()` | `window.close()` or remove | -| `sdk.actions.composeCast(...)` | Remove from the main app | -| `sdk.actions.addMiniApp()` | Remove call and UI | -| `sdk.actions.requestWalletAddress()` | Remove; wagmi handles wallet access | -| `sdk.actions.viewProfile(fid)` | `window.open(\`https://warpcast.com/~/profiles/${fid}\`, "_blank")` | -| `sdk.actions.viewToken(opts)` | `window.open(\`https://basescan.org/token/${opts.token}\`, "_blank")` | -| `sdk.actions.sendToken(opts)` | Replace with wagmi flow if the feature matters, otherwise remove | -| `sdk.actions.swapToken(opts)` | Remove call and UI unless there is a real app-specific swap feature outside Farcaster | -| `sdk.actions.signIn(...)` | Remove; auth handled by normal web auth | -| `sdk.actions.setPrimaryButton(...)` | Remove entirely | -| `sdk.actions.openMiniApp(...)` | Remove from the main app | -| `sdk.isInMiniApp()` | Remove conditional and keep the non-Farcaster branch | -| `sdk.context` | Remove from the main app | -| `sdk.getCapabilities()` | Remove or replace with `async () => []` | -| `sdk.haptics.*` | Remove entirely | -| `sdk.back.*` | Remove entirely | -| `sdk.wallet.*` | Remove; wagmi handles wallet access | - -For conditional `isInMiniApp` branches: - -```tsx -// BEFORE -if (isInMiniApp) { - sdk.actions.openUrl(url); -} else { - window.open(url, "_blank"); -} - -// AFTER -window.open(url, "_blank"); -``` - -Always keep the normal web branch. - -#### 2D-2. Path B overrides - -`Path B` does **not** mean "recreate everything." It means "keep the main app clean and preserve the smallest separate Farcaster surface possible." - -- `sdk.context` - - Remove from the main app - - For the isolated surface, prefer replacing it with read-only fetched data or explicit route params - - Remove `context.location`, `context.client`, safe area, and other host-derived assumptions unless the user explicitly insists on preserving a host-specific page - -- `sdk.actions.composeCast(...)` - - Remove from the main app - - If the user only needs read-only, delete it entirely - - If they insist on preserving it, keep it isolated behind a dedicated page/path and flag it as a manual follow-up rather than rebuilding it by default - -- `sdk.actions.openMiniApp(...)` - - Remove from the main app - - Only keep it in an isolated route if the user explicitly wants a Farcaster-only surface - -- notifications / haptics / host buttons - - Remove from the main app - - Preserve only if the isolated route truly depends on them and the user has explicitly opted into that complexity - -### 2E. Optional Read-Only Farcaster Data (Path B only) - -If the user wants an isolated Farcaster surface, prefer **read-only** data first. - -**Create `lib/farcaster-readonly.ts`** (or equivalent) only if the app needs it: - -```typescript -const HUB_URL = "https://hub.farcaster.xyz"; - -export async function getUserData(fid: number) { - const res = await fetch(`${HUB_URL}/v1/userDataByFid?fid=${fid}`); - if (!res.ok) throw new Error(`Hub user data fetch failed: ${res.status}`); - return res.json(); -} - -export async function getCastsByFid(fid: number, pageSize = 25) { - const res = await fetch(`${HUB_URL}/v1/castsByFid?fid=${fid}&pageSize=${pageSize}`); - if (!res.ok) throw new Error(`Hub casts fetch failed: ${res.status}`); - return res.json(); -} -``` - -Then: -- Keep these calls inside the isolated route/page only -- Do not thread Farcaster data requirements through the main app shell -- If the project already has a small isolated Neynar-based read-only integration, you may keep it only if removing it would create more churn than it saves -- Do not add new Neynar packages for this by default - -### 2F. Manifest Route (All Paths) - -Delete `.well-known/farcaster.json` route: -- `app/.well-known/farcaster.json/route.ts` -- `public/.well-known/farcaster.json` -- `api/farcaster-manifest.ts` or similar helpers - -If the `.well-known` directory becomes empty, delete it. - -### 2G. Meta Tags (All Paths) - -In root layout or metadata files, remove: -- `` tags with `property="fc:miniapp*"` or `property="fc:frame*"` -- `Metadata.other` entries that only exist for Farcaster tags -- `generateMetadata` logic that only produces Mini App metadata - -### 2H. Dependencies - -**All paths — remove:** -- `@farcaster/miniapp-sdk`, `@farcaster/miniapp-wagmi-connector`, `@farcaster/miniapp-core` -- `@farcaster/frame-sdk`, `@farcaster/frame-wagmi-connector` -- `@farcaster/quick-auth`, `@farcaster/auth-kit` - -**Path A — also remove:** -- `@neynar/nodejs-sdk`, `@neynar/react` -- any other Neynar packages or helpers - -**Path B — remove by default:** -- `@neynar/nodejs-sdk`, `@neynar/react`, and Neynar helpers unless they remain inside the intentionally isolated Farcaster surface - -**All paths — add only if truly needed:** -- `siwe` if SIWE auth is introduced and not already present - -Do not add new Neynar packages as part of the default conversion. - -### 2I. Farcaster-Specific Routes & Components - -**Path A:** -- Delete `app/farcaster/`, `pages/farcaster/`, and Farcaster-only components entirely -- Delete Farcaster-only API routes such as `/api/farcaster/*` and `/api/neynar/*` -- Remove any navigation links that point to deleted routes - -**Path B:** -- Keep the main app route tree clean -- Move preserved Farcaster UI behind one dedicated route/page if it is not already isolated -- Prefer names like `app/farcaster/` or `app/social/` over spreading Farcaster logic throughout generic shared pages -- Remove any component that has no purpose outside that isolated surface -- Keep any remaining Neynar usage, if any, confined to that isolated route/page and its server helpers only - ---- - -## Phase 3: Cleanup - -### 3A. Update `package.json` - -**All paths — remove Mini App packages:** -- `@farcaster/miniapp-sdk`, `@farcaster/miniapp-wagmi-connector`, `@farcaster/miniapp-core` -- `@farcaster/frame-sdk`, `@farcaster/frame-wagmi-connector` -- `@farcaster/quick-auth`, `@farcaster/auth-kit` - -**Path A — also remove:** -- all `@neynar/*` packages - -**Path B — remove unless still isolated and intentionally preserved:** -- `@neynar/*` - -**All paths — add if introduced:** -- `siwe` - -### 3B. Environment Variables - -**Path A — remove from all `.env*` files:** -- `NEYNAR_API_KEY` -- `NEXT_PUBLIC_NEYNAR_CLIENT_ID` -- `FARCASTER_*` -- `FC_*` -- `NEXT_PUBLIC_FC_*` -- `NEXT_PUBLIC_FARCASTER_*` - -**Path B — remove from the main app by default:** -- `FARCASTER_*` -- `FC_*` -- `NEXT_PUBLIC_FC_*` -- `NEXT_PUBLIC_FARCASTER_*` - -Only keep `NEYNAR_*` vars if the isolated surface explicitly still depends on existing Neynar integration. - -Also update env validation schemas (`env.ts`, `env.mjs`, zod schemas, etc.). - -### 3C. Dead Code Cleanup - -- Remove unused imports from modified files -- Remove unused Farcaster types and helper functions -- Remove empty import statements -- Remove dead hooks or API wrappers that only existed for the Mini App SDK - -### 3D. Tailwind Colors - -If `tailwind.config.ts` or `tailwind.config.js` includes Farcaster brand colors such as `farcaster: "#8B5CF6"`, remove them unless that branding is intentionally kept inside an isolated Farcaster-only surface. - -### 3E. Install Dependencies - -Tell the user: - -```bash -npm install -``` - ---- - -## Phase 4: Verification - -### 4A. Search for Remaining References - -**All paths — search for:** -``` -@farcaster -farcasterMiniApp -miniAppConnector -sdk.actions -sdk.quickAuth -sdk.context -fc:miniapp -fc:frame -``` - -**Path A — also search for:** -``` -@neynar -NEYNAR_API_KEY -NEXT_PUBLIC_NEYNAR_CLIENT_ID -api.neynar.com -``` - -**Path B — if Neynar was intentionally preserved:** -- verify that remaining `@neynar` imports and env vars exist only inside the isolated Farcaster surface and its server helpers - -Report any source matches, ignoring `node_modules`, lock files, and git history. - -### 4B. Type Check - -```bash -npx tsc --noEmit -``` - -Report and fix type errors. - -### 4C. Build Check - -```bash -npm run build -``` - -Report and fix build errors. - -### 4D. Conversion Summary - -``` -## Conversion Complete — Path [X]: [Name] - -**Files modified:** [count] -**Files deleted:** [count] -**Files created:** [count, if any] -**Packages removed:** [list] -**Packages added:** [list, if any] - -### What was done -- [x] Removed Farcaster Mini App wagmi connector -- [x] Stubbed or removed Mini App provider/context -- [x] Replaced Mini App auth with normal web auth or removed it -- [x] Removed or replaced SDK action calls -- [x] Deleted manifest route -- [x] Removed Farcaster meta tags -- [x] Cleaned up dependencies and env vars -``` - -**Path B summary additionally includes:** -``` -- [x] Kept the main app as a normal web app -- [x] Confined remaining Farcaster functionality to a dedicated route/page -- [x] Preferred read-only Farcaster data where possible -- [x] Removed Farcaster host/runtime coupling from shared app infrastructure -``` - -**All paths end with:** -``` -### Manual steps -- [ ] Run `npm install` -- [ ] Test wallet connection flow -- [ ] If FID migration is needed, migrate from FID-based identity to wallet address -- [ ] If Path B preserves a Farcaster-only area, verify it stays isolated from the main app shell - -### Verification -- TypeScript: [pass/fail] -- Build: [pass/fail] -- Remaining Farcaster references: [none / list] -``` - ---- - -## Edge Cases - -### No wagmi - -Skip Phase 2A. Do not introduce wagmi unless the app actually needs wallet connectivity. - -### No auth system - -Skip Phase 2C. Do not add SIWE unnecessarily. - -### `@farcaster/frame-sdk` (older) - -Treat identically to `@farcaster/miniapp-sdk`. - -### Monorepo - -Ask which workspace(s) to convert. Only modify those. - -### FID as database primary key - -Do not change schema automatically. Flag it and warn in Phase 4. - -### Conditional `isInMiniApp` branches - -Always keep the normal web branch and remove the Mini App branch. - -### Components with no non-Farcaster purpose - -Delete them entirely in `Path A`. In `Path B`, keep them only if they live inside the isolated Farcaster route/page. - -### Existing Neynar usage - -If the project already uses Neynar: -- remove it in `Path A` -- keep it only if it remains inside the isolated `Path B` surface -- do not add more Neynar usage than already exists unless the user explicitly requests it - -### Read-only is usually enough - -If the user says they want to "keep Farcaster stuff," bias toward: -- profile links -- read-only profile or cast display -- a dedicated social page - -Do not assume they want write access, notifications, or host/runtime behavior. - -### Quiz ambiguity - -If the scan and quiz conflict, point it out and ask the user to confirm the smaller keep-surface first. - ---- - -## Security - -- **Validate wallet setup** — ensure `coinbaseWallet` or the chosen wallet connector is configured correctly -- **FID-based identity** — requires manual database migration if used as a primary key -- **SIWE verification** — verify signatures server-side before trusting them -- **Preserved isolated surface** — do not let a Farcaster-only route/page leak host/runtime assumptions into the main app shell -- **Existing Neynar usage** — keep API keys server-side only, and only if that isolated surface still depends on them \ No newline at end of file diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/auth.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/auth.md deleted file mode 100644 index 8bb9bc3..0000000 --- a/skills/build-on-base/references/migrations/minikit-to-farcaster/auth.md +++ /dev/null @@ -1,48 +0,0 @@ -# Quick Auth Migration - -`useAuthenticate` → `sdk.quickAuth` - -## Client - -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -// Make authenticated request (recommended) -const res = await sdk.quickAuth.fetch('/api/auth'); - -// Or get token directly -const { token } = await sdk.quickAuth.getToken(); -``` - -## Server (Next.js) - -```bash -npm install @farcaster/quick-auth -``` - -```typescript -// app/api/auth/route.ts -import { createClient } from '@farcaster/quick-auth'; -import { NextRequest, NextResponse } from 'next/server'; - -const client = createClient(); - -export async function GET(request: NextRequest) { - const auth = request.headers.get('Authorization'); - if (!auth?.startsWith('Bearer ')) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - try { - const payload = await client.verifyJwt({ - token: auth.split(' ')[1], - domain: 'your-domain.com', - }); - return NextResponse.json({ fid: payload.sub }); - } catch { - return NextResponse.json({ error: 'Invalid token' }, { status: 401 }); - } -} -``` - -See [Farcaster Quick Auth docs](https://miniapps.farcaster.xyz/docs/sdk/quick-auth). diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/dependencies.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/dependencies.md deleted file mode 100644 index e897688..0000000 --- a/skills/build-on-base/references/migrations/minikit-to-farcaster/dependencies.md +++ /dev/null @@ -1,54 +0,0 @@ -# Dependencies - -## Requirements - -- Node.js 22.11.0+ -- Farcaster SDK 0.2.0+ (breaking changes from 0.1.x) - -## Quick Install - -```bash -npm uninstall @coinbase/onchainkit && \ -npm install @farcaster/miniapp-sdk @farcaster/miniapp-wagmi-connector wagmi viem @tanstack/react-query -``` - -## package.json - -```json -{ - "engines": { "node": ">=22.11.0" }, - "dependencies": { - "@farcaster/miniapp-sdk": "^0.3.0", - "@farcaster/miniapp-wagmi-connector": "^0.0.15", - "@tanstack/react-query": "^5.50.0", - "viem": "^2.17.0", - "wagmi": "^2.12.0" - } -} -``` - -## Check Version - -```bash -npm list @farcaster/miniapp-sdk -``` - -## Common Errors - -**Peer dependency conflict:** -```bash -npm install @farcaster/miniapp-sdk@^0.3.0 @farcaster/miniapp-wagmi-connector@^0.0.15 -``` - -**Node.js too old:** -```bash -nvm install 22 && nvm use 22 -``` - -## Optional: Server Auth - -```bash -npm install @farcaster/quick-auth -``` - -See [AUTH.md](AUTH.md) for server-side token verification. diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/examples.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/examples.md deleted file mode 100644 index 0cb6ffe..0000000 --- a/skills/build-on-base/references/migrations/minikit-to-farcaster/examples.md +++ /dev/null @@ -1,202 +0,0 @@ -# Conversion Examples - -## Contents -- Social actions -- User profile -- App initialization -- Primary button -- Sign-in flow -- Safe area insets - ---- - -## Social Actions - -**Before:** -```typescript -import { useClose, useOpenUrl, useViewProfile } from '@coinbase/onchainkit/minikit'; - -function Actions({ fid }) { - const close = useClose(); - const viewProfile = useViewProfile(); - return ( - <> - - - - ); -} -``` - -**After:** -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function Actions({ fid }) { - return ( - <> - - - - ); -} -``` - ---- - -## User Profile - -**Before:** -```typescript -const { context } = useMiniKit(); -const { fid, username } = context?.user ?? {}; -``` - -**After:** -```typescript -const [user, setUser] = useState(null); - -useEffect(() => { - const load = async () => { - const ctx = await sdk.context; - setUser(ctx?.user); - }; - load(); -}, []); - -const { fid, username } = user ?? {}; -``` - -Or use FrameProvider (see [PROVIDER.md](PROVIDER.md)): -```typescript -import { useFrameContext } from '@/components/providers/FrameProvider'; - -const frameContext = useFrameContext(); -const { fid, username } = frameContext?.context?.user ?? {}; -``` - ---- - -## App Initialization - -**Before:** -```typescript -const { setFrameReady, context, isSDKLoaded } = useMiniKit(); - -useEffect(() => { - if (isSDKLoaded) setFrameReady(); -}, [isSDKLoaded]); -``` - -**After:** -```typescript -const [ready, setReady] = useState(false); -const [context, setContext] = useState(null); - -useEffect(() => { - const init = async () => { - const inMiniApp = await sdk.isInMiniApp(); - if (inMiniApp) { - const ctx = await sdk.context; - setContext(ctx); - await sdk.actions.ready(); - } - setReady(true); - }; - init(); -}, []); -``` - ---- - -## Primary Button (Breaking Change) - -**Before:** -```typescript -usePrimaryButton( - { text: `Clicked ${count}`, disabled: false }, - () => setCount(c => c + 1) -); -``` - -**After (no callback support):** -```typescript -useEffect(() => { - const setup = async () => { - await sdk.actions.setPrimaryButton({ - text: "Action", - disabled: false, - hidden: false, - loading: false - }); - }; - setup(); -}, []); - -// Use regular React buttons for click handling -``` - ---- - -## Sign-In Flow - -**Before:** -```typescript -const { signIn } = useAuthenticate(); -const result = await signIn({ nonce }); -if (result === false) { /* failed */ } -``` - -**After (Quick Auth):** -```typescript -const { token } = await sdk.quickAuth.getToken(); -await fetch('/api/auth', { - headers: { Authorization: `Bearer ${token}` } -}); -``` - -Or use authenticated fetch: -```typescript -const res = await sdk.quickAuth.fetch('/api/auth'); -``` - ---- - -## Safe Area Insets - -**Before:** -```typescript -const { context } = useMiniKit(); -const insets = context?.client?.safeAreaInsets; -``` - -**After:** -```typescript -const [insets, setInsets] = useState(null); - -useEffect(() => { - const load = async () => { - const ctx = await sdk.context; - setInsets(ctx?.client?.safeAreaInsets); - }; - load(); -}, []); -``` - ---- - -## Add Mini App - -**Before:** -```typescript -const addFrame = useAddFrame(); -const result = await addFrame(); -``` - -**After:** -```typescript -const result = await sdk.actions.addMiniApp(); -if (result) { - saveTokenToServer(result.url, result.token); -} -``` diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/manifest.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/manifest.md deleted file mode 100644 index f2c5f17..0000000 --- a/skills/build-on-base/references/migrations/minikit-to-farcaster/manifest.md +++ /dev/null @@ -1,50 +0,0 @@ -# Manifest Migration - -Change root key from `frame` to `miniapp` in `/.well-known/farcaster.json`. - -## Before - -```typescript -return Response.json({ - accountAssociation: { ... }, - frame: { - version: "1", - name: "My App", - ... - } -}); -``` - -## After - -```typescript -return Response.json({ - accountAssociation: { ... }, - miniapp: { - version: "1", - name: "My App", - homeUrl: "https://yourapp.com", - iconUrl: "https://yourapp.com/icon.png", - splashImageUrl: "https://yourapp.com/splash.png", - splashBackgroundColor: "#000000", - // Optional - subtitle: "Short tagline", - description: "Longer description", - primaryCategory: "utilities", - webhookUrl: "https://yourapp.com/api/webhook", - } -}); -``` - -## Required Fields - -- `version`: Always `"1"` -- `name`: App name (max 32 chars) -- `homeUrl`: Main app URL -- `iconUrl`: 1:1 ratio, min 200x200 -- `splashImageUrl`: 1:1 ratio -- `splashBackgroundColor`: Hex color - -## Categories - -`games` | `social` | `finance` | `utilities` | `productivity` | `entertainment` | `news` | `shopping` | `health` | `education` diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/mapping.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/mapping.md deleted file mode 100644 index a1e24a3..0000000 --- a/skills/build-on-base/references/migrations/minikit-to-farcaster/mapping.md +++ /dev/null @@ -1,452 +0,0 @@ -# MiniKit to Farcaster SDK Mapping - -Complete reference for converting each MiniKit hook to Farcaster SDK calls. - -## Table of Contents - -- [Import Changes](#import-changes) -- [useMiniKit](#useminikit) -- [useClose](#useclose) -- [useOpenUrl](#useopenurl) -- [useViewProfile](#useviewprofile) -- [useViewCast](#useviewcast) -- [useComposeCast](#usecomposecast) -- [useAddFrame](#useaddframe) -- [useAuthenticate](#useauthenticate) -- [useNotification](#usenotification) - ---- - -## Import Changes - -### Before (MiniKit) -```typescript -import { - useMiniKit, - useClose, - useOpenUrl, - useViewProfile, - useViewCast, - useComposeCast, - useAddFrame, - useAuthenticate -} from '@coinbase/onchainkit/minikit'; -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; -``` - -**Note**: All hooks become direct SDK method calls. No React hooks needed. - ---- - -## useMiniKit - -The main hook that provides context and ready signal. - -### Before (MiniKit) -```typescript -import { useMiniKit } from '@coinbase/onchainkit/minikit'; - -function App() { - const { setFrameReady, isFrameReady, context } = useMiniKit(); - - useEffect(() => { - if (!isFrameReady) { - setFrameReady(); - } - }, [setFrameReady, isFrameReady]); - - // Access user info - const userFid = context?.user?.fid; - const username = context?.user?.username; - - return
Hello {username}
; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function App() { - const [isReady, setIsReady] = useState(false); - const [context, setContext] = useState(null); - - useEffect(() => { - const init = async () => { - // Get context first (must await - it's a Promise) - const context = await sdk.context; - setContext(context); - - // Signal ready to hide splash screen - await sdk.actions.ready(); - setIsReady(true); - }; - init(); - }, []); - - // Access user info - const userFid = context?.user?.fid; - const username = context?.user?.username; - - return
Hello {username}
; -} -``` - -### Context Structure (Same for Both) -```typescript -type MiniAppContext = { - user: { - fid: number; - username?: string; - displayName?: string; - pfpUrl?: string; - }; - client: { - clientFid: number; - added: boolean; - notificationDetails?: { - url: string; - token: string; - }; - safeAreaInsets?: { - top: number; - bottom: number; - left: number; - right: number; - }; - }; - location?: LocationContext; -}; -``` - ---- - -## useClose - -Closes the mini app. - -### Before (MiniKit) -```typescript -import { useClose } from '@coinbase/onchainkit/minikit'; - -function CloseButton() { - const close = useClose(); - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function CloseButton() { - const handleClose = async () => { - await sdk.actions.close(); - }; - - return ; -} -``` - ---- - -## useOpenUrl - -Opens an external URL in the browser. - -### Before (MiniKit) -```typescript -import { useOpenUrl } from '@coinbase/onchainkit/minikit'; - -function LinkButton() { - const openUrl = useOpenUrl(); - - const handleClick = () => { - openUrl('https://example.com'); - }; - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function LinkButton() { - const handleClick = async () => { - await sdk.actions.openUrl('https://example.com'); - }; - - return ; -} -``` - ---- - -## useViewProfile - -Opens a Farcaster user's profile. - -### Before (MiniKit) -```typescript -import { useViewProfile } from '@coinbase/onchainkit/minikit'; - -function ProfileLink({ fid }) { - const viewProfile = useViewProfile(); - - const handleClick = () => { - viewProfile(fid); - }; - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function ProfileLink({ fid }) { - const handleClick = async () => { - await sdk.actions.viewProfile({ fid }); - }; - - return ; -} -``` - -**Note**: The SDK requires an object with `fid` property, not just the fid directly. - ---- - -## useViewCast - -Opens a specific cast. - -### Before (MiniKit) -```typescript -import { useViewCast } from '@coinbase/onchainkit/minikit'; - -function CastLink({ hash }) { - const viewCast = useViewCast(); - - const handleClick = () => { - viewCast(hash); - }; - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function CastLink({ hash }) { - const handleClick = async () => { - await sdk.actions.viewCast({ hash }); - }; - - return ; -} -``` - ---- - -## useComposeCast - -Opens the cast composer with prefilled content. - -### Before (MiniKit) -```typescript -import { useComposeCast } from '@coinbase/onchainkit/minikit'; - -function ShareButton() { - const { composeCast } = useComposeCast(); - - const handleShare = () => { - composeCast({ - text: 'Check out this app!', - embeds: ['https://myapp.com'] - }); - }; - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function ShareButton() { - const handleShare = async () => { - const result = await sdk.actions.composeCast({ - text: 'Check out this app!', - embeds: ['https://myapp.com'] - }); - - // result.cast contains the posted cast if successful - if (result?.cast) { - console.log('Cast posted:', result.cast.hash); - } - }; - - return ; -} -``` - -### Full Options -```typescript -await sdk.actions.composeCast({ - text: string; // Suggested text (user can modify) - embeds?: string[]; // URLs to embed (max 2) - parent?: { // Reply to a cast - hash: string; - }; - channelKey?: string; // Post to a channel - close?: boolean; // Close app after posting -}); -``` - ---- - -## useAddFrame - -Prompts user to add/save the mini app. - -### Before (MiniKit) -```typescript -import { useAddFrame } from '@coinbase/onchainkit/minikit'; - -function SaveButton() { - const addFrame = useAddFrame(); - - const handleAdd = async () => { - const result = await addFrame(); - if (result) { - console.log('Added! Token:', result.token); - // Save result.url and result.token for notifications - } - }; - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function SaveButton() { - const handleAdd = async () => { - const result = await sdk.actions.addMiniApp(); - if (result) { - console.log('Added! Token:', result.token); - // Save result.url and result.token for notifications - } - }; - - return ; -} -``` - ---- - -## useAuthenticate - -Authenticates the user with Sign In with Farcaster. - -### Before (MiniKit) -```typescript -import { useAuthenticate } from '@coinbase/onchainkit/minikit'; - -function AuthButton() { - const authenticate = useAuthenticate(); - - const handleAuth = async () => { - const result = await authenticate(); - if (result) { - // Send result to your backend for verification - await verifyOnBackend(result); - } - }; - - return ; -} -``` - -### After (Farcaster SDK) -```typescript -import { sdk } from '@farcaster/miniapp-sdk'; - -function AuthButton() { - const handleAuth = async () => { - const result = await sdk.actions.signIn({ - // Optional: specify nonce for verification - nonce: 'your-random-nonce' - }); - - if (result) { - // result contains signature for verification - await verifyOnBackend(result); - } - }; - - return ; -} -``` - -**Important**: Always verify the signature on your backend for security-critical operations. - ---- - -## useNotification - -Notifications require server-side implementation. See [NOTIFICATIONS.md](NOTIFICATIONS.md) for details. - -### Before (MiniKit) -```typescript -import { useNotification } from '@coinbase/onchainkit/minikit'; - -function NotifyButton() { - const sendNotification = useNotification(); - - const handleNotify = async () => { - await sendNotification({ - title: 'Hello!', - body: 'You have a new message' - }); - }; - - return ; -} -``` - -### After (Farcaster SDK) -Notifications are sent via webhook from your server, not from the client. - -```typescript -// Client: Just trigger your backend -function NotifyButton() { - const handleNotify = async () => { - await fetch('/api/send-notification', { - method: 'POST', - body: JSON.stringify({ - title: 'Hello!', - body: 'You have a new message' - }) - }); - }; - - return ; -} -``` - -See [NOTIFICATIONS.md](NOTIFICATIONS.md) for server-side implementation. diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/overview.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/overview.md deleted file mode 100644 index 8acb2b2..0000000 --- a/skills/build-on-base/references/migrations/minikit-to-farcaster/overview.md +++ /dev/null @@ -1,89 +0,0 @@ -# MiniKit to Farcaster SDK - -Converts Mini Apps from MiniKit (OnchainKit) to native Farcaster SDK. - -## Breaking Changes (SDK v0.2.0+) - -1. `sdk.context` is a **Promise** — must await -2. `sdk.isInMiniApp()` accepts **no parameters** -3. `sdk.actions.setPrimaryButton()` has no onClick callback - -Check version: `npm list @farcaster/miniapp-sdk` - -## Quick Reference - -| MiniKit | Farcaster SDK | Notes | -|---------|---------------|-------| -| `useMiniKit().setFrameReady()` | `await sdk.actions.ready()` | | -| `useMiniKit().context` | `await sdk.context` | **Async** | -| `useMiniKit().isSDKLoaded` | `await sdk.isInMiniApp()` | No params | -| `useClose()` | `await sdk.actions.close()` | | -| `useOpenUrl(url)` | `await sdk.actions.openUrl(url)` | | -| `useViewProfile(fid)` | `await sdk.actions.viewProfile({ fid })` | | -| `useViewCast(hash)` | `await sdk.actions.viewCast({ hash })` | | -| `useComposeCast()` | `await sdk.actions.composeCast({ text, embeds })` | | -| `useAddFrame()` | `await sdk.actions.addMiniApp()` | | -| `usePrimaryButton(opts, cb)` | `await sdk.actions.setPrimaryButton(opts)` | No callback | -| `useAuthenticate()` | `sdk.quickAuth.getToken()` | See [auth.md](auth.md) | - -## Context Access Pattern - -```typescript -// WRONG -const fid = sdk.context?.user?.fid; - -// CORRECT -const context = await sdk.context; -const fid = context?.user?.fid; -``` - -In React components, use state: - -```typescript -const [context, setContext] = useState(null); - -useEffect(() => { - const load = async () => { - const ctx = await sdk.context; - setContext(ctx); - }; - load(); -}, []); -``` - -## Conversion Workflow - -1. Verify Node.js >= 22.11.0 -2. Update dependencies — see [dependencies.md](dependencies.md) -3. Replace imports: `@coinbase/onchainkit/minikit` → `@farcaster/miniapp-sdk` -4. Convert hooks using reference above -5. Add FrameProvider — see [provider.md](provider.md) -6. Update manifest: `frame` → `miniapp` — see [manifest.md](manifest.md) - -## Common Errors - -**"Property 'user' does not exist on type 'Promise'"** -→ Await `sdk.context` before accessing properties - -**"Expected 0 arguments, but got 1"** -→ Remove parameters from `sdk.isInMiniApp()` - -**Context is null in components** -→ Ensure FrameProvider is in your provider chain - -## References - -- [mapping.md](mapping.md) — Complete hook-by-hook conversion reference -- [examples.md](examples.md) — Before/after code examples -- [provider.md](provider.md) — Provider setup with FrameProvider -- [pitfalls.md](pitfalls.md) — Common errors and solutions -- [dependencies.md](dependencies.md) — Package updates -- [auth.md](auth.md) — Quick Auth migration -- [manifest.md](manifest.md) — farcaster.json changes - -## Helper Scripts - -Two Python scripts are bundled at `scripts/` (skill root) to automate analysis and validation: - -- **`scripts/analyze_project.py `** — Scans all source files and reports every MiniKit import, hook usage, and provider location. Run before starting conversion to understand blast radius. -- **`scripts/validate_conversion.py `** — Validates the converted project: no remaining MiniKit imports/hooks/providers, Farcaster SDK wired correctly, `package.json` updated, manifest uses `miniapp` key. Exit code 0 = pass, 1 = errors found. diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/pitfalls.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/pitfalls.md deleted file mode 100644 index eacd8d6..0000000 --- a/skills/build-on-base/references/migrations/minikit-to-farcaster/pitfalls.md +++ /dev/null @@ -1,225 +0,0 @@ -# Common Pitfalls & Errors - -## Contents -- Type errors (sdk.context, isInMiniApp, setPrimaryButton) -- Runtime issues (context null, detection fails) -- React patterns (useEffect with async) -- Sign-in migration - ---- - -## Type Errors - -### "Property 'user' does not exist on type 'Promise'" - -Accessing `sdk.context` without awaiting. - -```typescript -// WRONG -const fid = sdk.context?.user?.fid; - -// CORRECT -const context = await sdk.context; -const fid = context?.user?.fid; -``` - -### "Expected 0 arguments, but got 1" - -Passing parameters to `sdk.isInMiniApp()`. - -```typescript -// WRONG -await sdk.isInMiniApp({ timeoutMs: 500 }); - -// CORRECT -await sdk.isInMiniApp(); -``` - -Custom timeout workaround: -```typescript -const checkWithTimeout = async (ms = 5000) => { - try { - return await Promise.race([ - sdk.isInMiniApp(), - new Promise((_, r) => setTimeout(() => r(new Error('Timeout')), ms)) - ]); - } catch { - return false; - } -}; -``` - -### "Type 'Promise' is not assignable..." - -Assigning `sdk.context` to state without awaiting. - -```typescript -// WRONG -const context = sdk.context; -setFrameContext({ context, isInMiniApp: true }); - -// CORRECT -const context = await sdk.context; -setFrameContext({ context, isInMiniApp: true }); -``` - -### "'onClick' does not exist in type 'SetPrimaryButtonOptions'" - -`setPrimaryButton` no longer supports callbacks. - -```typescript -// WRONG (MiniKit pattern) -usePrimaryButton( - { text: "Click" }, - () => handleClick() -); - -// CORRECT - state only, no callback -await sdk.actions.setPrimaryButton({ - text: "Click", - disabled: false, - hidden: false, - loading: false -}); -``` - -For click handling, use regular React buttons. - ---- - -## Runtime Issues - -### isInMiniApp returns false unexpectedly - -Possible causes: -- Not running in iframe or React Native WebView -- Server-side rendering (detection is client-side only) -- Missing `'use client'` directive - -### Context is null in components - -FrameProvider not in provider chain. - -```typescript -// WRONG -export function Providers({ children }) { - return {children}; -} - -// CORRECT -export function Providers({ children }) { - return ( - - {children} - - ); -} -``` - -### Context is null even when isInMiniApp is true - -Not awaiting `sdk.context`: - -```typescript -// WRONG -const context = sdk.context; // Promise, not data - -// CORRECT -const context = await sdk.context; -``` - ---- - -## React Patterns - -### Async useEffect - -```typescript -// WRONG - returns Promise -useEffect(async () => { - await sdk.actions.ready(); -}, []); - -// CORRECT - wrap in function -useEffect(() => { - const init = async () => { - await sdk.actions.ready(); - }; - init(); -}, []); -``` - -### Loading context in components - -```typescript -function MyComponent() { - const [context, setContext] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const load = async () => { - try { - const isInMiniApp = await sdk.isInMiniApp(); - if (isInMiniApp) { - const ctx = await sdk.context; - setContext(ctx); - } - } finally { - setLoading(false); - } - }; - load(); - }, []); - - if (loading) return null; - return
{context?.user?.fid}
; -} -``` - ---- - -## Sign-In Migration - -### "This comparison appears to be unintentional..." - -`signIn` returns `SignInResult`, not boolean. - -```typescript -// WRONG (MiniKit pattern) -const result = await signIn({ nonce }); -if (result === false) { ... } - -// CORRECT -const result = await sdk.actions.signIn({ nonce }); -if (!result) { - // Sign-in cancelled or failed -} -``` - -For SDK v0.2.0+, prefer Quick Auth: - -```typescript -const { token } = await sdk.quickAuth.getToken(); -// Or use authenticated fetch -const res = await sdk.quickAuth.fetch('/api/auth'); -``` - ---- - -## Validation Commands - -After conversion, verify: - -```bash -# No MiniKit imports remaining -grep -r "@coinbase/onchainkit/minikit" src/ - -# Check sdk.context usage (should be awaited) -grep -r "sdk\.context" src/ - -# Check isInMiniApp calls (no parameters) -grep -r "isInMiniApp(" src/ - -# Build and type check -npm run build && npx tsc --noEmit -``` diff --git a/skills/build-on-base/references/migrations/minikit-to-farcaster/provider.md b/skills/build-on-base/references/migrations/minikit-to-farcaster/provider.md deleted file mode 100644 index df9b597..0000000 --- a/skills/build-on-base/references/migrations/minikit-to-farcaster/provider.md +++ /dev/null @@ -1,170 +0,0 @@ -# Provider Migration - -Remove `MiniKitProvider`, add FrameProvider and Wagmi setup. - -## Contents -- FrameProvider setup -- Wagmi provider setup -- Combined providers -- Usage in components - ---- - -## Step 1: Create FrameProvider - -`src/components/providers/FrameProvider.tsx`: - -```typescript -'use client' - -import { sdk } from '@farcaster/miniapp-sdk'; -import { createContext, useContext, useEffect, useState, ReactNode } from "react"; - -type FrameContextType = { - context: any; - isInMiniApp: boolean; -} | null; - -const FrameContext = createContext(null); - -export const useFrameContext = () => useContext(FrameContext); - -export default function FrameProvider({ children }: { children: ReactNode }) { - const [frameContext, setFrameContext] = useState(null); - - useEffect(() => { - const init = async () => { - try { - // No parameters in v0.2.0+ - const isInMiniApp = await sdk.isInMiniApp(); - - if (isInMiniApp) { - // Must await - context is a Promise - const context = await sdk.context; - setFrameContext({ context, isInMiniApp: true }); - } else { - setFrameContext({ context: null, isInMiniApp: false }); - } - } catch (error) { - console.error('FrameProvider init error:', error); - setFrameContext({ context: null, isInMiniApp: false }); - } - }; - init(); - }, []); - - return ( - - {children} - - ); -} -``` - ---- - -## Step 2: Create Wagmi Provider - -`src/components/providers/WagmiProvider.tsx`: - -```typescript -'use client' - -import { createConfig, http, WagmiProvider as WagmiBase } from 'wagmi'; -import { base } from 'wagmi/chains'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { farcasterMiniApp } from '@farcaster/miniapp-wagmi-connector'; -import { ReactNode, useState } from 'react'; - -const config = createConfig({ - chains: [base], - transports: { [base.id]: http() }, - connectors: [farcasterMiniApp()], -}); - -export default function WagmiProvider({ children }: { children: ReactNode }) { - const [queryClient] = useState(() => new QueryClient()); - - return ( - - - {children} - - - ); -} -``` - ---- - -## Step 3: Combine Providers - -`src/app/providers.tsx`: - -```typescript -'use client' - -import { ReactNode } from 'react'; -import FrameProvider from '@/components/providers/FrameProvider'; -import WagmiProvider from '@/components/providers/WagmiProvider'; - -export function Providers({ children }: { children: ReactNode }) { - return ( - - - {children} - - - ); -} -``` - ---- - -## Step 4: Use in Layout - -`src/app/layout.tsx`: - -```typescript -import { Providers } from './providers'; - -export default function RootLayout({ children }) { - return ( - - - {children} - - - ); -} -``` - ---- - -## Using the Context - -```typescript -import { useFrameContext } from '@/components/providers/FrameProvider'; - -function MyComponent() { - const frameContext = useFrameContext(); - - if (!frameContext) return
Loading...
; - if (!frameContext.isInMiniApp) return
Open in Farcaster
; - - return
Welcome {frameContext.context?.user?.displayName}
; -} -``` - ---- - -## Remove Old Imports - -```typescript -// Delete these -import { MiniKitProvider } from '@coinbase/onchainkit/minikit'; -import '@coinbase/onchainkit/styles.css'; - -// Delete from .env -NEXT_PUBLIC_ONCHAINKIT_API_KEY=xxx -``` diff --git a/skills/build-on-base/references/migrations/onchainkit/overview.md b/skills/build-on-base/references/migrations/onchainkit/overview.md deleted file mode 100644 index ed59077..0000000 --- a/skills/build-on-base/references/migrations/onchainkit/overview.md +++ /dev/null @@ -1,131 +0,0 @@ -# OnchainKit Migration - -Migrate apps from `@coinbase/onchainkit` to standalone `wagmi`/`viem` components with zero OnchainKit dependency. - -## Before Starting - -Create a `mistakes.md` file in the project root: - -```markdown -# Migration Mistakes & Learnings - -Track errors, fixes, and lessons learned during OnchainKit migration. - -## Errors - -## Lessons Learned -``` - -Append every error, fix, and lesson to this file throughout the migration. This file improves the skill over time. - -## Migration Workflow - -Migrations MUST happen in this order. Do not skip steps. - -### Step 1: Detection - -Scan the project to understand current OnchainKit usage: - -1. Search all files for `@coinbase/onchainkit` imports -2. Identify which components are used: - - **Provider**: `OnchainKitProvider` (always present if using OnchainKit) - - **Wallet**: `Wallet`, `ConnectWallet`, `WalletDropdown`, `WalletModal`, `Connected` - - **Transaction**: `Transaction`, `TransactionButton`, `TransactionStatus` - - **Other**: `Identity`, `Avatar`, `Name`, `Address`, `Swap`, `Checkout`, etc. -3. Check `package.json` for existing `wagmi`, `viem`, `@tanstack/react-query` dependencies -4. Identify the project's styling approach (Tailwind, CSS Modules, styled-components, etc.) -5. Report findings to the user before proceeding - -### Step 2: Provider Migration (always first) - -Read [provider.md](provider.md) for detailed instructions and code. - -Summary: -1. Ensure `wagmi`, `viem`, and `@tanstack/react-query` are installed -2. Create `wagmi-config.ts` with chain and connector configuration -3. Replace `OnchainKitProvider` with `WagmiProvider` + `QueryClientProvider` -4. Remove `@coinbase/onchainkit/styles.css` import -5. Remove any `SafeArea` or MiniKit imports from layout files - -**Validation gate**: Run `npm run build` (or the project's build command). Must pass before continuing. If it fails, fix errors and document them in `mistakes.md`. - -### Step 3: Wallet Migration (after provider) - -Read [wallet.md](wallet.md) for detailed instructions and code. - -Summary: -1. Create a `WalletConnect` component using wagmi hooks (`useAccount`, `useConnect`, `useDisconnect`) -2. Component includes a modal with wallet options: Base Account, Coinbase Wallet, MetaMask -3. Shows truncated address + disconnect button when connected -4. Replace all OnchainKit wallet imports and component usage - -**Validation gate**: Run `npm run build`. Must pass before continuing. Document any errors in `mistakes.md`. - -### Step 4: Transaction Migration (after wallet) - -Read [transaction.md](transaction.md) for detailed instructions and code. - -Summary: -1. Check the `chainId` prop on existing `` components -- add any missing chains to `wagmi-config.ts` -2. Create a `TransactionForm` component using wagmi hooks (`useWriteContract`, `useWaitForTransactionReceipt`, `useSwitchChain`) -3. Component handles the full lifecycle: idle, pending wallet confirmation, confirming on-chain, success, error -4. Replace all OnchainKit transaction imports (`Transaction`, `TransactionButton`, `TransactionStatus`, `TransactionSponsor`, etc.) -5. Update the `calls` array format -- use `address`, `abi`, `functionName`, `args` with proper `as const` typing -6. Map `onStatus` callback to the new lifecycle status names (init, pending, confirmed, success, error) - -**Validation gate**: Run `npm run build`. Must pass before continuing. Document any errors in `mistakes.md`. - -### Step 5: Cleanup - -1. Search for any remaining `@coinbase/onchainkit` imports -- there should be none -2. Optionally remove `@coinbase/onchainkit` from `package.json` dependencies -3. Run final `npm run build` to verify everything works -4. Update `mistakes.md` with final lessons learned - -## Troubleshooting - -See [troubleshooting.md](troubleshooting.md) for common build and runtime errors. - -### Build fails after provider migration -- **Missing dependencies**: Ensure `wagmi`, `viem`, `@tanstack/react-query` are installed -- **Type errors with wagmi config**: Check that `chains` array has at least one chain and `transports` has a matching entry -- **"use client" missing**: Both the provider and wallet components must have `"use client"` directive - -### MetaMask SDK react-native warning -- The warning `Can't resolve '@react-native-async-storage/async-storage'` is harmless in web builds. It comes from MetaMask SDK and does not affect functionality. - -### Wallet won't connect -- Verify the wagmi config has the correct connectors configured -- Check that `WagmiProvider` wraps the component tree before any wallet hooks are used -- Ensure `QueryClientProvider` is inside `WagmiProvider` - -### Transaction receipt stuck in pending -- **Symptom**: Transaction hash appears, tx confirms on-chain, but UI stays stuck on "Transaction in progress..." forever -- **Cause**: `useWaitForTransactionReceipt` has no RPC to poll because the transaction's chain is missing from the wagmi config's `chains` + `transports` -- **Fix**: (1) Add the target chain to `wagmi-config.ts` chains array AND transports object. (2) Always pass `chainId` to `useWaitForTransactionReceipt({ hash, chainId })` so it polls the correct chain - -### Transaction targets wrong chain -- The `TransactionForm` auto-switches chains, but the target chain must exist in the wagmi config's `chains` array and `transports` object -- Common: add `baseSepolia` for testnet transactions (chainId 84532) - -### Next.js page export restrictions -- Next.js only allows specific named exports from page files. Exporting contract call arrays or ABI constants from a page file will cause a build error -- Fix: make them non-exported `const` declarations or move them to a separate module - -### ABI type errors after transaction migration -- When defining ABIs inline, use `as const` on the array for proper type inference -- Mark individual fields like `type: 'function' as const` and `stateMutability: 'nonpayable' as const` - -### Existing wagmi setup detected -- If the project already wraps with `WagmiProvider`, do NOT add another one -- Instead, just update the existing wagmi config to include the needed connectors -- OnchainKit's provider detects and defers to existing wagmi providers -- the standalone setup should too - -## Important Notes - -- Always use `wagmi` and `viem` directly. Never import from `@coinbase/onchainkit`. -- The `baseAccount` connector comes from `wagmi/connectors`, not from a separate package. -- `wagmi-config.ts` must include every chain the app transacts on. If the original OnchainKit `` used a specific chain, that chain must be in both `chains` and `transports`. Without it, `useWaitForTransactionReceipt` will hang forever. -- If the project uses Tailwind, use Tailwind classes for the components. If not, adapt to inline styles or the project's existing styling approach (e.g., CSS Modules). -- Do not export contract call arrays, ABI constants, or other non-page values from Next.js page files. Use non-exported constants or a separate module. -- Inspect the OnchainKit source code in `node_modules/@coinbase/onchainkit/src/` if you need to understand how a specific component works internally. diff --git a/skills/build-on-base/references/migrations/onchainkit/provider.md b/skills/build-on-base/references/migrations/onchainkit/provider.md deleted file mode 100644 index 0fc7adf..0000000 --- a/skills/build-on-base/references/migrations/onchainkit/provider.md +++ /dev/null @@ -1,193 +0,0 @@ -# Provider Migration: OnchainKitProvider to WagmiProvider - -Replace `OnchainKitProvider` from `@coinbase/onchainkit` with direct `WagmiProvider` + `QueryClientProvider` setup. - -## What OnchainKitProvider Does Internally - -OnchainKitProvider is a wrapper that: -1. Creates a wagmi config with `base` + `baseSepolia` chains -2. Uses `cookieStorage` for persistence and `ssr: true` -3. Default connector: `baseAccount()` from `wagmi/connectors` -4. Sets up CDP RPC URLs if `apiKey` is provided -5. Creates a default `QueryClient` from `@tanstack/react-query` -6. Applies theme/appearance settings via CSS custom properties - -The provider detects if `WagmiProvider` or `QueryClientProvider` already exist in the React tree and skips creating them if so. - -## Prerequisites - -Ensure these packages are installed. They are likely already present since OnchainKit depends on them: - -```bash -npm install wagmi viem @tanstack/react-query -``` - -If the project uses Tailwind CSS and it's not yet installed: - -```bash -npm install tailwindcss @tailwindcss/postcss postcss -``` - -## Step-by-Step Migration - -### 1. Create wagmi-config.ts - -Create a new file for the wagmi configuration. Place it alongside the existing provider file (typically `app/wagmi-config.ts`). - -```typescript -import { http, cookieStorage, createConfig, createStorage } from "wagmi"; -import { base } from "wagmi/chains"; -import { coinbaseWallet, metaMask } from "wagmi/connectors"; - -export const wagmiConfig = createConfig({ - chains: [base], - connectors: [ - coinbaseWallet({ appName: "My App", preference: "all" }), - metaMask(), - ], - storage: createStorage({ storage: cookieStorage }), - ssr: true, - transports: { - [base.id]: http(), - }, -}); -``` - -**Adapt based on the existing OnchainKitProvider config:** -- If `chain` prop was set to something other than `base`, use that chain instead -- If `apiKey` was set, you can use CDP RPC URLs: `http(\`https://api.developer.coinbase.com/rpc/v1/base/${apiKey}\`)` -- If `config.wallet.preference` was `"smartWalletOnly"`, adjust the coinbaseWallet connector accordingly -- Add additional chains to the `chains` array and `transports` object as needed - -### 2. Rewrite the Provider Component - -Replace the existing provider file (typically `rootProvider.tsx` or `providers.tsx`). - -**Before (OnchainKit):** -```typescript -"use client"; -import { ReactNode } from "react"; -import { base } from "wagmi/chains"; -import { OnchainKitProvider } from "@coinbase/onchainkit"; -import "@coinbase/onchainkit/styles.css"; - -export function RootProvider({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} -``` - -**After (wagmi/viem):** -```typescript -"use client"; -import { type ReactNode, useState } from "react"; -import { WagmiProvider } from "wagmi"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { wagmiConfig } from "./wagmi-config"; - -export function RootProvider({ children }: { children: ReactNode }) { - const [queryClient] = useState(() => new QueryClient()); - - return ( - - - {children} - - - ); -} -``` - -Key changes: -- Remove `@coinbase/onchainkit` import -- Remove `@coinbase/onchainkit/styles.css` import -- `QueryClient` is created with `useState` to avoid re-creation on re-renders -- `WagmiProvider` must wrap `QueryClientProvider` - -### 3. Update Layout File - -Remove any OnchainKit-specific imports from the layout: - -- Remove `SafeArea` from `@coinbase/onchainkit/minikit` -- Remove `minikitConfig` imports -- Remove MiniKit-related metadata generation -- Move `` inside `` (wagmi provider must be a client component, so it should wrap the content, not the `` tag) - -**Before:** -```typescript -import { SafeArea } from "@coinbase/onchainkit/minikit"; - -export default function RootLayout({ children }) { - return ( - - - - {children} - - - - ); -} -``` - -**After:** -```typescript -export default function RootLayout({ children }) { - return ( - - - {children} - - - ); -} -``` - -### 4. Verify - -Run the build command: -```bash -npm run build -``` - -Expected: Build succeeds. The MetaMask SDK warning about `@react-native-async-storage/async-storage` is harmless and can be ignored. - -## Edge Cases - -### Project already has a WagmiProvider -If the project wraps with its own `WagmiProvider` outside of OnchainKit, simply remove the `OnchainKitProvider` wrapper. Update the existing wagmi config to include any connectors that were configured via OnchainKit. - -### Project uses CDP API key for RPC -If the existing setup relied on `apiKey` for RPC access, add the CDP RPC URL to the transport: - -```typescript -transports: { - [base.id]: http(`https://api.developer.coinbase.com/rpc/v1/base/${process.env.NEXT_PUBLIC_ONCHAINKIT_API_KEY}`), -}, -``` - -### Project uses multiple chains -Add all needed chains to both the `chains` array and `transports` object: - -```typescript -import { base, baseSepolia } from "wagmi/chains"; - -export const wagmiConfig = createConfig({ - chains: [base, baseSepolia], - transports: { - [base.id]: http(), - [baseSepolia.id]: http(), - }, - // ...rest -}); -``` diff --git a/skills/build-on-base/references/migrations/onchainkit/transaction.md b/skills/build-on-base/references/migrations/onchainkit/transaction.md deleted file mode 100644 index 86c2089..0000000 --- a/skills/build-on-base/references/migrations/onchainkit/transaction.md +++ /dev/null @@ -1,528 +0,0 @@ -# Transaction Migration: OnchainKit Transaction to wagmi - -Replace OnchainKit's `Transaction`, `TransactionButton`, `TransactionStatus`, `TransactionSponsor`, and related components with a standalone `TransactionForm` component built on wagmi hooks. - -## What OnchainKit's Transaction Components Do - -OnchainKit provides a composable transaction system: -- `` -- container that manages the full transaction lifecycle, accepts `calls`, `chainId`, `onStatus` -- `` -- submits the transaction, shows status-dependent text (Transact/Confirm/Try again/View transaction) -- `` -- displays current transaction state with label and action -- `` -- text label ("Confirm in wallet", "Transaction in progress", "Successful", error message) -- `` -- link to block explorer or call status viewer -- `` -- shows "Zero transaction fee" when paymaster is configured -- `LifecycleStatus` type -- status object with `statusName` and `statusData` - -Internally, OnchainKit uses two submission paths: -- **Smart Wallet (batched):** `useSendCalls` (EIP-5792) for wallets with `atomicBatch` capability -- **EOA (single):** `useSendTransaction` with `encodeFunctionData` for standard wallets - -The replacement component uses `useWriteContract` which handles both EOA and smart wallet scenarios for single contract calls. - -## Prerequisites - -- Provider migration must be completed first (WagmiProvider + QueryClientProvider in the tree) -- Tailwind CSS installed (if not, install it or adapt styles) -- If the transaction targets a chain other than what's in the wagmi config, add that chain to `wagmi-config.ts` - -## Important: Chain Configuration - -OnchainKit's Transaction accepts a `chainId` prop and handles chain switching. The replacement does too, BUT the target chain must exist in the wagmi config's `chains` array and `transports` object. - -For example, if transactions target Base Sepolia (84532): - -```typescript -import { base, baseSepolia } from "wagmi/chains"; - -export const wagmiConfig = createConfig({ - chains: [base, baseSepolia], - transports: { - [base.id]: http(), - [baseSepolia.id]: http(), - }, - // ...rest -}); -``` - -## The TransactionForm Component - -Create `app/components/TransactionForm.tsx` (or wherever components live in the project): - -```typescript -"use client"; -import { useCallback, useEffect, useState } from "react"; -import { - useAccount, - useWriteContract, - useWaitForTransactionReceipt, - useSwitchChain, -} from "wagmi"; -import type { Abi, Address } from "viem"; - -type ContractCall = { - address: Address; - abi: Abi; - functionName: string; - args?: readonly unknown[]; - value?: bigint; -}; - -type LifecycleStatus = - | { statusName: "init"; statusData: null } - | { statusName: "pending"; statusData: null } - | { statusName: "confirmed"; statusData: { transactionHash: string } } - | { - statusName: "success"; - statusData: { transactionHash: string; blockNumber: bigint }; - } - | { statusName: "error"; statusData: { message: string } }; - -type TransactionFormProps = { - calls: ContractCall[]; - chainId?: number; - buttonText?: string; - onStatus?: (status: LifecycleStatus) => void; - disabled?: boolean; - className?: string; -}; - -export function TransactionForm({ - calls, - chainId, - buttonText = "Transact", - onStatus, - disabled = false, - className, -}: TransactionFormProps) { - const { isConnected, chainId: currentChainId } = useAccount(); - const { switchChainAsync } = useSwitchChain(); - - const [status, setStatus] = useState({ - statusName: "init", - statusData: null, - }); - - const updateStatus = useCallback( - (newStatus: LifecycleStatus) => { - setStatus(newStatus); - onStatus?.(newStatus); - }, - [onStatus] - ); - - const { - writeContract, - data: txHash, - isPending: isWritePending, - reset: resetWrite, - } = useWriteContract(); - - // CRITICAL: Always pass chainId so wagmi polls the correct chain's RPC. - // Without this, if the user's wallet is on a different chain than the - // transaction target, wagmi has no transport to poll and the receipt - // is never found -- the UI hangs in "pending" forever. - const { data: receipt, isLoading: isWaiting } = - useWaitForTransactionReceipt({ - hash: txHash, - chainId, - }); - - useEffect(() => { - if (isWritePending) { - updateStatus({ statusName: "pending", statusData: null }); - } - }, [isWritePending, updateStatus]); - - useEffect(() => { - if (txHash && !receipt) { - updateStatus({ - statusName: "confirmed", - statusData: { transactionHash: txHash }, - }); - } - }, [txHash, receipt, updateStatus]); - - useEffect(() => { - if (receipt) { - updateStatus({ - statusName: "success", - statusData: { - transactionHash: receipt.transactionHash, - blockNumber: receipt.blockNumber, - }, - }); - } - }, [receipt, updateStatus]); - - const handleSubmit = useCallback(async () => { - if (!isConnected || calls.length === 0) return; - - try { - if (chainId && currentChainId !== chainId) { - await switchChainAsync({ chainId }); - } - - const call = calls[0]; - writeContract( - { - address: call.address, - abi: call.abi, - functionName: call.functionName, - args: call.args ?? [], - value: call.value, - chainId, - }, - { - onError: (error) => { - const isUserRejection = - error.message?.includes("User rejected") || - error.message?.includes("User denied") || - error.message?.includes("Request denied"); - const message = isUserRejection - ? "Request denied." - : error.message || "Transaction failed"; - updateStatus({ statusName: "error", statusData: { message } }); - }, - } - ); - } catch (error) { - const message = - error instanceof Error ? error.message : "Transaction failed"; - updateStatus({ statusName: "error", statusData: { message } }); - } - }, [ - isConnected, - calls, - chainId, - currentChainId, - switchChainAsync, - writeContract, - updateStatus, - ]); - - const handleReset = useCallback(() => { - resetWrite(); - updateStatus({ statusName: "init", statusData: null }); - }, [resetWrite, updateStatus]); - - const isLoading = isWritePending || isWaiting; - - return ( -
- {status.statusName === "success" ? ( -
- - -
- ) : ( - - )} - - -
- ); -} - -function TransactionStatusDisplay({ - status, - chainId, -}: { - status: LifecycleStatus; - chainId?: number; -}) { - if (status.statusName === "init") return null; - - const explorerBase = - chainId === 84532 - ? "https://sepolia.basescan.org" - : "https://basescan.org"; - - return ( -
- {status.statusName === "pending" && ( -

- Confirm in wallet. -

- )} - {status.statusName === "confirmed" && ( -

- Transaction in progress... -

- )} - {status.statusName === "success" && ( -
-

Successful!

- - View on explorer - -
- )} - {status.statusName === "error" && ( -

- {status.statusData.message} -

- )} -
- ); -} -``` - -## Step-by-Step Replacement - -### 1. Check Chain Configuration - -Look at the `chainId` prop on the existing `` component. If it references a chain not in the wagmi config, add it: - -```typescript -// Common: Base Sepolia for testnet -import { base, baseSepolia } from "wagmi/chains"; -// Add to wagmi config chains array and transports -``` - -### 2. Create the Component File - -Copy the `TransactionForm` component code above into the project's components directory. - -### 3. Replace OnchainKit Transaction Imports and Usage - -**Before (OnchainKit):** -```typescript -import { - Transaction, - TransactionButton, - TransactionSponsor, - TransactionStatus, - TransactionStatusAction, - TransactionStatusLabel, -} from '@coinbase/onchainkit/transaction'; -import type { LifecycleStatus } from '@coinbase/onchainkit/transaction'; - -const calls = [ - { - address: '0x67c97D1FB8184F038592b2109F854dfb09C77C75', - abi: clickContractAbi, - functionName: 'click', - args: [], - } -]; - - - - - - - - - -``` - -**After (wagmi):** -```typescript -import { TransactionForm } from "./components/TransactionForm"; -import type { Address } from "viem"; - -const clickContractAddress: Address = '0x67c97D1FB8184F038592b2109F854dfb09C77C75'; -const clickContractAbi = [ - { - type: 'function' as const, - name: 'click', - inputs: [], - outputs: [], - stateMutability: 'nonpayable' as const, - }, -] as const; - -const calls = [ - { - address: clickContractAddress, - abi: clickContractAbi, - functionName: 'click', - args: [], - }, -]; - - -``` - -### 4. Handle the onStatus Callback - -The OnchainKit `LifecycleStatus` type has these states: `init`, `transactionIdle`, `buildingTransaction`, `transactionPending`, `transactionLegacyExecuted`, `success`, `error`, `reset`. - -The replacement uses a simplified set: `init`, `pending`, `confirmed`, `success`, `error`. - -**Mapping:** - -| OnchainKit Status | Replacement Status | -|---|---| -| `init` / `transactionIdle` | `init` | -| `buildingTransaction` / `transactionPending` | `pending` | -| `transactionLegacyExecuted` | `confirmed` | -| `success` | `success` | -| `error` | `error` | - -If the existing `onStatus` callback checks specific OnchainKit status names, update the checks to use the new names. - -### 5. Verify - -Run `npm run build` and confirm no errors. - -## What's Not Covered - -### Gas Sponsorship (TransactionSponsor) -OnchainKit's `TransactionSponsor` uses a paymaster URL to sponsor gas fees. This requires a paymaster service (e.g., Coinbase Developer Platform Paymaster). The replacement component does not include paymaster support. To add it, you would need to use wagmi's `useSendCalls` with the paymaster capability. - -### Batched Calls (EIP-5792) -OnchainKit's Transaction supports batching multiple calls into a single transaction for smart wallets. The replacement uses `useWriteContract` which handles one call at a time. For batched calls, use wagmi's `useSendCalls` hook directly. - -### Transaction Toast -OnchainKit's `TransactionToast` provides toast-style notifications. The replacement shows inline status instead. Add a toast library if toast notifications are needed. - -## Common Issues - -### Transaction receipt stuck in pending (UI hangs after wallet confirms) -**This is the most common bug.** The transaction hash appears, the tx confirms on-chain, but the UI stays stuck on "Transaction in progress..." forever. - -**Cause:** `useWaitForTransactionReceipt` needs an RPC to poll for the receipt. If the transaction's chain is not in the wagmi config's `chains` + `transports`, wagmi has no RPC endpoint to poll, so `isSuccess` never becomes `true`. - -**Fix (two parts):** -1. Add the transaction's target chain to `wagmi-config.ts`: -```typescript -import { base, baseSepolia } from "wagmi/chains"; - -export const wagmiConfig = createConfig({ - chains: [base, baseSepolia], // Must include every chain the app transacts on - transports: { - [base.id]: http(), - [baseSepolia.id]: http(), // Must have a transport for each chain - }, - // ...rest -}); -``` -2. Always pass `chainId` to `useWaitForTransactionReceipt`: -```typescript -const { data: receipt } = useWaitForTransactionReceipt({ - hash: txHash, - chainId, // Ensures polling uses the correct chain's transport -}); -``` - -### Next.js page export restrictions -Next.js only allows specific named exports from page files (`default`, `metadata`, `generateMetadata`, `generateStaticParams`, etc.). If you export contract call arrays, ABI constants, or other non-page values from a page file, the build will fail with an error like: `"calls" is not a valid Page export field`. - -**Fix:** Move contract call arrays, ABIs, and addresses to a separate module (e.g., `contracts.ts`) or make them non-exported `const` declarations within the page file. - -### Type error: comparison with "UserRejectedRequestError" -The wagmi error types don't include `UserRejectedRequestError` as a direct name match. Instead, check `error.message` for "User rejected" or "User denied" strings. - -### Transaction targets wrong chain -The component auto-switches chains via `useSwitchChain`. But the target chain must exist in the wagmi config. If you get a chain error, add the chain to `wagmi-config.ts`. - -### "useWriteContract must be used within WagmiProvider" -Same as wallet: ensure the component is inside the WagmiProvider tree. - -### ABI type errors -When defining the ABI inline, use `as const` on the array to get proper type inference: -```typescript -const abi = [ - { - type: 'function' as const, - name: 'click', - inputs: [], - outputs: [], - stateMutability: 'nonpayable' as const, - }, -] as const; -``` diff --git a/skills/build-on-base/references/migrations/onchainkit/troubleshooting.md b/skills/build-on-base/references/migrations/onchainkit/troubleshooting.md deleted file mode 100644 index 382bbbf..0000000 --- a/skills/build-on-base/references/migrations/onchainkit/troubleshooting.md +++ /dev/null @@ -1,79 +0,0 @@ -# Troubleshooting OnchainKit Migration - -## Build Errors - -### `Module not found: Can't resolve '@react-native-async-storage/async-storage'` -**Cause**: MetaMask SDK includes a react-native dependency that doesn't resolve in web environments. -**Impact**: Warning only. Does not affect functionality. -**Solution**: Ignore. This is a known issue with MetaMask SDK's web bundle. - -### `Type error: Cannot find module 'wagmi/connectors'` -**Cause**: Outdated wagmi version. -**Solution**: Update wagmi to >= 2.16: -```bash -npm install wagmi@latest -``` - -### `Error: useAccount must be used within WagmiConfig` -**Cause**: A component using wagmi hooks is rendering outside the WagmiProvider tree. -**Solution**: Ensure `WagmiProvider` wraps the entire app. In Next.js, this goes in the root provider component. Both the provider and any component using wagmi hooks must have `"use client"` directive. - -### `Error: No QueryClient set, use QueryClientProvider` -**Cause**: `QueryClientProvider` is missing from the provider tree. -**Solution**: Add `QueryClientProvider` inside `WagmiProvider`: -```typescript - - - {children} - - -``` - -### `Error: Invalid chain configuration` -**Cause**: The `transports` object doesn't have an entry for every chain in the `chains` array. -**Solution**: Every chain in `chains` needs a matching transport: -```typescript -createConfig({ - chains: [base, baseSepolia], - transports: { - [base.id]: http(), - [baseSepolia.id]: http(), // Must match - }, -}); -``` - -## Runtime Errors - -### Wallet modal opens but nothing happens on click -**Cause**: The connector might not be available or the wallet extension isn't installed. -**Solution**: For extension-based wallets (MetaMask), the user needs the extension installed. For Coinbase Wallet and Base Account, they work via popup/redirect without an extension. - -### Connection succeeds but address doesn't display -**Cause**: Component not re-rendering after connection state change. -**Solution**: Ensure the component using `useAccount()` is a client component with `"use client"`. wagmi hooks trigger re-renders automatically when state changes. - -### Dark mode styles not working -**Cause**: Tailwind dark mode not configured. -**Solution**: Tailwind v4 uses `prefers-color-scheme` by default. If the project uses class-based dark mode, ensure the `` element has the `dark` class. For Tailwind v3, check `tailwind.config.js` has `darkMode: 'class'`. - -## Migration-Specific Issues - -### OnchainKit styles break after removing the import -**Cause**: Some layouts depended on OnchainKit's global CSS. -**Solution**: The OnchainKit CSS mainly provides: -- Custom `ock-*` CSS variables for theming -- Rounded corner and color utilities -- Font styling - -These are replaced by Tailwind utilities. If specific layouts break, inspect the element and add equivalent Tailwind classes. - -### Multiple wallet connection prompts -**Cause**: The wagmi config has connectors that auto-connect on page load. -**Solution**: Use `cookieStorage` for persistence (prevents reconnection prompts): -```typescript -storage: createStorage({ storage: cookieStorage }), -``` - -### SSR hydration mismatch -**Cause**: Wallet state differs between server and client render. -**Solution**: Ensure the wagmi config has `ssr: true` and the provider component has `"use client"` directive. Use `cookieStorage` for state persistence across SSR. diff --git a/skills/build-on-base/references/migrations/onchainkit/wallet.md b/skills/build-on-base/references/migrations/onchainkit/wallet.md deleted file mode 100644 index 88e9c56..0000000 --- a/skills/build-on-base/references/migrations/onchainkit/wallet.md +++ /dev/null @@ -1,346 +0,0 @@ -# Wallet Migration: OnchainKit Wallet to WalletConnect - -Replace OnchainKit's `Wallet`, `ConnectWallet`, `WalletDropdown`, `WalletModal`, and `Connected` components with a standalone `WalletConnect` component built on wagmi hooks. - -## What OnchainKit's Wallet Components Do - -OnchainKit provides several wallet components: -- `` -- container that manages open/closed state -- `` -- button that triggers connection (renders as "Connect Wallet" when disconnected) -- `` -- dropdown with identity info and actions -- `` -- modal with multiple wallet options (Base Account, Coinbase, MetaMask, Phantom, etc.) -- `` -- conditional renderer based on wallet connection state - -The replacement `WalletConnect` component combines all of this into one component. - -## Prerequisites - -- Provider migration must be completed first (WagmiProvider + QueryClientProvider in the tree) -- Tailwind CSS installed (if not, install it or adapt styles) - -## The WalletConnect Component - -Create `app/components/WalletConnect.tsx` (or wherever components live in the project): - -```typescript -"use client"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { useAccount, useConnect, useDisconnect } from "wagmi"; -import { - baseAccount, - coinbaseWallet, - metaMask, -} from "wagmi/connectors"; - -function truncateAddress(address: string): string { - return `${address.slice(0, 6)}...${address.slice(-4)}`; -} - -type WalletOption = { - id: string; - name: string; - connect: () => void; -}; - -function WalletModal({ - onClose, - appName = "My App", -}: { - onClose: () => void; - appName?: string; -}) { - const { connect } = useConnect(); - const backdropRef = useRef(null); - - const handleBackdropClick = useCallback( - (e: React.MouseEvent) => { - if (e.target === backdropRef.current) onClose(); - }, - [onClose] - ); - - useEffect(() => { - const handleEsc = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - }; - document.addEventListener("keydown", handleEsc); - return () => document.removeEventListener("keydown", handleEsc); - }, [onClose]); - - const walletOptions: WalletOption[] = [ - { - id: "base-account", - name: "Sign in with Base", - connect: () => { - connect({ connector: baseAccount({ appName }) }); - onClose(); - }, - }, - { - id: "coinbase-wallet", - name: "Coinbase Wallet", - connect: () => { - connect({ - connector: coinbaseWallet({ appName, preference: "all" }), - }); - onClose(); - }, - }, - { - id: "metamask", - name: "MetaMask", - connect: () => { - connect({ - connector: metaMask({ - dappMetadata: { - name: appName, - url: typeof window !== "undefined" ? window.location.origin : "", - }, - }), - }); - onClose(); - }, - }, - ]; - - const [primaryOption, ...otherOptions] = walletOptions; - - return ( -
-
- - -

- Connect Wallet -

- -
- - -
-
-
-
-
- - or use another wallet - -
-
- - {otherOptions.map((wallet) => ( - - ))} -
-
-
- ); -} - -export function WalletConnect({ appName = "My App" }: { appName?: string }) { - const { address, isConnected } = useAccount(); - const { disconnect } = useDisconnect(); - const [isModalOpen, setIsModalOpen] = useState(false); - - if (isConnected && address) { - return ( -
- - {truncateAddress(address)} - - -
- ); - } - - return ( - <> - - {isModalOpen && ( - setIsModalOpen(false)} - appName={appName} - /> - )} - - ); -} -``` - -## Step-by-Step Replacement - -### 1. Create the Component File - -Copy the component code above into the project's components directory. - -### 2. Replace OnchainKit Wallet Imports - -Find all files importing from `@coinbase/onchainkit/wallet` or using `Connected` from `@coinbase/onchainkit`: - -**Before:** -```typescript -import { Wallet } from "@coinbase/onchainkit/wallet"; -// or -import { ConnectWallet, Wallet, WalletDropdown, WalletDropdownDisconnect } from "@coinbase/onchainkit/wallet"; -// or -import { Connected } from "@coinbase/onchainkit"; -``` - -**After:** -```typescript -import { WalletConnect } from "./components/WalletConnect"; -``` - -### 3. Replace Component Usage - -**Simple ``:** -```typescript -// Before - - -// After - -``` - -**Composed wallet with children:** -```typescript -// Before - - - - - - - - - -
- - - - - -// After - -``` - -**`` conditional rendering:** -```typescript -// Before -import { Connected } from "@coinbase/onchainkit"; - -Please connect

}> -

You are connected

-
- -// After -- use wagmi's useAccount directly -import { useAccount } from "wagmi"; - -const { isConnected } = useAccount(); -{isConnected ?

You are connected

:

Please connect

} -``` - -### 4. Remove MiniKit Usage - -If the page uses `useMiniKit` or other MiniKit hooks, remove them: - -```typescript -// Remove these -import { useMiniKit } from "@coinbase/onchainkit/minikit"; -const { setMiniAppReady, isMiniAppReady } = useMiniKit(); -useEffect(() => { - if (!isMiniAppReady) setMiniAppReady(); -}, [setMiniAppReady, isMiniAppReady]); -``` - -### 5. Verify - -Run `npm run build` and confirm no errors. - -## Customization - -### Changing the app name -Pass the `appName` prop to `WalletConnect`: -```typescript - -``` - -### Adding more wallet options -Add entries to the `walletOptions` array in the `WalletModal` component. Use `injected({ target: 'walletName' })` from `wagmi/connectors` for browser extension wallets. - -### Changing styles -The component uses Tailwind utility classes. Modify the `className` strings to match the project's design system. All styling is inline via Tailwind -- no external CSS files needed. - -### Using without Tailwind -If the project doesn't use Tailwind, convert the Tailwind classes to inline styles or CSS modules. The key visual elements are: -- Fixed overlay with semi-transparent black background -- Centered card with white background, rounded corners, shadow -- Primary button (blue) for Base Account -- Secondary buttons (white/bordered) for other wallets -- Dark mode support via `dark:` variants - -## Common Issues - -### "useAccount must be used within WagmiProvider" -The component is being rendered outside the provider tree. Ensure `WagmiProvider` wraps the entire app in the layout or root provider. - -### Modal doesn't close after connecting -This can happen if the connection is async and the component unmounts. The current implementation calls `onClose()` synchronously after `connect()`. If you need to wait for the connection, use the `onSuccess` callback from `useConnect`. - -### baseAccount connector not found -Ensure wagmi version is >= 2.16. The `baseAccount` connector was added in recent wagmi versions. Check with: -```bash -npm ls wagmi -``` diff --git a/skills/build-on-base/scripts/analyze_project.py b/skills/build-on-base/scripts/analyze_project.py deleted file mode 100644 index e649395..0000000 --- a/skills/build-on-base/scripts/analyze_project.py +++ /dev/null @@ -1,290 +0,0 @@ -#!/usr/bin/env python3 -""" -Analyze a project directory for MiniKit usage. -Finds all imports and hook usages that need conversion. - -Usage: - python scripts/analyze_project.py - python scripts/analyze_project.py ./my-minikit-app - -Output: - JSON report of all MiniKit usage found -""" - -import os -import re -import json -import sys -from pathlib import Path -from typing import TypedDict, List - -class HookUsage(TypedDict): - hook: str - file: str - line: int - code: str - -class ImportInfo(TypedDict): - file: str - line: int - imports: List[str] - -class AnalysisReport(TypedDict): - project_dir: str - files_scanned: int - files_with_minikit: int - imports: List[ImportInfo] - hook_usages: List[HookUsage] - provider_locations: List[dict] - summary: dict - -# MiniKit hooks to search for -MINIKIT_HOOKS = [ - 'useMiniKit', - 'useClose', - 'useOpenUrl', - 'useViewProfile', - 'useViewCast', - 'useComposeCast', - 'useAddFrame', - 'useAuthenticate', - 'useNotification', - 'usePrimaryButton', -] - -# Import patterns - both @coinbase/onchainkit/minikit AND @coinbase/onchainkit -MINIKIT_IMPORT_PATTERN = re.compile( - r"from\s+['\"]@coinbase/onchainkit(?:/minikit)?['\"]" -) - -# OnchainKitProvider with miniKit prop -ONCHAINKIT_PROVIDER_PATTERN = re.compile( - r" List[Path]: - """Find all relevant source files in directory.""" - files = [] - for root, _, filenames in os.walk(directory): - # Skip node_modules and .next - if 'node_modules' in root or '.next' in root: - continue - for filename in filenames: - if filename.endswith(extensions): - files.append(Path(root) / filename) - return files - -def analyze_file(filepath: Path) -> dict: - """Analyze a single file for MiniKit usage.""" - result = { - 'imports': [], - 'hooks': [], - 'provider': False, - 'provider_lines': [], - 'provider_type': None - } - - try: - content = filepath.read_text(encoding='utf-8') - except Exception as e: - print(f"Warning: Could not read {filepath}: {e}", file=sys.stderr) - return result - - lines = content.split('\n') - - for i, line in enumerate(lines, 1): - # Check for imports from @coinbase/onchainkit or @coinbase/onchainkit/minikit - if MINIKIT_IMPORT_PATTERN.search(line): - # Extract what's being imported - import_match = re.search(r"import\s+\{([^}]+)\}", line) - if import_match: - imports = [s.strip() for s in import_match.group(1).split(',')] - result['imports'].append({ - 'line': i, - 'imports': imports - }) - - # Check for hook usages - for hook in MINIKIT_HOOKS: - hook_pattern = re.compile(rf'\b{hook}\s*\(') - if hook_pattern.search(line): - result['hooks'].append({ - 'hook': hook, - 'line': i, - 'code': line.strip() - }) - - # Check for OnchainKitProvider (may have miniKit prop) - if ONCHAINKIT_PROVIDER_PATTERN.search(line): - result['provider'] = True - result['provider_type'] = 'OnchainKitProvider' - result['provider_lines'].append({ - 'line': i, - 'code': line.strip() - }) - - # Check for MiniKitProvider - if MINIKIT_PROVIDER_PATTERN.search(line): - result['provider'] = True - result['provider_type'] = 'MiniKitProvider' - result['provider_lines'].append({ - 'line': i, - 'code': line.strip() - }) - - # Check for miniKit prop - if MINIKIT_PROP_PATTERN.search(line): - result['provider_lines'].append({ - 'line': i, - 'code': line.strip(), - 'is_minikit_prop': True - }) - - return result - -def analyze_project(project_dir: str) -> AnalysisReport: - """Analyze entire project for MiniKit usage.""" - project_path = Path(project_dir).resolve() - - if not project_path.exists(): - print(f"Error: Directory {project_dir} does not exist", file=sys.stderr) - sys.exit(1) - - files = find_files(str(project_path)) - - report: AnalysisReport = { - 'project_dir': str(project_path), - 'files_scanned': len(files), - 'files_with_minikit': 0, - 'imports': [], - 'hook_usages': [], - 'provider_locations': [], - 'summary': {} - } - - hook_counts = {hook: 0 for hook in MINIKIT_HOOKS} - - for filepath in files: - result = analyze_file(filepath) - rel_path = str(filepath.relative_to(project_path)) - - has_minikit = bool(result['imports'] or result['hooks'] or result['provider']) - - if has_minikit: - report['files_with_minikit'] += 1 - - # Collect imports - for imp in result['imports']: - report['imports'].append({ - 'file': rel_path, - 'line': imp['line'], - 'imports': imp['imports'] - }) - - # Collect hook usages - for hook_usage in result['hooks']: - report['hook_usages'].append({ - 'hook': hook_usage['hook'], - 'file': rel_path, - 'line': hook_usage['line'], - 'code': hook_usage['code'] - }) - hook_counts[hook_usage['hook']] += 1 - - # Collect provider locations - if result['provider']: - for provider_info in result['provider_lines']: - report['provider_locations'].append({ - 'file': rel_path, - 'line': provider_info['line'], - 'code': provider_info['code'] - }) - - # Build summary - report['summary'] = { - 'total_hook_usages': sum(hook_counts.values()), - 'hooks_by_type': {k: v for k, v in hook_counts.items() if v > 0}, - 'has_provider': len(report['provider_locations']) > 0, - 'unique_hooks_used': [k for k, v in hook_counts.items() if v > 0] - } - - return report - -def print_report(report: AnalysisReport): - """Print human-readable report.""" - print("\n" + "=" * 60) - print("MINIKIT ANALYSIS REPORT") - print("=" * 60) - print(f"\nProject: {report['project_dir']}") - print(f"Files scanned: {report['files_scanned']}") - print(f"Files with MiniKit: {report['files_with_minikit']}") - - print("\n--- IMPORTS ---") - if report['imports']: - for imp in report['imports']: - print(f" {imp['file']}:{imp['line']}") - print(f" Imports: {', '.join(imp['imports'])}") - else: - print(" No MiniKit imports found") - - print("\n--- HOOK USAGES ---") - if report['hook_usages']: - for usage in report['hook_usages']: - print(f" {usage['file']}:{usage['line']}") - print(f" Hook: {usage['hook']}") - print(f" Code: {usage['code'][:80]}...") - else: - print(" No hook usages found") - - print("\n--- PROVIDER LOCATIONS ---") - if report['provider_locations']: - for loc in report['provider_locations']: - print(f" {loc['file']}:{loc['line']}") - print(f" {loc['code'][:80]}") - else: - print(" No MiniKitProvider found") - - print("\n--- SUMMARY ---") - summary = report['summary'] - print(f" Total hook usages: {summary['total_hook_usages']}") - print(f" Unique hooks: {', '.join(summary['unique_hooks_used']) or 'None'}") - print(f" Has provider: {'Yes' if summary['has_provider'] else 'No'}") - - if summary['hooks_by_type']: - print("\n Hooks by frequency:") - for hook, count in sorted(summary['hooks_by_type'].items(), key=lambda x: -x[1]): - print(f" {hook}: {count}") - - print("\n" + "=" * 60) - -def main(): - if len(sys.argv) < 2: - print("Usage: python analyze_project.py [--json]") - print("Example: python analyze_project.py ./my-minikit-app") - sys.exit(1) - - project_dir = sys.argv[1] - output_json = '--json' in sys.argv - - report = analyze_project(project_dir) - - if output_json: - print(json.dumps(report, indent=2)) - else: - print_report(report) - # Also save JSON report - report_path = Path(project_dir) / 'minikit-analysis.json' - with open(report_path, 'w') as f: - json.dump(report, f, indent=2) - print(f"\nJSON report saved to: {report_path}") - -if __name__ == '__main__': - main() diff --git a/skills/build-on-base/scripts/validate_conversion.py b/skills/build-on-base/scripts/validate_conversion.py deleted file mode 100644 index fd18dcd..0000000 --- a/skills/build-on-base/scripts/validate_conversion.py +++ /dev/null @@ -1,336 +0,0 @@ -#!/usr/bin/env python3 -""" -Validate a converted project to ensure MiniKit has been fully removed -and Farcaster SDK is properly set up. - -Usage: - python scripts/validate_conversion.py - -Checks: - 1. No remaining MiniKit imports - 2. No remaining MiniKit hooks - 3. No MiniKitProvider in code - 4. Farcaster SDK is imported where needed - 5. sdk.actions.ready() is called somewhere - 6. package.json has correct dependencies -""" - -import os -import re -import json -import sys -from pathlib import Path -from typing import List, Tuple - -class ValidationResult: - def __init__(self): - self.errors: List[str] = [] - self.warnings: List[str] = [] - self.passed: List[str] = [] - - def add_error(self, msg: str): - self.errors.append(msg) - - def add_warning(self, msg: str): - self.warnings.append(msg) - - def add_pass(self, msg: str): - self.passed.append(msg) - - @property - def is_valid(self) -> bool: - return len(self.errors) == 0 - -# Patterns to check -MINIKIT_IMPORT = re.compile(r"from\s+['\"]@coinbase/onchainkit/minikit['\"]") -ONCHAINKIT_IMPORT = re.compile(r"from\s+['\"]@coinbase/onchainkit['\"]") -MINIKIT_HOOKS = re.compile(r'\b(useMiniKit|useClose|useOpenUrl|useViewProfile|useViewCast|useComposeCast|useAddFrame|useAuthenticate|useNotification|usePrimaryButton)\s*\(') -MINIKIT_PROVIDER = re.compile(r' List[Path]: - """Find all source files.""" - files = [] - for root, _, filenames in os.walk(directory): - if 'node_modules' in root or '.next' in root: - continue - for filename in filenames: - if filename.endswith(('.tsx', '.ts', '.jsx', '.js')): - files.append(Path(root) / filename) - return files - -def check_no_minikit_imports(files: List[Path], project_path: Path, result: ValidationResult): - """Check that no MiniKit imports remain.""" - minikit_found = [] - onchainkit_found = [] - - for filepath in files: - try: - content = filepath.read_text(encoding='utf-8') - rel_path = str(filepath.relative_to(project_path)) - - if MINIKIT_IMPORT.search(content): - minikit_found.append(rel_path) - if ONCHAINKIT_IMPORT.search(content): - onchainkit_found.append(rel_path) - except Exception: - pass - - if minikit_found: - result.add_error(f"MiniKit imports still present in: {', '.join(minikit_found)}") - - if onchainkit_found: - result.add_error(f"OnchainKit imports still present in: {', '.join(onchainkit_found)} - replace with @farcaster/miniapp-sdk") - - if not minikit_found and not onchainkit_found: - result.add_pass("No MiniKit/OnchainKit imports found") - -def check_no_minikit_hooks(files: List[Path], project_path: Path, result: ValidationResult): - """Check that no MiniKit hooks are being used.""" - found = [] - for filepath in files: - try: - content = filepath.read_text(encoding='utf-8') - matches = MINIKIT_HOOKS.findall(content) - if matches: - rel_path = filepath.relative_to(project_path) - found.append(f"{rel_path}: {', '.join(set(matches))}") - except Exception: - pass - - if found: - result.add_error(f"MiniKit hooks still in use:\n " + "\n ".join(found)) - else: - result.add_pass("No MiniKit hooks found") - -def check_no_provider(files: List[Path], project_path: Path, result: ValidationResult): - """Check that MiniKitProvider and OnchainKitProvider with miniKit are removed.""" - minikit_provider_found = [] - onchainkit_with_minikit_found = [] - - for filepath in files: - try: - content = filepath.read_text(encoding='utf-8') - rel_path = str(filepath.relative_to(project_path)) - - if MINIKIT_PROVIDER.search(content): - minikit_provider_found.append(rel_path) - - # Check for OnchainKitProvider with miniKit prop - if ONCHAINKIT_PROVIDER.search(content) and MINIKIT_PROP.search(content): - onchainkit_with_minikit_found.append(rel_path) - except Exception: - pass - - if minikit_provider_found: - result.add_error(f"MiniKitProvider still present in: {', '.join(minikit_provider_found)}") - - if onchainkit_with_minikit_found: - result.add_error(f"OnchainKitProvider with miniKit prop found in: {', '.join(onchainkit_with_minikit_found)} - replace with WagmiProvider + farcasterMiniApp connector") - - if not minikit_provider_found and not onchainkit_with_minikit_found: - result.add_pass("MiniKit providers removed") - -def check_farcaster_sdk_usage(files: List[Path], project_path: Path, result: ValidationResult): - """Check that Farcaster SDK is imported and ready() is called.""" - has_sdk_import = False - has_wagmi_connector = False - has_ready = False - sdk_import_files = [] - wagmi_connector_files = [] - ready_files = [] - - for filepath in files: - try: - content = filepath.read_text(encoding='utf-8') - rel_path = str(filepath.relative_to(project_path)) - - if FARCASTER_IMPORT.search(content): - has_sdk_import = True - sdk_import_files.append(rel_path) - if FARCASTER_WAGMI_IMPORT.search(content): - has_wagmi_connector = True - wagmi_connector_files.append(rel_path) - if SDK_READY.search(content): - has_ready = True - ready_files.append(rel_path) - except Exception: - pass - - if has_sdk_import: - result.add_pass(f"Farcaster SDK imported in: {', '.join(sdk_import_files)}") - else: - result.add_error("Farcaster SDK not imported - add: import { sdk } from '@farcaster/miniapp-sdk'") - - if has_wagmi_connector: - result.add_pass(f"Farcaster wagmi connector imported in: {', '.join(wagmi_connector_files)}") - else: - result.add_error("Farcaster wagmi connector not imported - add: import { farcasterMiniApp } from '@farcaster/miniapp-wagmi-connector'") - - if has_ready: - result.add_pass(f"sdk.actions.ready() called in: {', '.join(ready_files)}") - else: - result.add_error("sdk.actions.ready() not found - add to your main page useEffect: await sdk.actions.ready()") - -def check_package_json(project_path: Path, result: ValidationResult): - """Check package.json for correct dependencies.""" - package_json_path = project_path / 'package.json' - - if not package_json_path.exists(): - result.add_warning("package.json not found") - return - - try: - with open(package_json_path) as f: - pkg = json.load(f) - except Exception as e: - result.add_error(f"Could not parse package.json: {e}") - return - - deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})} - - # Check for old OnchainKit - this should be removed - if '@coinbase/onchainkit' in deps: - result.add_error("@coinbase/onchainkit still installed - run: npm uninstall @coinbase/onchainkit") - else: - result.add_pass("@coinbase/onchainkit removed") - - # Check for required Farcaster packages - required_packages = [ - ('@farcaster/miniapp-sdk', 'npm install @farcaster/miniapp-sdk'), - ('@farcaster/miniapp-wagmi-connector', 'npm install @farcaster/miniapp-wagmi-connector'), - ('wagmi', 'npm install wagmi'), - ('@tanstack/react-query', 'npm install @tanstack/react-query'), - ] - - for package, install_cmd in required_packages: - if package in deps: - result.add_pass(f"{package} installed (version: {deps[package]})") - else: - result.add_error(f"{package} not installed - run: {install_cmd}") - -def check_env_variables(project_path: Path, result: ValidationResult): - """Check for old environment variables that should be removed.""" - env_files = ['.env', '.env.local', '.env.example'] - old_vars = ['NEXT_PUBLIC_ONCHAINKIT_API_KEY', 'NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME'] - - for env_file in env_files: - env_path = project_path / env_file - if env_path.exists(): - try: - content = env_path.read_text() - found = [var for var in old_vars if var in content] - if found: - result.add_warning(f"{env_file} contains old MiniKit vars: {', '.join(found)}") - except Exception: - pass - -def check_manifest(project_path: Path, result: ValidationResult): - """Check farcaster.json manifest uses 'miniapp' instead of 'frame'.""" - # Check for manifest route - manifest_paths = [ - project_path / 'app' / '.well-known' / 'farcaster.json' / 'route.ts', - project_path / 'app' / '.well-known' / 'farcaster.json' / 'route.js', - project_path / 'pages' / 'api' / '.well-known' / 'farcaster.json.ts', - project_path / 'pages' / 'api' / '.well-known' / 'farcaster.json.js', - project_path / 'public' / '.well-known' / 'farcaster.json', - ] - - manifest_found = False - for manifest_path in manifest_paths: - if manifest_path.exists(): - manifest_found = True - try: - content = manifest_path.read_text() - - # Check for old 'frame' key - if MANIFEST_FRAME_KEY.search(content): - result.add_error(f"Manifest uses 'frame' key - change to 'miniapp': {manifest_path.relative_to(project_path)}") - - # Check for new 'miniapp' key - if MANIFEST_MINIAPP_KEY.search(content): - result.add_pass(f"Manifest correctly uses 'miniapp' key: {manifest_path.relative_to(project_path)}") - elif not MANIFEST_FRAME_KEY.search(content): - result.add_warning(f"Manifest found but no 'miniapp' or 'frame' key detected: {manifest_path.relative_to(project_path)}") - - except Exception as e: - result.add_warning(f"Could not read manifest: {e}") - break - - if not manifest_found: - result.add_warning("No farcaster.json manifest found - create app/.well-known/farcaster.json/route.ts") - -def validate_project(project_dir: str) -> ValidationResult: - """Run all validation checks.""" - project_path = Path(project_dir).resolve() - result = ValidationResult() - - if not project_path.exists(): - result.add_error(f"Directory does not exist: {project_dir}") - return result - - files = find_source_files(str(project_path)) - - if not files: - result.add_warning("No source files found") - return result - - # Run checks - check_no_minikit_imports(files, project_path, result) - check_no_minikit_hooks(files, project_path, result) - check_no_provider(files, project_path, result) - check_farcaster_sdk_usage(files, project_path, result) - check_package_json(project_path, result) - check_env_variables(project_path, result) - check_manifest(project_path, result) - - return result - -def print_result(result: ValidationResult): - """Print validation results.""" - print("\n" + "=" * 60) - print("CONVERSION VALIDATION REPORT") - print("=" * 60) - - if result.passed: - print("\n✅ PASSED:") - for msg in result.passed: - print(f" {msg}") - - if result.warnings: - print("\n⚠️ WARNINGS:") - for msg in result.warnings: - print(f" {msg}") - - if result.errors: - print("\n❌ ERRORS:") - for msg in result.errors: - print(f" {msg}") - - print("\n" + "-" * 60) - if result.is_valid: - print("✅ VALIDATION PASSED - Conversion looks complete!") - else: - print("❌ VALIDATION FAILED - Please fix the errors above") - print("=" * 60 + "\n") - -def main(): - if len(sys.argv) < 2: - print("Usage: python validate_conversion.py ") - print("Example: python validate_conversion.py ./my-converted-app") - sys.exit(1) - - project_dir = sys.argv[1] - result = validate_project(project_dir) - print_result(result) - - sys.exit(0 if result.is_valid else 1) - -if __name__ == '__main__': - main()