Skip to content

Instantly share code, notes, and snippets.

@Th3Ya0vi
Last active December 22, 2025 17:17
Show Gist options
  • Select an option

  • Save Th3Ya0vi/b10ee671f7f0e1be0adb3bbd2fe0d2e9 to your computer and use it in GitHub Desktop.

Select an option

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
/**
* ============================================================================
* 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