Web3 Integration Guide Part 4: Sending Cryptocurrency Payments

Master sending crypto payments to users at scale. Learn batch transactions, gas optimization, payment queues, withdrawal systems, and error handling for production Web3 applications.

By GodFake Team24 min read
Web3PaymentsBatch TransactionsGas OptimizationEthereumVue 3Withdrawals
Web3 Integration Guide

Web3 Integration Guide Part 4: Sending Cryptocurrency Payments

Introduction

Sending cryptocurrency payments efficiently and securely is crucial for applications that need to pay users, process refunds, handle withdrawals, or distribute rewards. Unlike traditional payment systems, blockchain transactions require careful gas management, queue handling, and error recovery strategies.

In this guide, you'll learn how to:

  • Send ETH and ERC-20 tokens to users
  • Batch multiple payments into single transactions
  • Optimize gas costs and transaction speed
  • Build a withdrawal request system
  • Implement payment queues with retry logic
  • Handle failed transactions and edge cases
  • Monitor and track outgoing payments

Understanding Payment Scenarios

Common Use Cases

  1. Withdrawals: Users request to withdraw earnings/balance
  2. Refunds: Return payments for cancelled orders
  3. Rewards/Airdrops: Distribute tokens to many addresses
  4. Payroll: Pay freelancers/contractors in crypto
  5. Royalties: Automatic payments to content creators

Payment Architecture

Request → Validation → Queue → Batch → Execute → Confirm → Complete
   |          |          |        |        |         |         |
 User      Check      Store    Group    Send     Monitor   Update
 Action   Balance     DB       Txs      Tx       Status    Records

Building the Withdrawal System

Create src/composables/useWithdrawals.js

import { ref, computed } from 'vue'
import { BrowserProvider, Contract, parseUnits, formatUnits } from 'ethers'
import { useWallet } from './useWallet'

const ERC20_ABI = [
  'function transfer(address to, uint256 amount) returns (bool)',
  'function balanceOf(address owner) view returns (uint256)',
  'function decimals() view returns (uint8)',
  'event Transfer(address indexed from, address indexed to, uint256 value)'
]

export function useWithdrawals() {
  const { provider, account } = useWallet()

  // Withdrawal state
  const withdrawals = ref([])
  const pendingWithdrawal = ref(null)
  const processing = ref(false)
  const error = ref(null)

  /**
   * Create withdrawal request
   */
  const createWithdrawal = async ({
    recipientAddress,
    amount,
    currency = 'ETH',
    tokenAddress = null,
    memo = ''
  }) => {
    try {
      processing.value = true
      error.value = null

      // Validate recipient address
      if (!recipientAddress || !/^0x[a-fA-F0-9]{40}$/.test(recipientAddress)) {
        throw new Error('Invalid recipient address')
      }

      // Validate amount
      if (!amount || parseFloat(amount) <= 0) {
        throw new Error('Invalid amount')
      }

      // Check if sender has sufficient balance
      const balance = await getBalance(currency, tokenAddress)
      if (parseFloat(balance) < parseFloat(amount)) {
        throw new Error(`Insufficient ${currency} balance`)
      }

      const withdrawal = {
        id: generateWithdrawalId(),
        recipientAddress,
        amount: amount.toString(),
        currency,
        tokenAddress,
        memo,
        status: 'pending',
        createdAt: Date.now(),
        txHash: null,
        gasUsed: null,
        error: null
      }

      withdrawals.value.push(withdrawal)
      pendingWithdrawal.value = withdrawal

      console.log('✅ Withdrawal request created:', withdrawal.id)
      return withdrawal

    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      processing.value = false
    }
  }

  /**
   * Execute withdrawal (send payment)
   */
  const executeWithdrawal = async (withdrawalId) => {
    try {
      processing.value = true
      error.value = null

      const withdrawal = withdrawals.value.find(w => w.id === withdrawalId)
      if (!withdrawal) {
        throw new Error('Withdrawal not found')
      }

      if (withdrawal.status !== 'pending') {
        throw new Error('Withdrawal already processed')
      }

      withdrawal.status = 'processing'

      console.log(`💸 Executing withdrawal ${withdrawalId}`)

      let tx
      if (withdrawal.currency === 'ETH') {
        tx = await sendETH(withdrawal.recipientAddress, withdrawal.amount)
      } else {
        tx = await sendToken(
          withdrawal.tokenAddress,
          withdrawal.recipientAddress,
          withdrawal.amount
        )
      }

      withdrawal.txHash = tx.hash
      withdrawal.status = 'confirming'

      console.log(`Transaction sent: ${tx.hash}`)

      // Wait for confirmation
      const receipt = await tx.wait()

      withdrawal.status = 'completed'
      withdrawal.completedAt = Date.now()
      withdrawal.gasUsed = receipt.gasUsed.toString()

      console.log(`✅ Withdrawal ${withdrawalId} completed`)

      return withdrawal

    } catch (err) {
      const withdrawal = withdrawals.value.find(w => w.id === withdrawalId)
      if (withdrawal) {
        withdrawal.status = 'failed'
        withdrawal.error = err.message
      }

      error.value = err.message
      throw err
    } finally {
      processing.value = false
    }
  }

  /**
   * Send ETH to address
   */
  const sendETH = async (recipientAddress, amountInEth) => {
    if (!provider.value) {
      throw new Error('Wallet not connected')
    }

    const signer = await provider.value.getSigner()
    const amountInWei = parseUnits(amountInEth.toString(), 18)

    // Estimate gas
    const gasEstimate = await signer.estimateGas({
      to: recipientAddress,
      value: amountInWei
    })

    // Add 10% buffer to gas estimate
    const gasLimit = (gasEstimate * 110n) / 100n

    // Get current gas price
    const feeData = await provider.value.getFeeData()

    const tx = await signer.sendTransaction({
      to: recipientAddress,
      value: amountInWei,
      gasLimit,
      maxFeePerGas: feeData.maxFeePerGas,
      maxPriorityFeePerGas: feeData.maxPriorityFeePerGas
    })

    return tx
  }

  /**
   * Send ERC-20 tokens to address
   */
  const sendToken = async (tokenAddress, recipientAddress, amount) => {
    if (!provider.value) {
      throw new Error('Wallet not connected')
    }

    const signer = await provider.value.getSigner()
    const contract = new Contract(tokenAddress, ERC20_ABI, signer)

    // Get token decimals
    const decimals = await contract.decimals()
    const amountInWei = parseUnits(amount.toString(), decimals)

    // Estimate gas
    const gasEstimate = await contract.transfer.estimateGas(
      recipientAddress,
      amountInWei
    )

    // Add 10% buffer
    const gasLimit = (gasEstimate * 110n) / 100n

    const tx = await contract.transfer(recipientAddress, amountInWei, {
      gasLimit
    })

    return tx
  }

  /**
   * Get balance for currency
   */
  const getBalance = async (currency, tokenAddress = null) => {
    if (!provider.value || !account.value) {
      throw new Error('Wallet not connected')
    }

    if (currency === 'ETH') {
      const balance = await provider.value.getBalance(account.value)
      return formatUnits(balance, 18)
    } else {
      const contract = new Contract(tokenAddress, ERC20_ABI, provider.value)
      const decimals = await contract.decimals()
      const balance = await contract.balanceOf(account.value)
      return formatUnits(balance, decimals)
    }
  }

  /**
   * Cancel pending withdrawal
   */
  const cancelWithdrawal = (withdrawalId) => {
    const withdrawal = withdrawals.value.find(w => w.id === withdrawalId)
    if (!withdrawal) {
      throw new Error('Withdrawal not found')
    }

    if (withdrawal.status !== 'pending') {
      throw new Error('Cannot cancel withdrawal that is already processing')
    }

    withdrawal.status = 'cancelled'
    withdrawal.cancelledAt = Date.now()

    console.log(`❌ Withdrawal ${withdrawalId} cancelled`)
  }

  /**
   * Generate unique withdrawal ID
   */
  const generateWithdrawalId = () => {
    return `WD-${Date.now()}-${Math.random().toString(36).substr(2, 9).toUpperCase()}`
  }

  /**
   * Get withdrawals by status
   */
  const getWithdrawalsByStatus = (status) => {
    return withdrawals.value.filter(w => w.status === status)
  }

  // Computed properties
  const pendingWithdrawals = computed(() => 
    withdrawals.value.filter(w => w.status === 'pending')
  )

  const completedWithdrawals = computed(() => 
    withdrawals.value.filter(w => w.status === 'completed')
  )

  const failedWithdrawals = computed(() => 
    withdrawals.value.filter(w => w.status === 'failed')
  )

  return {
    // State
    withdrawals,
    pendingWithdrawal,
    processing,
    error,

    // Computed
    pendingWithdrawals,
    completedWithdrawals,
    failedWithdrawals,

    // Methods
    createWithdrawal,
    executeWithdrawal,
    cancelWithdrawal,
    getWithdrawalsByStatus,
    getBalance
  }
}

Batch Payments for Gas Efficiency

Sending many individual transactions is expensive. Instead, use batch transfers to save gas.

Batch Transfer Smart Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract BatchTransfer is Ownable {
    
    /**
     * Batch send ETH to multiple addresses
     */
    function batchSendETH(
        address[] calldata recipients,
        uint256[] calldata amounts
    ) external payable onlyOwner {
        require(recipients.length == amounts.length, "Arrays length mismatch");
        require(recipients.length > 0, "Empty recipients");
        
        uint256 totalAmount = 0;
        for (uint256 i = 0; i < amounts.length; i++) {
            totalAmount += amounts[i];
        }
        
        require(msg.value >= totalAmount, "Insufficient ETH sent");
        
        for (uint256 i = 0; i < recipients.length; i++) {
            (bool success, ) = recipients[i].call{value: amounts[i]}("");
            require(success, "Transfer failed");
        }
        
        // Refund excess ETH
        if (msg.value > totalAmount) {
            (bool refundSuccess, ) = msg.sender.call{value: msg.value - totalAmount}("");
            require(refundSuccess, "Refund failed");
        }
    }
    
    /**
     * Batch send ERC-20 tokens to multiple addresses
     */
    function batchSendToken(
        address tokenAddress,
        address[] calldata recipients,
        uint256[] calldata amounts
    ) external onlyOwner {
        require(recipients.length == amounts.length, "Arrays length mismatch");
        require(recipients.length > 0, "Empty recipients");
        
        IERC20 token = IERC20(tokenAddress);
        
        for (uint256 i = 0; i < recipients.length; i++) {
            require(
                token.transferFrom(msg.sender, recipients[i], amounts[i]),
                "Transfer failed"
            );
        }
    }
    
    /**
     * Batch send same amount to multiple addresses (airdrops)
     */
    function batchSendTokenFixedAmount(
        address tokenAddress,
        address[] calldata recipients,
        uint256 amountEach
    ) external onlyOwner {
        require(recipients.length > 0, "Empty recipients");
        
        IERC20 token = IERC20(tokenAddress);
        
        for (uint256 i = 0; i < recipients.length; i++) {
            require(
                token.transferFrom(msg.sender, recipients[i], amountEach),
                "Transfer failed"
            );
        }
    }
}

Create src/composables/useBatchPayments.js

import { ref } from 'vue'
import { Contract, parseUnits } from 'ethers'
import { useWallet } from './useWallet'

// Replace with your deployed BatchTransfer contract address
const BATCH_CONTRACT_ADDRESS = '0x...'

const BATCH_TRANSFER_ABI = [
  'function batchSendETH(address[] recipients, uint256[] amounts) payable',
  'function batchSendToken(address tokenAddress, address[] recipients, uint256[] amounts)',
  'function batchSendTokenFixedAmount(address tokenAddress, address[] recipients, uint256 amountEach)'
]

const ERC20_ABI = [
  'function approve(address spender, uint256 amount) returns (bool)',
  'function allowance(address owner, address spender) view returns (uint256)'
]

export function useBatchPayments() {
  const { provider, account } = useWallet()

  const processing = ref(false)
  const error = ref(null)
  const txHash = ref(null)

  /**
   * Batch send ETH to multiple recipients
   */
  const batchSendETH = async (payments) => {
    try {
      processing.value = true
      error.value = null
      txHash.value = null

      if (!provider.value) {
        throw new Error('Wallet not connected')
      }

      // Validate payments array
      if (!Array.isArray(payments) || payments.length === 0) {
        throw new Error('Invalid payments array')
      }

      // Extract recipients and amounts
      const recipients = payments.map(p => p.recipient)
      const amounts = payments.map(p => parseUnits(p.amount.toString(), 18))

      // Calculate total amount needed
      const totalAmount = amounts.reduce((sum, amt) => sum + amt, 0n)

      const signer = await provider.value.getSigner()
      const contract = new Contract(BATCH_CONTRACT_ADDRESS, BATCH_TRANSFER_ABI, signer)

      console.log(`💸 Sending batch payment to ${recipients.length} recipients`)
      console.log(`Total amount: ${formatUnits(totalAmount, 18)} ETH`)

      // Send batch transaction
      const tx = await contract.batchSendETH(recipients, amounts, {
        value: totalAmount
      })

      txHash.value = tx.hash
      console.log(`Transaction sent: ${tx.hash}`)

      const receipt = await tx.wait()
      console.log(`✅ Batch payment completed!`)
      console.log(`Gas used: ${receipt.gasUsed.toString()}`)

      return {
        success: true,
        txHash: tx.hash,
        gasUsed: receipt.gasUsed.toString(),
        recipients: recipients.length
      }

    } catch (err) {
      error.value = err.message
      console.error('Batch payment error:', err)
      throw err
    } finally {
      processing.value = false
    }
  }

  /**
   * Batch send ERC-20 tokens to multiple recipients
   */
  const batchSendToken = async (tokenAddress, payments) => {
    try {
      processing.value = true
      error.value = null
      txHash.value = null

      if (!provider.value) {
        throw new Error('Wallet not connected')
      }

      const signer = await provider.value.getSigner()
      const batchContract = new Contract(BATCH_CONTRACT_ADDRESS, BATCH_TRANSFER_ABI, signer)
      const tokenContract = new Contract(tokenAddress, ERC20_ABI, signer)

      // Get token decimals
      const decimals = await tokenContract.decimals()

      // Extract recipients and amounts
      const recipients = payments.map(p => p.recipient)
      const amounts = payments.map(p => parseUnits(p.amount.toString(), decimals))

      // Calculate total amount
      const totalAmount = amounts.reduce((sum, amt) => sum + amt, 0n)

      // Check allowance
      const allowance = await tokenContract.allowance(account.value, BATCH_CONTRACT_ADDRESS)

      if (allowance < totalAmount) {
        console.log('Approving batch contract...')
        const approveTx = await tokenContract.approve(BATCH_CONTRACT_ADDRESS, totalAmount)
        await approveTx.wait()
        console.log('✅ Approval confirmed')
      }

      console.log(`💸 Sending batch token payment to ${recipients.length} recipients`)

      // Send batch transaction
      const tx = await batchContract.batchSendToken(tokenAddress, recipients, amounts)

      txHash.value = tx.hash
      console.log(`Transaction sent: ${tx.hash}`)

      const receipt = await tx.wait()
      console.log(`✅ Batch token payment completed!`)

      return {
        success: true,
        txHash: tx.hash,
        gasUsed: receipt.gasUsed.toString(),
        recipients: recipients.length
      }

    } catch (err) {
      error.value = err.message
      console.error('Batch token payment error:', err)
      throw err
    } finally {
      processing.value = false
    }
  }

  /**
   * Calculate gas savings from batching
   */
  const calculateBatchSavings = async (numRecipients, currency = 'ETH') => {
    if (!provider.value) return null

    // Approximate gas costs
    const singleTxGas = currency === 'ETH' ? 21000 : 65000 // ETH transfer vs ERC-20
    const batchTxBaseGas = 50000
    const batchTxPerRecipientGas = currency === 'ETH' ? 15000 : 40000

    const individualGasCost = singleTxGas * numRecipients
    const batchGasCost = batchTxBaseGas + (batchTxPerRecipientGas * numRecipients)

    const savings = individualGasCost - batchGasCost
    const savingsPercent = ((savings / individualGasCost) * 100).toFixed(1)

    // Get current gas price
    const feeData = await provider.value.getFeeData()
    const gasPrice = feeData.gasPrice

    const savingsInEth = Number(savings * gasPrice) / 1e18

    return {
      individualGas: individualGasCost,
      batchGas: batchGasCost,
      savedGas: savings,
      savedPercent: savingsPercent,
      savedEth: savingsInEth.toFixed(6),
      recommended: savings > 0
    }
  }

  return {
    processing,
    error,
    txHash,
    batchSendETH,
    batchSendToken,
    calculateBatchSavings
  }
}

Payment Queue System

For high-volume applications, implement a queue to manage payments efficiently.

Create src/composables/usePaymentQueue.js

import { ref, computed } from 'vue'
import { useWithdrawals } from './useWithdrawals'
import { useBatchPayments } from './useBatchPayments'

export function usePaymentQueue() {
  const { executeWithdrawal } = useWithdrawals()
  const { batchSendETH, batchSendToken } = useBatchPayments()

  const queue = ref([])
  const processing = ref(false)
  const processedCount = ref(0)
  const failedCount = ref(0)

  // Queue configuration
  const config = ref({
    batchSize: 50, // Max recipients per batch
    delayBetweenBatches: 5000, // 5 seconds
    maxRetries: 3,
    processingMode: 'batch' // 'individual' or 'batch'
  })

  /**
   * Add payment to queue
   */
  const addToQueue = (payment) => {
    const queueItem = {
      ...payment,
      id: `Q-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
      status: 'queued',
      retries: 0,
      addedAt: Date.now(),
      processedAt: null,
      error: null
    }

    queue.value.push(queueItem)
    console.log(`➕ Added to queue: ${queueItem.id}`)
    
    return queueItem
  }

  /**
   * Add multiple payments to queue
   */
  const addBulkToQueue = (payments) => {
    const queueItems = payments.map(p => addToQueue(p))
    console.log(`➕ Added ${queueItems.length} payments to queue`)
    return queueItems
  }

  /**
   * Process queue
   */
  const processQueue = async () => {
    if (processing.value) {
      console.log('⚠️ Queue already processing')
      return
    }

    processing.value = true
    processedCount.value = 0
    failedCount.value = 0

    console.log(`🚀 Processing queue (${pendingItems.value.length} items)`)

    try {
      if (config.value.processingMode === 'batch') {
        await processBatched()
      } else {
        await processIndividual()
      }

      console.log(`✅ Queue processing complete`)
      console.log(`Processed: ${processedCount.value}, Failed: ${failedCount.value}`)

    } catch (err) {
      console.error('Queue processing error:', err)
    } finally {
      processing.value = false
    }
  }

  /**
   * Process queue in batches
   */
  const processBatched = async () => {
    const pending = pendingItems.value

    // Group by currency
    const ethPayments = pending.filter(p => p.currency === 'ETH')
    const tokenGroups = new Map()

    pending
      .filter(p => p.currency !== 'ETH')
      .forEach(p => {
        if (!tokenGroups.has(p.tokenAddress)) {
          tokenGroups.set(p.tokenAddress, [])
        }
        tokenGroups.get(p.tokenAddress).push(p)
      })

    // Process ETH batches
    if (ethPayments.length > 0) {
      await processCurrencyBatches(ethPayments, 'ETH')
    }

    // Process token batches
    for (const [tokenAddress, payments] of tokenGroups) {
      await processCurrencyBatches(payments, 'TOKEN', tokenAddress)
      // Delay between different token batches
      if (tokenGroups.size > 1) {
        await sleep(config.value.delayBetweenBatches)
      }
    }
  }

  /**
   * Process batches for a specific currency
   */
  const processCurrencyBatches = async (payments, type, tokenAddress = null) => {
    const batchSize = config.value.batchSize
    const batches = []

    // Split into batches
    for (let i = 0; i < payments.length; i += batchSize) {
      batches.push(payments.slice(i, i + batchSize))
    }

    console.log(`Processing ${batches.length} batches for ${type}`)

    // Process each batch
    for (let i = 0; i < batches.length; i++) {
      const batch = batches[i]
      
      console.log(`Processing batch ${i + 1}/${batches.length} (${batch.length} payments)`)

      try {
        // Mark as processing
        batch.forEach(item => {
          item.status = 'processing'
        })

        // Prepare payment data
        const paymentData = batch.map(item => ({
          recipient: item.recipientAddress,
          amount: item.amount
        }))

        // Execute batch
        let result
        if (type === 'ETH') {
          result = await batchSendETH(paymentData)
        } else {
          result = await batchSendToken(tokenAddress, paymentData)
        }

        // Mark as completed
        batch.forEach(item => {
          item.status = 'completed'
          item.processedAt = Date.now()
          item.txHash = result.txHash
          processedCount.value++
        })

        console.log(`✅ Batch ${i + 1} completed: ${result.txHash}`)

      } catch (err) {
        console.error(`❌ Batch ${i + 1} failed:`, err.message)

        // Mark as failed or retry
        batch.forEach(item => {
          if (item.retries < config.value.maxRetries) {
            item.status = 'queued'
            item.retries++
            console.log(`🔄 Retrying ${item.id} (attempt ${item.retries})`)
          } else {
            item.status = 'failed'
            item.error = err.message
            failedCount.value++
          }
        })
      }

      // Delay between batches
      if (i < batches.length - 1) {
        await sleep(config.value.delayBetweenBatches)
      }
    }
  }

  /**
   * Process queue individually
   */
  const processIndividual = async () => {
    const pending = pendingItems.value

    for (const item of pending) {
      try {
        item.status = 'processing'

        await executeWithdrawal(item.withdrawalId)

        item.status = 'completed'
        item.processedAt = Date.now()
        processedCount.value++

        console.log(`✅ Processed: ${item.id}`)

      } catch (err) {
        console.error(`❌ Failed: ${item.id}`, err.message)

        if (item.retries < config.value.maxRetries) {
          item.status = 'queued'
          item.retries++
        } else {
          item.status = 'failed'
          item.error = err.message
          failedCount.value++
        }
      }

      // Small delay between individual transactions
      await sleep(2000)
    }
  }

  /**
   * Retry failed items
   */
  const retryFailed = async () => {
    const failed = failedItems.value

    failed.forEach(item => {
      item.status = 'queued'
      item.retries = 0
      item.error = null
    })

    console.log(`🔄 Retrying ${failed.length} failed items`)

    await processQueue()
  }

  /**
   * Clear completed items from queue
   */
  const clearCompleted = () => {
    const completedCount = completedItems.value.length
    queue.value = queue.value.filter(item => item.status !== 'completed')
    console.log(`🗑️ Cleared ${completedCount} completed items`)
  }

  /**
   * Clear all items from queue
   */
  const clearAll = () => {
    const count = queue.value.length
    queue.value = []
    console.log(`🗑️ Cleared all ${count} items from queue`)
  }

  /**
   * Helper: Sleep function
   */
  const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))

  // Computed properties
  const pendingItems = computed(() => 
    queue.value.filter(item => item.status === 'queued')
  )

  const processingItems = computed(() => 
    queue.value.filter(item => item.status === 'processing')
  )

  const completedItems = computed(() => 
    queue.value.filter(item => item.status === 'completed')
  )

  const failedItems = computed(() => 
    queue.value.filter(item => item.status === 'failed')
  )

  const queueStats = computed(() => ({
    total: queue.value.length,
    pending: pendingItems.value.length,
    processing: processingItems.value.length,
    completed: completedItems.value.length,
    failed: failedItems.value.length
  }))

  return {
    // State
    queue,
    processing,
    config,
    processedCount,
    failedCount,

    // Computed
    pendingItems,
    processingItems,
    completedItems,
    failedItems,
    queueStats,

    // Methods
    addToQueue,
    addBulkToQueue,
    processQueue,
    retryFailed,
    clearCompleted,
    clearAll
  }
}

Gas Optimization Strategies

1. Dynamic Gas Pricing

/**
 * Get optimal gas price based on urgency
 */
async function getOptimalGasPrice(provider, urgency = 'standard') {
  const feeData = await provider.getFeeData()
  
  // Get base fee and priority fee
  const baseFee = feeData.maxFeePerGas
  const priorityFee = feeData.maxPriorityFeePerGas
  
  // Adjust based on urgency
  const multipliers = {
    slow: 0.8,      // 80% - May take longer
    standard: 1.0,  // 100% - Normal speed
    fast: 1.2,      // 120% - Faster confirmation
    instant: 1.5    // 150% - Very fast
  }
  
  const multiplier = multipliers[urgency] || 1.0
  
  return {
    maxFeePerGas: BigInt(Math.floor(Number(baseFee) * multiplier)),
    maxPriorityFeePerGas: BigInt(Math.floor(Number(priorityFee) * multiplier))
  }
}

// Usage
const { maxFeePerGas, maxPriorityFeePerGas } = await getOptimalGasPrice(provider, 'fast')

const tx = await signer.sendTransaction({
  to: recipient,
  value: amount,
  maxFeePerGas,
  maxPriorityFeePerGas
})

2. Gas Price Monitoring

/**
 * Wait for favorable gas prices
 */
async function waitForLowGas(provider, targetGwei = 30, timeoutMs = 300000) {
  const startTime = Date.now()
  
  while (Date.now() - startTime < timeoutMs) {
    const feeData = await provider.getFeeData()
    const currentGwei = Number(feeData.gasPrice) / 1e9
    
    console.log(`Current gas price: ${currentGwei.toFixed(2)} gwei`)
    
    if (currentGwei <= targetGwei) {
      console.log(`✅ Gas price acceptable!`)
      return feeData
    }
    
    console.log(`⏳ Waiting for lower gas (target: ${targetGwei} gwei)`)
    await new Promise(resolve => setTimeout(resolve, 30000)) // Check every 30s
  }
  
  throw new Error('Timeout waiting for low gas prices')
}

// Usage
try {
  const feeData = await waitForLowGas(provider, 25) // Wait for 25 gwei or lower
  // Proceed with transaction
} catch (err) {
  console.log('Using current gas prices instead')
}

3. Transaction Replacement (Speed Up/Cancel)

/**
 * Speed up a pending transaction
 */
async function speedUpTransaction(provider, originalTx, increasePercent = 20) {
  const signer = await provider.getSigner()
  
  // Get original transaction
  const tx = await provider.getTransaction(originalTx.hash)
  
  if (!tx || tx.blockNumber) {
    throw new Error('Transaction already mined or not found')
  }
  
  // Increase gas price by percentage
  const newGasPrice = (tx.gasPrice * BigInt(100 + increasePercent)) / 100n
  
  // Send replacement transaction with same nonce but higher gas
  const speedUpTx = await signer.sendTransaction({
    to: tx.to,
    value: tx.value,
    data: tx.data,
    nonce: tx.nonce, // Same nonce replaces the transaction
    gasLimit: tx.gasLimit,
    gasPrice: newGasPrice
  })
  
  console.log(`🚀 Speed up tx sent: ${speedUpTx.hash}`)
  return speedUpTx
}

/**
 * Cancel a pending transaction
 */
async function cancelTransaction(provider, originalTx) {
  const signer = await provider.getSigner()
  const account = await signer.getAddress()
  
  const tx = await provider.getTransaction(originalTx.hash)
  
  if (!tx || tx.blockNumber) {
    throw new Error('Transaction already mined or not found')
  }
  
  // Send 0 ETH to yourself with same nonce but higher gas
  const cancelTx = await signer.sendTransaction({
    to: account,
    value: 0,
    nonce: tx.nonce,
    gasLimit: 21000,
    gasPrice: (tx.gasPrice * 120n) / 100n // 20% higher
  })
  
  console.log(`❌ Cancel tx sent: ${cancelTx.hash}`)
  return cancelTx
}

Building a Withdrawal Dashboard

Create src/components/WithdrawalDashboard.vue

<template>
  <div class="withdrawal-dashboard">
    <h2>💸 Payment Queue Management</h2>

    <!-- Queue Stats -->
    <div class="stats-grid">
      <div class="stat-card">
        <div class="stat-value">{{ queueStats.total }}</div>
        <div class="stat-label">Total in Queue</div>
      </div>
      <div class="stat-card">
        <div class="stat-value">{{ queueStats.pending }}</div>
        <div class="stat-label">Pending</div>
      </div>
      <div class="stat-card success">
        <div class="stat-value">{{ queueStats.completed }}</div>
        <div class="stat-label">Completed</div>
      </div>
      <div class="stat-card danger">
        <div class="stat-value">{{ queueStats.failed }}</div>
        <div class="stat-label">Failed</div>
      </div>
    </div>

    <!-- Controls -->
    <div class="controls">
      <button 
        @click="processQueue"
        :disabled="processing || queueStats.pending === 0"
        class="btn-primary"
      >
        {{ processing ? 'Processing...' : 'Process Queue' }}
      </button>

      <button 
        @click="retryFailed"
        :disabled="processing || queueStats.failed === 0"
        class="btn-warning"
      >
        Retry Failed ({{ queueStats.failed }})
      </button>

      <button 
        @click="clearCompleted"
        :disabled="queueStats.completed === 0"
        class="btn-secondary"
      >
        Clear Completed
      </button>

      <button 
        @click="showAddModal = true"
        class="btn-success"
      >
        + Add Payment
      </button>
    </div>

    <!-- Processing Progress -->
    <div v-if="processing" class="processing-status">
      <div class="progress-bar">
        <div 
          class="progress-fill"
          :style="{ width: progressPercent + '%' }"
        ></div>
      </div>
      <p>Processing: {{ processedCount }} completed, {{ failedCount }} failed</p>
    </div>

    <!-- Queue Items -->
    <div class="queue-list">
      <h3>Queue Items</h3>
      
      <div v-if="queue.length === 0" class="empty-state">
        <p>No payments in queue</p>
      </div>

      <div v-else>
        <div 
          v-for="item in queue"
          :key="item.id"
          class="queue-item"
          :class="'status-' + item.status"
        >
          <div class="item-header">
            <span class="item-id">{{ item.id }}</span>
            <span class="item-status" :class="'badge-' + item.status">
              {{ item.status }}
            </span>
          </div>

          <div class="item-details">
            <div class="detail">
              <span class="label">Recipient:</span>
              <span class="value">{{ shortenAddress(item.recipientAddress) }}</span>
            </div>
            <div class="detail">
              <span class="label">Amount:</span>
              <span class="value">{{ item.amount }} {{ item.currency }}</span>
            </div>
            <div v-if="item.retries > 0" class="detail">
              <span class="label">Retries:</span>
              <span class="value">{{ item.retries }}</span>
            </div>
          </div>

          <div v-if="item.txHash" class="item-tx">
            <a 
              :href="`https://sepolia.etherscan.io/tx/${item.txHash}`"
              target="_blank"
            >
              View Transaction →
            </a>
          </div>

          <div v-if="item.error" class="item-error">
            ⚠️ {{ item.error }}
          </div>
        </div>
      </div>
    </div>

    <!-- Add Payment Modal -->
    <div v-if="showAddModal" class="modal-overlay" @click="showAddModal = false">
      <div class="modal" @click.stop>
        <h3>Add Payment to Queue</h3>
        
        <label>Recipient Address:</label>
        <input 
          v-model="newPayment.recipientAddress"
          placeholder="0x..."
          class="input"
        />

        <label>Amount:</label>
        <input 
          v-model="newPayment.amount"
          type="number"
          step="0.01"
          placeholder="0.00"
          class="input"
        />

        <label>Currency:</label>
        <select v-model="newPayment.currency" class="input">
          <option value="ETH">ETH</option>
          <option value="USDC">USDC</option>
          <option value="DAI">DAI</option>
        </select>

        <div class="modal-actions">
          <button @click="handleAddPayment" class="btn-primary">
            Add to Queue
          </button>
          <button @click="showAddModal = false" class="btn-secondary">
            Cancel
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { usePaymentQueue } from '../composables/usePaymentQueue'

const {
  queue,
  processing,
  processedCount,
  failedCount,
  queueStats,
  addToQueue,
  processQueue: processQueueFn,
  retryFailed: retryFailedFn,
  clearCompleted: clearCompletedFn
} = usePaymentQueue()

const showAddModal = ref(false)
const newPayment = ref({
  recipientAddress: '',
  amount: '',
  currency: 'ETH',
  tokenAddress: null
})

const progressPercent = computed(() => {
  const total = processedCount.value + failedCount.value
  const target = queueStats.value.pending + queueStats.value.processing
  return target > 0 ? (total / target) * 100 : 0
})

const handleAddPayment = () => {
  if (!newPayment.value.recipientAddress || !newPayment.value.amount) {
    alert('Please fill all fields')
    return
  }

  addToQueue({
    recipientAddress: newPayment.value.recipientAddress,
    amount: newPayment.value.amount,
    currency: newPayment.value.currency,
    tokenAddress: newPayment.value.tokenAddress
  })

  // Reset form
  newPayment.value = {
    recipientAddress: '',
    amount: '',
    currency: 'ETH',
    tokenAddress: null
  }

  showAddModal.value = false
}

const processQueue = () => processQueueFn()
const retryFailed = () => retryFailedFn()
const clearCompleted = () => clearCompletedFn()

const shortenAddress = (address) => {
  if (!address) return ''
  return `${address.slice(0, 6)}...${address.slice(-4)}`
}
</script>

<style scoped>
.withdrawal-dashboard {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

h2 {
  margin-bottom: 2rem;
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
  margin-bottom: 2rem;
}

.stat-card {
  background: white;
  padding: 1.5rem;
  border-radius: 12px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  text-align: center;
}

.stat-card.success {
  background: #d4edda;
}

.stat-card.danger {
  background: #f8d7da;
}

.stat-value {
  font-size: 2rem;
  font-weight: 700;
  color: #333;
}

.stat-label {
  font-size: 0.9rem;
  color: #666;
  margin-top: 0.5rem;
}

.controls {
  display: flex;
  gap: 1rem;
  margin-bottom: 2rem;
  flex-wrap: wrap;
}

.btn-primary, .btn-secondary, .btn-success, .btn-warning {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-primary {
  background: #667eea;
  color: white;
}

.btn-secondary {
  background: #e0e0e0;
  color: #333;
}

.btn-success {
  background: #28a745;
  color: white;
}

.btn-warning {
  background: #ffc107;
  color: #333;
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.processing-status {
  background: #fff3cd;
  padding: 1rem;
  border-radius: 8px;
  margin-bottom: 2rem;
}

.progress-bar {
  height: 8px;
  background: #e9ecef;
  border-radius: 4px;
  overflow: hidden;
  margin-bottom: 0.5rem;
}

.progress-fill {
  height: 100%;
  background: #28a745;
  transition: width 0.3s;
}

.queue-list {
  background: white;
  padding: 2rem;
  border-radius: 12px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.queue-item {
  padding: 1rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  margin-bottom: 1rem;
}

.queue-item.status-completed {
  border-color: #28a745;
  background: #f0f9f4;
}

.queue-item.status-failed {
  border-color: #dc3545;
  background: #fdf4f5;
}

.queue-item.status-processing {
  border-color: #ffc107;
  background: #fffbf0;
}

.item-header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 0.75rem;
}

.item-id {
  font-weight: 600;
  font-family: monospace;
}

.badge-queued {
  background: #e9ecef;
  padding: 0.25rem 0.75rem;
  border-radius: 12px;
  font-size: 0.85rem;
}

.badge-processing {
  background: #fff3cd;
  padding: 0.25rem 0.75rem;
  border-radius: 12px;
  font-size: 0.85rem;
}

.badge-completed {
  background: #d4edda;
  padding: 0.25rem 0.75rem;
  border-radius: 12px;
  font-size: 0.85rem;
}

.badge-failed {
  background: #f8d7da;
  padding: 0.25rem 0.75rem;
  border-radius: 12px;
  font-size: 0.85rem;
}

.item-details {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 0.5rem;
}

.detail {
  display: flex;
  gap: 0.5rem;
}

.label {
  color: #666;
  font-weight: 500;
}

.value {
  font-family: monospace;
  font-weight: 600;
}

.item-tx {
  margin-top: 0.75rem;
}

.item-tx a {
  color: #667eea;
  text-decoration: none;
  font-weight: 600;
}

.item-error {
  margin-top: 0.75rem;
  color: #dc3545;
  font-size: 0.9rem;
}

.empty-state {
  text-align: center;
  padding: 3rem;
  color: #666;
}

/* Modal */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal {
  background: white;
  padding: 2rem;
  border-radius: 12px;
  max-width: 500px;
  width: 90%;
}

.input {
  width: 100%;
  padding: 0.75rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  margin-bottom: 1rem;
  font-size: 1rem;
}

.modal-actions {
  display: flex;
  gap: 1rem;
  margin-top: 1.5rem;
}

.modal-actions button {
  flex: 1;
}
</style>

Production Best Practices

1. Secure Private Key Management

// ❌ Never do this!
const privateKey = '0x1234...' // Hardcoded private key

// ✅ Use environment variables
const privateKey = process.env.PAYMENT_WALLET_PRIVATE_KEY

// ✅ Even better: Use AWS KMS, Google Secret Manager, or HashiCorp Vault
const { SecretManagerServiceClient } = require('@google-cloud/secret-manager')
const client = new SecretManagerServiceClient()

async function getPrivateKey() {
  const [version] = await client.accessSecretVersion({
    name: 'projects/PROJECT_ID/secrets/payment-wallet-key/versions/latest'
  })
  return version.payload.data.toString()
}

2. Transaction Monitoring & Alerts

// Send alerts for critical events
async function sendAlert(type, message, data) {
  // Slack notification
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `🚨 ${type}: ${message}`,
      attachments: [{
        color: type === 'ERROR' ? 'danger' : 'warning',
        fields: Object.entries(data).map(([key, value]) => ({
          title: key,
          value: String(value),
          short: true
        }))
      }]
    })
  })

  // Email notification
  // PagerDuty for critical issues
  // etc.
}

// Usage
try {
  await batchSendETH(payments)
} catch (err) {
  await sendAlert('ERROR', 'Batch payment failed', {
    error: err.message,
    paymentCount: payments.length,
    timestamp: new Date().toISOString()
  })
}

3. Wallet Balance Monitoring

async function checkWalletBalance(provider, address, minBalanceEth = 0.1) {
  const balance = await provider.getBalance(address)
  const balanceEth = Number(balance) / 1e18

  if (balanceEth < minBalanceEth) {
    await sendAlert('WARNING', 'Low wallet balance', {
      address,
      balance: balanceEth.toFixed(4),
      minimum: minBalanceEth
    })
  }

  return balanceEth
}

// Run periodically
setInterval(async () => {
  await checkWalletBalance(provider, PAYMENT_WALLET_ADDRESS, 0.5)
}, 3600000) // Every hour

What's Next?

You now have a complete payment sending system with batching, queues, and optimization! In the final part, we'll cover production deployment and security:

  • Part 5: Security auditing, testing strategies, production monitoring, incident response, compliance

Resources