From 52a7a5f440b3a4e92d15ecb00f6cffb00ca09b48 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Tue, 17 Mar 2026 17:39:44 +0000 Subject: [PATCH 1/2] [#36] Farcaster mini app manifest and official wagmi connector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add public/.well-known/farcaster.json manifest (accountAssociation placeholder — operator must fill with domain signature) - Install @farcaster/miniapp-wagmi-connector, replace custom connector - Extract isFarcasterMiniApp() into lib/farcaster-detect.ts - Update ConnectWallet to use official connector type "farcasterMiniApp" - Remove custom lib/farcaster-connector.ts (replaced by official package) Existing FarcasterMiniApp component (sdk.actions.ready()) unchanged. Fixes #36 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/farcaster-connector.ts | 137 ------------------------------ lib/farcaster-detect.ts | 14 +++ lib/wagmi.ts | 4 +- package-lock.json | 105 ++++++++++++++++------- package.json | 1 + public/.well-known/farcaster.json | 18 ++++ src/components/ConnectWallet.tsx | 6 +- 7 files changed, 114 insertions(+), 171 deletions(-) delete mode 100644 lib/farcaster-connector.ts create mode 100644 lib/farcaster-detect.ts create mode 100644 public/.well-known/farcaster.json diff --git a/lib/farcaster-connector.ts b/lib/farcaster-connector.ts deleted file mode 100644 index a0ae21cf..00000000 --- a/lib/farcaster-connector.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { type Address, type EIP1193Provider, getAddress } from "viem"; -import { createConnector } from "wagmi"; - -// Lazily resolved SDK module & provider -let sdkModule: typeof import("@farcaster/miniapp-sdk") | undefined; -let cachedProvider: EIP1193Provider | undefined; - -async function getSDK() { - if (!sdkModule) { - sdkModule = await import("@farcaster/miniapp-sdk"); - } - return sdkModule.sdk; -} - -async function resolveProvider(): Promise { - if (cachedProvider) return cachedProvider; - const sdk = await getSDK(); - // getEthereumProvider() may return a promise in some SDK versions - cachedProvider = (await sdk.wallet.getEthereumProvider()) as EIP1193Provider; - return cachedProvider; -} - -/** - * Detect whether we are running inside a Farcaster Mini App context. - * Safe to call on server (returns false) and outside Farcaster (returns false). - */ -export async function isFarcasterMiniApp(): Promise { - if (typeof window === "undefined") return false; - try { - const sdk = await getSDK(); - const ctx = await sdk.context; - return !!ctx; - } catch { - return false; - } -} - -farcaster.type = "farcaster" as const; - -/** - * Custom wagmi v3 connector that wraps `sdk.wallet.getEthereumProvider()` - * from `@farcaster/miniapp-sdk`. Only usable inside a Farcaster Mini App. - */ -export function farcaster() { - return createConnector((config) => ({ - id: "farcaster", - name: "Farcaster", - type: farcaster.type, - - async connect(parameters?) { - const provider = await resolveProvider(); - const accounts = (await provider.request({ - method: "eth_requestAccounts", - })) as Address[]; - let currentChainId = Number( - await provider.request({ method: "eth_chainId" }), - ); - - const chainId = parameters?.chainId; - if (chainId && currentChainId !== chainId) { - const chain = await this.switchChain!({ chainId }); - currentChainId = chain.id; - } - - const result: { accounts: readonly Address[]; chainId: number } = { - accounts: accounts.map((a) => getAddress(a)), - chainId: currentChainId, - }; - // wagmi v3 connect() is generic over withCapabilities — safe to widen - return result as never; - }, - - async disconnect() { - // The Farcaster provider does not support programmatic disconnect - }, - - async getAccounts() { - const provider = await resolveProvider(); - const accounts = (await provider.request({ - method: "eth_accounts", - })) as Address[]; - return accounts.map((a) => getAddress(a)) as readonly Address[]; - }, - - async getChainId() { - const provider = await resolveProvider(); - const chainId = await provider.request({ method: "eth_chainId" }); - return Number(chainId); - }, - - async getProvider() { - return await resolveProvider(); - }, - - async isAuthorized() { - try { - const accounts = await this.getAccounts(); - return accounts.length > 0; - } catch { - return false; - } - }, - - async switchChain({ chainId }) { - const provider = await resolveProvider(); - const chain = config.chains.find((c) => c.id === chainId); - if (!chain) throw new Error(`Chain ${chainId} not configured`); - - await provider.request({ - method: "wallet_switchEthereumChain", - params: [{ chainId: `0x${chainId.toString(16)}` }], - }); - - config.emitter.emit("change", { chainId }); - return chain; - }, - - onAccountsChanged(accounts) { - if (accounts.length === 0) { - config.emitter.emit("disconnect"); - } else { - config.emitter.emit("change", { - accounts: accounts.map((a) => getAddress(a as Address)), - }); - } - }, - - onChainChanged(chain) { - const chainId = Number(chain); - config.emitter.emit("change", { chainId }); - }, - - onDisconnect() { - config.emitter.emit("disconnect"); - }, - })); -} diff --git a/lib/farcaster-detect.ts b/lib/farcaster-detect.ts new file mode 100644 index 00000000..54e4bc63 --- /dev/null +++ b/lib/farcaster-detect.ts @@ -0,0 +1,14 @@ +/** + * Detect whether we are running inside a Farcaster Mini App context. + * Safe to call on server (returns false) and outside Farcaster (returns false). + */ +export async function isFarcasterMiniApp(): Promise { + if (typeof window === "undefined") return false; + try { + const { sdk } = await import("@farcaster/miniapp-sdk"); + const ctx = await sdk.context; + return !!ctx; + } catch { + return false; + } +} diff --git a/lib/wagmi.ts b/lib/wagmi.ts index c7def4fe..87c35670 100644 --- a/lib/wagmi.ts +++ b/lib/wagmi.ts @@ -1,11 +1,11 @@ import { http, createConfig } from "wagmi"; import { base, baseSepolia } from "wagmi/chains"; import { injected } from "wagmi/connectors"; -import { farcaster } from "./farcaster-connector"; +import { farcasterMiniApp } from "@farcaster/miniapp-wagmi-connector"; export const config = createConfig({ chains: [base, baseSepolia], - connectors: [farcaster(), injected()], + connectors: [farcasterMiniApp(), injected()], transports: { [base.id]: http( process.env.NEXT_PUBLIC_CHAIN_ID === "8453" diff --git a/package-lock.json b/package-lock.json index 4e3e31ab..5fa6e2a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.1009.0", "@farcaster/miniapp-sdk": "^0.2.3", + "@farcaster/miniapp-wagmi-connector": "^1.1.1", "@supabase/supabase-js": "^2.99.1", "@tanstack/react-query": "^5.90.21", "next": "16.1.6", @@ -1795,6 +1796,17 @@ "ox": "^0.4.4" } }, + "node_modules/@farcaster/miniapp-wagmi-connector": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@farcaster/miniapp-wagmi-connector/-/miniapp-wagmi-connector-1.1.1.tgz", + "integrity": "sha512-sdKhRRZcbM1XXl6Z49UTLlgRBYMmyxM6z4KE3LTWQtjRzlc0JwGaxmjvhjl3mZNZFnyUAlhpoU7RfCJ4c2aTGg==", + "license": "MIT", + "peerDependencies": { + "@farcaster/miniapp-sdk": "^0.2.3", + "@wagmi/core": "^2.14.1", + "viem": "^2.21.55" + } + }, "node_modules/@farcaster/quick-auth": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@farcaster/quick-auth/-/quick-auth-0.0.6.tgz", @@ -5009,6 +5021,41 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@wagmi/core": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-2.22.1.tgz", + "integrity": "sha512-cG/xwQWsBEcKgRTkQVhH29cbpbs/TdcUJVFXCyri3ZknxhMyGv0YEjTcrNpRgt2SaswL1KrvslSNYKKo+5YEAg==", + "license": "MIT", + "peer": true, + "dependencies": { + "eventemitter3": "5.0.1", + "mipd": "0.0.7", + "zustand": "5.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@tanstack/query-core": ">=5.0.0", + "typescript": ">=5.0.4", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "@tanstack/query-core": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@wagmi/core/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT", + "peer": true + }, "node_modules/abitype": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", @@ -10671,35 +10718,6 @@ } } }, - "node_modules/wagmi/node_modules/@wagmi/core/node_modules/zustand": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz", - "integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - }, "node_modules/wagmi/node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -10949,6 +10967,35 @@ "zod": "^3.25.0 || ^4.0.0" } }, + "node_modules/zustand": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz", + "integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "packages/cli": { "name": "plotlink-cli", "version": "0.1.0", diff --git a/package.json b/package.json index 1bd034cd..d38ccb21 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.1009.0", "@farcaster/miniapp-sdk": "^0.2.3", + "@farcaster/miniapp-wagmi-connector": "^1.1.1", "@supabase/supabase-js": "^2.99.1", "@tanstack/react-query": "^5.90.21", "next": "16.1.6", diff --git a/public/.well-known/farcaster.json b/public/.well-known/farcaster.json new file mode 100644 index 00000000..6658002c --- /dev/null +++ b/public/.well-known/farcaster.json @@ -0,0 +1,18 @@ +{ + "accountAssociation": { + "header": "", + "payload": "", + "signature": "" + }, + "miniapp": { + "version": "1", + "name": "PlotLink", + "iconUrl": "https://plotlink.xyz/favicon.ico", + "homeUrl": "https://plotlink.xyz", + "splashBackgroundColor": "#0a0a0a", + "description": "Tokenise your story from day 1. Publish plots, drive trading, earn royalties from every trade.", + "primaryCategory": "entertainment", + "tags": ["stories", "writing", "onchain", "base"], + "requiredChains": ["eip155:8453"] + } +} diff --git a/src/components/ConnectWallet.tsx b/src/components/ConnectWallet.tsx index 2cf3e891..0e8d9cb1 100644 --- a/src/components/ConnectWallet.tsx +++ b/src/components/ConnectWallet.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { useAccount, useConnect, useDisconnect } from "wagmi"; -import { isFarcasterMiniApp } from "../../lib/farcaster-connector"; +import { isFarcasterMiniApp } from "../../lib/farcaster-detect"; import { truncateAddress } from "../../lib/utils"; import { useConnectedIdentity } from "../hooks/useConnectedIdentity"; @@ -25,7 +25,7 @@ export function ConnectWallet() { if (autoConnectAttempted.current || isConnected) return; autoConnectAttempted.current = true; - const farcasterConnector = connectors.find((c) => c.type === "farcaster"); + const farcasterConnector = connectors.find((c) => c.type === "farcasterMiniApp"); if (!farcasterConnector) return; farcasterConnector.isAuthorized().then((authorized) => { @@ -66,7 +66,7 @@ export function ConnectWallet() { onClick={() => { // Use Farcaster connector only when confirmed inside a mini app const farcasterConnector = inMiniApp - ? connectors.find((c) => c.type === "farcaster") + ? connectors.find((c) => c.type === "farcasterMiniApp") : undefined; const fallback = connectors.find((c) => c.type === "injected"); const connector = farcasterConnector ?? fallback; From aa31f2f8aa9c79c6328ab18ae2ce730cb17a6d27 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Tue, 17 Mar 2026 17:43:11 +0000 Subject: [PATCH 2/2] [#36] Fix iconUrl to use PNG app icon Switch iconUrl from favicon.ico to icon.png (Farcaster manifest requires PNG). Add generated 200x200 PNG with PlotLink branding (green P on dark background). Co-Authored-By: Claude Opus 4.6 (1M context) --- public/.well-known/farcaster.json | 2 +- public/icon.png | Bin 0 -> 1324 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 public/icon.png diff --git a/public/.well-known/farcaster.json b/public/.well-known/farcaster.json index 6658002c..16d03f8d 100644 --- a/public/.well-known/farcaster.json +++ b/public/.well-known/farcaster.json @@ -7,7 +7,7 @@ "miniapp": { "version": "1", "name": "PlotLink", - "iconUrl": "https://plotlink.xyz/favicon.ico", + "iconUrl": "https://plotlink.xyz/icon.png", "homeUrl": "https://plotlink.xyz", "splashBackgroundColor": "#0a0a0a", "description": "Tokenise your story from day 1. Publish plots, drive trading, earn royalties from every trade.", diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2c704fb43a7fa4003ad380f1a2717efa4ff791a6 GIT binary patch literal 1324 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>f#tKOi(^Q|oVRx!`>uNNv|W_@ z{wji9J2CSOSIZF=m;dI=CKsvNH5>1KG*!0#Qu2}8zkmPce{lcaE_nuPVGb6iMh67} zT2jYYKYe>F&0bo+T9$*Qk;6rSNwC9#MM(h6yy^6P`>jR?0fY!xa)NFN0p;g-msGPA z>)lEI&MebsS*|vrUGCfzOa1#);)iiPdHegi{{9mCTMuUx-<0G3|Djx_{F+;Y=;u48IHA^O=H@-qAMNFO zvJj-qdH0Vad*?}cd{>e=x5v_c&i^H|MNc^M19ixs+pjf2{qOs$OAgO8dfxtde#!Rb zte5r*v>O+n+rivuG3RCRFi>o><1eb5!6<|1?McdqJpze^2tIhdwhuU(mBFJk3=3 z0>Wl_RIuz^WrjFVfADjgE!;r;D~siBwF32DvE+a21l0d(PJ5Z+gsttyuHUx{bvSIh z_IE9d(uB38_1cXbDcN`Ldnqv8jDkz$$$NZ%z$AEL=EOO2CuEgh+U>G_`7ccSFxte(j+C~|@4lrfO#L76J&5nD(geYCyCNGoo=n`v^Mns5 zE7uq@O?ARWJ!Jh<&Uu2Fx-_e+?Yw@;sjJ+eGq>9HG9pr Tw2L2rr67Z+tDnm{r-UW|tUMqm literal 0 HcmV?d00001