Skip to content

icdevsorg/index.mo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ICRC Index-NG (Motoko)

A Motoko implementation of the ICRC Index-NG canister for indexing ICRC-1/ICRC-3 ledger transactions.

Overview

The ICRC Index-NG canister provides fast transaction lookups and balance queries for any ICRC-1 compliant ledger. It indexes all transactions from the ledger and allows querying:

  • Account balances (mirrors icrc1_balance_of)
  • Transaction history per account
  • All blocks in the ledger
  • Subaccounts for a principal
  • Fee collector information

Features

  • Full ICRC-3 Support: Parses blocks in ICRC-3 Value format
  • Legacy Support: Also supports pre-ICRC-3 get_transactions endpoint
  • Enhanced Orthogonal Persistence: Uses Motoko 1.1.0+ with 64-bit heap
  • Automatic Sync: Timer-based polling of the ledger
  • Balance Tracking: Maintains accurate balances from indexed transactions
  • HTTP Interface: Exposes /metrics and /logs endpoints

Installation

Prerequisites

Add to Your Project

mops add icrc-fungible-index

Or add manually to mops.toml:

[dependencies]
icrc-fungible-index = "0.2.0"

Deploy Standalone

  1. Clone this repository
  2. Configure dfx.json:
{
  "canisters": {
    "icrc_index": {
      "type": "motoko",
      "main": "src/Canisters/Index.mo"
    }
  }
}
  1. Deploy with init arguments:
dfx deploy icrc_index --argument '(opt variant { Init = record { 
  ledger_id = principal "YOUR_LEDGER_CANISTER_ID";
  retrieve_blocks_from_ledger_interval_seconds = opt 5;
}})'

Usage

Query Endpoints

// Get the ledger being indexed
ledger_id : () -> (principal) query;

// Get current sync status
status : () -> (record { num_blocks_synced : nat64 }) query;

// Get account balance (same as ledger)
icrc1_balance_of : (Account) -> (nat) query;

// Get transactions for an account with pagination
get_account_transactions : (record {
  account : Account;
  start : opt nat64;      // Optional starting block ID
  max_results : nat;      // Max transactions to return
}) -> (variant {
  ok : record {
    balance : nat;
    transactions : vec TransactionWithId;
    oldest_tx_id : opt nat64;
  };
  err : record { message : text };
}) query;

// Get oldest transaction ID for account
get_oldest_tx_id : (Account) -> (opt nat64) query;

// List subaccounts for a principal
list_subaccounts : (record {
  owner : principal;
  start : opt blob;       // Optional starting subaccount for pagination
}) -> (vec blob) query;

// Get blocks by range
get_blocks : (record {
  start : nat;
  length : nat;
}) -> (record {
  chain_length : nat64;
  blocks : vec Value;
}) query;

// Get fee collector information
get_fee_collectors_ranges : () -> (record {
  fee_collector : opt Account;
  ranges : vec record { start : nat64; length : nat64 };
}) query;

Example Usage (JavaScript)

import { Actor, HttpAgent } from "@dfinity/agent";

// Create actor
const agent = new HttpAgent({ host: "https://ic0.app" });
const index = Actor.createActor(indexIdlFactory, {
  agent,
  canisterId: "INDEX_CANISTER_ID",
});

// Get account transactions
const result = await index.get_account_transactions({
  account: { owner: Principal.fromText("..."), subaccount: [] },
  start: [],
  max_results: 100n,
});

if ("ok" in result) {
  console.log(`Balance: ${result.ok.balance}`);
  console.log(`Transactions: ${result.ok.transactions.length}`);
}

Push Notifications from Ledger

The index canister exposes a notify endpoint that allows the ledger to push new block availability instead of relying solely on polling:

// Called by the ledger canister when new blocks are available
notify : (latest_block : nat) -> ();

Authorization: Only the configured ledger_id principal may call notify. All other callers are rejected.

When a notification arrives, the index compares latest_block against its current sync height. If new blocks are available, it triggers an immediate sync (non-blocking via a 0-delay timer).

Configuring Push Notifications with ICDevs ICRC-1 Ledgers

Ledgers built on icdevsorg/icrc1.mo (such as ICRC_fungible) support push notifications out of the box. To enable:

  1. Set the index canister on your ledger:
dfx canister call token admin_set_index_canister '(opt principal "INDEX_CANISTER_ID")'
  1. Verify the configuration:
dfx canister call token get_index_canister '()'
  1. Disable notifications (set to null):
dfx canister call token admin_set_index_canister '(null)'

How it works: The ledger registers an ICRC-3 record_added_listener that fires on every new block. To avoid excessive inter-canister calls, notifications are batched with a 2-second delay — if multiple blocks are added in quick succession, only a single notify call is made with the latest block index. The call is scheduled via TimerTool and uses best-effort messaging with a 60-second timeout.

When push notifications are enabled, the index's polling interval can be set much higher (e.g., once per day as a fallback) since the ledger will push updates in near real-time.

Configuration

Init Arguments

Field Type Description
ledger_id Principal The ledger canister to index (required)
retrieve_blocks_from_ledger_interval_seconds ?Nat64 Sync interval in seconds (default: 5)

Upgrade Arguments

Field Type Description
ledger_id ?Principal Change the indexed ledger (optional)
retrieve_blocks_from_ledger_interval_seconds ?Nat64 Update sync interval (optional)

Architecture

src/
├── Canisters/
│   └── Index.mo           # Main canister actor
└── Index/
    ├── lib.mo             # ICRC_Index class (main logic)
    ├── service.mo         # Candid types
    ├── TransactionTypes.mo # Transaction/event types
    ├── BlockParser.mo     # Block parsing (ICRC-3 + legacy)
    ├── LedgerInterface.mo # Inter-canister calls
    ├── SyncEngine.mo      # Sync orchestration
    ├── BalanceManager.mo  # Balance tracking
    └── migrations/        # State versioning

Key Components

  • BlockParser: Parses blocks from both ICRC-3 Value format and legacy LedgerTransaction format
  • BalanceManager: Tracks balances per account using SHA256-based keys
  • SyncEngine: Timer-based polling with configurable interval
  • LedgerInterface: Handles inter-canister calls to ledger and archives

Testing

PocketIC Integration Tests

cd pic
npm install
npm test

Test Coverage

  • ✅ Basic queries (ledger_id, status, balance)
  • ✅ Transaction queries
  • ✅ Block syncing
  • ✅ Balance accuracy vs ledger
  • ✅ Upgrade persistence
  • ✅ HTTP endpoints (/metrics, /logs)

Compatibility

  • Ledger Requirements: Any ICRC-1 compliant ledger
  • ICRC-3 Support: Full support for ICRC-3 block format
  • Legacy Support: Also works with ledgers using get_transactions
  • Motoko Version: Requires moc 1.1.0+ (Enhanced Orthogonal Persistence)

HTTP Endpoints

GET /metrics

Returns Prometheus-style metrics:

# HELP index_number_of_blocks Total indexed blocks
# TYPE index_number_of_blocks gauge
index_number_of_blocks 1234

# HELP index_number_of_accounts Total tracked accounts
# TYPE index_number_of_accounts gauge
index_number_of_accounts 567

GET /logs

Returns JSON array of log entries:

[
  {
    "timestamp": 1234567890,
    "level": "info",
    "message": "Synced 100 blocks (index 0 to 99)"
  }
]

Development

Build

dfx build icrc_index

Local Testing

# Start local replica
dfx start --clean --background

# Deploy test ledger (ICRC_fungible)
cd ../ICRC_fungible && dfx deploy token

# Deploy index pointing to ledger
cd ../index.mo
dfx deploy icrc_index --argument '(opt variant { Init = record { 
  ledger_id = principal "LEDGER_CANISTER_ID";
  retrieve_blocks_from_ledger_interval_seconds = opt 1;
}})'

Dependencies

License

MIT License

Contributing

Contributions welcome! Please open an issue or PR.

Related

About

index canister in motoko

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors