Skip to main content

Transaction Handling

Managing blockchain transactions and confirmations

Transaction management forms core blockchain application interaction. Low costs (an EVM transfer is 0.0000021 SEL) simplify fee management.

Transaction Lifecycle

StageTime on SelendraUser Visibility
CreatedInstantShow confirmation dialog
Signed1-5 secondsWaiting for signature
Broadcast<1 secondTransaction submitted
Pending1-2 secondsProcessing
Included~1 secondTransaction confirmed
Finalized~1s after inclusionFully confirmed

Finality latency is measured, not bounded by the protocol: 1.00s median and 1.37s at p95, with occasional excursions past 2s. See Consensus. Budget one to two seconds before treating value as settled.


Basic Transaction Flow

Send Transaction

import { Web3 } from "web3";

const web3 = new Web3(window.ethereum);

const tx = {
  from: userAddress,
  to: recipientAddress,
  value: web3.utils.toWei("1", "ether"), // 1 SEL
  gas: 21000,
  gasPrice: await web3.eth.getGasPrice(),
};

try {
  const txHash = await web3.eth.sendTransaction(tx);
  const receipt = await web3.eth.getTransactionReceipt(txHash);

  if (receipt.status) {
    console.log("Transaction successful");
  } else {
    console.log("Transaction failed");
  }
} catch (error) {
  console.error("Transaction error:", error);
}

With Status Updates

async function sendWithUpdates(tx, onUpdate) {
  try {
    onUpdate({ status: "signing", message: "Awaiting signature..." });

    const txHash = await web3.eth.sendTransaction(tx);

    onUpdate({
      status: "pending",
      message: "Transaction submitted",
      hash: txHash,
    });

    // Poll for receipt
    let receipt = null;
    while (!receipt) {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      receipt = await web3.eth.getTransactionReceipt(txHash);
    }

    onUpdate({
      status: receipt.status ? "confirmed" : "failed",
      message: receipt.status
        ? "Transaction confirmed"
        : "Transaction reverted",
      receipt,
    });

    return receipt;
  } catch (error) {
    onUpdate({ status: "error", message: error.message });
    throw error;
  }
}

Gas Management

Estimation with Buffer

// Estimate gas
const gasEstimate = await web3.eth.estimateGas(tx);

// Add 10% buffer
const gasLimit = Math.floor(gasEstimate * 1.1);

const txWithGas = {
  ...tx,
  gas: gasLimit,
};

Contract Gas Estimation

const contract = new web3.eth.Contract(abi, contractAddress);

const gasEstimate = await contract.methods
  .transfer(recipient, amount)
  .estimateGas({ from: userAddress });

const tx = contract.methods.transfer(recipient, amount).send({
  from: userAddress,
  gas: Math.floor(gasEstimate * 1.1),
});

Cost Calculation

async function estimateCost(gasLimit) {
  const gasPrice = await web3.eth.getGasPrice();
  const costWei = BigInt(gasLimit) * gasPrice;
  const costSel = web3.utils.fromWei(costWei.toString(), "ether");

  return {
    wei: costWei,
    sel: costSel,
    usd: parseFloat(costSel) * selPriceInUSD,
  };
}

Confirmation Waiting

Wait for Confirmation

async function waitForConfirmation(txHash, confirmations = 1) {
  let receipt = null;

  // Wait for mining
  while (!receipt) {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    receipt = await web3.eth.getTransactionReceipt(txHash);
  }

  if (confirmations === 1) {
    return receipt;
  }

  // Wait for additional confirmations
  const targetBlock = receipt.blockNumber + confirmations - 1;
  let currentBlock = await web3.eth.getBlockNumber();

  while (currentBlock < targetBlock) {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    currentBlock = await web3.eth.getBlockNumber();
  }

  return receipt;
}

With Progress Callback

async function waitWithProgress(txHash, confirmations, onProgress) {
  const receipt = await waitForReceipt(txHash);
  const targetBlock = receipt.blockNumber + confirmations - 1;

  onProgress({
    current: 0,
    required: confirmations,
    receipt,
  });

  const interval = setInterval(async () => {
    const currentBlock = await web3.eth.getBlockNumber();
    const confirmed = Math.min(
      currentBlock - receipt.blockNumber + 1,
      confirmations
    );

    onProgress({
      current: confirmed,
      required: confirmations,
      receipt,
    });

    if (confirmed >= confirmations) {
      clearInterval(interval);
    }
  }, 1000);
}

Error Handling

Common Errors

async function handleTransaction(tx) {
  try {
    return await web3.eth.sendTransaction(tx);
  } catch (error) {
    // User rejected
    if (error.code === 4001) {
      throw new Error("Transaction rejected by user");
    }

    // Insufficient funds
    if (error.message.includes("insufficient funds")) {
      throw new Error("Insufficient SEL balance");
    }

    // Gas estimation failed
    if (error.message.includes("gas required exceeds allowance")) {
      throw new Error("Transaction would fail - check parameters");
    }

    // Network error
    if (error.message.includes("network")) {
      throw new Error("Network connection failed - please retry");
    }

    throw new Error(`Transaction failed: ${error.message}`);
  }
}

Retry Logic

Automatic Retry

async function sendWithRetry(tx, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await web3.eth.sendTransaction(tx);
    } catch (error) {
      // Don't retry user rejection
      if (error.code === 4001) throw error;

      // Don't retry if would fail anyway
      if (error.message.includes("gas required exceeds")) throw error;

      // Last attempt
      if (attempt === maxAttempts) throw error;

      // Exponential backoff
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise((resolve) => setTimeout(resolve, delay));

      console.log(`Retry attempt ${attempt}/${maxAttempts}`);
    }
  }
}

Batch Transactions

Sequential Execution

async function executeBatch(transactions) {
  const results = [];

  for (const tx of transactions) {
    try {
      const receipt = await web3.eth.sendTransaction(tx);
      await waitForConfirmation(receipt.transactionHash);
      results.push({ success: true, receipt });
    } catch (error) {
      results.push({ success: false, error: error.message });
      break; // Stop on first failure
    }
  }

  return results;
}

Parallel Execution

async function executeParallel(transactions) {
  const promises = transactions.map((tx) =>
    web3.eth
      .sendTransaction(tx)
      .then((hash) => ({ success: true, hash }))
      .catch((error) => ({ success: false, error: error.message }))
  );

  return await Promise.all(promises);
}

Gas Sponsorship

Backend pays fees for users. Enables free experience.

Frontend Request

async function sponsoredTransaction(action, params) {
  const response = await fetch("/api/sponsor-tx", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${authToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      userAddress,
      action,
      params,
    }),
  });

  const { transactionHash } = await response.json();

  const receipt = await waitForConfirmation(transactionHash);

  return receipt;
}

Backend Implementation

async function sponsorTransaction(req, res) {
  const { userAddress, action, params } = req.body;

  await checkRateLimit(userAddress);

  const relayer = new Web3.eth.accounts.Wallet(process.env.RELAYER_PRIVATE_KEY);

  const tx = await contract.methods[action](...params).send({
    from: relayer[0].address,
  });

  await logSponsoredTransaction({
    userAddress,
    txHash: tx.transactionHash,
    cost: tx.gasUsed * tx.effectiveGasPrice,
  });

  res.json({ transactionHash: tx.transactionHash });
}

Transaction Queue

class TransactionQueue {
  constructor() {
    this.queue = [];
    this.processing = false;
  }

  async add(tx) {
    return new Promise((resolve, reject) => {
      this.queue.push({ tx, resolve, reject });
      this.process();
    });
  }

  async process() {
    if (this.processing || this.queue.length === 0) return;

    this.processing = true;
    const { tx, resolve, reject } = this.queue.shift();

    try {
      const receipt = await web3.eth.sendTransaction(tx);
      await waitForConfirmation(receipt.transactionHash);
      resolve(receipt);
    } catch (error) {
      reject(error);
    }

    this.processing = false;
    this.process(); // Process next
  }
}

UI Component

function TransactionStatus({ status, hash }) {
  const statusConfig = {
    signing: {
      icon: "✍️",
      color: "blue",
      message: "Awaiting signature...",
    },
    pending: {
      icon: "⏳",
      color: "yellow",
      message: "Processing transaction...",
    },
    confirmed: {
      icon: "✅",
      color: "green",
      message: "Transaction confirmed!",
    },
    failed: {
      icon: "❌",
      color: "red",
      message: "Transaction failed",
    },
  };

  const config = statusConfig[status];

  return (
    <div className={`status-${config.color}`}>
      <span>{config.icon}</span>
      <p>{config.message}</p>
      {hash && (
        <a href={`https://explorer.selendra.org/tx/${hash}`}>
          View on Explorer
        </a>
      )}
    </div>
  );
}

Event Monitoring | Wallet Integration | EVM Smart Contracts

Community

Telegram: t.me/selendranetwork | X: @selendranetwork | GitHub: github.com/selendra/selendra

Transaction Handling - Selendra