Skip to main content

Send Transaction

Send and broadcast Solana transactions using a user’s embedded wallet. MoonKey handles transaction signing and submission to the Solana network.

Overview

Sending transactions on Solana enables:
  • SOL transfers: Send native SOL tokens between addresses
  • SPL token transfers: Transfer fungible tokens (SPL tokens)
  • Program interactions: Execute instructions on Solana programs
  • NFT operations: Mint, transfer, or burn NFTs
MoonKey uses @solana/web3.js for transaction construction. Transactions are serialized, signed, and broadcast to the network automatically.

React SDK

  • Basic Usage
  • With UI Customization
  • With Callbacks
To send a transaction, use the sendTransaction method from the useSendTransaction hook:
import { useMoonKey } from '@moon-key/react-auth';
import { useSendTransaction } from '@moon-key/react-auth/solana';
import {
  Transaction,
  SystemProgram,
  PublicKey,
  Connection,
  LAMPORTS_PER_SOL
} from '@solana/web3.js';

function SendSolButton() {
  const { user } = useMoonKey();
  const { sendTransaction } = useSendTransaction();

  const handleSend = async () => {
    if (!user?.wallet) {
      alert('No wallet found');
      return;
    }

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

      // Create transaction
      const transaction = new Transaction().add(
        SystemProgram.transfer({
          fromPubkey: new PublicKey(user.wallet.address),
          toPubkey: new PublicKey('RecipientAddressHere...'),
          lamports: 0.001 * LAMPORTS_PER_SOL // 0.001 SOL
        })
      );

      // Get recent blockhash
      const { blockhash } = await connection.getLatestBlockhash();
      transaction.recentBlockhash = blockhash;
      transaction.feePayer = new PublicKey(user.wallet.address);

      // Send transaction
      const signature = await sendTransaction({
        transaction,
        connection
      });

      console.log('Transaction sent:', signature);
      alert(`Transaction successful! Signature: ${signature}`);
    } catch (error) {
      console.error('Failed to send transaction:', error);
    }
  };

  return (
    <button onClick={handleSend}>
      Send 0.001 SOL
    </button>
  );
}

Parameters

transaction
Transaction
required
The Solana transaction to send. Must be a Transaction object from @solana/web3.js.Example:
const transaction = new Transaction().add(
  SystemProgram.transfer({
    fromPubkey: new PublicKey(fromAddress),
    toPubkey: new PublicKey(toAddress),
    lamports: amount
  })
);
connection
Connection
required
The Solana RPC connection to use for broadcasting the transaction.Example:
const connection = new Connection('https://api.devnet.solana.com');

Options Object

The second parameter is an optional configuration object:
uiConfig
object
Configuration for the transaction confirmation modal.

Hook Callbacks

onSuccess
(result: { signature: string }) => void
Callback executed after successful transaction. Receives the transaction signature.
onError
(error: Error) => void
Callback executed if transaction fails or user cancels.

Returns

signature
string
The transaction signature (base58 encoded string).

Complete Examples

SOL Transfer

'use client';
import { useMoonKey } from '@moon-key/react-auth';
import { useSendTransaction } from '@moon-key/react-auth/solana';
import {
  Transaction,
  SystemProgram,
  PublicKey,
  Connection,
  LAMPORTS_PER_SOL
} from '@solana/web3.js';
import { useState } from 'react';

export default function SolTransfer() {
  const { user, isAuthenticated } = useMoonKey();
  const [recipient, setRecipient] = useState('');
  const [amount, setAmount] = useState('');
  const [isSending, setIsSending] = useState(false);

  const { sendTransaction } = useSendTransaction({
    onSuccess: ({ signature }) => {
      console.log('Transfer successful:', signature);
      alert(`Transfer successful! Signature: ${signature}`);
      setRecipient('');
      setAmount('');
      setIsSending(false);
    },
    onError: (error) => {
      console.error('Transfer failed:', error);
      alert('Transfer failed. Please try again.');
      setIsSending(false);
    }
  });

  const handleTransfer = async () => {
    if (!user?.wallet) {
      alert('Please connect your wallet');
      return;
    }

    if (!recipient || !amount) {
      alert('Please enter recipient and amount');
      return;
    }

    setIsSending(true);

    try {
      // Validate recipient address
      const recipientPubkey = new PublicKey(recipient);

      // Create connection (use devnet for testing)
      const connection = new Connection('https://api.devnet.solana.com');

      // Check balance
      const balance = await connection.getBalance(new PublicKey(user.wallet.address));
      const transferAmount = parseFloat(amount) * LAMPORTS_PER_SOL;

      if (balance < transferAmount) {
        alert('Insufficient balance');
        setIsSending(false);
        return;
      }

      // Create transaction
      const transaction = new Transaction().add(
        SystemProgram.transfer({
          fromPubkey: new PublicKey(user.wallet.address),
          toPubkey: recipientPubkey,
          lamports: transferAmount
        })
      );

      // Get recent blockhash
      const { blockhash } = await connection.getLatestBlockhash();
      transaction.recentBlockhash = blockhash;
      transaction.feePayer = new PublicKey(user.wallet.address);

      // Send transaction
      await sendTransaction({
        transaction,
        connection
      }, {
        uiConfig: {
          title: 'Confirm Transfer',
          description: `Send ${amount} SOL to ${recipient.slice(0, 8)}...${recipient.slice(-8)}`,
          confirmButtonText: 'Send SOL',
          transactionInfo: {
            action: 'SOL Transfer',
            cost: `${amount} SOL + network fees`
          }
        }
      });
    } catch (error: any) {
      console.error('Transfer error:', error);
      if (error.message?.includes('Invalid public key')) {
        alert('Invalid recipient address');
      } else {
        alert('Transfer failed: ' + error.message);
      }
      setIsSending(false);
    }
  };

  if (!isAuthenticated) {
    return <p>Please sign in to transfer SOL</p>;
  }

  return (
    <div className="sol-transfer">
      <h2>Transfer SOL</h2>

      <div className="form">
        <div className="field">
          <label>Recipient Address:</label>
          <input
            type="text"
            placeholder="Solana address"
            value={recipient}
            onChange={(e) => setRecipient(e.target.value)}
          />
        </div>

        <div className="field">
          <label>Amount (SOL):</label>
          <input
            type="number"
            placeholder="0.001"
            step="0.001"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
          />
        </div>

        <button
          onClick={handleTransfer}
          disabled={isSending || !recipient || !amount}
        >
          {isSending ? 'Sending...' : 'Send SOL'}
        </button>
      </div>
    </div>
  );
}

SPL Token Transfer

'use client';
import { useMoonKey } from '@moon-key/react-auth';
import { useSendTransaction } from '@moon-key/react-auth/solana';
import {
  Transaction,
  PublicKey,
  Connection
} from '@solana/web3.js';
import {
  getAssociatedTokenAddress,
  createTransferInstruction,
  TOKEN_PROGRAM_ID
} from '@solana/spl-token';
import { useState } from 'react';

export default function SPLTokenTransfer() {
  const { user } = useMoonKey();
  const { sendTransaction } = useSendTransaction();
  const [recipient, setRecipient] = useState('');
  const [amount, setAmount] = useState('');

  const handleTokenTransfer = async () => {
    if (!user?.wallet) return;

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

      // Token mint address (example: USDC on devnet)
      const tokenMintAddress = new PublicKey('TokenMintAddressHere...');

      // Get source and destination token accounts
      const sourceAccount = await getAssociatedTokenAddress(
        tokenMintAddress,
        new PublicKey(user.wallet.address)
      );

      const destinationAccount = await getAssociatedTokenAddress(
        tokenMintAddress,
        new PublicKey(recipient)
      );

      // Create transfer instruction
      const transferInstruction = createTransferInstruction(
        sourceAccount,
        destinationAccount,
        new PublicKey(user.wallet.address),
        parseFloat(amount) * Math.pow(10, 6), // Assuming 6 decimals
        [],
        TOKEN_PROGRAM_ID
      );

      // Create transaction
      const transaction = new Transaction().add(transferInstruction);

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

      // Send transaction
      const signature = await sendTransaction({
        transaction,
        connection
      }, {
        uiConfig: {
          title: 'Transfer Tokens',
          description: `Send ${amount} tokens`,
          confirmButtonText: 'Confirm Transfer'
        }
      });

      console.log('Token transfer successful:', signature);
      alert('Tokens sent successfully!');
    } catch (error) {
      console.error('Token transfer failed:', error);
      alert('Failed to transfer tokens');
    }
  };

  return (
    <div className="token-transfer">
      <h2>Transfer SPL Tokens</h2>

      <input
        type="text"
        placeholder="Recipient address"
        value={recipient}
        onChange={(e) => setRecipient(e.target.value)}
      />

      <input
        type="number"
        placeholder="Amount"
        value={amount}
        onChange={(e) => setAmount(e.target.value)}
      />

      <button onClick={handleTokenTransfer}>
        Transfer Tokens
      </button>
    </div>
  );
}

Program Interaction

'use client';
import { useMoonKey } from '@moon-key/react-auth';
import { useSendTransaction } from '@moon-key/react-auth/solana';
import {
  Transaction,
  TransactionInstruction,
  PublicKey,
  Connection
} from '@solana/web3.js';

export default function ProgramInteraction() {
  const { user } = useMoonKey();
  const { sendTransaction } = useSendTransaction();

  const handleProgramCall = async () => {
    if (!user?.wallet) return;

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

      // Your program ID
      const programId = new PublicKey('YourProgramIdHere...');

      // Create instruction data (depends on your program)
      const instructionData = Buffer.from([
        /* your instruction data */
      ]);

      // Create instruction
      const instruction = new TransactionInstruction({
        keys: [
          {
            pubkey: new PublicKey(user.wallet.address),
            isSigner: true,
            isWritable: true
          }
          // Add more account keys as needed
        ],
        programId,
        data: instructionData
      });

      // Create transaction
      const transaction = new Transaction().add(instruction);

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

      // Send transaction
      const signature = await sendTransaction({
        transaction,
        connection
      }, {
        uiConfig: {
          title: 'Execute Program',
          description: 'Interact with Solana program',
          confirmButtonText: 'Execute'
        }
      });

      console.log('Program execution successful:', signature);
    } catch (error) {
      console.error('Program execution failed:', error);
    }
  };

  return (
    <button onClick={handleProgramCall}>
      Execute Program
    </button>
  );
}

Transaction Construction

Basic Transaction Setup

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

// Create connection
const connection = new Connection('https://api.devnet.solana.com');

// Create transaction
const transaction = new Transaction();

// Add instruction
transaction.add(
  SystemProgram.transfer({
    fromPubkey: new PublicKey(fromAddress),
    toPubkey: new PublicKey(toAddress),
    lamports: 0.001 * LAMPORTS_PER_SOL
  })
);

// Set recent blockhash and fee payer
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(fromAddress);

Multiple Instructions

const transaction = new Transaction();

// Add multiple instructions
transaction.add(
  SystemProgram.transfer({
    fromPubkey: senderPubkey,
    toPubkey: recipient1Pubkey,
    lamports: 0.001 * LAMPORTS_PER_SOL
  }),
  SystemProgram.transfer({
    fromPubkey: senderPubkey,
    toPubkey: recipient2Pubkey,
    lamports: 0.002 * LAMPORTS_PER_SOL
  })
);

const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = senderPubkey;

RPC Endpoints

Use appropriate RPC endpoints for different networks:
// Devnet (for testing)
const devnetConnection = new Connection('https://api.devnet.solana.com');

// Testnet
const testnetConnection = new Connection('https://api.testnet.solana.com');

// Mainnet Beta
const mainnetConnection = new Connection('https://api.mainnet-beta.solana.com');

// Custom RPC (recommended for production)
const customConnection = new Connection('https://your-rpc-provider.com');
For production applications, use a dedicated RPC provider like Helius, QuickNode, or Alchemy for better reliability and rate limits.

Install Dependencies

npm install @solana/web3.js @solana/spl-token

UI Customization

Customize the transaction confirmation modal:
await sendTransaction({
  transaction,
  connection
}, {
  uiConfig: {
    title: 'Confirm Transaction',
    description: 'Review and confirm your transaction',
    confirmButtonText: 'Confirm',
    cancelButtonText: 'Cancel',
    transactionInfo: {
      action: 'Transfer SOL',
      cost: '0.001 SOL + fees'
    }
  }
});

Hide Modal

await sendTransaction({
  transaction,
  connection
}, {
  uiConfig: {
    showWalletUI: false
  }
});
Hiding the modal removes the user’s ability to review the transaction before signing. Only use this for trusted operations or after obtaining explicit user consent.

Error Handling

Handle common transaction errors:
const { sendTransaction } = useSendTransaction({
  onError: (error: any) => {
    if (error.message?.includes('insufficient funds')) {
      alert('Insufficient balance to complete transaction');
    } else if (error.message?.includes('blockhash')) {
      alert('Transaction expired. Please try again.');
    } else if (error.message?.includes('simulation failed')) {
      alert('Transaction simulation failed. Check transaction parameters.');
    } else if (error.code === 'USER_REJECTED') {
      console.log('User cancelled transaction');
    } else {
      console.error('Transaction error:', error);
      alert('Transaction failed: ' + error.message);
    }
  }
});

Best Practices

Always verify sufficient balance:
const connection = new Connection('https://api.devnet.solana.com');
const balance = await connection.getBalance(new PublicKey(user.wallet.address));
const transferAmount = 0.001 * LAMPORTS_PER_SOL;

if (balance < transferAmount) {
  alert('Insufficient balance');
  return;
}

// Proceed with transaction
Always fetch a recent blockhash before sending:
// ✅ Good - Fresh blockhash
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;

// ❌ Bad - Reusing old blockhash
// Blockhashes expire after ~60 seconds
Validate recipient addresses before sending:
import { PublicKey } from '@solana/web3.js';

function isValidSolanaAddress(address: string): boolean {
  try {
    new PublicKey(address);
    return true;
  } catch {
    return false;
  }
}

if (!isValidSolanaAddress(recipient)) {
  alert('Invalid recipient address');
  return;
}
Wait for transaction confirmation:
const signature = await sendTransaction({
  transaction,
  connection
});

// Wait for confirmation
const confirmation = await connection.confirmTransaction(signature, 'confirmed');

if (confirmation.value.err) {
  console.error('Transaction failed:', confirmation.value.err);
} else {
  console.log('Transaction confirmed!');
}
Implement retry logic for network issues:
async function sendWithRetry(
  transaction: Transaction,
  connection: Connection,
  maxRetries = 3
) {
  let attempts = 0;

  while (attempts < maxRetries) {
    try {
      const signature = await sendTransaction({
        transaction,
        connection
      });
      return signature;
    } catch (error: any) {
      attempts++;
      if (attempts >= maxRetries) throw error;
      
      console.log(`Retry ${attempts}/${maxRetries}`);
      await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
    }
  }
}
Choose the right network for your use case:
// Development and testing
const connection = new Connection('https://api.devnet.solana.com');

// Production
const connection = new Connection(
  process.env.NEXT_PUBLIC_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com'
);

Troubleshooting

Blockhashes expire quickly on Solana (~60 seconds):
// Always get fresh blockhash right before sending
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(user.wallet.address);

// Send immediately
await sendTransaction({ transaction, connection });
Check both SOL balance and required amount:
const balance = await connection.getBalance(
  new PublicKey(user.wallet.address)
);

// Account for transfer amount + transaction fees (~0.000005 SOL)
const requiredAmount = transferAmount + (5000); // 5000 lamports for fees

if (balance < requiredAmount) {
  alert(`Insufficient balance. You need ${requiredAmount / LAMPORTS_PER_SOL} SOL`);
  return;
}
Transaction simulation can fail for various reasons:
// Check if accounts exist
const accountInfo = await connection.getAccountInfo(
  new PublicKey(recipientAddress)
);

// For SPL tokens, check if token account exists
const tokenAccount = await getAssociatedTokenAddress(
  tokenMintAddress,
  new PublicKey(recipientAddress)
);

const tokenAccountInfo = await connection.getAccountInfo(tokenAccount);

if (!tokenAccountInfo) {
  // May need to create associated token account first
  console.log('Token account does not exist');
}
Use appropriate timeouts and retry logic:
const connection = new Connection(
  'https://api.devnet.solana.com',
  {
    commitment: 'confirmed',
    confirmTransactionInitialTimeout: 60000 // 60 seconds
  }
);

Next Steps