CCTP Protocol

In this tutorial, we’ll show you how to index CCTP using the Blockflow CLI. Blockflow provides a seamless and efficient solution for building "Serverless Backends" through our Managed Databases, On-chain Instances, and Custom APIs.

Cross-Chain Transfer Protocol (CCTP) is a permissionless on-chain utility that facilitates USDC transfers securely between blockchains via native burning and minting. It is available on mainnet for Arbitrum, Avalanche, Base, Ethereum, Noble, OP Mainnet, Polygon PoS, and Solana.

Using Blockflow for CCTP indexing, we are capturing a comprehensive portion of the Circle Cross-Chain Transfer Protocol data. This includes detailed transaction records such as burn and mint transactions, attestation details, and domain-specific information. Additionally, we index fee information and aggregate transactional data over daily, weekly, monthly, and yearly periods, as well as all-time statistics which ensures a robust and insightful data management for the protocol.


Project Overview

The purpose of this tutorial is to teach you how index CCTP by creating the subgraphs with the aid of Blockflow and its utilities. A short breakdown for the project is below -

  • Create a project directory and cd into it. Use the command blockflow init through the terminal and provide with the relevant contract address to be tracked.

  • Supply the appropriate schema for depending upon the data emitted by the events in the contract.

  • Write the handler function for each event for tracking.

  • Test the project using the command blockflow test and verify the data stored over the database.


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 use the cd command over it to make it the 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 relevant data from CCTP:

export interface burnTransactionsTable {
  id: String;
  transactionHash: string;
  sourceDomain: string;
  destinationDomain: string;
  amount: number;
  mintRecipient: string;
  messageSender: string;
  timeStamp: string;
}

export interface mintTransactionsTable {
  id: String;
  transactionHash: string;
  sourceDomain: string;
  destinationDomain: string;
  amount: number;
  mintRecipient: string;
  timeStamp: string;
}

export interface attestationTable {
  id: String;
  attestationHash: string;
  messageHash: string;
  timeStamp: string;
}

export interface DomainsTable {
  id: String;
  domainName: string;
  chainId: string;
  tokenAddress: string;
  permessageburnlimit: number;
}

export interface FeeInfo {
  id: String;
  feeInUSDC: number;
}

export interface cctpDayDataDB {
  id: String; 
  date: string;
  txCount: string;
  dailyVolume: string; 
  deposited: string; 
  withdrawal: string; 
  totalFee: string;
}

export interface cctpWeekDataDB {
  id: String; 
  week: string;
  txCount: string;
  weeklyVolume: string;
  deposited: string;
  withdrawal: string;
  totalFee: string;
}

export interface cctpMonthDataDB {
  id: String;
  month: string;
  txCount: string;
  monthlyVolume: string;
  deposited: string;
  withdrawal: string;
  totalFee: string;
}

export interface cctpYearDataDB {
  id: String;
  year: string;
  txCount: string;
  yearlyVolume: string;
  deposited: string;
  withdrawal: string;
  totalFee: string;
}

export interface cctpAllTimeDB {
  id: String;
  txCount: string;
  allTimeVolume: string;
  deposited: string;
  withdrawal: string;
  totalFee: string;
}

On completing the schema, use the command blockflow typegen to generate types.


Writing the handler function

Now we move onto writing the handler functions for the project, before doing so, we move onto removing the unwanted handlers in the studio.yaml. As shown below the studio.yaml will let us track only the DepositForBurn event:

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: '0xBd3fa81B58Ba92a82136038B25aDec7066af3155'
    Triggers:
      - Event: DepositForBurn(uint64 indexed,address indexed,uint256,address indexed,bytes32,uint32,bytes32,bytes32)
        Handler: src/handlers/blockflow/DepositForBurn.DepositForBurnHandler

Use the command blockflow codegen to generate handler templates in /src/handlers directory.

The top part of the code in DespositForBurn.ts includes imports, in which we add the importing of classes and interfaces of both the DBs from /types/schema.

import {
  burnTransactionsTable,
  IburnTransactionsTable,
  cctpDayDataDB,
  IcctpDayDataDB,
  cctpWeekDataDB,
  IcctpWeekDataDB,
  cctpMonthDataDB,
  IcctpMonthDataDB,
  cctpYearDataDB,
  IcctpYearDataDB,
  cctpAllTimeDB,
  IcctpAllTimeDB,
} from "../types/schema";

We now have to write the handler code logic below the boiler plate code provided on the DepositForBurn handler:

export const DepositForBurnHandler = async (
  context: IEventContext,
  bind: IBind,
  secrets: ISecrets
) => {
  const { event, transaction, block, log } = context;
  let {
    nonce,
    burnToken,
    amount,
    depositor,
    mintRecipient,
    destinationDomain,
    destinationTokenMessenger,
    destinationCaller,
  } = event;

We now create an Id for the burntransaction database which will be later populated by the data emitted from the DepositForBurn event. and then bind connection with the burntransactionDB:

const burnId =`${nonce.toString()}_${block.chain_id}_${dstChainId}`.toLowerCase();
const burntransactionDB: Instance = bind(burnTransactionsTable);

On being done with the connections, we move onto doing CRUD operations with the DB. For this, we create a variable burntransaction and specify its type with the IBurnTransactionsTable interface. Upon not finding a unique burnId, it creates a new document in the burntransactionDB and allots the following properties, we will go on each field and how to obtain them.

let burntransaction: IburnTransactionsTable = await burntransactionDB.findOne(
    {
      id: burnId,
    }
  );

  burntransaction ??= await burntransactionDB.create({
    id: burnId,
    burnToken: burnToken.toString(),
    transactionHash: transaction.transaction_hash,
    sourceDomain: dstChainId,
    destinationDomain: destinationDomain.toString(),
    amount: amount,
    mintRecipient: mintRecipient.toString(),
    messageSender: depositor.toString(),
    timeStamp: block.block_timestamp,
    destinationTokenMessenger: destinationTokenMessenger.toString(),
    destinationCaller: destinationCaller.toString(),
  });
  • burnToken: this field has the burnToken value which is already emitted by the event.

  • transactionHash: we get the transaction hash by the method transaction.transaction_hash

  • sourceDomain: dstChainId is a field obtaine by placing the below line, for this we have to create a function called domainToChainId in a file helper.ts in src/utils which is mentioned later.

    const dstChainId: string = domainToChainId[destinationDomain];
  • destinationDomain: we place the destinationDomain variable emitted in the event.

  • amount: the amount variable is also taken from the events emitted.

  • mintRecipient: emitted in the event.

  • messageSender: emitted in the event.

  • timeStamp: we use the blockflow utility of block.block_timestamp to get the block timestamp for the transaction

  • destinationTokenMessenger: emitted in the event.

  • destinationCaller: emitted in the event.

The contents of helper.ts are as follows:

import { Interface } from "ethers";
export const domainToChainId: { [key: string]: string } = {
  "0": "1", // ethereum
  "1": "43114", // avalanche
  "2": "10", // op
  "3": "42161", // arb
  "6": "8453", // base
  "7": "137", // polygon
};

CCTP states the destination domain as single digit integers for all their networks ranging from 0-7. The above function maps the CCTP domainId to the original chain Id for each network.

P.S. : Don't forget to import the function in the handler file.


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.

And, we're good to go, to have a look at the complete indexing and all the necessary handlers for indexing CCTP using blockflow CLI, do check the github repository.

You can checkout the indexed smart contracts here.