A Motoko implementation of the ICRC Index-NG canister for indexing ICRC-1/ICRC-3 ledger transactions.
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
- Full ICRC-3 Support: Parses blocks in ICRC-3 Value format
- Legacy Support: Also supports pre-ICRC-3
get_transactionsendpoint - 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
/metricsand/logsendpoints
mops add icrc-fungible-indexOr add manually to mops.toml:
[dependencies]
icrc-fungible-index = "0.2.0"- Clone this repository
- Configure
dfx.json:
{
"canisters": {
"icrc_index": {
"type": "motoko",
"main": "src/Canisters/Index.mo"
}
}
}- 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;
}})'// 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;
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}`);
}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).
Ledgers built on icdevsorg/icrc1.mo (such as ICRC_fungible) support push notifications out of the box. To enable:
- Set the index canister on your ledger:
dfx canister call token admin_set_index_canister '(opt principal "INDEX_CANISTER_ID")'- Verify the configuration:
dfx canister call token get_index_canister '()'- 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.
| 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) |
| Field | Type | Description |
|---|---|---|
ledger_id |
?Principal |
Change the indexed ledger (optional) |
retrieve_blocks_from_ledger_interval_seconds |
?Nat64 |
Update sync interval (optional) |
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
- BlockParser: Parses blocks from both ICRC-3
Valueformat and legacyLedgerTransactionformat - 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
cd pic
npm install
npm test- ✅ Basic queries (ledger_id, status, balance)
- ✅ Transaction queries
- ✅ Block syncing
- ✅ Balance accuracy vs ledger
- ✅ Upgrade persistence
- ✅ HTTP endpoints (/metrics, /logs)
- 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)
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
Returns JSON array of log entries:
[
{
"timestamp": 1234567890,
"level": "info",
"message": "Synced 100 blocks (index 0 to 99)"
}
]dfx build icrc_index# 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;
}})'- mo:core - Motoko core libraries
- mo:vector - Efficient vector implementation
- mo:map - Hash maps
- mo:sha2 - SHA256 hashing
- mo:account - Account utilities
- mo:rep-indy-hash - Representation-independent hashing
- mo:candy - Value type handling
MIT License
Contributions welcome! Please open an issue or PR.