Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions lib/farcaster-connector.ts
Original file line number Diff line number Diff line change
@@ -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<EIP1193Provider> {
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<boolean> {
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<EIP1193Provider>((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");
},
}));
}
3 changes: 2 additions & 1 deletion lib/wagmi.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
38 changes: 35 additions & 3 deletions src/components/ConnectWallet.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
"use client";

import { useEffect, useRef, useState } from "react";
import { useAccount, useConnect, useDisconnect } from "wagmi";
import { injected } from "wagmi/connectors";
import { isFarcasterMiniApp } from "../../lib/farcaster-connector";
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);
const [inMiniApp, setInMiniApp] = useState(false);

// Detect Farcaster mini app context once on mount
useEffect(() => {
isFarcasterMiniApp().then(setInMiniApp);
}, []);

// Auto-connect with the Farcaster connector when inside a mini app
useEffect(() => {
if (!inMiniApp) return;
if (autoConnectAttempted.current || isConnected) return;
autoConnectAttempted.current = true;

const farcasterConnector = connectors.find((c) => c.type === "farcaster");
if (!farcasterConnector) return;

farcasterConnector.isAuthorized().then((authorized) => {
if (authorized) {
connect({ connector: farcasterConnector });
}
});
}, [inMiniApp, connectors, connect, isConnected]);

if (isConnected && address) {
return (
Expand All @@ -27,7 +51,15 @@ export function ConnectWallet() {

return (
<button
onClick={() => connect({ connector: injected() })}
onClick={() => {
// Use Farcaster connector only when confirmed inside a mini app
const farcasterConnector = inMiniApp
? connectors.find((c) => c.type === "farcaster")
: undefined;
const fallback = connectors.find((c) => c.type === "injected");
const connector = farcasterConnector ?? fallback;
if (connector) connect({ connector });
}}
disabled={isPending}
className="border-accent text-accent hover:bg-accent hover:text-background rounded border px-4 py-2 text-sm transition-colors disabled:opacity-50"
>
Expand Down
Loading