Credit-Debit

In this tutorial, we will show you how to create a credit-debit data tracking of an ERC-20 token using Blockflow CLI.

Creating a credit-debit table on the blockchain offers enhanced security and transparency for financial transactions. This approach also facilitates seamless audits and reconciliations, as all transactions are permanently recorded and accessible as stored and managed at Blockflow.


Project Overview

The purpose of this tutorial is to teach you how to index ERC20 credit-debit data by creating databases for both. A short breakdown for the project is below -

  • Create a project directory and cd into it. Here' we will initialize the project and get the standard blockflow template according to the event to be indexed.

  • Providing the appropriate schema.

  • Write the handler function to manage data from the tracked event.

  • Test the project and verify the data stored over the databases.


Setting up the project

  • Download the Blockflow CLI using the command: npm i -g @blockflow-labs/cli. We can remove the -g tag if global installation is not required.

  • Create a project directory and cd into it to make it the present working directory.

  • Use the blockflow init command to initiate the project.

  • Below are the fields that need to be entered for the project. You can also let the default values for each field by pressing [ENTER]. The last field requires you to select the instance trigger type with 4 choices. You can also add more than one trigger for a project.

Setting up the schema

The below-mentioned schema is appropriate to track the credit-debit data for an ERC20 token transaction.

export interface ERC20Table {
  id: String;
  address: string;
  counterPartyAddress: string;
  tokenAddress: string;
  tokenName: string;
  tokenSymbol: string;
  rawAmount: number;
  rawAmountString: string;
  amount: string;
  amountString: string;
  usdValue: number;
  usdExchangeRate: number;
  transactionHash: string;
  logIndex: number;
  blockTimestamp: string;
  blockNumber: number;
  blockHash: string;
}

On placing the schema values at studio.schema.ts, use the command blockflow typegen to update the schema.ts file at src/types.


Writing the handler function

Now we move onto writing the handler function to manage the data and populate the databases. We remove any unwanted handlers in the studio.yaml because it will generate unwanted handler files in the src/handlers directory. We only require to track the data of the Transfer event so we remove all other handler. The final studio.yaml will look somewhat like this:

name: Project Apollo
description: A top-secret research project to the moon
startBlock: latest
userId: XXXXXXXX-XXXX-XXXX-XXXXXXXX-XXXXXXXX
projectId: XXXXXXXX-XXXX-XXXX-XXXXXXXX-XXXXXXXX
network: Ethereum
user: Jane-doe
schema:
  file: ./studio.schema.ts
execution: parallel
Resources:
  - Name: blockflow
    Abi: src/abis/blockflow.json
    Type: contract/event
    Address: "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"
    Triggers:
      - Event: Transfer(address indexed,address indexed,uint256)
        Handler: src/handlers/blockflow/Transfer.TransferHandler

Use the command blockflow codegen to generate handler templates.

A transfer.ts file will appear in the handlers' directory with the template code as:

export const TransferHandler = async (
  context: IEventContext,
  bind: IBind,
  secrets: ISecrets,
) => {
  const { event, transaction, block, log } = context;
  const { from, to, value } = event;

We add import statements above this portion of code to import the schema, bignumber.js library which will be used later and lastly the tokenMetadata for the ERC20 token which is stored in src/utils.

import { ERC20Table, IERC20Table } from "../../types/schema";
import BigNumber from "bignumber.js";
import { getTokenMetadata } from "../../utils/ERC20Metadata";

Now, before working onto populating the tables, we create two unique Id's, one for the credit table and the other for the debit one. We concatenate the prefix credit or debit based on the transaction type.

const uniqueIdcredit = `${transaction.transaction_hash}-${log.log_index}-"credit"`;
const uniqueIddebit = `${transaction.transaction_hash}-${log.log_index}-"debit"`;

We add the number of decimals to a variable declared by fetching it from the tokenMetadata because each token representation has a unique number of decimals. We place the amount of token value transferred in the variable valuebignumber. These variables are then used to create the amount fields for both the credit and debit tables. Bignumber.js is used over here as to prevent overflow of integers. We add a factor of multiplying by -1 in the debitAmount.

const decimalsBigNumber = new BigNumber(tokenMetadata.decimals);
const divisionValue = new BigNumber(10).pow(decimalsBigNumber);
const valueBigNumber = new BigNumber(value.toString());
const amount = valueBigNumber.dividedBy(divisionValue).toString();
const debitAmount = valueBigNumber.dividedBy(divisionValue).times(-1).toString();

We now make the database connections and then query for a uniqueId, upon not getting an Id, we create a new document and populate it with the data for the respective table. Below is the code for DB binding and data popuation of the credit table. Similarly, we can populate the data for the debit table.

const erc20CreditDebitDB: Instance = bind(ERC20Table);

  let erc20Table: IERC20Table = await erc20CreditDebitDB.findOne({
    id: uniqueIdcredit,
  });

  erc20Table ??= await erc20CreditDebitDB.create({
    id: uniqueIdcredit,
    address: address,
    counterPartyAddress: counterPartyAddress,
    tokenAddress: log.log_address,
    tokenName: tokenMetadata.tokenName,
    tokenSymbol: tokenMetadata.tokenSymbol,
    rawAmount: value,
    rawAmountString: value.toString(),
    amount: amount,
    amountString: amount.toString(),
    transactionHash: transaction.transaction_hash,
    logIndex: log.log_index,
    blockTimestamp: block.block_timestamp,
    blockNumber: block.block_number,
    blockHash: block.block_hash,
  });

Note that various fields in the table such as logIndex, blockNumber, etc. are populated by placing the respective values from blockflow's blockdata stored. Regarding the tokenMetdata, we create a tokenmetadata file in src/utils to store the data for a particular ERC20 token which is being tracked. The contents of the tokenMetadata file are below:

interface TokenInfo {
  decimals: number;
  tokenName: string;
  tokenSymbol: string;
}

export const TOKENS: Record<string, TokenInfo> = {
  "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3": {
    decimals: 6,
    tokenName: "USD Ethena",
    tokenSymbol: "USDE",
  },
};

export const getTokenMetadata = (token: string) => {
  const findedToken = Object.keys(TOKENS).filter(
    (tokenAddr) => tokenAddr.toLowerCase() === token.toLowerCase(),
  );
  return TOKENS[findedToken[0]];
};

The token tracked over here for credit-debit tables is USDE(ethena).


Testing

We use the below command to test if the handler logic is correct and the data gets stored on our specified collection in Mongo.

blockflow instance-test --startBlock <block number> --clean --range 10 --uri <connection string>

The <block number> can be put of any range, but we can put it to latest if no opinion provided. The — uri holds the MongoDB connection URL which is tested locally shall be filled with mongodb://localhost:27017/blockflow_studio.

Checkout the repo for credit-debit of USDE by Blockflow on github.

Last updated