airdropGenerator.js
Processes XENBlocks blockchain data to generate weekly Merkle trees, calculate token allocations, and automatically submit airdrop creation transactions.
const sqlite3 = require("sqlite3").verbose();
const fs = require("fs");
const readline = require("readline");
const { MerkleTree } = require("merkletreejs");
const { ethers, keccak256, solidityPackedKeccak256, isAddress, getAddress } = require("ethers");
// File to store the last processed block ID
const SNAPSHOT_FILE = "/root/lastSnapshot.txt";
const DB_PATH = "/root/xenminer/blockchain.db";
const OUTPUT_DIR = "/root/snapshots/";
const MERKLE_DIR = "/root/merkleTrees/";
const BATCH_SIZE = 10000;
// Ensure output directories exist
if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true });
if (!fs.existsSync(MERKLE_DIR)) fs.mkdirSync(MERKLE_DIR, { recursive: true });
let totalRegularBlocks = 0;
let totalSuperBlocks = 0;
let totalXuniBlocks = 0;
let totalRegularBlocksBeforeHalving = 0;
// Function to get the last processed block from file
function getLastBlockId() {
if (fs.existsSync(SNAPSHOT_FILE)) {
const lastId = parseInt(fs.readFileSync(SNAPSHOT_FILE, "utf8"));
if (!isNaN(lastId) && lastId > 0) {
return lastId;
}
}
return null;
}
// Function to get the latest block from the database
function getLatestBlockId() {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(DB_PATH);
db.get("SELECT MAX(id) as max_id FROM blockchain;", (err, row) => {
db.close();
if (err) {
reject(err);
} else {
resolve(row?.max_id || 0);
}
});
});
}
// Main execution function
(async function () {
let START_BLOCK = getLastBlockId();
let END_BLOCK = await getLatestBlockId();
const snapshotTimestamp = Date.now();
console.log(`\nš¾ Last processed block: ${START_BLOCK || "None (First run)"}`);
console.log(`š¢ Latest blockchain block at execution: ${END_BLOCK}`);
if (!START_BLOCK) {
console.log("ā ļø No previous snapshot found. Starting from block 1.");
START_BLOCK = 1;
}
if (START_BLOCK >= END_BLOCK) {
console.log("ā
No new blocks to process. Exiting.");
return;
}
console.log(`ā
Processing records from Block ${START_BLOCK} to ${END_BLOCK}`);
const db = new sqlite3.Database(DB_PATH);
const outputFileName = `${OUTPUT_DIR}snapshot_${snapshotTimestamp}.csv`;
const ws = fs.createWriteStream(outputFileName);
ws.write("account,regularblocks,superblocks,xuniblocks\n");
const query = `
SELECT blockchain.id,
json_extract(value, '$.account') AS account,
json_extract(value, '$.hash_to_verify') AS hash_to_verify,
json_extract(value, '$.block_id') AS block_id,
json_extract(value, '$.xuni_id') AS xuni_id,
json_extract(value, '$.date') AS date
FROM blockchain, json_each(blockchain.records_json)
WHERE blockchain.id >= ?
AND blockchain.id <= ?
ORDER BY blockchain.id ASC
LIMIT ?;
`;
let processedRecords = 0;
const accountStats = {};
let lastProcessedBlock = START_BLOCK;
function processBatch(startId) {
db.all(query, [startId, END_BLOCK, BATCH_SIZE], (err, rows) => {
if (err) {
console.error("ā SQL ERROR:", err);
return;
}
if (rows.length === 0) {
console.log("ā
No more records. Writing final CSV...");
for (const [account, data] of Object.entries(accountStats)) {
ws.write(`${account},${data.regularblocks},${data.superblocks},${data.xuniblocks}\n`);
}
ws.end(() => {
fs.writeFileSync(SNAPSHOT_FILE, lastProcessedBlock.toString());
console.log(`ā
Snapshot completed! CSV file saved as ${outputFileName}`);
console.log(`š Last processed block ID saved: ${lastProcessedBlock}`);
db.close();
// ā
Now the file is fully written ā safe to generate Merkle Tree
generateMerkleTree(outputFileName, {
regularblocks: totalRegularBlocks,
superblocks: totalSuperBlocks,
xuniblocks: totalXuniBlocks,
regularblocksbeforehalving: totalRegularBlocksBeforeHalving
}, snapshotTimestamp);
});
return;
}
rows.forEach((row) => {
if (!row.account) return;
const isXuni = !!row.xuni_id;
const hasBlock = !!row.block_id;
if (!accountStats[row.account]) {
accountStats[row.account] = { regularblocks: 0, superblocks: 0, xuniblocks: 0 };
}
if (isXuni) {
accountStats[row.account].xuniblocks += 1;
totalXuniBlocks += 1;
} else if (hasBlock && row.hash_to_verify) {
const realHashPart = row.hash_to_verify.split("$").pop() || "";
const uppercaseCount = (realHashPart.match(/[A-Z]/g) || []).length;
//const dateThreshold = new Date("2024-09-16T20:59:55Z");
//const recordDate = new Date(row.date);
const bonus = row.block_id <= 29818420 ? 10 : 5;
accountStats[row.account].regularblocks += bonus;
totalRegularBlocks += bonus;
if (row.block_id <= 29818420) {
totalRegularBlocksBeforeHalving += bonus;
}
if (uppercaseCount >= 50) {
accountStats[row.account].superblocks += 1;
totalSuperBlocks += 1;
}
}
processedRecords++;
lastProcessedBlock = row.id;
if (processedRecords % 10000 === 0) {
console.log(`š¢ Processed: ${processedRecords} records`);
}
});
console.log(`š¹ Processed up to ID: ${lastProcessedBlock}`);
processBatch(lastProcessedBlock + 1);
});
}
processBatch(START_BLOCK);
})();
function generateMerkleTree(csvFilePath, totals, timestamp) {
console.log(`š¹ Generating Merkle Tree from ${csvFilePath}`);
if (!fs.existsSync(csvFilePath)) {
console.error(`ā CSV file not found: ${csvFilePath}`);
return;
}
const lines = fs.readFileSync(csvFilePath, "utf-8").trim().split("\n");
const data = lines.slice(1).map((line) => {
let [account, regularblocks, superblocks, xuniblocks] = line.split(",");
if (!isAddress(account)) {
console.warn(`ā ļø Skipping invalid address: ${account}`);
return null;
}
account = getAddress(account.toLowerCase());
return {
account,
regularblocks: BigInt(regularblocks || "0"),
superblocks: BigInt(superblocks || "0"),
xuniblocks: BigInt(xuniblocks || "0"),
};
}).filter((entry) => entry !== null);
const leaves = data.map(({ account, regularblocks, superblocks, xuniblocks }) => {
return solidityPackedKeccak256(
["address", "uint256", "uint256", "uint256"],
[account, regularblocks, superblocks, xuniblocks]
);
});
const merkleTree = new MerkleTree(leaves, keccak256, { sortPairs: true });
const merkleRoot = merkleTree.getHexRoot();
console.log("Merkle Root:", merkleRoot);
const proofs = data.map(({ account, regularblocks, superblocks, xuniblocks }) => {
const leaf = solidityPackedKeccak256(
["address", "uint256", "uint256", "uint256"],
[account, regularblocks, superblocks, xuniblocks]
);
return {
account,
regularblocks: regularblocks.toString(),
superblocks: superblocks.toString(),
xuniblocks: xuniblocks.toString(),
proof: merkleTree.getHexProof(leaf),
};
});
const jsonFileName = `${MERKLE_DIR}proofs_snapshot_${timestamp}.json`;
fs.writeFileSync(jsonFileName, JSON.stringify({ merkleRoot, totals, timestamp, proofs }, null, 2));
console.log(`ā
Proofs saved to ${jsonFileName}`);
notifySmartContract(merkleRoot, totals);
}
async function notifySmartContract(merkleRoot, totals) {
const RPC_URL = process.env.RPC_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS;
const ABI = [
{
"inputs": [
{
"internalType": "bytes32",
"name": "_merkleRoot",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "totalWNM",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "totalWBLK",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "totalWUNI",
"type": "uint256"
}
],
"name": "createAirdrop",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
];
const provider = new ethers.JsonRpcProvider(RPC_URL);
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, wallet);
// Convert totals to 18 decimals (BigInt)
const totalWNM = BigInt(totals.regularblocks) * 10n ** 18n;
const totalWBLK = BigInt(totals.superblocks) * 10n ** 18n;
const totalWUNI = BigInt(totals.xuniblocks) * 10n ** 18n;
console.log(`š¦ Preparing to call createAirdrop with:`);
console.log(`- Merkle Root: ${merkleRoot}`);
console.log(`- Total WNM: ${totalWNM.toString()}`);
console.log(`- Total WBLK: ${totalWBLK.toString()}`);
console.log(`- Total WUNI: ${totalWUNI.toString()}`);
try {
console.log("š” Calling createAirdrop()...");
const tx = await contract.createAirdrop(merkleRoot, totalWNM, totalWBLK, totalWUNI);
console.log(`ā³ Transaction submitted: ${tx.hash}`);
const receipt = await tx.wait();
if (receipt.status === 1) {
console.log("ā
Airdrop successfully created on-chain.");
} else {
console.error("ā Transaction reverted or failed.");
}
} catch (err) {
console.error("ā Error calling createAirdrop:", err);
}
}
Last updated