Selendra

Documentation

Transaction Handling

Managing blockchain transactions and confirmations

Transaction management forms core blockchain application interaction. Selendra fast finality enables quick confirmations. Low costs (~$0.00025) 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
Finalized1-2 secondsFully confirmed

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

Contribute

Found an issue or want to contribute?

Help us improve this documentation by editing this page on GitHub.

Edit this page on GitHub