Overview

Gas fees on Solana can be sponsored using a dedicated CDP Solana account or third-party provider(s), allowing you to abstract fees from end users. We will use the feePayer property on Solana transactions to enable this feature.

This guide is useful to enable the following cases:

  1. Utilizing a CDP Solana account as the fee payer for end-user wallets.
  2. Setting up a single CDP Solana account to pay fees on behalf of other CDP accounts.
  3. Integrating external relayers with CDP Solana accounts.

Prerequisites

It is assumed you have already completed the Quickstart guide.

Orchestrate gas sponsorship with CDP SDK

In this example, we will demonstrate how to use a single CDP Solana account to pay fees on behalf of other CDP Solana accounts. Follow the steps outlined below:

  1. Set up a fee payer account
  2. Set up a source account
  3. Use CDP Faucet to get SOL on fee payer account for gas
  4. Use CDP Faucet to get SOL on source account for sending
  5. Sign the transaction using the source account
  6. Sign the transaction using the fee payer account
  7. Send the transaction
    import { CdpClient } from "@coinbase/cdp-sdk";
    import "dotenv/config";

    import {
      Connection,
      PublicKey,
      SystemProgram,
      Transaction,
    } from "@solana/web3.js";

    async function main(sourceAddress?: string) {
      const cdp = new CdpClient();

      // Required: Destination address to send SOL to
      const destinationAddress = "3KzDtddx4i53FBkvCzuDmRbaMozTZoJBb1TToWhz3JfE";

      // Amount of lamports to send (default: 1000 = 0.000001 SOL)
      const lamportsToSend = 1000;

      try {
        const connection = new Connection("https://api.devnet.solana.com");

        // Set up a dedicated fee payer account.
        const feePayer = await cdp.solana.getOrCreateAccount({
          name: "test-sol-account-fee-payer",
        });
        console.log("Fee payer address: " + feePayer.address);

        // Request funds on the feePayer address to pay for the gas.
        await requestFaucetAndWaitForBalance(cdp, feePayer.address, connection);

        let fromAddress: string;
        if (sourceAddress) {
          fromAddress = sourceAddress;
          console.log("Using existing SOL account:", fromAddress);
        } else {
          // Set up a source account.
          const account = await cdp.solana.getOrCreateAccount({
            name: "test-sol-account",
          })

          fromAddress = account.address;
          console.log("Successfully created new SOL account:", fromAddress);

          // Request funds on the source account for transaction amount.
          await requestFaucetAndWaitForBalance(cdp, fromAddress, connection);
        }

        const balance = await connection.getBalance(new PublicKey(fromAddress));
        if (balance < lamportsToSend) {
          throw new Error(
            `Insufficient balance: ${balance} lamports, need at least ${lamportsToSend} lamports`
          );
        }

        const transaction = new Transaction();
        transaction.add(
          SystemProgram.transfer({
            fromPubkey: new PublicKey(fromAddress),
            toPubkey: new PublicKey(destinationAddress),
            lamports: lamportsToSend,
          })
        );

        const { blockhash } = await connection.getLatestBlockhash();
        transaction.recentBlockhash = blockhash;
        transaction.feePayer = new PublicKey(feePayer.address);

        const serializedTx = Buffer.from(
          transaction.serialize({ requireAllSignatures: false })
        ).toString("base64");

        // Sign with the funding account.
        const signedTxResponse = await cdp.solana.signTransaction({
          address: fromAddress,
          transaction: serializedTx,
        });

        const signedBase64Tx = signedTxResponse.signature;

        // Sign with the feePayer account.
        const finalSignedTxResponse = await cdp.solana.signTransaction({
          address: feePayer.address,
          transaction: signedBase64Tx,
        });

        // Send the signed transaction to the network.
        const signature = await connection.sendRawTransaction(Buffer.from(finalSignedTxResponse.signature, 'base64'));

        const latestBlockhash = await connection.getLatestBlockhash();

        const confirmation = await connection.confirmTransaction({
          signature,
          blockhash: latestBlockhash.blockhash,
          lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
        });

        if (confirmation.value.err) {
          throw new Error(
            `Transaction failed: ${confirmation.value.err.toString()}`
          );
        }

        console.log(
          "Transaction confirmed:",
          confirmation.value.err ? "failed" : "success"
        );
        console.log(
          `Transaction explorer link: https://explorer.solana.com/tx/${signature}?cluster=devnet`
        );

        return {
          fromAddress,
          destinationAddress,
          amount: lamportsToSend / 1e9,
          signature,
          success: !confirmation.value.err,
        };
      } catch (error) {
        console.error("Error processing SOL transaction:", error);
        throw error;
      }
    }


    /**
     * Sleeps for a given number of milliseconds
     *
     * @param {number} ms - The number of milliseconds to sleep
     * @returns {Promise<void>} A promise that resolves when the sleep is complete
     */
    function sleep(ms: number): Promise<void> {
      return new Promise((resolve) => setTimeout(resolve, ms));
    }


    /**
     * Requests funds from the faucet and waits for the balance to be available
     *
     * @param {CdpClient} cdp - The CDP client instance
     * @param {string} address - The address to fund
     * @param {Connection} connection - The Solana connection
     * @param {string} token - The token to request (default: "sol")
     * @returns {Promise<void>} A promise that resolves when the account is funded
     */
    async function requestFaucetAndWaitForBalance(
        cdp: CdpClient,
        address: string,
        connection: Connection,
    ): Promise<void> {
      // Request funds from faucet
      const faucetResp = await cdp.solana.requestFaucet({
        address: address,
        token: "sol",
      });
      console.log(
          `Successfully requested SOL from faucet:`,
          faucetResp.signature
      );

      // Wait until the address has balance
      let balance = 0;
      let attempts = 0;
      const maxAttempts = 30;

      while (balance === 0 && attempts < maxAttempts) {
        balance = await connection.getBalance(new PublicKey(address));
        if (balance === 0) {
          console.log("Waiting for funds...");
          await sleep(1000);
          attempts++;
        }
      }

      if (balance === 0) {
        throw new Error("Account not funded after multiple attempts");
      }

      console.log("Account funded with", balance / 1e9, "SOL");
      return;
    }


    const sourceAddress = process.argv.length > 2 ? process.argv[2] : undefined;

    main(sourceAddress).catch(console.error);

You can extend this example to do the following:

  1. Use an external gas relayer with CDP account
  2. Use CDP Solana account as fee payer on user wallets