Skip to main content

ERC20 Decoder

Decode USDC Transfer events. Use case: Decode ERC20 token transfers with typed event data.
import {
  evmPortalSource,
  evmDecoder,
  commonAbis,
} from "@subsquid/pipes/evm";

async function main() {
  const source = evmPortalSource({
    portal: "https://portal.sqd.dev/datasets/ethereum-mainnet",
  });

  const decoder = evmDecoder({
    range: { from: 20000000, to: 20000100 },
    contracts: ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"], // USDC
    events: {
      transfer: commonAbis.erc20.events.Transfer,
    },
  });

  for await (const { data } of source.pipe(decoder)) {
    for (const t of data.transfer) {
      console.log({
        from: t.event.from,
        to: t.event.to,
        value: t.event.value.toString(),
        block: t.block.number,
        tx: t.rawEvent.transactionHash,
      });
    }
  }
}

void main();

Composite Decoder

Decode multiple event types simultaneously. Use case: Index ERC20 transfers and Uniswap swaps in one pipe.
import {
  evmPortalSource,
  evmDecoder,
  commonAbis,
} from "@subsquid/pipes/evm";
import * as uniswapAbi from "./abi/uniswap-v3-pool";

async function main() {
  const source = evmPortalSource({
    portal: "https://portal.sqd.dev/datasets/ethereum-mainnet",
  });

  const pipeline = source.pipeComposite({
    usdc: evmDecoder({
      range: { from: 20000000, to: 20000100 },
      contracts: ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"],
      events: {
        transfer: commonAbis.erc20.events.Transfer,
      },
    }),
    uniswap: evmDecoder({
      range: { from: 20000000, to: 20000100 },
      contracts: ["0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640"],
      events: {
        swap: uniswapAbi.events.Swap,
      },
    }),
  });

  for await (const { data } of pipeline) {
    console.log({
      usdcTransfers: data.usdc.transfer.length,
      uniswapSwaps: data.uniswap.swap.length,
    });
  }
}

void main();

Multiple Event Types

Decode Transfers and Approvals from same contract. Use case: Index multiple event types from one contract.
Use @subsquid/evm-typegen to generate typed ABIs instead of manually finding raw event signatures. This approach provides type safety and eliminates the need to look up topic0 hashes. See the Custom ABI section below.
import {
  evmPortalSource,
  evmDecoder,
  commonAbis,
} from "@subsquid/pipes/evm";

async function main() {
  const source = evmPortalSource({
    portal: "https://portal.sqd.dev/datasets/ethereum-mainnet",
  });

  const decoder = evmDecoder({
    range: { from: 20000000, to: 20000100 },
    contracts: ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"],
    events: {
      transfer: commonAbis.erc20.events.Transfer,
      approval: commonAbis.erc20.events.Approval,
    },
  });

  for await (const { data } of source.pipe(decoder)) {
    console.log({
      transfers: data.transfer.length,
      approvals: data.approval.length,
    });

    for (const t of data.transfer) {
      console.log(
        `Transfer: ${t.event.from} -> ${t.event.to}: ${t.event.value}`
      );
    }

    for (const a of data.approval) {
      console.log(
        `Approval: ${a.event.owner} approved ${a.event.spender}: ${a.event.value}`
      );
    }
  }
}

void main();

Filter Events by Parameters

Filter events by indexed parameters to reduce data transfer and processing. Use case: Monitor specific wallet activity or track events with specific indexed values.
import {
  evmPortalSource,
  evmDecoder,
  commonAbis,
} from "@subsquid/pipes/evm";

async function main() {
  const SPECIFIC_SENDER = "0x87482e84503639466fad82d1dce97f800a410945";
  const SPECIFIC_RECEIVER = "0x10b32a54eeb05d2c9cd1423b4ad90c3671a2ed5f";
  
  const source = evmPortalSource({
    portal: "https://portal.sqd.dev/datasets/ethereum-mainnet",
  });

  const decoder = evmDecoder({
    range: { from: 24171448, to: 24171449 },
    events: {
      // Capture all Approval events (no filtering)
      approvals: commonAbis.erc20.events.Approval,
      
      // Filter Transfer events by indexed parameters
      transfers: {
        event: commonAbis.erc20.events.Transfer,
        params: {
          from: [SPECIFIC_SENDER], // Array of addresses (OR logic)
          to: SPECIFIC_RECEIVER,     // Single address
        },
      },
    },
  });

  for await (const { data } of source.pipe(decoder)) {
    console.log({
      approvals: data.approvals.length,
      transfers: data.transfers.length, // Only transfers matching params
    });
    
    for (const transfer of data.transfers) {
      console.log({
        from: transfer.event.from,
        to: transfer.event.to,
        value: transfer.event.value.toString(),
        block: transfer.block.number,
      });
    }
  }
}

void main();

How It Works

  1. Portal-Level Filtering: Topic filters are sent to the Portal API, reducing data transfer at the source.
  2. Type Safety: TypeScript ensures only indexed parameters can be used in params.
  3. Flexible Values: Parameters accept single values or arrays for OR logic.
  4. Case Insensitive: Address matching is case-insensitive for consistency.

Filter Syntax

Single Parameter Filter by one indexed parameter:
events: {
  transfers: {
    event: commonAbis.erc20.events.Transfer,
    params: {
      from: "0x87482e84503639466fad82d1dce97f800a410945",
    },
  },
}
Multiple Parameters (AND Logic) All parameters must match:
events: {
  transfers: {
    event: commonAbis.erc20.events.Transfer,
    params: {
      from: "0x87482e84503639466fad82d1dce97f800a410945",
      to: "0x10b32a54eeb05d2c9cd1423b4ad90c3671a2ed5f",
    },
  },
}
Array Values (OR Logic) Match any value in the array:
const MONITORED_ADDRESSES = [
  "0x87482e84503639466fad82d1dce97f800a410945",
  "0x10b32a54eeb05d2c9cd1423b4ad90c3671a2ed5f",
  "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
];

events: {
  transfers: {
    event: commonAbis.erc20.events.Transfer,
    params: {
      from: MONITORED_ADDRESSES, // Match any of these addresses
    },
  },
}
When to use parameter filtering:
  • Monitoring activity from specific addresses or contracts
  • Tracking events with specific indexed values (token IDs, pool addresses, etc.)
  • Reducing data transfer when you only need a subset of events
When to use client-side filtering:
  • Filtering by non-indexed parameters
  • Complex filtering logic that can’t be expressed in topic filters
  • When you need to filter based on multiple events’ relationships
Only indexed event parameters can be used in the params object. Non-indexed parameters cannot be filtered at the Portal API level and must be filtered client-side. Parameter matching for addresses is case-insensitive.

Common Use Cases

  • Wallet Monitoring - Track transfers from/to specific addresses
  • DEX Trading Activity - Filter swaps involving specific token pairs
  • Protocol Analytics - Monitor interactions with specific contracts
  • Liquidation Tracking - Watch for events from lending protocol liquidators

Custom ABI

Generate and use custom ABIs. Use case: Index events from contracts with custom ABIs.

Generate ABI

npx @subsquid/evm-typegen src/abi abi/MyContract.json

Use Generated ABI

import { evmPortalSource, evmDecoder } from "@subsquid/pipes/evm";
import * as myContractAbi from "./abi/MyContract";

async function main() {
  const source = evmPortalSource({
    portal: "https://portal.sqd.dev/datasets/ethereum-mainnet",
  });

const decoder = evmDecoder({
  range: { from: 20000000 },
  contracts: ["0x..."],
  events: {
    myEvent: myContractAbi.events.MyCustomEvent,
    anotherEvent: myContractAbi.events.AnotherEvent,
  },
});

for await (const { data } of source.pipe(decoder)) {
  console.log(data.myEvent);
  console.log(data.anotherEvent);
}
}

void main();