// If you don't want to set up your own nodejs environment,
// this script is also available at https://batch.xtz.tools/
// with a friendlier interface.

require('dotenv').config();

const fs = require("fs");
const path = require('path');

const parse = require('csv-parse/lib/sync');

const sotez = require('sotez');
const cryptoUtils = sotez.cryptoUtils;

const TezosToolkit = require("@taquito/taquito").TezosToolkit;
const InMemorySigner = require("@taquito/signer").InMemorySigner;

const contractAddress = process.env.CONTRACT || "KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton";

const inputFile = process.env.INPUT || path.join('data', 'send.csv');
const outputFile = process.env.OUTPUT || path.join('data', 'completed.csv');

const txConfirmations = process.env.CONFIRMATIONS || 3;
const txConfirmationTimeout = process.env.CONFIRMATIONTIMEOUT || txConfirmations * (2 * 60);

const main = async () => {
    const Tezos = new TezosToolkit("https://mainnet.smartpy.io/");

    var signer;

    if (process.env.MNEMONIC) {
        const keys = await cryptoUtils.generateKeys(process.env.MNEMONIC);

        signer = new InMemorySigner(keys.sk);
    } else if (process.env.SECRET) {
        signer = new InMemorySigner(process.env.SECRET);
    } else {
        throw "Neither MNEMONIC nor SECRET is set!";
    }

    const walletAddress = await signer.publicKeyHash();

    console.log("Running for sender " + walletAddress);

    Tezos.setProvider({ signer });

    // Prepare smart contract parameters
    const params = [
        {
            from_: walletAddress,
            txs: []
        }
    ];

    console.log("Reading input " + inputFile);

    const content = fs.readFileSync(inputFile, 'utf8');

    console.log("Parsing input");

    // Parse CSV input
    const records = parse(content, {
        columns: ['target_address', 'token_id', 'amount'],
        skip_empty_lines: true,
        trim: true,
        bom: true
    });

    if (records.length == 0) {
        throw "No entries to process!";
    }

    records.forEach(record => {
        if (record.target_address == "target_address") {
            return;
        }

        console.log(`Found entry: ${record.amount} × #${record.token_id} to ${record.target_address}`);

        params[0].txs.push({
            to_: record.target_address,
            token_id: record.token_id,
            amount: record.amount
        });
    });

    console.log("Getting information for FA2 contract " + contractAddress);

    const contract = await Tezos.contract.at(contractAddress);

    console.log("Sending transaction");

    const operation = await contract.methods.transfer(params).send({ amount: 0, mutez: true });

    console.log("Waiting for " + txConfirmations + " transaction confirmations with a timeout of " + txConfirmationTimeout + " seconds, see https://tzstats.com/" + operation.hash);

    await operation.confirmation(txConfirmations, 10, txConfirmationTimeout);

    console.log("Transaction confirmed!");
    console.log("Saving transaction hash to " + outputFile);

    if (!fs.existsSync(outputFile)) {
        fs.writeFileSync(outputFile, 'txn,date');
    }

    fs.appendFileSync(outputFile, operation.hash + ',' + new Date().toISOString());

    console.log("Done!");
};

main().catch(error => {
    console.error("Failed! Here is why:");
    console.error(error);
});
            

# ONE OF THE NEXT TWO IS REQUIRED
SECRET=
MNEMONIC=

# EVERYTHING BELOW HERE IS OPTIONAL
CONTRACT=
CONFIRMATIONS=
CONFIRMATIONTIMEOUT=
INPUT=
OUTPUT=
            

{
  "name": "objkt-batch-sender",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "author": "PureSpider",
  "license": "CC-BY-NC-SA-4.0",
  "private": true,
  "dependencies": {
    "@taquito/signer": "^9.0.0",
    "@taquito/taquito": "^9.0.0",
    "csv-parse": "^4.15.4",
    "dotenv": "^8.2.0",
    "sotez": "^8.0.1"
  }
}
            

target_address,token_id,amount
tz1PqooZUtCQqP539PmMzUWHtaUX6EQtFY5S,64620,1