Last active
December 22, 2025 17:17
-
-
Save Th3Ya0vi/b10ee671f7f0e1be0adb3bbd2fe0d2e9 to your computer and use it in GitHub Desktop.
USDC/SPL Token Transfer with Phantom Embedded Wallet SDK - Example for transferring USDC on Solana
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * ============================================================================ | |
| * USDC/SPL Token Transfer with Phantom Embedded Wallet SDK | |
| * ============================================================================ | |
| * | |
| * This example shows how to transfer USDC (or any SPL token) using the | |
| * Phantom React SDK for embedded wallets. | |
| * | |
| * ⚠️ VERIFIED APPROACH: This uses the standard @solana/spl-token library | |
| * with createTransferInstruction. The transaction is built client-side | |
| * and signed by Phantom's embedded wallet - this is the correct pattern. | |
| * | |
| * Use Cases: | |
| * 1. User buys ticket with USDC → transfer from user wallet to your wallet | |
| * 2. Distribute awards → transfer from your wallet to users (see server example) | |
| * | |
| * Prerequisites: | |
| * npm install @solana/spl-token@^0.4.0 @solana/web3.js@^1.95.0 @phantom/react-sdk@^1.0.0 | |
| * | |
| * Docs: https://docs.phantom.com/sdks/react-sdk/sign-and-send-transaction | |
| * ============================================================================ | |
| */ | |
| "use client"; | |
| import { useState, useCallback } from "react"; | |
| import { useSolana, usePhantom } from "@phantom/react-sdk"; | |
| import { | |
| Connection, | |
| PublicKey, | |
| Transaction, | |
| TransactionInstruction, | |
| } from "@solana/web3.js"; | |
| import { | |
| getAssociatedTokenAddress, | |
| createAssociatedTokenAccountInstruction, | |
| createTransferInstruction, | |
| getAccount, | |
| TokenAccountNotFoundError, | |
| TokenInvalidAccountOwnerError, | |
| } from "@solana/spl-token"; | |
| // ============================================================================ | |
| // CONFIGURATION | |
| // ============================================================================ | |
| /** | |
| * USDC Mint Addresses | |
| * | |
| * Mainnet USDC: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v | |
| * | |
| * For testing on devnet, you can: | |
| * 1. Use the devnet faucet: https://spl-token-faucet.com/ | |
| * 2. Or create your own test token | |
| */ | |
| const USDC_MINT = { | |
| mainnet: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), | |
| devnet: new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"), // Common devnet USDC | |
| }; | |
| // USDC has 6 decimals (like cents: 1 USDC = 1,000,000 units) | |
| const USDC_DECIMALS = 6; | |
| // Your RPC endpoint - use a dedicated RPC for production (Helius, QuickNode, etc.) | |
| const RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"; | |
| // ============================================================================ | |
| // HELPER FUNCTIONS | |
| // ============================================================================ | |
| /** Convert human-readable USDC amount to token units */ | |
| function toTokenUnits(amount: number, decimals: number = USDC_DECIMALS): bigint { | |
| return BigInt(Math.round(amount * Math.pow(10, decimals))); | |
| } | |
| /** Check if token account exists */ | |
| async function tokenAccountExists(connection: Connection, address: PublicKey): Promise<boolean> { | |
| try { | |
| await getAccount(connection, address); | |
| return true; | |
| } catch (error) { | |
| // These specific errors mean the account doesn't exist yet | |
| if (error instanceof TokenAccountNotFoundError || | |
| error instanceof TokenInvalidAccountOwnerError) { | |
| return false; | |
| } | |
| throw error; // Re-throw unexpected errors | |
| } | |
| } | |
| // ============================================================================ | |
| // CORE TRANSFER FUNCTION | |
| // ============================================================================ | |
| /** | |
| * Create a transaction to transfer SPL tokens (USDC) between wallets | |
| * | |
| * @param connection - Solana RPC connection | |
| * @param sender - Sender's public key | |
| * @param recipient - Recipient's wallet address | |
| * @param amount - Amount in token units (e.g., 5.00 for $5 USDC) | |
| * @param mint - Token mint address (defaults to USDC mainnet) | |
| * @returns Transaction ready for signing | |
| */ | |
| export async function createTokenTransferTransaction( | |
| connection: Connection, | |
| sender: PublicKey, | |
| recipient: string | PublicKey, | |
| amount: number, | |
| mint: PublicKey = USDC_MINT.mainnet | |
| ): Promise<Transaction> { | |
| const recipientPubkey = typeof recipient === "string" ? new PublicKey(recipient) : recipient; | |
| // Get Associated Token Accounts (ATAs) | |
| const senderATA = await getAssociatedTokenAddress(mint, sender); | |
| const recipientATA = await getAssociatedTokenAddress(mint, recipientPubkey); | |
| const instructions: TransactionInstruction[] = []; | |
| // If recipient doesn't have a token account, create one (sender pays ~0.002 SOL) | |
| if (!(await tokenAccountExists(connection, recipientATA))) { | |
| instructions.push( | |
| createAssociatedTokenAccountInstruction( | |
| sender, // payer | |
| recipientATA, // token account to create | |
| recipientPubkey, // owner of new account | |
| mint // token mint | |
| ) | |
| ); | |
| } | |
| // Add transfer instruction | |
| instructions.push( | |
| createTransferInstruction( | |
| senderATA, // from | |
| recipientATA, // to | |
| sender, // authority (signer) | |
| toTokenUnits(amount) | |
| ) | |
| ); | |
| // Build transaction | |
| const { blockhash } = await connection.getLatestBlockhash(); | |
| const transaction = new Transaction().add(...instructions); | |
| transaction.recentBlockhash = blockhash; | |
| transaction.feePayer = sender; | |
| return transaction; | |
| } | |
| // ============================================================================ | |
| // REACT HOOK FOR EASY INTEGRATION | |
| // ============================================================================ | |
| export function useUSDCTransfer() { | |
| const { solana, isAvailable } = useSolana(); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const transfer = useCallback(async ( | |
| recipientAddress: string, | |
| amount: number, | |
| options?: { mint?: PublicKey; network?: "mainnet" | "devnet" } | |
| ) => { | |
| if (!isAvailable || !solana) { | |
| throw new Error("Wallet not available"); | |
| } | |
| setIsLoading(true); | |
| setError(null); | |
| try { | |
| const publicKey = await solana.getPublicKey(); | |
| if (!publicKey) throw new Error("Wallet not connected"); | |
| const connection = new Connection(RPC_URL); | |
| const mint = options?.mint || USDC_MINT[options?.network || "mainnet"]; | |
| const transaction = await createTokenTransferTransaction( | |
| connection, | |
| new PublicKey(publicKey), | |
| recipientAddress, | |
| amount, | |
| mint | |
| ); | |
| const result = await solana.signAndSendTransaction(transaction); | |
| return result.signature; | |
| } catch (err) { | |
| const message = err instanceof Error ? err.message : "Transfer failed"; | |
| setError(message); | |
| throw err; | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }, [solana, isAvailable]); | |
| return { transfer, isLoading, error }; | |
| } | |
| // ============================================================================ | |
| // EXAMPLE COMPONENT | |
| // ============================================================================ | |
| interface Props { | |
| merchantWallet: string; | |
| ticketPrice?: number; | |
| } | |
| export default function USDCPayment({ merchantWallet, ticketPrice = 10 }: Props) { | |
| const { isConnected } = usePhantom(); | |
| const { transfer, isLoading, error } = useUSDCTransfer(); | |
| const [success, setSuccess] = useState<string | null>(null); | |
| const handlePayment = async () => { | |
| try { | |
| const signature = await transfer(merchantWallet, ticketPrice); | |
| setSuccess(`Payment successful! TX: ${signature?.slice(0, 20)}...`); | |
| } catch { | |
| // Error handled by hook | |
| } | |
| }; | |
| if (!isConnected) return <p>Please connect your wallet</p>; | |
| return ( | |
| <div className="p-4 space-y-4"> | |
| <h2 className="text-xl font-bold">Pay with USDC</h2> | |
| <div className="p-3 bg-gray-100 rounded"> | |
| <p>Amount: <strong>${ticketPrice} USDC</strong></p> | |
| <p className="text-sm text-gray-600">To: {merchantWallet.slice(0, 8)}...</p> | |
| </div> | |
| <button | |
| onClick={handlePayment} | |
| disabled={isLoading} | |
| className="w-full py-3 px-4 bg-blue-600 text-white rounded-lg disabled:opacity-50" | |
| > | |
| {isLoading ? "Processing..." : `Pay $${ticketPrice} USDC`} | |
| </button> | |
| {error && <p className="text-red-600">{error}</p>} | |
| {success && <p className="text-green-600">{success}</p>} | |
| </div> | |
| ); | |
| } | |
| // ============================================================================ | |
| // USAGE EXAMPLES | |
| // ============================================================================ | |
| /* | |
| * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | |
| * EXAMPLE 1: Simple Payment Button | |
| * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | |
| * | |
| * import USDCPayment from './USDCPayment'; | |
| * | |
| * function TicketPage() { | |
| * return ( | |
| * <USDCPayment | |
| * merchantWallet="YourWalletAddressHere" | |
| * ticketPrice={25} | |
| * /> | |
| * ); | |
| * } | |
| */ | |
| /* | |
| * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | |
| * EXAMPLE 2: Custom Integration with Hook | |
| * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | |
| * | |
| * function CheckoutPage() { | |
| * const { transfer, isLoading } = useUSDCTransfer(); | |
| * | |
| * async function handleCheckout(cart: CartItem[]) { | |
| * const total = cart.reduce((sum, item) => sum + item.price, 0); | |
| * | |
| * const signature = await transfer("MerchantWalletAddress", total); | |
| * | |
| * // Save to your backend | |
| * await saveOrder({ cartItems: cart, txSignature: signature }); | |
| * } | |
| * | |
| * return <button onClick={() => handleCheckout(cart)}>Checkout</button>; | |
| * } | |
| */ | |
| /* | |
| * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | |
| * EXAMPLE 3: Award Distribution (SERVER-SIDE) | |
| * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | |
| * | |
| * For sending USDC FROM your wallet TO users (awards/payouts), | |
| * you need server-side signing - NOT the embedded wallet SDK. | |
| * | |
| * // api/distribute-award.ts (Next.js API route) | |
| * import { Keypair, Connection, sendAndConfirmTransaction } from "@solana/web3.js"; | |
| * import bs58 from "bs58"; | |
| * | |
| * export async function POST(req: Request) { | |
| * const { userWallet, amount } = await req.json(); | |
| * | |
| * // Load YOUR wallet's private key (keep secret in env!) | |
| * const merchantKeypair = Keypair.fromSecretKey( | |
| * bs58.decode(process.env.MERCHANT_PRIVATE_KEY!) | |
| * ); | |
| * | |
| * const connection = new Connection(process.env.SOLANA_RPC_URL!); | |
| * | |
| * // Use same createTokenTransferTransaction function | |
| * const transaction = await createTokenTransferTransaction( | |
| * connection, | |
| * merchantKeypair.publicKey, | |
| * userWallet, | |
| * amount | |
| * ); | |
| * | |
| * // Sign with your private key and send | |
| * const signature = await sendAndConfirmTransaction( | |
| * connection, | |
| * transaction, | |
| * [merchantKeypair] | |
| * ); | |
| * | |
| * return Response.json({ signature }); | |
| * } | |
| */ | |
| /* | |
| * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | |
| * EXAMPLE 4: Using with ANY SPL Token (not just USDC) | |
| * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | |
| * | |
| * const BONK_MINT = new PublicKey("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"); | |
| * | |
| * // Transfer 1000 BONK | |
| * const transaction = await createTokenTransferTransaction( | |
| * connection, | |
| * senderPubkey, | |
| * recipientAddress, | |
| * 1000, | |
| * BONK_MINT | |
| * ); | |
| * | |
| * await solana.signAndSendTransaction(transaction); | |
| */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment