From d18931c836b703cf7e284c0efaf5d866a75eccb2 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sun, 15 Mar 2026 15:35:40 +0000 Subject: [PATCH 1/2] [#37] Add Farcaster wallet connector with auto-detection Custom wagmi v3 connector wrapping sdk.wallet.getEthereumProvider() from @farcaster/miniapp-sdk. Auto-connects when inside a Farcaster Mini App, falls back to injected wallet outside Farcaster. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/farcaster-connector.ts | 137 +++++++++++++++++++++++++++++++ lib/wagmi.ts | 3 +- src/components/ConnectWallet.tsx | 32 +++++++- 3 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 lib/farcaster-connector.ts diff --git a/lib/farcaster-connector.ts b/lib/farcaster-connector.ts new file mode 100644 index 00000000..a0ae21cf --- /dev/null +++ b/lib/farcaster-connector.ts @@ -0,0 +1,137 @@ +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/wagmi.ts b/lib/wagmi.ts index d2cfc4f7..c7def4fe 100644 --- a/lib/wagmi.ts +++ b/lib/wagmi.ts @@ -1,10 +1,11 @@ import { http, createConfig } from "wagmi"; import { base, baseSepolia } from "wagmi/chains"; import { injected } from "wagmi/connectors"; +import { farcaster } from "./farcaster-connector"; export const config = createConfig({ chains: [base, baseSepolia], - connectors: [injected()], + connectors: [farcaster(), injected()], transports: { [base.id]: http( process.env.NEXT_PUBLIC_CHAIN_ID === "8453" diff --git a/src/components/ConnectWallet.tsx b/src/components/ConnectWallet.tsx index c8b53100..e9e8dec9 100644 --- a/src/components/ConnectWallet.tsx +++ b/src/components/ConnectWallet.tsx @@ -1,13 +1,31 @@ "use client"; +import { useEffect, useRef } from "react"; import { useAccount, useConnect, useDisconnect } from "wagmi"; -import { injected } from "wagmi/connectors"; import { truncateAddress } from "../../lib/utils"; export function ConnectWallet() { const { address, isConnected } = useAccount(); - const { connect, isPending } = useConnect(); + const { connect, connectors, isPending } = useConnect(); const { disconnect } = useDisconnect(); + const autoConnectAttempted = useRef(false); + + // Auto-connect with the Farcaster connector when inside a mini app + useEffect(() => { + if (autoConnectAttempted.current || isConnected) return; + autoConnectAttempted.current = true; + + const farcasterConnector = connectors.find((c) => c.type === "farcaster"); + if (!farcasterConnector) return; + + // Only auto-connect if the Farcaster connector reports it's authorized + // (i.e. we're inside a Farcaster client with an active wallet) + farcasterConnector.isAuthorized().then((authorized) => { + if (authorized) { + connect({ connector: farcasterConnector }); + } + }); + }, [connectors, connect, isConnected]); if (isConnected && address) { return ( @@ -27,7 +45,15 @@ export function ConnectWallet() { return (