체인의정석

ethers, hardhat 환경에서 이벤트를 통한 대용량 토큰 스냅샷 처리 프로그램 제작하기 (엑셀파일로 스냅샷 만들기) 본문

블록체인/Ethers & web3

ethers, hardhat 환경에서 이벤트를 통한 대용량 토큰 스냅샷 처리 프로그램 제작하기 (엑셀파일로 스냅샷 만들기)

체인의정석 2023. 3. 16. 18:24
728x90
반응형

1. const 파일 만들기

먼저 constant에 해당될 토큰 주소와 decimal 토큰 이름들은 지정해두어서 해당 값들만 바꾸면 스냅샷이 가능하도록 만들어준다.

const { ethers } = require("hardhat");
import { tokenName, tokenAddress, startBlock, endBlock } from '../const';
const fs = require('fs');

2. 엑셀로 TransferTx 저장시키기

먼저 다음 구문을 통해서 이벤트를 다 가져온다.

이벤트를 가져올때 필터를 설정할 수 있는데 이때 transfer 필터를 잡아주고 token Address를 넣어준다.

async function getEvents() {
    const concatArr: Array<any> = [];
    for(let i=startBlock; i < endBlock; i+=86400) {
        const endBlock = i+86400
        console.log(`getting event tx fromBlockNumber${i} to to blockNumber${endBlock}`)
        const erc20 = await ethers.getContractAt("MYTOKEN", tokenAddress);
        const erc20Filter = {
            address: tokenAddress,
            fromBlock: i,
            toBlock: endBlock,
            topics: [erc20.filters.Transfer().topics]
        };
        const erc20Events = await ethers.provider.getLogs(erc20Filter);
        concatArr.push(erc20Events);
    }
    const finalArray = concatArr.reduce((accumulator:any, currentValue:any) => accumulator.concat(currentValue));
    return Promise.resolve(finalArray);
}

이를 writeFileSync를 통해서 엑셀파일로 만들어준다.

export function saveFileTransferTx() {
    getEvents()
    .then(async (txArray) => {
        const answerArr: Array<any> = []
        for(let i=0; i < txArray.length; i+=1000) {
            console.log(`getting tx hash from ${i} txNum ${i+1000}`)
            answerArr.push(await txArray.slice(i,i+1000).map((tx: any) => tx.transactionHash));
        }
        const finalArray = answerArr.reduce((accumulator:any, currentValue:any) => accumulator.concat(currentValue));
        const setData = new Set(finalArray);
        const newArrayData = [...setData];
        console.log(`total transaction count is ${newArrayData.length}`)
        fs.writeFileSync(
            `./scripts/output/${tokenName}_${endBlock}_Transfer.json`, 
            JSON.stringify(newArrayData, null, 1))
    })
}

이렇게 되면 트랜잭션 해시들만 나오게된다.

3.  parseTransferEvents

parseTransferEvents를 해주면 그제서야 실제 이벤트 내역이 나오게 된다. 이렇게 되면 엑셀파일로 실제 전송된 값들이 저장되게 된다.

import fs from 'fs';
import { tokenName, tokenAddress, size, endBlock } from '../const';
import { parseTransferEvents } from './helper/logParseHelper';
import { flattened } from './helper/arrayFunctions';
const abi = require("../../../artifacts/contracts/interfaces/IERC20.sol/IERC20.json").abi;

async function getAllTxreceipt() {
    const txListArr : Array<any>= []
    for(let a=0; a < size; a+=30000) { //개수 보고 반복문 조정
        const txList: Array<string> = JSON.parse(fs.readFileSync(`./scripts/quickSnapShot/output/${tokenName}_${endBlock}_txReceipt_from${a}_to${a+30000}.json`,'utf-8'));
        txListArr.push(txList)
    }
    return flattened(txListArr);
}

export async function saveFileTransferData(): Promise<void> {
    await getAllTxreceipt()
    .then(async(txArray) => {
        let answerArr: Array<any> = [];
        for(let i=0; i < txArray.length; i+=500) {
            console.log(`starting parseTransferEvents of ${i}/${txArray.length}`)
            const answer= await
                Promise.all(txArray.slice(i,i+500).map(async (tx: any) => {
                    const parsedEventInternal = await parseTransferEvents(tx.logs, abi, tokenAddress);
                    const returnJsonData = parsedEventInternal;
                    if(returnJsonData.value == '0') {
                        return '';
                    }
                    return returnJsonData;
                }))
            answerArr.push(answer)
        }
        return answerArr
    })
    .then((answerArr: any) => {
            console.log(`starting flattening answerArr, length is ${answerArr.length}`)
            const flatanswerArr: Array<any> = flattened(flattened(flattened(answerArr)));
            const exceptNull = flatanswerArr.filter((r : any) => r != ('' || null || undefined));
            const flatanswerArrJson: string = JSON.stringify(exceptNull, null, 1)
            console.log(`total transaction count is ${exceptNull.length}`)
            fs.writeFileSync(
                `./scripts/output/${tokenName}_${endBlock}_transferInfo.json`, 
                flatanswerArrJson            
            )
        })
    }

4. 스냅샷 실행하기

스냅샷을 실행할 때는 각 from주소와 to 주소에 맞게 데이터를 처리해주는 로직을 넣어준다.

각 주소별 자산의 수를 계산하기 때문에 map을 사용하였다.

또한 수를 계산할 때는 bignumber를 사용해서 해주어야 한다.

const { ethers } = require("hardhat");
import fs from 'fs';
// import BigNumber from 'bignumber.js';
import 'blockchain-prototypes'
import { tokenName, decimal, endBlock } from '../const';
import { noExponents } from './helper/arrayFunctions';

const holderMap = new Map();
const addressZero = "0x0000000000000000000000000000000000000000";

async function getAllInfo() {
    const txList = JSON.parse(fs.readFileSync(`./scripts/output/${tokenName}_${endBlock}_transferInfo.json`,'utf-8'));
    console.log("total transfer count : ", txList.length);
    return txList
}

export async function saveFileSnapShot() {
    await getAllInfo()
    .then((holder) =>{
        for(const i in holder) {
            holderMap.set(holder[i].to,0)
            holderMap.set(holder[i].from,0)
        }
        holderMap.set(addressZero,0);
        return {holderMap, holder}
    })
    .then((infos) => {
        //holder 리스트 정보 초기화
        const holderMap = infos.holderMap
        const holder = infos.holder;
        //각 holder 들의 잔고 계산
        //holder 리스트 확보
        for( let event of holder) {
            const to = event.to;
            const from = event.from;
            const value = new ethers.BigNumber.from(event.value);

            //mint인 경우 계산
            if(from==addressZero) {
                let beforeBalance = ethers.BigNumber.from(holderMap.get(to))
                let afterBalance =  beforeBalance.add(value);
                holderMap.set(to,afterBalance);
            }else if(to==addressZero) {
                let beforeBalance = ethers.BigNumber.from(holderMap.get(from))
                let afterBalace =  beforeBalance.sub(value);
                holderMap.set(from,afterBalace);
                let afterBalanceZero = afterBalace.add(value);
                holderMap.set(addressZero,afterBalanceZero);
            }else if(from==addressZero && to==addressZero){
            }else if(value=="0"){
            }else {
                let beforeBalanceFrom = ethers.BigNumber.from(holderMap.get(from))
                let afterBalaceFrom =  beforeBalanceFrom.sub(value);
                holderMap.set(from,afterBalaceFrom);

                let beforeBalanceTo = ethers.BigNumber.from(holderMap.get(to))
                let afterBalaceTo =  beforeBalanceTo.add(value);
                holderMap.set(to,afterBalaceTo);
            }
        }
        return holderMap
    })
    .then(async(holderMap) => {
        noExponents
        const arrayHolderMap: Array<any> = Array.from(holderMap)
        const toStringHolder: Array<Array<string>> = arrayHolderMap.map((list) => {
        list[1] = (list[1].toString() / 10 ** decimal).noExponents();
        return list
        })
        console.log("total holder count : ", toStringHolder.length);
        fs.writeFileSync(
            `./scripts/output/${tokenName}_${endBlock}_snapShot.json`, 
            JSON.stringify(toStringHolder,null,1)
        )
    })
}

5. 만든 json파일 엑셀로 만들기

스냅샷의 경우 여러 데이터가 들어갈 경우 부하가 커진다 따라서 엑셀파일로 만든 후 이후 작업을 해주는 것이 수월하다.

excel.js를 사용하면 해당 작업을 할 수 있다.

import Excel from 'exceljs';
import path from 'path';
import fs from 'fs';
import { endBlock, tokenName } from '../const';

type User = {
  address: string;
  txhash: string;
  tokenAmount: string;
};

export const exportUserFile = async () => {
  console.log("exportUserFile", tokenName)
  const users: Array<User> = JSON.parse(fs.readFileSync(`./scripts/output/${tokenName}_${endBlock}_snapShot.json`,'utf-8'));
  const workbook = new Excel.Workbook();
  const worksheet = workbook.addWorksheet('User List');

  worksheet.columns = [
    { key: 'address', header: 'account address' },
    { key: 'amount', header: 'amount' },
  ];

  users.forEach((item) => {
    worksheet.addRow(item);
  });

  const exportPath = path.resolve(__dirname, `../output/${tokenName}_${endBlock}_snapShot_data.xlsx`);

  await workbook.xlsx.writeFile(exportPath);
};

// exportUserFile();

 

여기까지 된다면 엑셀파일에 토큰의 amount가 나오게 되는데 이 때부터는 엑셀함수를 이용하여 여러가지 연산을 하거나 하면 된다.

 

또한 다차원 배열을 1차원으로 축소시키거나 가수부 관련 표기를 숫자로 바꾸는 부분은 다음 함수를 사용하였다.

export function flattened(array: Array<any>) { 
    const flattenedArray = array.reduce(
        function(accumulator, currentValue) {
            return accumulator.concat(currentValue);
        },[]
        );
    return flattenedArray
}

export const noExponents = Number.prototype.noExponents = function () {
    var data = String(this).split(/[eE]/);
    if (data.length == 1) return data[0];

    var z = '',
        sign = this < 0 ? '-' : '',
        str = data[0].replace('.', ''),
        mag = Number(data[1]) + 1;

    if (mag < 0) {
        z = sign + '0.';
        while (mag++) z += '0';
        return z + str.replace(/^\-/, '');
    }
    mag -= str.length;
    while (mag--) z += '0';
    return str + z;
}
728x90
반응형
Comments