Pattern 3: Use Only Backend

If you want to create a more traditional web2 type of app then this approach could be the one you would like to choose. You can handle all the logic related to the submission/validation of actions with your backend (as we showed in the previous point) and also use the backend to distribute the rewards, without the need to set up and deploy any smart contract.

To do that you will use the @vechain/sdk package to interact with the X2EarnRewardsPool contract to distribute the rewards.

In the following code snippet, you can see a javascript script that can be executed from the command line that will parse a CSV file containing a list of addresses and amounts to reward and then distribute the rewards to those addresses.

This script accepts a few command line parameters to function: the contract address of the X2EarnRewardsPool and the public and private keys of the wallet that will execute the transaction (which needs to be added as a Reward Distributor through the VeBetterDAO dApp).

const { ThorClient, HttpClient } = require("@vechain/sdk-network");
const { ethers } = require("ethers");
const { read } = require("read");
const fs = require("fs");
const { addressUtils, fragment, unitsUtils } = require("@vechain/sdk-core");

///////////////////
// User settings
//////////////////

require('dotenv').config();
const contractAddress = process.env.X2EARN_REWARDS_POOL_CONTRACT_ADDRESS;
const fromAddress = process.env.FROM_ADDRESS_PUB;
const defaultPrivateKey = process.env.FROM_ADDRESS_PRIV;
const batchSize = parseInt(process.env.BATCH_SIZE, 10);
const APP_ID = "0xefdbfa0ad748787c7dac2e89c0733fbaee0b92ada73c6eb4c8dfd8b76769b96f"; // Generated by adding app to VeBetterDAO

///////////////////
//// end settings
///////////////////

// This function will read the private key from the prompt and parse the a CSV file
// containing all the addresses and the amounts to distribute
const start = async () => {
  const privateKey = await read({
    prompt: "Private Key: ",
    default: defaultPrivateKey
  });

  const csvFile = await read({
    prompt: "CSV File: ",
    default: "addresses.csv",
  });

  if (!fs.existsSync(csvFile)) {
    console.log("File not found", csvFile);
    return;
  }

  const records = readCsv(csvFile);

  const totalRecords = records.length;
  const totalAmount = records.reduce((sum, record) => sum + parseFloat(record.amount), 0);

  console.log(`Total Records: ${totalRecords}`);
  console.log(`Total Amount: ${totalAmount}`);

  const confirm = await read({
    prompt: "Proceed? (y/n): ",
  });

  if (confirm !== "y") {
    console.log("Aborted");
    return;
  }

  await airdrop(records, privateKey);
};

const client = new ThorClient(new HttpClient("https://mainnet.vechain.org/"));
const distributeRewardAbi = {
  "inputs": [
    {
      "internalType": "bytes32",
      "name": "appId",
      "type": "bytes32"
    },
    {
      "internalType": "uint256",
      "name": "amount",
      "type": "uint256"
    },
    {
      "internalType": "address",
      "name": "receiver",
      "type": "address"
    },
    {
      "internalType": "string",
      "name": "proof",
      "type": "string"
    }
  ],
  "name": "distributeReward",
  "outputs": [],
  "stateMutability": "nonpayable",
  "type": "function"
};

const distributeRewardFragment = new fragment.Function(
  ethers.FunctionFragment.from(distributeRewardAbi),
);

const buildClauses = (records) => {
  const clauses = [];

  for (let i = 0; i < records.length; i++) {
    const { address, amount } = records[i];

    const _amount = unitsUtils.parseUnits(amount, 18);

    const data = distributeRewardFragment.encodeInput([APP_ID, _amount, address, ""]);

    clauses.push({
      to: contractAddress,
      data,
      value: "0x0",
    });
  }

  if (clauses.length === 0) {
    console.log("No clauses to build");
    throw new Error("No clauses to build");
  }

  return clauses;
};

const airdropBatch = async (batch, privateKey) => {
  const clauses = buildClauses(batch);
  const from = addressUtils.fromPrivateKey(Buffer.from(privateKey, "hex"));

  // Options to use gasPadding
  const options = {
    gasPadding: 0.2 // 50%
  };

  const estimation = await client.gas.estimateGas(
    clauses,
    fromAddress
  );

  if (estimation.reverted) {
    console.log("Estimation failed", estimation);
    throw new Error("Estimation failed");
  }

  const txBody = await client.transactions.buildTransactionBody(
    clauses,
    estimation.totalGas,
  );

  const signed = await client.transactions.signTransaction(txBody, privateKey);

  const tx = await client.transactions.sendTransaction(signed);

  //console.log(`Transaction ID: ${tx.id}`);
  console.log(`https://vechainstats.com/transaction/${tx.id}`);
};

const airdrop = async (records, privateKey) => {
  for (let i = 0; i < records.length; i += batchSize) {
    const batch = records.slice(i, i + batchSize);
    await airdropBatch(batch, privateKey);
  }
};

/**
 * @returns {Array}
 */
const readCsv = (csvFile) => {
  const data = fs.readFileSync(csvFile, "utf8");

  const lines = data.split("\r\n");
  const records = [];

  for (let i = 0; i < lines.length; i++) {
    const [address, amount] = lines[i].split(",");

    if (!addressUtils.isAddress(address)) {
      console.log(`Invalid address @ L${i + 1}: ${address}`);
      throw new Error("Invalid address");
    }

    if (isNaN(parseInt(amount))) {
      console.log(`Invalid amount @ L${i + 1}: ${amount}`);
      throw new Error("Invalid amount");
    }

    records.push({ address, amount });
  }

  return records;
};

start();

Last updated