Skip to main content

Overview

This guide covers Solana-specific implementation details for delegating wallets. Solana uses a different delegation model than EVM chains, leveraging SPL Token’s built-in delegation functionality.
Use the /v1/delegation/solana/post-approval endpoint for Solana delegations.

Solana Network

Network Name: Solana Mainnet Beta Currencies: USDC, USDT Block Explorer: https://explorer.solana.com RPC URL: https://api.mainnet-beta.solana.com Advantages:
  • Extremely low transaction fees (~$0.00025 per transaction)
  • Fast block times (~400ms)
  • High throughput (65,000+ TPS capability)
  • No gas price fluctuations
Transaction Cost: Typically less than $0.001

SPL Token Delegation Model

Unlike EVM’s ERC20 approval pattern, Solana uses SPL Token’s native delegation system.

How It Works

1

User Has Token Account

User has an Associated Token Account (ATA) for their USDC/USDT
2

Approve Delegate

User calls SPL Token’s Approve instruction, setting a delegate with spending limit
3

Delegate Can Transfer

Platform’s delegate address can call Transfer on behalf of the user, up to approved amount
4

Allowance Decreases

Each transfer decreases the delegated amount automatically

Key Differences from EVM

AspectEVM (ERC20)Solana (SPL Token)
Approval TargetSmart contract addressDelegate public key
Account ModelSingle wallet addressToken accounts per token
Delegation StorageToken contract stateToken account data
Fee ModelVariable gasFixed ~0.000005 SOL
Confirmation Time12-15 seconds (Ethereum), 2-3 seconds (Linea)~400ms

Implementation

Prerequisites

Install required packages:
npm install @solana/web3.js @solana/spl-token @solana/wallet-adapter-react @solana/wallet-adapter-wallets

Token Mint Addresses

const SOLANA_TOKENS = {
  usdc: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC on Solana
  usdt: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'  // USDT on Solana
};

const PLATFORM_DELEGATE = 'YourPlatformDelegateAddress...'; // Provided by platform
Always verify token mint addresses are correct. Solana has multiple USDC/USDT versions (native, bridged, etc.).

Complete Solana Delegation Flow

import {
  Connection,
  PublicKey,
  Transaction,
  TransactionInstruction
} from '@solana/web3.js';
import {
  createApproveInstruction,
  getAssociatedTokenAddress,
  TOKEN_PROGRAM_ID
} from '@solana/spl-token';

interface SolanaDelegationParams {
  currency: 'usdc' | 'usdt';
  amount: string;
  mintAddress: string;
  delegateAddress: string;
  delegationToken: string;
}

async function delegateSolanaWallet(
  wallet: any, // Wallet adapter instance
  params: SolanaDelegationParams
): Promise<{
  address: string;
  network: 'solana';
  currency: string;
  amount: string;
  txHash: string;
  sigHash: string;
  sigMessage: string;
  token: string;
}> {

  if (!wallet.connected || !wallet.publicKey) {
    throw new Error('Wallet not connected. Please connect a Solana wallet.');
  }

  const connection = new Connection(
    'https://api.mainnet-beta.solana.com',
    'confirmed'
  );

  const userPublicKey = wallet.publicKey;
  const mintPublicKey = new PublicKey(params.mintAddress);
  const delegatePublicKey = new PublicKey(params.delegateAddress);

  console.log(`User wallet: ${userPublicKey.toBase58()}`);

  const tokenAccount = await getAssociatedTokenAddress(
    mintPublicKey,
    userPublicKey
  );

  console.log(`Token account: ${tokenAccount.toBase58()}`);

  const accountInfo = await connection.getAccountInfo(tokenAccount);
  if (!accountInfo) {
    throw new Error(
      `Token account not found. Ensure you have ${params.currency.toUpperCase()} in your wallet.`
    );
  }

  const decimals = 6; // USDC and USDT use 6 decimals
  const amountLamports = BigInt(parseFloat(params.amount) * Math.pow(10, decimals));

  console.log(
    `Creating approval for ${params.amount} ${params.currency.toUpperCase()}`
  );

  const approveInstruction = createApproveInstruction(
    tokenAccount,
    delegatePublicKey,
    userPublicKey,
    amountLamports,
    [],
    TOKEN_PROGRAM_ID
  );

  const transaction = new Transaction().add(approveInstruction);
  transaction.feePayer = userPublicKey;

  const { blockhash, lastValidBlockHeight } =
    await connection.getLatestBlockhash();
  transaction.recentBlockhash = blockhash;

  console.log('Requesting signature from wallet...');

  const signedTransaction = await wallet.signTransaction(transaction);

  console.log('Sending transaction...');

  const signature = await connection.sendRawTransaction(
    signedTransaction.serialize()
  );

  console.log('Transaction sent:', signature);
  console.log('Waiting for confirmation...');

  await connection.confirmTransaction({
    signature,
    blockhash,
    lastValidBlockHeight
  });

  console.log('Transaction confirmed!');

  const message = 'Prove wallet ownership for delegation';
  const messageBytes = new TextEncoder().encode(message);

  console.log('Requesting message signature...');

  const messageSignature = await wallet.signMessage(messageBytes);

  const sigHash = Buffer.from(messageSignature).toString('base64');

  return {
    address: userPublicKey.toBase58(),
    network: 'solana',
    currency: params.currency,
    amount: params.amount,
    txHash: signature,
    sigHash,
    sigMessage: message,
    token: params.delegationToken
  };
}

Wallet Adapter Setup

Set up Solana wallet adapter to support multiple wallets:
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
import {
  ConnectionProvider,
  WalletProvider
} from '@solana/wallet-adapter-react';
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
import {
  PhantomWalletAdapter,
  SolflareWalletAdapter,
  BackpackWalletAdapter
} from '@solana/wallet-adapter-wallets';
import { clusterApiUrl } from '@solana/web3.js';
import { useMemo } from 'react';

require('@solana/wallet-adapter-react-ui/styles.css');

export function SolanaWalletProvider({ children }: { children: React.ReactNode }) {
  const network = WalletAdapterNetwork.Mainnet;

  const endpoint = useMemo(
    () => clusterApiUrl(network),
    [network]
  );

  const wallets = useMemo(
    () => [
      new PhantomWalletAdapter(),
      new SolflareWalletAdapter(),
      new BackpackWalletAdapter()
    ],
    []
  );

  return (
    <ConnectionProvider endpoint={endpoint}>
      <WalletProvider wallets={wallets} autoConnect>
        <WalletModalProvider>
          {children}
        </WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  );
}

Message Signing

Solana uses Ed25519 signatures instead of ECDSA (used by EVM chains).

Sign Message

async function signSolanaMessage(
  wallet: any,
  message: string
): Promise<string> {
  if (!wallet.signMessage) {
    throw new Error('Wallet does not support message signing');
  }

  const messageBytes = new TextEncoder().encode(message);

  const signature = await wallet.signMessage(messageBytes);

  const signatureBase64 = Buffer.from(signature).toString('base64');

  return signatureBase64;
}

Check Token Account

Before delegation, verify user has the required token account:
import { getAccount, getAssociatedTokenAddress } from '@solana/spl-token';
import { Connection, PublicKey } from '@solana/web3.js';

async function checkTokenAccount(
  connection: Connection,
  userPublicKey: PublicKey,
  mintPublicKey: PublicKey
): Promise<{
  exists: boolean;
  balance?: number;
  address?: string;
}> {
  try {
    const tokenAccountAddress = await getAssociatedTokenAddress(
      mintPublicKey,
      userPublicKey
    );

    const accountInfo = await getAccount(connection, tokenAccountAddress);

    const decimals = 6; // USDC/USDT
    const balance = Number(accountInfo.amount) / Math.pow(10, decimals);

    return {
      exists: true,
      balance,
      address: tokenAccountAddress.toBase58()
    };
  } catch (error: any) {
    if (error.name === 'TokenAccountNotFoundError') {
      return { exists: false };
    }
    throw error;
  }
}

async function ensureTokenAccount(
  wallet: any,
  currency: 'usdc' | 'usdt',
  mintAddress: string
): Promise<void> {
  const connection = new Connection('https://api.mainnet-beta.solana.com');
  const mintPublicKey = new PublicKey(mintAddress);

  const accountInfo = await checkTokenAccount(
    connection,
    wallet.publicKey,
    mintPublicKey
  );

  if (!accountInfo.exists) {
    throw new Error(
      `No ${currency.toUpperCase()} account found. ` +
      `Please add ${currency.toUpperCase()} to your wallet first.`
    );
  }

  if (accountInfo.balance === 0) {
    console.warn(
      `Warning: ${currency.toUpperCase()} balance is 0. ` +
      `You may want to add funds before delegating.`
    );
  }

  console.log(
    `${currency.toUpperCase()} balance: ${accountInfo.balance}`
  );
}

Submit to API

After collecting all data, submit to the Solana-specific endpoint:

API Request

interface SolanaDelegationProof {
  address: string;          // Base58-encoded, 32-44 characters
  network: 'solana';
  currency: 'usdc' | 'usdt';
  amount: string;
  txHash: string;           // Base58-encoded transaction signature, 87-88 characters
  sigHash: string;          // Base64-encoded message signature
  sigMessage: string;       // Message that was signed
  token: string;            // From Step 1
}

async function submitSolanaDelegation(
  proof: SolanaDelegationProof,
  userAccessToken: string
): Promise<boolean> {
  const response = await fetch(
    'https://api.yourplatform.com/v1/delegation/solana/post-approval',
    {
      method: 'POST',
      headers: {
        'x-client-key': process.env.CLIENT_PUBLIC_KEY!,
        'Authorization': `Bearer ${userAccessToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(proof)
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Delegation failed');
  }

  const data = await response.json();
  return data.success;
}
API Reference: POST /v1/delegation/solana/post-approval

Validation Rules

The API performs strict Solana-specific validation:
  • Base58-encoded string
  • Typically 32-44 characters
  • Must be a valid Solana public key
  • Case-sensitive
Valid: DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK Invalid: 0xDYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK (has 0x prefix)
  • Base58-encoded string
  • Typically 87-88 characters
  • Must be a confirmed transaction on Solana mainnet
  • Case-sensitive
Valid: 5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW
  • Base64-encoded Ed25519 signature
  • Must verify against the provided address and message
  • Generated from signMessage() wallet adapter method
Valid: Base64 string from Ed25519 signature
  • Any string that was signed by the wallet
  • Commonly: "Prove wallet ownership for delegation"
  • Must match exactly what was signed

Error Handling

Common Solana Errors

async function handleSolanaDelegation() {
  try {
    await delegateSolanaWallet(wallet, params);
  } catch (error: any) {
    if (error.message?.includes('User rejected')) {
      console.error('User rejected transaction');
      alert('Please approve the transaction in your wallet to continue');
    }
    else if (error.message?.includes('Insufficient funds')) {
      console.error('Insufficient SOL for transaction fees');
      alert('You need a small amount of SOL (~0.00001) to pay for transaction fees');
    }
    else if (error.message?.includes('Token account not found')) {
      console.error('No token account');
      alert(`Please add ${params.currency.toUpperCase()} to your wallet first`);
    }
    else if (error.message?.includes('WalletNotConnectedError')) {
      console.error('Wallet not connected');
      alert('Please connect your Solana wallet');
    }
    else if (error.message?.includes('Blockhash not found')) {
      console.error('Transaction expired');
      alert('Transaction expired. Please try again.');
    }
    else {
      console.error('Unknown error:', error);
      alert(`Delegation failed: ${error.message}`);
    }
  }
}

API Error Responses

StatusErrorCauseSolution
400Invalid address formatAddress not valid base58Ensure valid Solana public key
400Invalid transaction signatureTransaction not found or invalid formatWait for confirmation, check signature
400Invalid message signatureSignature doesn’t verifyRe-sign message with correct wallet
400Expired tokenDelegation token expiredGenerate new token from Step 1
429Rate limitedToo many requestsWait and retry with exponential backoff

Testing

Test on Devnet

Before going to production, test on Solana devnet:
const DEVNET_CONFIG = {
  rpcUrl: 'https://api.devnet.solana.com',
  usdcMint: 'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr', // USDC on devnet
  explorer: 'https://explorer.solana.com/?cluster=devnet'
};

async function setupDevnet() {
  return new Connection(DEVNET_CONFIG.rpcUrl, 'confirmed');
}
Get devnet SOL from: Get devnet USDC:
  • Use SPL Token Faucet on devnet
  • Or airdrop test tokens

Performance Optimization

Transaction Confirmation Strategies

async function confirmTransactionFast(
  connection: Connection,
  signature: string
): Promise<void> {
  const { blockhash, lastValidBlockHeight } =
    await connection.getLatestBlockhash();

  await connection.confirmTransaction({
    signature,
    blockhash,
    lastValidBlockHeight
  }, 'confirmed'); // Can use 'processed', 'confirmed', or 'finalized'
}

async function confirmTransactionWithRetry(
  connection: Connection,
  signature: string,
  maxRetries: number = 3
): Promise<void> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const { blockhash, lastValidBlockHeight } =
        await connection.getLatestBlockhash();

      await connection.confirmTransaction({
        signature,
        blockhash,
        lastValidBlockHeight
      }, 'confirmed');

      return; // Success
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s
    }
  }
}

Priority Fees

For faster processing during network congestion:
import { ComputeBudgetProgram } from '@solana/web3.js';

async function createTransactionWithPriorityFee(
  instructions: TransactionInstruction[],
  feeLamports: number = 1000
): Promise<Transaction> {
  const transaction = new Transaction();

  const computeBudgetIx = ComputeBudgetProgram.setComputeUnitPrice({
    microLamports: feeLamports
  });

  transaction.add(computeBudgetIx);
  transaction.add(...instructions);

  return transaction;
}

Wallet Support

Supported Solana Wallets

Phantom

Most popular Solana wallet with excellent mobile and browser support

Solflare

Full-featured wallet with hardware wallet support

Backpack

Modern wallet with built-in xNFT app store

Wallet Detection

function detectSolanaWallets(): string[] {
  const wallets: string[] = [];

  if (window.phantom?.solana) wallets.push('Phantom');
  if (window.solflare) wallets.push('Solflare');
  if (window.backpack) wallets.push('Backpack');

  return wallets;
}

function promptWalletInstall() {
  const installed = detectSolanaWallets();

  if (installed.length === 0) {
    return (
      <div className="wallet-prompt">
        <p>No Solana wallet detected. Please install one:</p>
        <a href="https://phantom.app" target="_blank">Install Phantom</a>
        <a href="https://solflare.com" target="_blank">Install Solflare</a>
      </div>
    );
  }

  return null;
}

Next Steps