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
npm install @getpara/react-sdk @getpara/viem viem
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);
import { createPublicClient, http } from "viem";
import { holesky } from "viem/chains";
const RPC_URL = process.env.NEXT_PUBLIC_HOLESKY_RPC_URL ||
"https://ethereum-holesky-rpc.publicnode.com";
// Create public client for read operations
const publicClient = createPublicClient({
chain: holesky,
transport: http(RPC_URL)
});
Para Signer Setup
- Ethers v6
- Viem v2
import { createParaEthersSigner } 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 = createParaEthersSigner({ para: client, provider: provider });
}
import { createParaViemAccount, createParaViemClient } from "@getpara/viem";
import { useAccount, useClient } from "@getpara/react-sdk";
// Get Para client and account
const client = useClient();
const { data: account } = useAccount();
// Create Para Viem account and wallet client
let walletClient: any = null;
if (account?.isConnected && client) {
const viemAccount = createParaViemAccount(client);
walletClient = createParaViemClient(client, {
account: viemAccount,
chain: holesky,
transport: http(RPC_URL)
});
}
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;
};
import { parseEther, parseGwei } from "viem";
const constructTransaction = (
toAddress: `0x${string}`,
ethAmount: string,
userAddress: `0x${string}`
) => {
// Construct transaction object
const transaction = {
account: userAddress,
to: toAddress,
value: parseEther(ethAmount),
chain: holesky,
maxFeePerGas: parseGwei("100"), // Custom max fee
maxPriorityFeePerGas: parseGwei("3") // Custom priority fee
};
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
};
};
import { formatEther, parseEther } from "viem";
const estimateTransactionCost = async (
userAddress: `0x${string}`,
toAddress: `0x${string}`,
ethAmount: string
) => {
// Get current balance
const balance = await publicClient.getBalance({ address: userAddress });
const amountWei = parseEther(ethAmount);
// Estimate gas for the specific transaction
const estimatedGas = await publicClient.estimateGas({
account: userAddress,
to: toAddress,
value: amountWei
});
// Get current gas price
const gasPrice = await publicClient.getGasPrice();
const estimatedGasCost = estimatedGas * gasPrice;
const totalCost = amountWei + estimatedGasCost;
return {
balance: formatEther(balance),
amount: formatEther(amountWei),
estimatedGas: formatEther(estimatedGasCost),
totalCost: formatEther(totalCost),
hasSufficientBalance: totalCost <= balance
};
};
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;
};
import { isAddress } from "viem";
const validateTransaction = async (
userAddress: `0x${string}`,
toAddress: string,
ethAmount: string
) => {
// Basic parameter validation
if (!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,
toAddress as `0x${string}`,
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"
};
};
const sendEthTransfer = async (
toAddress: `0x${string}`,
ethAmount: string,
userAddress: `0x${string}`
) => {
// Validate transaction
await validateTransaction(userAddress, toAddress, ethAmount);
// Send transaction using Para wallet client
const hash = await walletClient.sendTransaction({
account: userAddress,
to: toAddress,
value: parseEther(ethAmount),
chain: holesky,
maxFeePerGas: parseGwei("100"),
maxPriorityFeePerGas: parseGwei("3")
});
console.log("Transaction sent:", hash);
// Wait for confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log("Transaction confirmed:", receipt.transactionHash);
console.log("Block number:", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
return {
hash: receipt.transactionHash,
blockNumber: receipt.blockNumber,
gasUsed: receipt.gasUsed.toString(),
status: receipt.status === "success" ? "success" : "failed"
};
};
Complete Implementation Example
Full Transfer Function
- Ethers v6
- Viem v2
import { ethers, parseEther, formatEther, toBigInt } from "ethers";
import { createParaEthersSigner } 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 = createParaEthersSigner({ para: paraClient, provider: 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;
}
}
}
import {
createPublicClient,
createParaViemClient,
createParaViemAccount,
http,
parseEther,
formatEther,
parseGwei,
isAddress
} from "viem";
import { holesky } from "viem/chains";
class ViemTransferService {
private publicClient: any;
private walletClient: any;
constructor(rpcUrl: string, paraClient: any) {
this.publicClient = createPublicClient({
chain: holesky,
transport: http(rpcUrl)
});
const account = createParaViemAccount(paraClient);
this.walletClient = createParaViemClient(paraClient, {
account,
chain: holesky,
transport: http(rpcUrl)
});
}
async transfer(
toAddress: `0x${string}`,
ethAmount: string,
fromAddress: `0x${string}`
) {
try {
// Validate inputs
if (!isAddress(toAddress)) {
throw new Error("Invalid recipient address");
}
// Check balance and estimate gas
const balance = await this.publicClient.getBalance({ address: fromAddress });
const amount = parseEther(ethAmount);
const estimatedGas = await this.publicClient.estimateGas({
account: fromAddress,
to: toAddress,
value: amount
});
const gasPrice = await this.publicClient.getGasPrice();
const estimatedGasCost = estimatedGas * gasPrice;
if (balance < amount + estimatedGasCost) {
throw new Error("Insufficient balance for transfer and gas");
}
// Send transaction
const hash = await this.walletClient.sendTransaction({
account: fromAddress,
to: toAddress,
value: amount,
chain: holesky,
maxFeePerGas: parseGwei("100"),
maxPriorityFeePerGas: parseGwei("3")
});
const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
return {
success: true,
hash: receipt.transactionHash,
blockNumber: receipt.blockNumber,
gasUsed: receipt.gasUsed.toString()
};
} catch (error) {
console.error("Transfer failed:", error);
throw error;
}
}
}
Key Differences
Library Comparison
| Feature | Ethers v6 | Viem v2 |
|---|---|---|
| Provider Setup | JsonRpcProvider | createPublicClient |
| Wallet Client | ParaEthersSigner | createParaViemClient |
| Unit Parsing | parseEther() | parseEther() |
| Gas Estimation | getFeeData() | estimateGas() + getGasPrice() |
| Transaction Send | signer.sendTransaction() | walletClient.sendTransaction() |
| Receipt Waiting | txResponse.wait() | publicClient.waitForTransactionReceipt() |
| Address Validation | ethers.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
};
// Viem allows manual gas price setting
const transaction = {
maxFeePerGas: parseGwei("100"),
maxPriorityFeePerGas: parseGwei("3")
};
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
ERC-20 Token Transfers
Learn to transfer ERC-20 tokens
Transaction Monitoring
Track transaction status and confirmations
Related Walkthroughs
Solana Transfers
Intermediate · 20 min · SOL transfers and Solana programs
EIP-712 Typed Data Signing
Intermediate · 20 min · Structured data signing
Aave v3 Integration
Advanced · 45 min · Lending, borrowing, and yield strategies