refactor: restructure for better ai comprehension

This commit is contained in:
veganbeef
2025-07-01 09:38:59 -07:00
parent 3a9f2cf8b8
commit bef42eddd4
24 changed files with 1636 additions and 793 deletions

View File

@@ -0,0 +1,102 @@
"use client";
import { useCallback, useMemo } from "react";
import { useAccount, useSendTransaction, useWaitForTransactionReceipt } from "wagmi";
import { base } from "wagmi/chains";
import { Button } from "../Button";
import { truncateAddress } from "../../../lib/truncateAddress";
import { renderError } from "../../../lib/errorUtils";
/**
* SendEth component handles sending ETH transactions to protocol guild addresses.
*
* This component provides a simple interface for users to send small amounts
* of ETH to protocol guild addresses. It automatically selects the appropriate
* recipient address based on the current chain and displays transaction status.
*
* Features:
* - Chain-specific recipient addresses
* - Transaction status tracking
* - Error handling and display
* - Transaction hash display
*
* @example
* ```tsx
* <SendEth />
* ```
*/
export function SendEth() {
// --- Hooks ---
const { isConnected, chainId } = useAccount();
const {
sendTransaction,
data: ethTransactionHash,
error: ethTransactionError,
isError: isEthTransactionError,
isPending: isEthTransactionPending,
} = useSendTransaction();
const { isLoading: isEthTransactionConfirming, isSuccess: isEthTransactionConfirmed } =
useWaitForTransactionReceipt({
hash: ethTransactionHash,
});
// --- Computed Values ---
/**
* Determines the recipient address based on the current chain.
*
* Uses different protocol guild addresses for different chains:
* - Base: 0x32e3C7fD24e175701A35c224f2238d18439C7dBC
* - Other chains: 0xB3d8d7887693a9852734b4D25e9C0Bb35Ba8a830
*
* @returns string - The recipient address for the current chain
*/
const protocolGuildRecipientAddress = useMemo(() => {
// Protocol guild address
return chainId === base.id
? "0x32e3C7fD24e175701A35c224f2238d18439C7dBC"
: "0xB3d8d7887693a9852734b4D25e9C0Bb35Ba8a830";
}, [chainId]);
// --- Handlers ---
/**
* Handles sending the ETH transaction.
*
* This function sends a small amount of ETH (1 wei) to the protocol guild
* address for the current chain. The transaction is sent using the wagmi
* sendTransaction hook.
*/
const sendEthTransaction = useCallback(() => {
sendTransaction({
to: protocolGuildRecipientAddress,
value: 1n,
});
}, [protocolGuildRecipientAddress, sendTransaction]);
// --- Render ---
return (
<>
<Button
onClick={sendEthTransaction}
disabled={!isConnected || isEthTransactionPending}
isLoading={isEthTransactionPending}
>
Send Transaction (eth)
</Button>
{isEthTransactionError && renderError(ethTransactionError)}
{ethTransactionHash && (
<div className="mt-2 text-xs">
<div>Hash: {truncateAddress(ethTransactionHash)}</div>
<div>
Status:{" "}
{isEthTransactionConfirming
? "Confirming..."
: isEthTransactionConfirmed
? "Confirmed!"
: "Pending"}
</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,111 @@
"use client";
import { useCallback, useState } from "react";
import { useConnection as useSolanaConnection, useWallet as useSolanaWallet } from '@solana/wallet-adapter-react';
import { PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
import { Button } from "../Button";
import { truncateAddress } from "../../../lib/truncateAddress";
import { renderError } from "../../../lib/errorUtils";
/**
* SendSolana component handles sending SOL transactions on Solana.
*
* This component provides a simple interface for users to send SOL transactions
* using their connected Solana wallet. It includes transaction status tracking
* and error handling.
*
* Features:
* - SOL transaction sending
* - Transaction status tracking
* - Error handling and display
* - Loading state management
*
* Note: This component is a placeholder implementation. In a real application,
* you would integrate with a Solana wallet adapter and transaction library
* like @solana/web3.js to handle actual transactions.
*
* @example
* ```tsx
* <SendSolana />
* ```
*/
export function SendSolana() {
const [solanaTransactionState, setSolanaTransactionState] = useState<
| { status: 'none' }
| { status: 'pending' }
| { status: 'error'; error: Error }
| { status: 'success'; signature: string }
>({ status: 'none' });
const { connection: solanaConnection } = useSolanaConnection();
const { sendTransaction, publicKey } = useSolanaWallet();
// This should be replaced but including it from the original demo
// https://github.com/farcasterxyz/frames-v2-demo/blob/main/src/components/Demo.tsx#L718
const ashoatsPhantomSolanaWallet = 'Ao3gLNZAsbrmnusWVqQCPMrcqNi6jdYgu8T6NCoXXQu1';
/**
* Handles sending the Solana transaction
*/
const sendSolanaTransaction = useCallback(async () => {
setSolanaTransactionState({ status: 'pending' });
try {
if (!publicKey) {
throw new Error('no Solana publicKey');
}
const { blockhash } = await solanaConnection.getLatestBlockhash();
if (!blockhash) {
throw new Error('failed to fetch latest Solana blockhash');
}
const fromPubkeyStr = publicKey.toBase58();
const toPubkeyStr = ashoatsPhantomSolanaWallet;
const transaction = new Transaction();
transaction.add(
SystemProgram.transfer({
fromPubkey: new PublicKey(fromPubkeyStr),
toPubkey: new PublicKey(toPubkeyStr),
lamports: 0n,
}),
);
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(fromPubkeyStr);
const simulation = await solanaConnection.simulateTransaction(transaction);
if (simulation.value.err) {
// Gather logs and error details for debugging
const logs = simulation.value.logs?.join('\n') ?? 'No logs';
const errDetail = JSON.stringify(simulation.value.err);
throw new Error(`Simulation failed: ${errDetail}\nLogs:\n${logs}`);
}
const signature = await sendTransaction(transaction, solanaConnection);
setSolanaTransactionState({ status: 'success', signature });
} catch (e) {
if (e instanceof Error) {
setSolanaTransactionState({ status: 'error', error: e });
} else {
setSolanaTransactionState({ status: 'none' });
}
}
}, [sendTransaction, publicKey, solanaConnection]);
return (
<>
<Button
onClick={sendSolanaTransaction}
disabled={solanaTransactionState.status === 'pending'}
isLoading={solanaTransactionState.status === 'pending'}
className="mb-4"
>
Send Transaction (sol)
</Button>
{solanaTransactionState.status === 'error' && renderError(solanaTransactionState.error)}
{solanaTransactionState.status === 'success' && (
<div className="mt-2 text-xs">
<div>Hash: {truncateAddress(solanaTransactionState.signature)}</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,81 @@
"use client";
import { useCallback } from "react";
import { useAccount, useConnect, useSignMessage } from "wagmi";
import { base } from "wagmi/chains";
import { Button } from "../Button";
import { config } from "../../providers/WagmiProvider";
import { APP_NAME } from "../../../lib/constants";
import { renderError } from "../../../lib/errorUtils";
/**
* SignEvmMessage component handles signing messages on EVM-compatible chains.
*
* This component provides a simple interface for users to sign messages using
* their connected EVM wallet. It automatically handles wallet connection if
* the user is not already connected, and displays the signature result.
*
* Features:
* - Automatic wallet connection if needed
* - Message signing with app name
* - Error handling and display
* - Signature result display
*
* @example
* ```tsx
* <SignEvmMessage />
* ```
*/
export function SignEvmMessage() {
// --- Hooks ---
const { isConnected } = useAccount();
const { connectAsync } = useConnect();
const {
signMessage,
data: evmMessageSignature,
error: evmSignMessageError,
isError: isEvmSignMessageError,
isPending: isEvmSignMessagePending,
} = useSignMessage();
// --- Handlers ---
/**
* Handles the message signing process.
*
* This function first ensures the user is connected to an EVM wallet,
* then requests them to sign a message containing the app name.
* If the user is not connected, it automatically connects using the
* Farcaster Frame connector.
*
* @returns Promise<void>
*/
const signEvmMessage = useCallback(async () => {
if (!isConnected) {
await connectAsync({
chainId: base.id,
connector: config.connectors[0],
});
}
signMessage({ message: `Hello from ${APP_NAME}!` });
}, [connectAsync, isConnected, signMessage]);
// --- Render ---
return (
<>
<Button
onClick={signEvmMessage}
disabled={isEvmSignMessagePending}
isLoading={isEvmSignMessagePending}
>
Sign Message
</Button>
{isEvmSignMessageError && renderError(evmSignMessageError)}
{evmMessageSignature && (
<div className="mt-2 text-xs">
<div>Signature: {evmMessageSignature}</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,158 @@
"use client";
import { useCallback, useState } from "react";
import { signIn, signOut, getCsrfToken } from "next-auth/react";
import sdk, { SignIn as SignInCore } from "@farcaster/frame-sdk";
import { useSession } from "next-auth/react";
import { Button } from "../Button";
/**
* SignIn component handles Farcaster authentication using Sign-In with Farcaster (SIWF).
*
* This component provides a complete authentication flow for Farcaster users:
* - Generates nonces for secure authentication
* - Handles the SIWF flow using the Farcaster SDK
* - Manages NextAuth session state
* - Provides sign-out functionality
* - Displays authentication status and results
*
* The component integrates with both the Farcaster Frame SDK and NextAuth
* to provide seamless authentication within mini apps.
*
* @example
* ```tsx
* <SignIn />
* ```
*/
interface AuthState {
signingIn: boolean;
signingOut: boolean;
}
export function SignIn() {
// --- State ---
const [authState, setAuthState] = useState<AuthState>({
signingIn: false,
signingOut: false,
});
const [signInResult, setSignInResult] = useState<SignInCore.SignInResult>();
const [signInFailure, setSignInFailure] = useState<string>();
// --- Hooks ---
const { data: session, status } = useSession();
// --- Handlers ---
/**
* Generates a nonce for the sign-in process.
*
* This function retrieves a CSRF token from NextAuth to use as a nonce
* for the SIWF authentication flow. The nonce ensures the authentication
* request is fresh and prevents replay attacks.
*
* @returns Promise<string> - The generated nonce token
* @throws Error if unable to generate nonce
*/
const getNonce = useCallback(async () => {
const nonce = await getCsrfToken();
if (!nonce) throw new Error("Unable to generate nonce");
return nonce;
}, []);
/**
* Handles the sign-in process using Farcaster SDK.
*
* This function orchestrates the complete SIWF flow:
* 1. Generates a nonce for security
* 2. Calls the Farcaster SDK to initiate sign-in
* 3. Submits the result to NextAuth for session management
* 4. Handles various error conditions including user rejection
*
* @returns Promise<void>
*/
const handleSignIn = useCallback(async () => {
try {
setAuthState((prev) => ({ ...prev, signingIn: true }));
setSignInFailure(undefined);
const nonce = await getNonce();
const result = await sdk.actions.signIn({ nonce });
setSignInResult(result);
await signIn("credentials", {
message: result.message,
signature: result.signature,
redirect: false,
});
} catch (e) {
if (e instanceof SignInCore.RejectedByUser) {
setSignInFailure("Rejected by user");
return;
}
setSignInFailure("Unknown error");
} finally {
setAuthState((prev) => ({ ...prev, signingIn: false }));
}
}, [getNonce]);
/**
* Handles the sign-out process.
*
* This function clears the NextAuth session and resets the local
* sign-in result state to complete the sign-out flow.
*
* @returns Promise<void>
*/
const handleSignOut = useCallback(async () => {
try {
setAuthState((prev) => ({ ...prev, signingOut: true }));
await signOut({ redirect: false });
setSignInResult(undefined);
} finally {
setAuthState((prev) => ({ ...prev, signingOut: false }));
}
}, []);
// --- Render ---
return (
<>
{/* Authentication Buttons */}
{status !== "authenticated" && (
<Button onClick={handleSignIn} disabled={authState.signingIn}>
Sign In with Farcaster
</Button>
)}
{status === "authenticated" && (
<Button onClick={handleSignOut} disabled={authState.signingOut}>
Sign out
</Button>
)}
{/* Session Information */}
{session && (
<div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 rounded-lg font-mono">
<div className="font-semibold text-gray-500 mb-1">Session</div>
<div className="whitespace-pre">
{JSON.stringify(session, null, 2)}
</div>
</div>
)}
{/* Error Display */}
{signInFailure && !authState.signingIn && (
<div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 rounded-lg font-mono">
<div className="font-semibold text-gray-500 mb-1">SIWF Result</div>
<div className="whitespace-pre">{signInFailure}</div>
</div>
)}
{/* Success Result Display */}
{signInResult && !authState.signingIn && (
<div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 rounded-lg font-mono">
<div className="font-semibold text-gray-500 mb-1">SIWF Result</div>
<div className="whitespace-pre">
{JSON.stringify(signInResult, null, 2)}
</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,87 @@
"use client";
import { useCallback, useState } from "react";
import { Button } from "../Button";
import { renderError } from "../../../lib/errorUtils";
interface SignSolanaMessageProps {
signMessage?: (message: Uint8Array) => Promise<Uint8Array>;
}
/**
* SignSolanaMessage component handles signing messages on Solana.
*
* This component provides a simple interface for users to sign messages using
* their connected Solana wallet. It accepts a signMessage function as a prop
* and handles the complete signing flow including error handling.
*
* Features:
* - Message signing with Solana wallet
* - Error handling and display
* - Signature result display (base64 encoded)
* - Loading state management
*
* @param props - Component props
* @param props.signMessage - Function to sign messages with Solana wallet
*
* @example
* ```tsx
* <SignSolanaMessage signMessage={solanaWallet.signMessage} />
* ```
*/
export function SignSolanaMessage({ signMessage }: SignSolanaMessageProps) {
// --- State ---
const [signature, setSignature] = useState<string | undefined>();
const [signError, setSignError] = useState<Error | undefined>();
const [signPending, setSignPending] = useState(false);
// --- Handlers ---
/**
* Handles the Solana message signing process.
*
* This function encodes a message as UTF-8 bytes, signs it using the provided
* signMessage function, and displays the base64-encoded signature result.
* It includes comprehensive error handling and loading state management.
*
* @returns Promise<void>
*/
const handleSignMessage = useCallback(async () => {
setSignPending(true);
try {
if (!signMessage) {
throw new Error('no Solana signMessage');
}
const input = new TextEncoder().encode("Hello from Solana!");
const signatureBytes = await signMessage(input);
const signature = btoa(String.fromCharCode(...signatureBytes));
setSignature(signature);
setSignError(undefined);
} catch (e) {
if (e instanceof Error) {
setSignError(e);
}
} finally {
setSignPending(false);
}
}, [signMessage]);
// --- Render ---
return (
<>
<Button
onClick={handleSignMessage}
disabled={signPending}
isLoading={signPending}
className="mb-4"
>
Sign Message
</Button>
{signError && renderError(signError)}
{signature && (
<div className="mt-2 text-xs">
<div>Signature: {signature}</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,5 @@
export { SignIn } from './SignIn';
export { SignEvmMessage } from './SignEvmMessage';
export { SendEth } from './SendEth';
export { SignSolanaMessage } from './SignSolanaMessage';
export { SendSolana } from './SendSolana';