Skip to content

Transactions

Simple guide to handling Solana transactions with LazorKit.

How It Works

Smart Wallet Flow:

  1. User action → Create instruction → Sign with passkey → Execute transaction

Two patterns:

  • Sign & Send: Complete handling (recommended)
  • Sign Only: Manual control

Basic Patterns

Sign & Send (Recommended)

import { useWallet } from '@lazorkit/wallet';
import { SystemProgram, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
 
function TransferComponent() {
  const { signAndSendTransaction, smartWalletPubkey, isSigning } = useWallet();
 
  const sendSOL = async () => {
    const instruction = SystemProgram.transfer({
      fromPubkey: smartWalletPubkey,
      toPubkey: new PublicKey(process.env.REACT_APP_RECIPIENT_ADDRESS!),
      lamports: LAMPORTS_PER_SOL * 0.1,
    });
 
    try {
      const signature = await signAndSendTransaction(instruction);
      console.log('Sent:', signature);
    } catch (error) {
      console.error('Failed:', error.code); // 'USER_CANCELLED' | 'INSUFFICIENT_FUNDS'
    }
  };
 
  return (
    <button onClick={sendSOL} disabled={isSigning}>
      {isSigning ? 'Sending...' : 'Send 0.1 SOL'}
    </button>
  );
}

Sign Only Pattern

function SignOnlyExample() {
  const { signTransaction, smartWalletPubkey } = useWallet();
  
  const signOnly = async () => {
    const instruction = SystemProgram.transfer({
      fromPubkey: smartWalletPubkey,
      toPubkey: new PublicKey('...'),
      lamports: LAMPORTS_PER_SOL * 0.1,
    });
 
    const signedTx = await signTransaction(instruction);
    
    // Manual sending with Connection if needed
    // const connection = new Connection('...');
    // await connection.sendTransaction(signedTx);
  };
 
  return <button onClick={signOnly}>Sign Only</button>;
}

Custom Transaction Hook

// hooks/useTransactionSender.ts
import { useState } from 'react';
import { useWallet } from '@lazorkit/wallet';
 
export function useTransactionSender() {
  const { signAndSendTransaction } = useWallet();
  const [state, setState] = useState({
    isLoading: false,
    signature: null,
    error: null,
  });
 
  const send = async (instruction) => {
    setState({ isLoading: true, signature: null, error: null });
 
    try {
      const signature = await signAndSendTransaction(instruction);
      setState({ isLoading: false, signature, error: null });
      return signature;
    } catch (error) {
      const message = error.code === 'USER_CANCELLED' 
        ? 'Transaction cancelled' 
        : error.message || 'Transaction failed';
      setState({ isLoading: false, signature: null, error: message });
      throw error;
    }
  };
 
  return { send, ...state, reset: () => setState({ isLoading: false, signature: null, error: null }) };
}
 
// Usage
function TransactionExample() {
  const { send, isLoading, signature, error } = useTransactionSender();
  const { smartWalletPubkey } = useWallet();
 
  const handleSend = async () => {
    const instruction = SystemProgram.transfer({
      fromPubkey: smartWalletPubkey,
      toPubkey: new PublicKey('...'),
      lamports: LAMPORTS_PER_SOL * 0.1,
    });
 
    await send(instruction);
  };
 
  return (
    <div>
      <button onClick={handleSend} disabled={isLoading}>
        {isLoading ? 'Sending...' : 'Send Transaction'}
      </button>
      {signature && <p>✅ Sent: {signature.slice(0, 8)}...</p>}
      {error && <p className="text-red-500">❌ {error}</p>}
    </div>
  );
}

Common Use Cases

SPL Token Transfer

import { createTransferInstruction, getAssociatedTokenAddress } from '@solana/spl-token';
 
function TokenTransfer() {
  const { signAndSendTransaction, smartWalletPubkey } = useWallet();
  
  const transferTokens = async () => {
    const mintAddress = new PublicKey('So11111111111111111111111111111111111111112'); // WSOL
    const recipientAddress = new PublicKey('...');
    
    const senderTokenAccount = await getAssociatedTokenAddress(mintAddress, smartWalletPubkey);
    const recipientTokenAccount = await getAssociatedTokenAddress(mintAddress, recipientAddress);
 
    const instruction = createTransferInstruction(
      senderTokenAccount,
      recipientTokenAccount,
      smartWalletPubkey,
      1000000, // Amount in token decimals
    );
 
    const signature = await signAndSendTransaction(instruction);
    console.log('Token sent:', signature);
  };
 
  return <button onClick={transferTokens}>Transfer Tokens</button>;
}

Custom Program Call

function CustomProgramExample() {
  const { signAndSendTransaction, smartWalletPubkey } = useWallet();
 
  const callProgram = async () => {
    const instruction = new TransactionInstruction({
      keys: [
        { pubkey: smartWalletPubkey, isSigner: true, isWritable: true },
        { pubkey: new PublicKey('...'), isSigner: false, isWritable: false },
      ],
      programId: new PublicKey('YourProgramId...'),
      data: Buffer.from([1, 2, 3]), // Instruction data
    });
 
    const signature = await signAndSendTransaction(instruction);
    console.log('Program called:', signature);
  };
 
  return <button onClick={callProgram}>Call Program</button>;
}

Transaction Monitoring

Simple Transaction History

function TransactionHistory() {
  const [transactions, setTransactions] = useState([]);
  const { signAndSendTransaction } = useWallet();
 
  const sendAndTrack = async (instruction) => {
    try {
      const signature = await signAndSendTransaction(instruction);
      
      // Add to history
      setTransactions(prev => [...prev, {
        signature,
        status: 'confirmed', // LazorKit handles confirmation
        timestamp: Date.now(),
      }]);
      
    } catch (error) {
      console.error('Transaction failed:', error);
    }
  };
 
  return (
    <div>
      <h3>Recent Transactions</h3>
      {transactions.map(tx => (
        <div key={tx.signature} className="p-2 border rounded mb-2">
          <p className="font-mono text-sm">{tx.signature.slice(0, 8)}...</p>
          <p className="text-green-600 text-sm">✅ {tx.status}</p>
        </div>
      ))}
    </div>
  );
}

Error Handling

Transaction Error Codes

const handleError = (error) => {
  switch (error.code) {
    case 'USER_CANCELLED':
      return 'User cancelled the transaction';
    case 'INSUFFICIENT_FUNDS':
      return 'Not enough balance for this transaction';
    case 'TRANSACTION_FAILED':
      return 'Transaction failed to execute';
    case 'NOT_CONNECTED':
      return 'Please connect your wallet first';
    default:
      return error.message || 'Transaction error occurred';
  }
};
 
// Usage
try {
  await signAndSendTransaction(instruction);
} catch (error) {
  const userMessage = handleError(error);
  setErrorMessage(userMessage);
}

Retry Pattern

function useRetryTransaction() {
  const { signAndSendTransaction } = useWallet();
 
  const sendWithRetry = async (instruction, maxAttempts = 2) => {
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        return await signAndSendTransaction(instruction);
      } catch (error) {
        if (attempt === maxAttempts || error.code === 'USER_CANCELLED') {
          throw error;
        }
        // Wait 1 second before retry
        await new Promise(resolve => setTimeout(resolve, 1000));
      }
    }
  };
 
  return { sendWithRetry };
}

Best Practices

1. User Feedback

Always show transaction status:

function TransactionButton() {
  const { isSigning } = useWallet();
  const [status, setStatus] = useState('idle');
 
  const statusMessages = {
    idle: 'Ready to send',
    signing: 'Confirm with passkey...',
    success: 'Transaction sent!',
    error: 'Transaction failed'
  };
 
  return (
    <div>
      <button disabled={isSigning}>
        {isSigning ? 'Signing...' : 'Send Transaction'}
      </button>
      <p>{statusMessages[status]}</p>
    </div>
  );
}

2. Error Recovery

Handle common errors gracefully:

const recoverableErrors = ['TRANSACTION_FAILED', 'BLOCKHASH_NOT_FOUND'];
 
if (recoverableErrors.includes(error.code)) {
  // Retry automatically
  await sendWithRetry(instruction);
} else if (error.code === 'USER_CANCELLED') {
  // Don't retry, just inform user
  setMessage('Transaction cancelled');
}

3. Validation

Basic validation before sending:

const validateTransaction = (instruction, wallet) => {
  const isSigner = instruction.keys.some(
    key => key.pubkey.equals(wallet) && key.isSigner
  );
  
  if (!isSigner) {
    throw new Error('Wallet must sign this transaction');
  }
  
  return true;
};

Gasless Transactions

When paymaster is configured, transactions are automatically gasless:

// No SOL needed for fees - paymaster handles it
function GaslessTransfer() {
  const { signAndSendTransaction, smartWalletPubkey } = useWallet();
 
  const sendGasless = async () => {
    const instruction = SystemProgram.transfer({
      fromPubkey: smartWalletPubkey,
      toPubkey: new PublicKey('...'),
      lamports: LAMPORTS_PER_SOL * 0.1,
    });
 
    // Transaction fees paid by paymaster
    const signature = await signAndSendTransaction(instruction);
    console.log('Gasless transaction:', signature);
  };
 
  return <button onClick={sendGasless}>Send (No Fees)</button>;
}