With embedded wallets, your app can sponsor gas fees for transactions on Solana, allowing users to transact without a SOL balance. This is done by configuring the feePayer property of the sponsored transaction to be a fee payer wallet that your app manages to pay users’ gas fees.

Overview

Sponsoring transactions on Solana involves the following steps:

1

Set up a fee payer wallet

Create a fee payer wallet in your backend to pay for users’ gas fees.

2

Prepare and sign the transaction

Prepare a transaction with a custom fee payer, sign it with the user’s wallet, and send it to your backend.

3

Verify and complete the transaction

Verify the transaction, sign it with the fee payer wallet, and broadcast it to the network.

To prepare transactions with a fee payer, we recommend using the @solana/web3.js library.

Setting up a fee payer wallet

To start, create a fee payer wallet in your backend to sponsor transactions sent by users. You can either:

  1. Generate a new keypair directly:
import {Keypair} from '@solana/web3.js';
import bs58 from 'bs58';

// Generate a new keypair
const feePayerWallet = new Keypair();
const feePayerAddress = feePayerWallet.publicKey.toBase58();
const feePayerPrivateKey = bs58.encode(feePayerWallet.secretKey);

// Make sure to store the private key securely; it should never leave your server
console.log('Fee Payer Address:', feePayerAddress);
console.log('Fee Payer Private Key:', feePayerPrivateKey);
  1. Or create a Solana server wallet to act as your fee payer for better security and key management.

Ensure you fund this wallet with SOL to pay for transaction fees.

Implementing Sponsored Transactions

With the React SDK, follow these steps to prepare and send a sponsored transaction:

import {useSolanaWallets} from '@privy-io/react-auth/solana';
import {
  TransactionMessage,
  PublicKey,
  VersionedTransaction,
  Connection
} from '@solana/web3.js';

// This function prepares and signs a sponsored transaction
async function prepareSponsoredTransaction(instructions, feePayerAddress) {
  // Find the user's embedded wallet
  const { wallets } = useSolanaWallets();
  const embeddedWallet = wallets.find(wallet => wallet.walletClientType === 'privy');

  if (!embeddedWallet) {
    throw new Error('No embedded wallet found');
  }

  // Create a connection to Solana
  const connection = new Connection('https://api.mainnet-beta.solana.com');
  const { blockhash } = await connection.getLatestBlockhash();

  // Create the transaction message with fee payer set to the backend wallet
  const message = new TransactionMessage({
    payerKey: new PublicKey(feePayerAddress),
    recentBlockhash: blockhash,
    instructions
  }).compileToV0Message();

  // Create transaction
  const transaction = new VersionedTransaction(message);

  // Serialize message for signing
  const serializedMessage = Buffer.from(transaction.message.serialize()).toString('base64');

  // Get provider and sign
  const provider = await embeddedWallet.getProvider();
  const { signature: serializedUserSignature } = await provider.request({
    method: 'signMessage',
    params: {
      message: serializedMessage
    }
  });

  // Add user signature to transaction
  const userSignature = Buffer.from(serializedUserSignature, 'base64');
  transaction.addSignature(new PublicKey(embeddedWallet.address), userSignature);

  // Serialize the transaction to send to backend
  const serializedTransaction = Buffer.from(transaction.serialize()).toString('base64');

  // Send to your backend
  const response = await fetch('your-backend-endpoint', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ transaction: serializedTransaction })
  });

  const { transactionHash } = await response.json();
  return transactionHash;
}

Backend Implementation

Here’s how to implement the server-side portion that receives the partially signed transaction, adds the fee payer signature, and broadcasts it:

// Backend implementation (Node.js with Express)
import express from 'express';
import {Keypair, VersionedTransaction, Connection, clusterApiUrl, PublicKey} from '@solana/web3.js';
import bs58 from 'bs58';

const app = express();
app.use(express.json());

// Your fee payer wallet's private key (Keep this secure!)
const FEE_PAYER_PRIVATE_KEY = 'your-base58-encoded-private-key';
const FEE_PAYER_ADDRESS = 'your-fee-payer-address';

// Initialize fee payer keypair
const feePayerWallet = Keypair.fromSecretKey(bs58.decode(FEE_PAYER_PRIVATE_KEY));

// Connect to Solana
const connection = new Connection(clusterApiUrl('mainnet-beta'));

// Endpoint to handle sponsored transactions
app.post('/sponsor-transaction', async (req, res) => {
  try {
    // Get the partially signed transaction from the request
    const {transaction: serializedTransaction} = req.body;

    if (!serializedTransaction) {
      return res.status(400).json({error: 'Missing transaction data'});
    }

    // Deserialize the transaction
    const transactionBuffer = Buffer.from(serializedTransaction, 'base64');
    const transaction = VersionedTransaction.deserialize(transactionBuffer);

    // Verify the transaction
    // 1. Check that it's using the correct fee payer
    const message = transaction.message;
    const accountKeys = message.getAccountKeys();
    const feePayerIndex = 0; // Fee payer is always the first account
    const feePayer = accountKeys.get(feePayerIndex);

    if (!feePayer || feePayer.toBase58() !== FEE_PAYER_ADDRESS) {
      return res.status(403).json({
        error: 'Invalid fee payer in transaction'
      });
    }

    // 2. Check for any unauthorized fund transfers
    for (const instruction of message.compiledInstructions) {
      const programId = accountKeys.get(instruction.programIndex);

      // Check if instruction is for System Program (transfers)
      if (programId && programId.toBase58() === '11111111111111111111111111111111') {
        // Check if it's a transfer (command 2)
        if (instruction.data[0] === 2) {
          const senderIndex = instruction.accountKeyIndexes[0];
          const senderAddress = accountKeys.get(senderIndex);

          // Don't allow transactions that transfer tokens from fee payer
          if (senderAddress && senderAddress.toBase58() === FEE_PAYER_ADDRESS) {
            return res.status(403).json({
              error: 'Transaction attempts to transfer funds from fee payer'
            });
          }
        }
      }
    }

    // 3. Sign with fee payer
    transaction.sign([feePayerWallet]);

    // 4. Send transaction
    const signature = await connection.sendTransaction(transaction);

    // Return the transaction hash
    return res.status(200).json({
      transactionHash: signature,
      message: 'Transaction sent successfully'
    });
  } catch (error) {
    console.error('Error processing transaction:', error);
    return res.status(500).json({
      error: 'Failed to process transaction',
      details: error.message
    });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Security Considerations

When implementing transaction sponsorship, be mindful of these security considerations:

Verify Transaction Contents

Always verify the transaction contents in your backend before signing with the fee payer. Ensure there are no unauthorized fund transfers.

Rate Limiting

Implement rate limiting to prevent abuse of your sponsorship service. Consider limits per user, per session, or per wallet.

Amount Validation

Validate the transaction amount if applicable. Consider setting maximum sponsorship amounts to prevent excessive spending.

Program ID Whitelisting

Only sponsor transactions for specific whitelisted program IDs that your app interacts with to prevent abuse.

Be extremely careful with your fee payer wallet’s private key. Never expose it in client-side code or store it in unsecured environments. Consider using environment variables, secret management services, or HSMs to securely store private keys in production.