Page 1 of 1

Everexport helpful tool for bookkeeping

Posted: Sun Jul 05, 2026 6:33 pm
by everexport
This tool allow node operators to enter their node wallet and reputation wallet, it will then generate a csv file for each.

It has a few settings you can pick to make the reports suitable for you, it also has a checkbox for aggregating transactions into daily (instead of 24 transactions in one day, you get one transaction that has all those 24 combined). You can also exclude dust transactions.

It hasn't been audited or verified and I am not an accountant. But this could be helpful, your accountant would know if its proper or not. Different countries have different rules.

Runs well on everpanel, dockerhub file everexport/everexport:latest

folder structure

/
/node_modules (created with npm i)
/public/index.html
/Dockerfile
/package.json
/server.js
/start.sh
/supervisord.conf

Dockerfile

Code: Select all

FROM everweb/test:latest

ENV DEBIAN_FRONTEND=noninteractive

RUN apt update && \
    apt install -y tzdata curl openssh-server openssl sudo bash nano supervisor && \
    ln -fs /usr/share/zoneinfo/Etc/UTC /etc/localtime && \
    dpkg-reconfigure --frontend noninteractive tzdata && \
    mkdir -p /var/log/supervisor && \
    rm -rf /var/lib/apt/lists/*

RUN useradd -m -s /bin/bash evertx && \
    echo "evertx:default" | chpasswd && \
    usermod -aG sudo evertx && \
    echo "evertx ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

RUN mkdir /var/run/sshd && \
    echo "PermitRootLogin no" >> /etc/ssh/sshd_config && \
    echo "ListenAddress 0.0.0.0" >> /etc/ssh/sshd_config

COPY supervisord.conf /home/evertx/supervisord.conf
COPY public /home/evertx/public
COPY start.sh /start.sh
COPY server.js /home/evertx/server.js
COPY package.json /home/evertx/package.json
COPY node_modules /home/evertx/node_modules

WORKDIR /home/evertx

RUN chmod +x /start.sh

ENTRYPOINT ["/start.sh"]
package.json

Code: Select all

{
  "name": "everexport",
  "version": "1.0.0",
  "description": "Xahau wallet transaction exporter with CSV output",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "node server.js"
  },
  "keywords": [
    "xahau",
    "xrpl",
    "crypto",
    "transactions",
    "csv",
    "export"
  ],
  "author": "evertx",
  "license": "MIT",
  "dependencies": {
    "axios": "^1.6.0",
    "csv-writer": "^1.6.0",
    "express": "^4.18.2",
    "ws": "^8.16.0"
  }
}
server.js

Code: Select all

const express = require("express");
const https = require("https");
const fs = require("fs");
const axios = require("axios");
const WebSocket = require("ws");

const app = express();
app.use(express.json());
app.use(express.static("public"));

const RIPPLE_EPOCH = 946684800;
const REWARDS_WALLET = "rHktfGUbjqzU4GsYCMc1pDjdHXb5CJamto";   // Official rewards wallet

function rippleToISO(date) {
  return date ? new Date((date + RIPPLE_EPOCH) * 1000).toISOString() : "";
}

async function fetchTransactions(node, wallet) {
  if (node.startsWith("ws")) {
    return await fetchViaWebSocket(node, wallet);
  } else {
    return await fetchViaHTTP(node, wallet);
  }
}

async function fetchViaWebSocket(node, wallet) {
  return new Promise((resolve, reject) => {
    let allTx = [];
    let marker = null;
    const ws = new WebSocket(node);

    function requestPage() {
      const payload = {
        id: 1,
        command: "account_tx",
        account: wallet,
        ledger_index_min: -1,
        ledger_index_max: -1,
        limit: 200
      };
      if (marker) payload.marker = marker;
      ws.send(JSON.stringify(payload));
    }

    ws.on("open", requestPage);

    ws.on("message", (data) => {
      const msg = JSON.parse(data);
      if (msg.status !== "success") {
        ws.close();
        return reject(new Error(msg.error_message || msg.error || "WebSocket error"));
      }

      const result = msg.result;
      allTx = allTx.concat(result.transactions || []);

      if (result.marker) {
        marker = result.marker;
        requestPage();
      } else {
        ws.close();
        resolve(allTx);
      }
    });

    ws.on("error", (err) => { ws.close(); reject(err); });
  });
}

async function fetchViaHTTP(node, wallet) {
  let allTx = [];
  let marker = null;

  while (true) {
    const payload = {
      method: "account_tx",
      params: [{
        account: wallet,
        ledger_index_min: -1,
        ledger_index_max: -1,
        limit: 200
      }]
    };
    if (marker) payload.params[0].marker = marker;

    const response = await axios.post(node, payload);
    const result = response.data.result;
    allTx = allTx.concat(result.transactions || []);

    if (result.marker) {
      marker = result.marker;
    } else {
      break;
    }
  }
  return allTx;
}

function getAmountInfo(txEntry) {
  const tx = txEntry.tx_json || txEntry.tx || {};
  const meta = txEntry.meta || {};

  let amt = meta.delivered_amount;
  if (amt && amt !== "unavailable") {
    if (typeof amt === "string") return { value: parseInt(amt)/1000000, currency: "XAH" };
    if (amt.value) return { value: parseFloat(amt.value), currency: amt.currency || "XAH" };
  }

  amt = tx.Amount;
  if (amt) {
    if (typeof amt === "string") return { value: parseInt(amt)/1000000, currency: "XAH" };
    if (amt.value) return { value: parseFloat(amt.value), currency: amt.currency || "XAH" };
  }
  return { value: 0, currency: "XAH" };
}

function toRecords(txs, wallet, showPlus, rewardClassification, reputationWallet, leaseClassification, refundClassification) {
  return txs.map(txEntry => {
    let tx = txEntry.tx_json || txEntry.tx || txEntry.root || txEntry || {};

    const { value: rawAmt, currency } = getAmountInfo(txEntry);

    const direction = (tx.Account === wallet) ? "OUT" : "IN";
    let signedAmount = direction === "OUT" ? -rawAmt : rawAmt;

    let amountStr = signedAmount.toFixed(8);
    if (direction === "IN" && showPlus) {
      amountStr = "+" + amountStr;
    }

    const feeDrops = parseInt(tx.Fee || 0);
    const fee = feeDrops / 1000000;
    const feeStr = (direction === "OUT") ? Number(fee.toFixed(8)) : "";

    // Memo Extraction
    let memoType = "";
    let memoDataDecoded = "";

    let memos = tx.Memos || txEntry.Memos || (tx.root && tx.root.Memos) || [];

    if (Array.isArray(memos) && memos.length > 0) {
      try {
        const memoObj = memos[0].Memo || memos[0];

        if (memoObj.MemoType) {
          let rawType = memoObj.MemoType;
          if (typeof rawType === 'string' && /^[0-9A-Fa-f]+$/.test(rawType)) {
            memoType = Buffer.from(rawType, 'hex').toString('utf8').trim();
          } else {
            memoType = rawType;
          }
        }

        if (memoObj.MemoData) {
          let rawData = memoObj.MemoData;
          if (/^[0-9A-Fa-f]+$/.test(rawData)) {
            memoDataDecoded = Buffer.from(rawData, 'hex').toString('utf8').trim();
          } else {
            memoDataDecoded = rawData;
          }
        }
      } catch (e) {}
    }

    // === LABEL LOGIC ===
    let label = "";

    const isEVR = (currency || "").toUpperCase() === "EVR";
    const repWallet = (reputationWallet || "").trim();

    const hasRefund = 
      memoType.toLowerCase().includes("evnrefund") ||
      memoDataDecoded.toLowerCase().includes("evnrefund");

    const hasLease = 
      memoType.toLowerCase().includes("evnacquirelease") ||
      memoDataDecoded.toLowerCase().includes("evnacquirelease");

    // 1. Reputation Wallet = ALWAYS "Transfer"
    if (repWallet && (tx.Account === repWallet || tx.Destination === repWallet)) {
      label = "Transfer";
    }
    // 2. EVN Refund
    else if (hasRefund) {
      label = refundClassification || "Refund";
    }
    // 3. EVN Lease Acquisition
    else if (hasLease) {
      label = leaseClassification || "Mining";
    }
    // 4. Official Evernode Rewards
    else if (isEVR && direction === "IN" && tx.Account === REWARDS_WALLET) {
      label = rewardClassification || "Mining";
    }

    return {
      ledger: txEntry.ledger_index || tx.ledger_index || "",
      direction,
      txtype: tx.TransactionType || "",
      date: rippleToISO(tx.date),
      currency: currency || "XAH",
      amount: amountStr,
      is_fee: "NO",
      fee: feeStr,
      fee_currency: "XAH",
      hash: txEntry.hash || tx.hash || "",
      aggregate_count: 1,

      Source: tx.Account || "",
      Destination: tx.Destination || "",
      DestinationTag: tx.DestinationTag ?? "",
      SourceTag: tx.SourceTag ?? "",

      label: label,
      memoType: memoType || "[none]",
      memoData: memoDataDecoded || "[none]"
    };
  });
}

function aggregateDailyRecords(records, showPlus) {
  const groups = new Map();

  for (const row of records) {
    const day = row.date ? row.date.slice(0, 10) : "unknown";
    const direction = row.direction || "";
    const currency = row.currency || "XAH";
    const label = row.label || "";

    // Important:
    // OUT transactions are grouped by Destination.
    // IN transactions are grouped by Source.
    // This prevents different wallets from being merged together.
    const counterparty =
      direction === "OUT"
        ? (row.Destination || "")
        : (row.Source || "");

    const key = [
      day,
      direction,
      currency,
      label,
      counterparty
    ].join("|");

    const amountNum = parseFloat(String(row.amount || "0").replace("+", "")) || 0;
    const feeNum = row.fee === "" ? 0 : (parseFloat(row.fee) || 0);

    if (!groups.has(key)) {
      groups.set(key, {
        ...row,
        date: day === "unknown" ? "" : `${day}T00:00:00.000Z`,
        ledger: "",
        txtype: "DailyAggregate",
        hash: `DAILY-${day}-${direction}-${currency}-${label || "none"}-${counterparty || "unknown"}`,
        DestinationTag: "",
        SourceTag: "",
        memoType: "[daily aggregate]",
        memoData: "[daily aggregate]",
        aggregate_count: 0,
        _amountTotal: 0,
        _feeTotal: 0
      });
    }

    const group = groups.get(key);
    group._amountTotal += amountNum;
    group._feeTotal += feeNum;
    group.aggregate_count += 1;
  }

  return Array.from(groups.values()).map(row => {
    let amountStr = row._amountTotal.toFixed(8);

    if (row.direction === "IN" && showPlus) {
      amountStr = "+" + amountStr;
    }

    row.amount = amountStr;
    row.fee = row._feeTotal > 0 ? row._feeTotal.toFixed(8) : "";

    delete row._amountTotal;
    delete row._feeTotal;

    return row;
  });
}

app.post("/api/transactions", async (req, res) => {
const { 
  wallet, 
  node, 
  minXAH = 0, 
  minEVR = 0, 
  excludeTicker = "", 
  excludeSellOffer = true,
  showPlus = true,
  directionFilter = "all",
  fromDate, 
  toDate,
  rewardClassification = "Mining",
  reputationWallet = "",
  leaseClassification = "Mining",
  refundClassification = "Refund",
  aggregateDaily = false
} = req.body;

  if (!wallet || !node) {
    return res.status(400).json({ error: "wallet and node are required" });
  }

  try {
    let rawTxs = await fetchTransactions(node, wallet);

    let filtered = rawTxs.filter(txEntry => {
      const tx = txEntry.tx_json || txEntry.tx || {};

      if (directionFilter !== "all") {
        const isOutgoing = (tx.Account === wallet);
        const direction = isOutgoing ? "OUT" : "IN";
        if (direction !== directionFilter) return false;
      }

      if (excludeSellOffer && tx.TransactionType === "URITokenCreateSellOffer") return false;

      const info = getAmountInfo(txEntry);
      const currency = (info.currency || "XAH").toUpperCase();
      const absAmount = Math.abs(info.value);

      if (currency === "XAH" && minXAH > 0 && absAmount < minXAH) return false;
      if (currency === "EVR" && minEVR > 0 && absAmount < minEVR) return false;
      if (excludeTicker && currency === excludeTicker.toUpperCase()) return false;

      const txDateStr = rippleToISO(tx.date);
      if (txDateStr) {
        const txTime = new Date(txDateStr).getTime();
        if (fromDate && txTime < fromDate) return false;
        if (toDate && txTime > toDate) return false;
      }

      return true;
    });

let records = toRecords(
  filtered,
  wallet,
  showPlus,
  rewardClassification,
  reputationWallet,
  leaseClassification,
  refundClassification
);

if (aggregateDaily) {
  records = aggregateDailyRecords(records, showPlus);
}

res.json({ count: records.length, transactions: records });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: err.message });
  }
});

const HTTPS_PORT = process.env.HTTPSPORT || __HTTPSPORT__;

const tlsOptions = {
  cert: fs.readFileSync("/contract/cfg/tlscert.pem"),
  key: fs.readFileSync("/contract/cfg/tlskey.pem"),
};

https.createServer(tlsOptions, app).listen(HTTPS_PORT, "0.0.0.0", () => {
  console.log(`✅ Xahau Exporter running on port ${HTTPS_PORT}`);
});
start.sh

Code: Select all

#!/bin/bash

set -e

if [ ! -f /etc/ssh/ssh_host_rsa_key ]; then
  ssh-keygen -A
fi

if [ -f /contract/env.vars ]; then
  source /contract/env.vars
fi
sleep 15

HTTPSPORT=${EXTERNAL_GPTCP1_PORT:-36525}

echo "Using HTTPS port: $HTTPSPORT"

sed -i "s/__HTTPSPORT__/${HTTPSPORT}/g" /home/evertx/server.js

exec /usr/bin/supervisord -c /home/evertx/supervisord.conf
supervisord.conf

Code: Select all

[supervisord]
nodaemon=true
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid

[unix_http_server]
file=/var/run/supervisor.sock
chmod=0770
chown=evertx:evertx

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock

[program:sshd]
startretries=100
startsecs=15
command=/usr/sbin/sshd -D
autostart=false
autorestart=false
stderr_logfile=/home/evertx/sshd.err.log
stdout_logfile=/home/evertx/sshd.out.log
priority=10

[program:hotpocket]
startretries=100
startsecs=15
command=/usr/local/bin/hotpocket/hpcore run /contract
autostart=false
autorestart=false
stderr_logfile=/home/evertx/hotpocket.err.log
stdout_logfile=/home/evertx/hotpocket.out.log
priority=20

[program:webserver]
startretries=100
startsecs=15
command=/usr/bin/node /home/evertx/server.js
autostart=true
autorestart=true
stderr_logfile=/home/evertx/webserver.err.log
stdout_logfile=/home/evertx/webserver.out.log
priority=30
index.html

Code: Select all

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Xahau Exporter</title>
  <style>
    :root {
      --primary: #00aaff;
      --primary-dark: #0088cc;
      --success: #00cc66;
      --danger: #ff4444;
      --gray: #888;
    }

    body {
      font-family: 'Segoe UI', Arial, sans-serif;
      margin: 0;
      padding: 40px;
      background: linear-gradient(135deg, #f0f4f8, #d9e6f0);
      color: #333;
    }

    .container {
      max-width: 1100px;
      margin: 0 auto;
      background: white;
      border-radius: 16px;
      box-shadow: 0 10px 30px rgba(0,0,0,0.1);
      overflow: hidden;
    }

    header {
      background: var(--primary);
      color: white;
      padding: 25px 40px;
      text-align: center;
    }

    h1 { margin: 0; font-size: 28px; }

    .content {
      padding: 40px;
    }

    label {
      display: block;
      margin-bottom: 6px;
      font-weight: 600;
      color: #444;
    }

    input[type="text"], input[type="number"], input[type="datetime-local"] {
      width: 95%;
      padding: 12px 16px;
      border: 2px solid #ddd;
      border-radius: 8px;
      font-size: 16px;
    }

    input:focus {
      border-color: var(--primary);
      outline: none;
    }

    .filter-row {
      display: flex;
      gap: 20px;
      flex-wrap: wrap;
      margin-bottom: 15px;
    }

    .note {
      font-size: 14.5px;
      color: #555;
    }

    .terms-accept {
      background: #f8f9fa;
      padding: 18px 22px;
      border-radius: 12px;
      border: 2px solid #eee;
      margin: 25px 0;
    }

    button {
      padding: 14px 28px;
      font-size: 17px;
      font-weight: 600;
      border: none;
      border-radius: 10px;
      cursor: pointer;
      width: 100%;
      margin-top: 8px;
      transition: all 0.3s;
    }

    #fetchBtn {
      background: var(--primary);
      color: white;
    }

    #fetchBtn:hover:not(:disabled) {
      background: var(--primary-dark);
      transform: translateY(-2px);
    }

    #fetchBtn:disabled {
      background: var(--gray);
      cursor: not-allowed;
    }

    #downloadBtn {
      background: #555;
      color: white;
    }

    #downloadBtn:hover:not(:disabled) {
      background: #333;
      transform: translateY(-2px);
    }

    #downloadBtn:disabled {
      background: #aaa;
      cursor: not-allowed;
    }

    .column-option {
      display: flex;
      align-items: center;
      gap: 12px;
      padding: 8px 0;
    }

    .modal {
      display: none;
      position: fixed;
      top: 0; left: 0;
      width: 100%; height: 100%;
      background: rgba(0,0,0,0.75);
      z-index: 2000;
      align-items: center;
      justify-content: center;
    }

    .modal-content {
      background: white;
      padding: 30px;
      border-radius: 16px;
      width: 90%;
      max-width: 720px;
      max-height: 88vh;
      overflow-y: auto;
    }

    .close { float: right; font-size: 32px; cursor: pointer; color: #999; }
    .close:hover { color: var(--danger); }

    .preview-container {
      margin-top: 30px;
      border: 1px solid #ddd;
      border-radius: 12px;
      overflow-x:scroll;
    }

    table { width: 100%; border-collapse: collapse; }
    th, td { padding: 12px 14px; border-bottom: 1px solid #eee; }
    th { background: var(--primary); color: white; font-weight: 600; }

    #status {
      font-weight: 600;
      padding: 12px;
      border-radius: 8px;
      margin: 15px 0;
    }
  </style>
</head>
<body>

<div class="container">
  <header>
    <h1>Xahau Wallet Exporter</h1>
    <p style="margin: 8px 0 0 0; opacity: 0.9;">Export clean & accurate transaction history</p>
  </header>

  <div class="content">
    <p style="text-align:center; margin-bottom:25px;">
      By using this tool you agree to the 
      <a href="#" id="termsLink" style="color:var(--primary); text-decoration:underline;">Terms & Conditions</a>.
      In most cases you just need to set a minimum xah amount to cut off a bunch of unnecessary transactions. The exports should work well with koinly.
      What to export is your node address and reputation wallet. The reputation wallet rents an instance from your node once per hour, these transactions are essentially transfers from your reputation wallet to your node wallet (not trades, sales or withdrawals/deposits).
    This is dockerized and work well with Evrpanel. Docker container: everexport/everexport:latest
    </p>

    <div class="input-group">
      <label>Wallet Address</label>
      <input id="wallet" placeholder="rYourXahauWalletAddressHere..." type="text">
    </div>

    <div class="input-group">
      <label>Node URL (optional)</label>
      <input id="node" value="wss://xahau.network" type="text">
    </div>

    <div class="filter-row">
      <div style="flex:1;">
        <label>Min XAH Amount (usually 0.0001 to hide spam)</label>
        <input id="minAmount" placeholder="0.001" value="0.00001" type="number" step="0.00001">
      </div>
      <div style="flex:1;">
        <label>Min EVR Amount (rarely used)</label>
        <input id="minEVR" placeholder="0" type="number" step="0.000001">
      </div>
    </div>

    <div class="filter-row">
      <div style="flex:1;">
        <label>From Date (UTC)</label>
        <input id="fromDate" type="datetime-local">
      </div>
      <div style="flex:1;">
        <label>To Date (UTC)</label>
        <input id="toDate" type="datetime-local">
      </div>
    </div>

    <div class="input-group">
      <label>Exclude Ticker</label>
      <input id="excludeTicker" placeholder="e.g. XAH (optional)" type="text">
    </div>

    <div class="filter-row">
      <div>
        <strong>Direction Filter:</strong><br>
        <label><input type="radio" name="direction" value="all" checked> All</label>  
        <label><input type="radio" name="direction" value="IN"> Incoming Only</label>  
        <label><input type="radio" name="direction" value="OUT"> Outgoing Only</label>
      </div>
    </div>
<div class="filter-row" style="margin: 25px 0 15px 0;">
      <div style="flex:1;">
        <label><strong>Classification for Evernode Rewards</strong></label>
        <select id="rewardClassification" style="width: 100%; padding: 12px 16px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px;">
          <option value="Mining" selected>Mining</option>
          <option value="Reward">Reward</option>
          <option value="Salary">Salary</option>
          <option value="Other income">Other income</option>
          <option value="Gift">Gift</option>
          <option value="Donation">Donation</option>
        </select>
        <small style="color:#666; font-size:13px; margin-top:6px; display:block;">
          ⚠️ This will automatically set the Label field for all EVR received from the official rewards address 
          <strong>rHktfGUbjqzU4GsYCMc1pDjdHXb5CJamto</strong>. Research the correct classification for your tax jurisdiction.
        </small>
      </div>
    </div>
        <div class="filter-row" style="margin: 30px 0 15px 0;">
      <div style="flex:1;">
        <label><strong>My Reputation Wallet Address</strong> <span style="font-weight:normal; color:#666;">(optional)</span></label>
        <input id="reputationWallet" placeholder="rYourReputationWalletHere..." type="text" style="width:100%;">
        <small style="color:#666; font-size:13px; margin-top:6px; display:block;">
          Enter your reputation wallet to auto-label transfers and unlock lease classification.
        </small>
<label class="note" style="margin-top:10px; display:block;">
  <input type="checkbox" id="excludeRepSmallTx" checked>
  Apply small transaction filters to reputation wallet too
</label>
      </div>
    </div>

    <div id="reputationSection" style="display:none; margin-bottom:20px;">
      <div class="filter-row">
        <div style="flex:1;">
          <label><strong>Classification for EVR Lease Acquisitions</strong></label>
          <select id="leaseClassification" style="width: 100%; padding: 12px 16px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px;">
            <option value="Mining">Mining</option>
            <option value="Reward" selected>Reward</option>
            <option value="Salary">Salary</option>
            <option value="Other income">Other income</option>
            <option value="Gift">Gift</option>
            <option value="Donation">Donation</option>
          </select>
          <small style="color:#666; font-size:13px; margin-top:6px; display:block;">
            ⚠️ This will label transactions containing <strong>evnAcquireLease</strong> in the memo.
          </small>
        </div>
      </div>
    </div>
      <div class="filter-row" style="margin-top:15px;">
        <div style="flex:1;">
          <label><strong>Classification for EVR Refunds</strong></label>
          <select id="refundClassification" style="width: 100%; padding: 12px 16px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px;">
            <option value="Refund">Refund</option>
            <option value="Cost" selected>Cost</option>
            <option value="Other fee">Other fee</option>
          </select>
        </div>
      </div>
    <label class="note">
      <input type="checkbox" id="excludeSellOffer" checked> Exclude URITokenCreateSellOffer transactions
    </label>

    <label class="note">
      <input type="checkbox" id="showPlus" checked> Show + sign on incoming amounts
    </label>
    <label class="note">
      <input type="checkbox" id="aggregateDaily">
      Aggregate same-day transactions by wallet
    </label>
<button id="toggleColumnsBtn" style="background:#eee;color:#333;margin:20px 0;">
  Show Columns ⚙️
</button>

<div id="columnsWrapper" style="display:none;">
    <h3 style="margin: 35px 0 15px 0;">Columns to Export</h3>
    <button onclick="selectAll(true)" style="background:#eee;color:#333;padding:8px 16px;font-size:14px;">Select All</button>
    <button onclick="selectAll(false)" style="background:#eee;color:#333;padding:8px 16px;font-size:14px;">Deselect All</button>

    <div id="columnSelector" style="margin-top:15px;"></div>
</div>
    <!-- Terms Acceptance - Right above Fetch button -->
    <div class="terms-accept">
      <label style="font-size:16.5px; display:flex; align-items:center; gap:12px; cursor:pointer;">
        <input type="checkbox" id="acceptTermsCheckbox" onchange="updateButtons()">
        <strong>I accept the Terms & Conditions</strong>
      </label>
    </div>

    <button id="fetchBtn" disabled>1. Fetch Transactions</button>
    <button id="downloadBtn" disabled>2. Download Main Wallet CSV</button>
<button id="downloadRepBtn" disabled style="display:none;">3. Download Reputation Wallet CSV</button>
    <p id="status"></p>

    <div id="previewContainer" class="preview-container hidden">
      <h3>Preview (First 50 rows)</h3>
      <table id="previewTable"></table>
    </div>
  </div>
</div>

<!-- Terms Modal -->
<div id="termsModal" class="modal">
  <div class="modal-content">
    <span class="close" onclick="closeModal()">×</span>
    <h2>Terms and Conditions - Xahau Exporter</h2>
    
    <h3>1. Disclaimer of Liability</h3>
    <p>This tool is provided "AS IS" without any warranties. The developer and any associated parties shall not be liable for any loss of funds, data, profits, or any other damages arising from the use of this tool.</p>

    <h3>2. No Financial Advice</h3>
    <p>This tool is for data export purposes only and does not constitute financial, investment, or tax advice.</p>

    <h3>3. User Responsibility</h3>
    <p>You are solely responsible for the security of your wallet and for verifying all exported transaction data.</p>

    <h3>4. Limitation of Liability</h3>
    <p>In no event shall the developer be liable for any damages.</p>

    <br>
    <button onclick="closeModal()" style="background:#666;color:white;width:100%;padding:14px;">Close</button>
  </div>
</div>

<script>
  let mainTransactions = [];
  let repTransactions = [];
  let termsAccepted = false;

const defaultColumns = [
  { id: "date", title: "Date", default: true },
  { id: "sent_amount", title: "Sent Amount", default: true },
  { id: "sent_currency", title: "Sent Currency", default: true },
  { id: "received_amount", title: "Received Amount", default: true },
  { id: "received_currency", title: "Received Currency", default: true },
  { id: "fee", title: "Fee Amount", default: true },
  { id: "fee_currency", title: "Fee Currency", default: true },
  { id: "label", title: "Label", default: true },
  { id: "hash", title: "TxHash", default: true },
  { id: "aggregate_count", title: "Aggregated Count", default: false },
  { id: "ledger", title: "ledger", default: false },
  { id: "direction", title: "direction", default: false },
  { id: "txtype", title: "txtype", default: false },
  { id: "currency", title: "currency", default: false },
  { id: "amount", title: "amount", default: false },

  { id: "Source", title: "Source", default: true },
  { id: "Destination", title: "Destination", default: true },
  { id: "DestinationTag", title: "DestinationTag", default: false },
  { id: "SourceTag", title: "SourceTag", default: false },

  { id: "memoType", title: "Memo Type", default: false },
  { id: "memoData", title: "Memo Data", default: false }
];

document.getElementById("reputationWallet").addEventListener("input", function() {
  const repWallet = this.value.trim();
  document.getElementById("reputationSection").style.display = repWallet.length > 5 ? "block" : "none";
});

function buildColumnSelector() {
  const container = document.getElementById("columnSelector");
  container.innerHTML = "";

  defaultColumns.forEach(col => {
    const div = document.createElement("div");
    div.className = "column-option";
    div.innerHTML = `
      <input type="checkbox" id="chk_${col.id}" ${col.default ? "checked" : ""}>
      <label for="chk_${col.id}">${col.title}</label>
      <input type="text" id="name_${col.id}" value="${col.title}" placeholder="Custom name">
    `;
    container.appendChild(div);
  });
}

let columnsVisible = false;
function toggleColumns() {
  const wrapper = document.getElementById("columnsWrapper");
  const btn = document.getElementById("toggleColumnsBtn");
  columnsVisible = !columnsVisible;
  wrapper.style.display = columnsVisible ? "block" : "none";
  btn.textContent = columnsVisible ? "Hide Columns ❌" : "Show Columns ⚙️";
}

function getSelectedColumns() {
  return defaultColumns.map(col => {
    const checked = document.getElementById(`chk_${col.id}`).checked;
    let name = document.getElementById(`name_${col.id}`).value.trim();
    return { id: col.id, title: name || col.title, include: checked };
  }).filter(c => c.include);
}

function selectAll(check) {
  defaultColumns.forEach(col => {
    const el = document.getElementById(`chk_${col.id}`);
    if (el) el.checked = check;
  });
}

function updateButtons() {
  termsAccepted = document.getElementById("acceptTermsCheckbox").checked;
  document.getElementById("fetchBtn").disabled = !termsAccepted;
}

function setStatus(msg, color = "#333") {
  const status = document.getElementById("status");
  status.textContent = msg;
  status.style.color = color;
  status.style.background = color === "#00cc66" ? "#e6fff0" : (color === "#ff4444" ? "#ffe6e6" : "transparent");
}

function showTermsModal() {
  document.getElementById("termsModal").style.display = "flex";
}

function closeModal() {
  document.getElementById("termsModal").style.display = "none";
}

function processTransactions(txs) {
  return txs.map(tx => {
    let sent_amount = "";
    let sent_currency = "";
    let received_amount = "";
    let received_currency = "";

    const rawAmount = parseFloat(tx.amount || 0);

    if (tx.direction === "OUT") {
      sent_amount = Math.abs(rawAmount).toFixed(8);
      sent_currency = tx.currency || "XAH";
    } else {
      received_amount = Math.abs(rawAmount).toFixed(8);
      received_currency = tx.currency || "XAH";
    }

    return {
      ...tx,
      sent_amount,
      sent_currency,
      received_amount,
      received_currency
    };
  });
}

async function fetchTransactions() {
  if (!termsAccepted) return;

  const wallet = document.getElementById("wallet").value.trim();
  if (!wallet) return setStatus("Main wallet address is required", "#ff4444");

  const reputationWallet = document.getElementById("reputationWallet").value.trim();
  const minXAH = parseFloat(document.getElementById("minAmount").value) || 0;

  setStatus("Fetching main wallet...", "#00aaff");
  const fetchBtn = document.getElementById("fetchBtn");
  fetchBtn.disabled = true;

  try {
    // Fetch Main Wallet
    let res = await fetch("/api/transactions", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        wallet,
        node: document.getElementById("node").value.trim() || "wss://xahau.network",
        minXAH: minXAH,
        minEVR: parseFloat(document.getElementById("minEVR").value) || 0,
        excludeTicker: document.getElementById("excludeTicker").value.trim().toUpperCase(),
        excludeSellOffer: document.getElementById("excludeSellOffer").checked,
        showPlus: document.getElementById("showPlus").checked,
        directionFilter: document.querySelector('input[name="direction"]:checked').value,
        fromDate: document.getElementById("fromDate").value ? new Date(document.getElementById("fromDate").value).getTime() : null,
        toDate: document.getElementById("toDate").value ? new Date(document.getElementById("toDate").value).getTime() : null,
        rewardClassification: document.getElementById("rewardClassification").value,
        reputationWallet: reputationWallet,
        leaseClassification: document.getElementById("leaseClassification").value,
        refundClassification: document.getElementById("refundClassification").value,
        aggregateDaily: document.getElementById("aggregateDaily").checked
      })
    });

    if (!res.ok) throw new Error(await res.text());
    const data = await res.json();
    mainTransactions = processTransactions(data.transactions || []);

    // Fetch Reputation Wallet if provided
    if (reputationWallet) {
      setStatus("Fetching reputation wallet...", "#00aaff");

      res = await fetch("/api/transactions", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          wallet: reputationWallet,
          node: document.getElementById("node").value.trim() || "wss://xahau.network",
minXAH: document.getElementById("excludeRepSmallTx").checked ? minXAH : 0,

minEVR: document.getElementById("excludeRepSmallTx").checked
  ? (parseFloat(document.getElementById("minEVR").value) || 0)
  : 0,
          excludeTicker: "",
          excludeSellOffer: true,
          showPlus: false,
          directionFilter: "all",
          fromDate: null,
          toDate: null,
          rewardClassification: "Mining",
          reputationWallet: reputationWallet,
          leaseClassification: document.getElementById("leaseClassification").value,
          refundClassification: document.getElementById("refundClassification").value,
          aggregateDaily: document.getElementById("aggregateDaily").checked
        })
      });

      if (res.ok) {
        const repData = await res.json();
        repTransactions = processTransactions(repData.transactions || []);

        // Label transfers to main wallet
        repTransactions.forEach(tx => {
          if (tx.Destination === wallet) tx.label = "Transfer";
        });
      }
    }

    renderPreview();
    document.getElementById("downloadBtn").disabled = false;
    if (reputationWallet && repTransactions.length > 0) {
      document.getElementById("downloadRepBtn").disabled = false;
      document.getElementById("downloadRepBtn").style.display = "block";
    }

    setStatus(`✅ Fetched successfully | Main: ${mainTransactions.length}${reputationWallet ? ` | Rep: ${repTransactions.length}` : ''}`, "#00cc66");

  } catch (err) {
    setStatus(`❌ Error: ${err.message}`, "#ff4444");
  } finally {
    fetchBtn.disabled = false;
  }
}

function renderPreview() {
  const container = document.getElementById("previewContainer");
  const table = document.getElementById("previewTable");
  container.classList.remove("hidden");

  const selected = getSelectedColumns();
  let html = "<thead><tr>" + selected.map(c => `<th>${c.title}</th>`).join("") + "</tr></thead><tbody>";

  mainTransactions.slice(0, 50).forEach(row => {
    html += "<tr>" + selected.map(c => `<td>${row[c.id] != null ? row[c.id] : ""}</td>`).join("") + "</tr>";
  });
  html += "</tbody>";
  table.innerHTML = html;
}

function getCurrentDate() {
  const d = new Date();
  return d.toISOString().slice(0, 10);
}

function downloadMainCSV() {
  const mainShort = document.getElementById("wallet").value.trim().slice(0, 8) || "main";
  const repShort = document.getElementById("reputationWallet").value.trim().slice(0, 8) || "norep";
  downloadCSV(mainTransactions, `main_${mainShort}_${repShort}_${getCurrentDate()}`);
}

function downloadRepCSV() {
  const mainShort = document.getElementById("wallet").value.trim().slice(0, 8) || "main";
  const repShort = document.getElementById("reputationWallet").value.trim().slice(0, 8) || "rep";
  downloadCSV(repTransactions, `reputation_${mainShort}_${repShort}_${getCurrentDate()}`);
}

function downloadCSV(transactions, filename) {
  if (!transactions.length) return;

  const selected = getSelectedColumns();
  let csv = selected.map(c => c.title).join(",") + "\n";

  transactions.forEach(row => {
    const line = selected.map(col => {
      let val = row[col.id] != null ? String(row[col.id]) : "";
      if (val.includes(",") || val.includes('"')) val = '"' + val.replace(/"/g, '""') + '"';
      return val;
    }).join(",");
    csv += line + "\n";
  });

  const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = `xahau_${filename}.csv`;
  link.click();
}

// Initialize
window.onload = () => {
  buildColumnSelector();
  document.getElementById("toggleColumnsBtn").addEventListener("click", toggleColumns);
  document.getElementById("fetchBtn").addEventListener("click", fetchTransactions);
  document.getElementById("downloadBtn").addEventListener("click", downloadMainCSV);
  document.getElementById("downloadRepBtn").addEventListener("click", downloadRepCSV);
  document.getElementById("termsLink").addEventListener("click", (e) => {
    e.preventDefault();
    showTermsModal();
  });
};
</script>
</body>
</html>

Remember to create the node_module folder with npm i after creating package.json.

xoxo