Skip to main content
This walkthrough covers sending basic Ethereum transfers using Para with both Ethers v6 and Viem v2. You’ll learn transaction construction, gas estimation, and signing patterns for each library.

Prerequisites

You need an authenticated Para client and basic knowledge of Ethereum transactions.

Core Dependencies

  • Ethers v6
  • Viem v2
npm install @getpara/react-sdk @getpara/ethers-v6-integration ethers

Provider Setup

RPC Configuration

  • Ethers v6
  • Viem v2
import { ethers } from "ethers";

const RPC_URL = process.env.NEXT_PUBLIC_HOLESKY_RPC_URL || 
  "https://ethereum-holesky-rpc.publicnode.com";

// Create JSON RPC provider
const provider = new ethers.JsonRpcProvider(RPC_URL);

Para Signer Setup

  • Ethers v6
  • Viem v2
import { ParaEthersSigner } from "@getpara/ethers-v6-integration";
import { useAccount, useClient } from "@getpara/react-sdk";

// Get Para client and account
const client = useClient();
const { data: account } = useAccount();

// Create Para Ethers signer
let signer: ParaEthersSigner | null = null;

if (account?.isConnected && provider && client) {
  signer = new ParaEthersSigner(client, provider);
}

Transaction Construction

Basic Transaction Parameters

  • Ethers v6
  • Viem v2
import { parseEther, toBigInt } from "ethers";

const constructTransaction = async (
  toAddress: string, 
  ethAmount: string,
  userAddress: string
) => {
  // Get transaction parameters
  const nonce = await provider.getTransactionCount(userAddress);
  const feeData = await provider.getFeeData();
  const gasLimit = toBigInt(21000); // Standard ETH transfer gas
  const value = parseEther(ethAmount);

  // Construct transaction object
  const transaction = {
    to: toAddress,
    value: value,
    nonce: nonce,
    gasLimit: gasLimit,
    maxFeePerGas: feeData.maxFeePerGas,
    maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
    chainId: 17000, // Holesky chain ID
  };

  return transaction;
};

Gas Estimation

Estimate Transaction Cost

  • Ethers v6
  • Viem v2
import { formatEther } from "ethers";

const estimateTransactionCost = async (
  userAddress: string,
  ethAmount: string
) => {
  // Get current balance
  const balanceWei = await provider.getBalance(userAddress);
  
  // Get fee data
  const feeData = await provider.getFeeData();
  const gasLimit = toBigInt(21000);
  
  // Calculate max gas cost
  const maxGasFee = gasLimit * (feeData.maxFeePerGas ?? toBigInt(0));
  const amountWei = parseEther(ethAmount);
  const totalCost = amountWei + maxGasFee;
  
  return {
    balance: formatEther(balanceWei),
    amount: formatEther(amountWei),
    estimatedGas: formatEther(maxGasFee),
    totalCost: formatEther(totalCost),
    hasSufficientBalance: totalCost <= balanceWei
  };
};

Transaction Validation

Balance and Parameter Checks

  • Ethers v6
  • Viem v2
const validateTransaction = async (
  userAddress: string,
  toAddress: string,
  ethAmount: string
) => {
  // Basic parameter validation
  if (!ethers.isAddress(toAddress)) {
    throw new Error("Invalid recipient address");
  }
  
  if (parseFloat(ethAmount) <= 0) {
    throw new Error("Amount must be greater than 0");
  }
  
  // Check balance and gas
  const estimation = await estimateTransactionCost(userAddress, ethAmount);
  
  if (!estimation.hasSufficientBalance) {
    throw new Error(
      `Insufficient balance. Need ${estimation.totalCost} ETH, have ${estimation.balance} ETH`
    );
  }
  
  return true;
};

Sending Transactions

Execute Transfer

  • Ethers v6
  • Viem v2
const sendEthTransfer = async (
  toAddress: string,
  ethAmount: string,
  userAddress: string
) => {
  // Validate transaction
  await validateTransaction(userAddress, toAddress, ethAmount);
  
  // Construct transaction
  const transaction = await constructTransaction(toAddress, ethAmount, userAddress);
  
  // Send transaction using Para signer
  const txResponse = await signer.sendTransaction(transaction);
  
  console.log("Transaction sent:", txResponse.hash);
  
  // Wait for confirmation
  const receipt = await txResponse.wait();
  
  console.log("Transaction confirmed:", receipt.hash);
  console.log("Block number:", receipt.blockNumber);
  console.log("Gas used:", receipt.gasUsed.toString());
  
  return {
    hash: receipt.hash,
    blockNumber: receipt.blockNumber,
    gasUsed: receipt.gasUsed.toString(),
    status: receipt.status === 1 ? "success" : "failed"
  };
};

Complete Implementation Example

Full Transfer Function

  • Ethers v6
  • Viem v2
import { ethers, parseEther, formatEther, toBigInt } from "ethers";
import { ParaEthersSigner } from "@getpara/ethers-v6-integration";

class EthersTransferService {
  private provider: ethers.JsonRpcProvider;
  private signer: ParaEthersSigner;
  
  constructor(rpcUrl: string, paraClient: any) {
    this.provider = new ethers.JsonRpcProvider(rpcUrl);
    this.signer = new ParaEthersSigner(paraClient, this.provider);
  }
  
  async transfer(toAddress: string, ethAmount: string, fromAddress: string) {
    try {
      // Validate inputs
      if (!ethers.isAddress(toAddress)) {
        throw new Error("Invalid recipient address");
      }
      
      // Check balance
      const balance = await this.provider.getBalance(fromAddress);
      const amount = parseEther(ethAmount);
      const feeData = await this.provider.getFeeData();
      const gasLimit = toBigInt(21000);
      const maxGasFee = gasLimit * (feeData.maxFeePerGas ?? toBigInt(0));
      
      if (balance < amount + maxGasFee) {
        throw new Error("Insufficient balance for transfer and gas");
      }
      
      // Construct and send transaction
      const nonce = await this.provider.getTransactionCount(fromAddress);
      
      const transaction = {
        to: toAddress,
        value: amount,
        nonce: nonce,
        gasLimit: gasLimit,
        maxFeePerGas: feeData.maxFeePerGas,
        maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
        chainId: 17000
      };
      
      const txResponse = await this.signer.sendTransaction(transaction);
      const receipt = await txResponse.wait();
      
      return {
        success: true,
        hash: receipt.hash,
        blockNumber: receipt.blockNumber,
        gasUsed: receipt.gasUsed.toString()
      };
      
    } catch (error) {
      console.error("Transfer failed:", error);
      throw error;
    }
  }
}

Key Differences

Library Comparison

FeatureEthers v6Viem v2
Provider SetupJsonRpcProvidercreatePublicClient
Wallet ClientParaEthersSignercreateParaViemClient
Unit ParsingparseEther()parseEther()
Gas EstimationgetFeeData()estimateGas() + getGasPrice()
Transaction Sendsigner.sendTransaction()walletClient.sendTransaction()
Receipt WaitingtxResponse.wait()publicClient.waitForTransactionReceipt()
Address Validationethers.isAddress()isAddress()

Gas Strategy Differences

  • Ethers v6
  • Viem v2
// Ethers uses provider fee data
const feeData = await provider.getFeeData();
const transaction = {
  maxFeePerGas: feeData.maxFeePerGas,
  maxPriorityFeePerGas: feeData.maxPriorityFeePerGas
};

Best Practices

Transaction Safety

  • Always validate recipient addresses before sending
  • Check balance including gas costs before transaction construction
  • Use appropriate gas limits (21000 for basic ETH transfers)
  • Handle network errors gracefully with proper try/catch blocks
  • Wait for receipt confirmation before considering transaction complete

Gas Optimization

  • Monitor network conditions for optimal gas pricing
  • Use EIP-1559 gas parameters for better fee prediction
  • Estimate gas dynamically rather than using static values
  • Consider gas limit buffers for complex operations

Error Handling

Common errors to handle:
  • Invalid recipient addresses
  • Insufficient balance
  • Network connectivity issues
  • Transaction reversion
  • Nonce management conflicts

Next Steps

I