Skip to main content

Overview

SVM Gas Sponsorship is Dynamic’s built-in feature that automatically sponsors Solana (SVM) transaction fees for your users. When enabled, Dynamic handles all the complexity of fee sponsorship behind the scenes, allowing your users to transact without needing SOL for gas fees. When enabled, Dynamic automatically replaces the fee payer in Solana transactions with a sponsored account, so your users don’t need SOL for gas.
SVM Gas Sponsorship is available exclusively for V3 MPC embedded wallets. It works automatically once enabled - no code changes required.

Prerequisites

Enabling SVM Gas Sponsorship

  1. Go to the Dynamic Dashboard
  2. Navigate to Settings > Embedded Wallets
  3. Ensure Solana (SOL) is enabled in your chain configurations
  4. Toggle on SVM Gas Sponsorship
The SVM Gas Sponsorship toggle only appears when Solana is enabled in your chain configurations and you’re using V3 MPC wallets.

How It Works

Once enabled, gas sponsorship is applied automatically to all transactions from your users’ embedded wallets. Your code remains unchanged:
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:solana/solana.dart';

final sdk = DynamicSDK.instance;

Future<String> sendSOLGasless({
  required BaseWallet wallet,
  required String recipientAddress,
  required double amount,
}) async {
  final signer = sdk.solana.createSigner(wallet: wallet);
  final connection = sdk.solana.createConnection();

  final fromPubKey = Pubkey.fromString(wallet.address);
  final toPubKey = Pubkey.fromString(recipientAddress);

  final BlockhashWithExpiryBlockHeight recentBlockhash =
      await connection.getLatestBlockhash();

  // Create transaction - sponsorship is handled automatically
  final transaction = Transaction.v0(
    payer: fromPubKey,
    recentBlockhash: recentBlockhash.blockhash,
    instructions: [
      SystemProgram.transfer(
        fromPubkey: fromPubKey,
        toPubkey: toPubKey,
        lamports: solToLamports(amount),
      ),
    ],
  );

  // Sign and send - gas fees are sponsored automatically
  final signature = await signer.signAndSendTransaction(
    transaction: transaction,
  );

  return signature;
}

int solToLamports(double sol) {
  return (sol * 1e9).toInt();
}

Transaction Flow

When your application sends a transaction:
  1. Transaction Creation: Your app creates a standard Solana transaction
  2. Automatic Sponsorship: The SDK intercepts the transaction before signing
  3. Backend Processing: The transaction is sent to Dynamic’s backend
  4. Sponsorship Processing: Dynamic sponsors the transaction
  5. Fee Payer Replacement: The transaction’s fee payer is replaced with Dynamic’s sponsored account
  6. User Signing: The sponsored transaction is returned for the user to sign
  7. Broadcast: The fully signed transaction is sent to the Solana network

Limitations

Wallet Requirements

  • Embedded wallets only: Sponsorship only works with Dynamic’s MPC embedded wallets
  • V3 wallets required: Must be using V3 MPC wallet configuration
  • Not for external wallets: External wallets (Phantom, Solflare, etc.) are not supported

Transaction Constraints

  • Transaction size: Maximum 2KB for the base64-encoded transaction
  • Already-signed transactions: Transactions that are already signed will not be sponsored
  • Single transaction: Each transaction is sponsored individually (no batching)

Fallback Behavior

If sponsorship fails, the SDK will fall back to using the original transaction, which requires the user to have SOL for gas fees.
When sponsorship fails, the transaction will proceed without sponsorship. Ensure your UX handles cases where users may need SOL for gas fees as a fallback.

Checking Sponsorship Status

You can check if gas sponsorship is enabled in your project settings:
final sdk = DynamicSDK.instance;

bool checkSponsorshipEnabled() {
  return sdk.projectSettings?.sdk?.embeddedWallets?.svmGasSponsorshipEnabled ?? false;
}

Error Handling

enum SolanaTransactionResult {
  success,
  insufficientBalance,
  sponsorshipFailed,
  error,
}

class TransactionResponse {
  final SolanaTransactionResult result;
  final String? signature;
  final String? errorMessage;

  TransactionResponse({
    required this.result,
    this.signature,
    this.errorMessage,
  });
}

Future<TransactionResponse> sendTransactionWithFallback({
  required BaseWallet wallet,
  required String recipient,
  required double amount,
}) async {
  try {
    // Attempt transaction - sponsorship applied automatically if enabled
    final signature = await sendSOLGasless(
      wallet: wallet,
      recipientAddress: recipient,
      amount: amount,
    );
    return TransactionResponse(
      result: SolanaTransactionResult.success,
      signature: signature,
    );
  } catch (e) {
    final errorDesc = e.toString().toLowerCase();

    if (errorDesc.contains('insufficient') || errorDesc.contains('balance')) {
      return TransactionResponse(
        result: SolanaTransactionResult.insufficientBalance,
        errorMessage: 'Insufficient balance for transaction',
      );
    } else if (errorDesc.contains('sponsor')) {
      return TransactionResponse(
        result: SolanaTransactionResult.sponsorshipFailed,
        errorMessage: 'Gas sponsorship failed, SOL required for fees',
      );
    } else {
      return TransactionResponse(
        result: SolanaTransactionResult.error,
        errorMessage: e.toString(),
      );
    }
  }
}

// Usage in Widget
void _handleSendTransaction() async {
  final response = await sendTransactionWithFallback(
    wallet: widget.wallet,
    recipient: _recipientController.text,
    amount: double.parse(_amountController.text),
  );

  setState(() {
    switch (response.result) {
      case SolanaTransactionResult.success:
        signature = response.signature;
        error = null;
        break;
      case SolanaTransactionResult.insufficientBalance:
      case SolanaTransactionResult.sponsorshipFailed:
      case SolanaTransactionResult.error:
        signature = null;
        error = response.errorMessage;
        break;
    }
  });
}

Complete Example Widget

import 'package:flutter/material.dart';
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:solana/solana.dart';

class GaslessSendWidget extends StatefulWidget {
  final BaseWallet wallet;

  const GaslessSendWidget({Key? key, required this.wallet}) : super(key: key);

  @override
  State<GaslessSendWidget> createState() => _GaslessSendWidgetState();
}

class _GaslessSendWidgetState extends State<GaslessSendWidget> {
  final sdk = DynamicSDK.instance;
  final _recipientController = TextEditingController();
  final _amountController = TextEditingController();

  String? signature;
  bool isLoading = false;
  String? error;

  bool get sponsorshipEnabled =>
      sdk.projectSettings?.sdk?.embeddedWallets?.svmGasSponsorshipEnabled ?? false;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // Show sponsorship status
          Container(
            padding: const EdgeInsets.all(8),
            decoration: BoxDecoration(
              color: sponsorshipEnabled
                  ? Colors.green.withOpacity(0.1)
                  : Colors.orange.withOpacity(0.1),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Row(
              children: [
                Icon(
                  sponsorshipEnabled ? Icons.check_circle : Icons.info,
                  color: sponsorshipEnabled ? Colors.green : Colors.orange,
                  size: 16,
                ),
                const SizedBox(width: 8),
                Text(
                  sponsorshipEnabled
                      ? 'Gas fees are sponsored'
                      : 'Gas sponsorship not enabled',
                  style: TextStyle(
                    color: sponsorshipEnabled ? Colors.green : Colors.orange,
                    fontSize: 12,
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 16),
          TextField(
            controller: _recipientController,
            decoration: const InputDecoration(
              labelText: 'Recipient Address',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 16),
          TextField(
            controller: _amountController,
            decoration: const InputDecoration(
              labelText: 'Amount (SOL)',
              border: OutlineInputBorder(),
            ),
            keyboardType: const TextInputType.numberWithOptions(decimal: true),
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: isLoading ? null : _sendTransaction,
            child: isLoading
                ? const SizedBox(
                    height: 20,
                    width: 20,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : Text(sponsorshipEnabled ? 'Send SOL (Gasless)' : 'Send SOL'),
          ),
          if (signature != null) ...[
            const SizedBox(height: 16),
            Container(
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Colors.green.withOpacity(0.1),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    'Success!',
                    style: TextStyle(
                      color: Colors.green,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    signature!,
                    style: const TextStyle(fontSize: 12),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                ],
              ),
            ),
          ],
          if (error != null) ...[
            const SizedBox(height: 16),
            Text(
              error!,
              style: const TextStyle(color: Colors.red, fontSize: 12),
            ),
          ],
        ],
      ),
    );
  }

  Future<void> _sendTransaction() async {
    // Implementation as shown in error handling section
  }

  @override
  void dispose() {
    _recipientController.dispose();
    _amountController.dispose();
    super.dispose();
  }
}

Best Practices

  1. Don’t assume sponsorship: Build your UI to handle cases where sponsorship might not be available
  2. Show transaction status: Provide feedback during the sponsorship and signing process
  3. Test in Sandbox first: Verify sponsorship works in your Sandbox environment before going live
  4. Monitor usage: Keep an eye on sponsorship usage in your dashboard

Next Steps