Skip to main content

ICP Ledger Canister

The ICP Ledger canister is the core token ledger for the Internet Computer Protocol. It implements the ICRC-1 and ICRC-2 token standards and manages all ICP token transfers and balances.

Overview

The ICP Ledger provides:
  • Token Transfers: Send ICP tokens between accounts
  • Balance Queries: Check account balances
  • Transaction History: Query historical blocks and transactions
  • ICRC-1 Standard: Full implementation of the ICRC-1 fungible token standard
  • ICRC-2 Standard: Approve/transfer_from pattern for delegation
  • Archive Canisters: Automatic archival of historical blocks for scalability

Account Model

The ICP Ledger supports two account identifier formats:

Legacy AccountIdentifier

A 32-byte blob that is the SHA-224 hash of:
  • Domain separator (“\x0Aaccount-id”)
  • Principal
  • Subaccount (32 bytes, defaults to zeros)

ICRC-1 Account

A record with:
  • owner: Principal that owns the account
  • subaccount: Optional 32-byte blob (defaults to zeros)
The ICRC-1 format is recommended for new applications.

Token Units

ICP uses e8s (10^-8 ICP) as its base unit:
  • 1 ICP = 100,000,000 e8s
  • Transfer fee: 10,000 e8s (0.0001 ICP)

Core Methods

ICRC-1 Standard Methods

icrc1_transfer

Transfer ICP tokens to another account.
from_subaccount
blob
Source subaccount (defaults to all zeros)
to
Account
required
Destination account with owner and optional subaccount
amount
nat
required
Amount to transfer in e8s
fee
nat
Transaction fee in e8s (defaults to standard fee of 10,000 e8s)
memo
blob
Optional memo (max 32 bytes)
created_at_time
nat64
Transaction creation timestamp in nanoseconds (for deduplication)
result
variant
Transfer result:
  • Ok: Block index of the transaction
  • Err: Transfer error with details
Possible Errors:
  • BadFee: Incorrect fee amount provided
  • InsufficientFunds: Source account has insufficient balance
  • TooOld: Transaction is older than 24 hours
  • CreatedInFuture: Transaction timestamp is in the future
  • Duplicate: Transaction with same memo and timestamp already exists
  • TemporarilyUnavailable: Ledger is temporarily unavailable
  • GenericError: Other error with description

icrc1_balance_of

Query the balance of an account.
account
Account
required
Account to query with owner and optional subaccount
balance
nat
Account balance in e8s

icrc1_metadata

Get ledger metadata.
metadata
vec record { text; Value }
List of metadata key-value pairs including:
  • icrc1:name: Token name (“Internet Computer”)
  • icrc1:symbol: Token symbol (“ICP”)
  • icrc1:decimals: Decimal places (8)
  • icrc1:fee: Transfer fee

icrc1_supported_standards

List supported token standards.
standards
vec record { name: text; url: text }
List of supported standards:
  • ICRC-1: Fungible token standard
  • ICRC-2: Approve and transfer from
  • ICRC-10: Supported standards query
  • ICRC-21: Canister call consent messages

icrc1_name

Get the token name.
name
text
Token name: “Internet Computer”

icrc1_symbol

Get the token symbol.
symbol
text
Token symbol: “ICP”

icrc1_decimals

Get the number of decimal places.
decimals
nat8
Decimal places: 8

icrc1_fee

Get the transfer fee.
fee
nat
Transfer fee in e8s: 10,000

icrc1_total_supply

Get the total token supply.
total_supply
nat
Total ICP supply in e8s

icrc1_minting_account

Get the minting account.
minting_account
Account
The account that can mint new tokens (governance minting account)

ICRC-2 Standard Methods

icrc2_approve

Approve a spender to transfer tokens on your behalf.
from_subaccount
blob
Source subaccount (defaults to all zeros)
spender
Account
required
Account to approve as spender
amount
nat
required
Amount to approve in e8s
expected_allowance
nat
Current allowance (for safety, prevents race conditions)
expires_at
nat64
Expiration timestamp in nanoseconds
fee
nat
Transaction fee (defaults to 10,000 e8s)
memo
blob
Optional memo
created_at_time
nat64
Transaction creation timestamp
result
variant
Approve result:
  • Ok: Block index of the approval
  • Err: Approval error
Possible Errors:
  • BadFee: Incorrect fee
  • InsufficientFunds: Insufficient balance to pay fee
  • AllowanceChanged: Current allowance doesn’t match expected_allowance
  • Expired: Expiration time is in the past
  • TooOld: Transaction too old
  • CreatedInFuture: Transaction timestamp in future
  • Duplicate: Duplicate transaction
  • TemporarilyUnavailable: Service temporarily unavailable
  • GenericError: Other error

icrc2_transfer_from

Transfer tokens from an approved account.
spender_subaccount
blob
Spender’s subaccount (defaults to all zeros)
from
Account
required
Account to transfer from (must have approval)
to
Account
required
Destination account
amount
nat
required
Amount to transfer in e8s
fee
nat
Transaction fee
memo
blob
Optional memo
created_at_time
nat64
Transaction creation timestamp
result
variant
Transfer result:
  • Ok: Block index
  • Err: Transfer error
Possible Errors:
  • InsufficientAllowance: Not enough allowance for transfer + fee
  • BadFee: Incorrect fee
  • InsufficientFunds: Source account has insufficient balance
  • TooOld: Transaction too old
  • CreatedInFuture: Transaction in future
  • Duplicate: Duplicate transaction
  • TemporarilyUnavailable: Service unavailable
  • GenericError: Other error

icrc2_allowance

Query the allowance between accounts.
account
Account
required
Account that granted the allowance
spender
Account
required
Spender account
allowance
nat
Current allowance amount in e8s
expires_at
nat64
Expiration timestamp (if set)

Legacy Methods

transfer

Legacy transfer method using AccountIdentifier format.
memo
nat64
required
Transaction memo (64-bit number)
amount
Tokens
required
Amount to transfer with e8s field
fee
Tokens
required
Transaction fee (must be 10,000 e8s)
from_subaccount
blob
Source subaccount
to
AccountIdentifier
required
Destination account identifier (32-byte blob)
created_at_time
TimeStamp
Transaction timestamp with timestamp_nanos field
result
TransferResult
Transfer result:
  • Ok: Block index
  • Err: Transfer error

account_balance

Query account balance using AccountIdentifier.
account
AccountIdentifier
required
Account identifier (32-byte blob)
balance
Tokens
Account balance with e8s field

account_identifier

Convert an ICRC-1 Account to AccountIdentifier format.
account
Account
required
ICRC-1 account to convert
account_identifier
AccountIdentifier
32-byte AccountIdentifier blob

Query Methods

query_blocks

Query a range of blocks from the ledger.
start
nat64
required
Starting block index
length
nat64
required
Number of blocks to fetch
chain_length
nat64
Total number of blocks in the ledger
blocks
vec Block
List of blocks in the specified range
first_block_index
nat64
Index of the first block in the response
archived_blocks
vec ArchivedBlocksRange
References to archive canisters for older blocks
certificate
blob
Certificate for the response (in non-replicated queries)

archives

Get list of archive canisters.
archives
vec Archive
List of archive canister IDs storing historical blocks

tip_of_chain

Get the latest block index.
tip_index
nat64
Index of the most recent block
certification
blob
Certificate for the tip

transfer_fee

Get the current transfer fee.
transfer_fee
Tokens
Current transfer fee (10,000 e8s)

Archive Canisters

As the ledger grows, old blocks are moved to archive canisters to keep the main ledger efficient.

Archive Query Methods

get_blocks

Query blocks from an archive canister.
start
nat64
required
Starting block index
length
nat64
required
Number of blocks to fetch
result
GetBlocksResult
  • Ok: BlockRange with blocks
  • Err: Error if start index is invalid

get_encoded_blocks

Query encoded blocks (more efficient for large queries).
start
nat64
required
Starting block index
length
nat64
required
Number of blocks to fetch
result
GetEncodedBlocksResult
  • Ok: Vec of encoded block blobs
  • Err: Error details

Data Types

Block

Represents a transaction block in the ledger.
parent_hash
blob
Hash of the parent block
transaction
Transaction
The transaction in this block
timestamp
TimeStamp
Block creation timestamp in nanoseconds

Transaction

memo
nat64
Legacy memo (64-bit number)
icrc1_memo
blob
ICRC-1 memo (up to 32 bytes)
operation
Operation
The operation performed:
  • Transfer: Transfer between accounts
  • Mint: Mint new tokens
  • Burn: Burn tokens
  • Approve: Set allowance for spender
created_at_time
TimeStamp
Transaction creation timestamp

Operation

Transfer
from
AccountIdentifier
Source account
to
AccountIdentifier
Destination account
amount
Tokens
Transfer amount
fee
Tokens
Transaction fee paid
spender
AccountIdentifier
Spender if using transfer_from
Mint
to
AccountIdentifier
Recipient account
amount
Tokens
Minted amount
Burn
from
AccountIdentifier
Source account
amount
Tokens
Burned amount
spender
AccountIdentifier
Spender if using burn_from
Approve
from
AccountIdentifier
Account granting approval
spender
AccountIdentifier
Account receiving approval
allowance
Tokens
Approved amount
fee
Tokens
Approval fee
expires_at
TimeStamp
Expiration time
expected_allowance
Tokens
Expected current allowance

Transaction Deduplication

The ledger implements deduplication to prevent double-spending:
  1. Each transaction can include a created_at_time timestamp
  2. Transactions are deduplicated based on (sender, memo, created_at_time)
  3. Deduplication window is 24 hours
  4. If a duplicate is detected, the original block index is returned

Best Practices

For Developers

  1. Use ICRC-1 Format: Prefer ICRC-1 Account format over legacy AccountIdentifier
  2. Set Timestamps: Always set created_at_time for deduplication
  3. Handle Errors: Properly handle all error variants
  4. Use Query Methods: Use query methods for read operations (faster, cheaper)
  5. Archive Access: Check archived_blocks when querying old transactions
  6. Fee Checking: Always check current fee with icrc1_fee() before transfers

For Users

  1. Transaction IDs: Save block indices for transaction tracking
  2. Subaccounts: Use subaccounts to manage multiple addresses under one principal
  3. Memo Usage: Use memos to track payment purposes
  4. Approval Limits: Set reasonable allowance limits and expiration times

Examples

Transfer ICP (ICRC-1)

const result = await ledger.icrc1_transfer({
  from_subaccount: [],
  to: {
    owner: recipientPrincipal,
    subaccount: []
  },
  amount: 100_000_000n, // 1 ICP
  fee: [],
  memo: [],
  created_at_time: [BigInt(Date.now()) * 1_000_000n]
});

if ('Ok' in result) {
  console.log('Transfer successful, block:', result.Ok);
} else {
  console.error('Transfer failed:', result.Err);
}

Check Balance

const balance = await ledger.icrc1_balance_of({
  owner: myPrincipal,
  subaccount: []
});

console.log('Balance:', balance, 'e8s');
console.log('Balance:', Number(balance) / 100_000_000, 'ICP');

Approve and Transfer From

// Step 1: Approve spender
const approveResult = await ledger.icrc2_approve({
  from_subaccount: [],
  spender: {
    owner: spenderPrincipal,
    subaccount: []
  },
  amount: 100_000_000n, // 1 ICP
  fee: [],
  memo: [],
  created_at_time: [BigInt(Date.now()) * 1_000_000n],
  expected_allowance: [],
  expires_at: []
});

// Step 2: Spender transfers from approved account
const transferResult = await ledger.icrc2_transfer_from({
  spender_subaccount: [],
  from: {
    owner: approverPrincipal,
    subaccount: []
  },
  to: {
    owner: recipientPrincipal,
    subaccount: []
  },
  amount: 50_000_000n, // 0.5 ICP
  fee: [],
  memo: [],
  created_at_time: [BigInt(Date.now()) * 1_000_000n]
});

Query Transaction History

const response = await ledger.query_blocks({
  start: 0n,
  length: 100n
});

console.log('Total blocks:', response.chain_length);
console.log('Returned blocks:', response.blocks.length);

// Check archived blocks
for (const archived of response.archived_blocks) {
  console.log('Archive canister:', archived.callback.principal);
  console.log('Block range:', archived.start, 'to', archived.start + archived.length);
}

Common Patterns

Subaccount Generation

Generate deterministic subaccounts:
import { sha256 } from '@noble/hashes/sha256';

function generateSubaccount(seed: string): Uint8Array {
  const hash = sha256(new TextEncoder().encode(seed));
  return new Uint8Array([...hash, ...new Array(8).fill(0)].slice(0, 32));
}

const subaccount = generateSubaccount('user-123');

Polling for Confirmation

async function waitForBlock(blockIndex: bigint): Promise<Block> {
  for (let i = 0; i < 30; i++) {
    const response = await ledger.query_blocks({
      start: blockIndex,
      length: 1n
    });
    
    if (response.blocks.length > 0) {
      return response.blocks[0];
    }
    
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
  
  throw new Error('Block not found after 30 seconds');
}

See Also

Build docs developers (and LLMs) love