Skip to main content

4.1 Using the ICP ledger

Advanced
Tutorial

Overview

Recall from previous modules that ICP is the native token of the Internet Computer. ICP tokens have three key functionalities on the Internet Computer:

  • Facilitating the network's governance through neuron staking. You'll dive deeper into staking tokens in 4.4 NNS governance and staking.

  • Creating cycles by converting ICP into cycles, which are then used to pay for canister computation and resources. The developer journey briefly covered converting ICP into cycles in 1.4 Acquiring and using cycles.

  • Rewarding NNS participants by providing ICP tokens to users that stake ICP in neurons and actively participate in voting on proposals. You'll dive deeper into NNS rewards in 4.4 NNS governance and staking.

To interact with the ICP token, such as send it to another account or convert it into cycles, a specialized canister known as the ICP ledger canister can be used. The ledger canister is used to hold ledger accounts and record a traceable history of all ICP transactions.

In this tutorial, you'll dive into how to deploy a local instance of the ICP ledger canister and how to interact with it. This workflow is important to learn since your local replica cannot access the mainnet ICP ledger, so to test your project's integration with the ICP ledger, you need to deploy a local instance of the ledger. The local instance, however, will not have the history and balances that the mainnet ICP ledger contains.

The ICP ledger can only be used to interact with ICP tokens; to interact with other tokens, such as ICRC tokens, an ICRC ledger will need to be used. This ledger is covered in the module 4.2 ICRC tokens.

Accounts

An ICP ledger account is identified by the AccountIdentifier value, which is a value derived from the account owner's ICP principal and subaccount identifier. Accounts can only be owned by one principal; however, since a principal can refer to a canister, joint accounts can be implemented as a canister. The ICP ledger uses AccountIdentifiers instead of just using principal IDs since AccountIdentifiers allow a principal to control multiple accounts. Accounts can include a subaccount, which is an optional bitstring that distinguishes between different accounts under the same account owner.

If you are familiar with an Ethereum or Bitcoin user's public key, a principal identifier can be thought of as ICP equivalent. When you use a principal, your corresponding secret key is used to sign messages, authenticate with the ledger, and execute transactions on your account.

Transaction types

Within the ICP ledger canister, there are three types of transactions:

  • Minting: A minting transaction generates a new token for an account.

  • Burning: A burning transaction eliminates a token from existence.

  • Transferring: A transferring transaction transfers ICP between accounts.

All transactions are recorded in the ledger canister as a hashed blockchain. The ICP ledger canister runs on the NNS system subnet blockchain on the mainnet.

When state changes are recorded, each new transaction is inserted into a block and assigned a unique index value. The entire blockchain is authenticated regularly by signing the latest block using a signature. This signature can be verified by anyone using the root public key of ICP. The ledger can be queried to retrieve specific transactions.

Deploying the ICP ledger locally

To deploy the ICP ledger canister locally, there are two workflows:

  • Using the dfx-nns command to deploy an entire instance of the NNS locally, since the ICP ledger is part of the NNS. This workflow will install an ICP ledger canister with the canister ID of ryjl3-tyaaa-aaaaa-aaaba-cai. This is a straightforward workflow, but it is heavyweight in the sense that it installs several things in addition to the ledger canister.

  • Deploy the ICP ledger canister locally using the Wasm file. This is the method that will be showcased in this tutorial since it provides greater control over the deployment, such as defining the ledger's minting account, customizing the initialization arguments, and choosing which Wasm version of the ledger to use.

The ICP ledger canister running on the mainnet is not meant to be used for other token deployments. It contains legacy code designed to be backwards compatible and should not be used for developing new ledgers. To develop a new token or ledger, the ICRC ledger can be used, which will be covered in a future module.

Prerequisites

Before you start, verify that you have set up your developer environment according to the instructions in 0.3 Developer environment setup.

Creating a new project

To get started, create a new project in your working directory. Open a terminal window, navigate into your working directory (developer_journey), then use the commands:

Use dfx new <project_name> to create a new project:

dfx start --clean --background
dfx new icp_ledger_canister

You will be prompted to select the language that your backend canister will use. Select 'Motoko':

? Select a backend language: ›
❯ Motoko
Rust
TypeScript (Azle)
Python (Kybra)

Then, select a frontend framework for your frontend canister. Select 'No frontend canister':

  ? Select a frontend framework: ›
SvelteKit
React
Vue
Vanilla JS
No JS template
❯ No frontend canister

Lastly, you can include extra features to be added to your project:

  ? Add extra features (space to select, enter to confirm) ›
⬚ Internet Identity
⬚ Bitcoin (Regtest)
⬚ Frontend tests

Then, navigate into the new project directory:

cd icp_ledger_canister

Locating the Wasm and Candid files

Next, you need to download the ledger's Wasm and Candid files from the latest replica version. You can find the latest replica version on the dashboard under the Elect new replica binary revision field.

Then, use the following URL to download the Wasm module: https://download.dfinity.systems/ic/<VERSION>/canisters/ledger-canister.wasm.gz.

For example, in this tutorial you'll use the URL https://download.dfinity.systems/ic/d87954601e4b22972899e9957e800406a0a6b929/canisters/ledger-canister.wasm.gz.

Similarly, the following URL can be used to download the Candid file: https://raw.githubusercontent.com/dfinity/ic/<VERSION>/rs/rosetta-api/icp_ledger/ledger.did.

In this tutorial, you'll use the URL https://raw.githubusercontent.com/dfinity/ic/d87954601e4b22972899e9957e800406a0a6b929/rs/rosetta-api/icp_ledger/ledger.did.

Now let's open the dfx.json file in the project's directory and replace the existing content with the following:

{
"canisters": {
"icp_ledger_canister": {
"type": "custom",
"candid": "https://raw.githubusercontent.com/dfinity/ic/d87954601e4b22972899e9957e800406a0a6b929/rs/rosetta-api/icp_ledger/ledger.did",
"wasm": "https://download.dfinity.systems/ic/d87954601e4b22972899e9957e800406a0a6b929/canisters/ledger-canister.wasm.gz",
"remote": {
"id": {
"ic": "ryjl3-tyaaa-aaaaa-aaaba-cai"
}
}
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"output_env_file": ".env",
"version": 1
}

Creating a minting account

To interact with our ICP ledger, create a new identity that will act as the minting account, then export the account's ID as the environment variable MINTER_ACCOUNT_ID:

dfx identity new minter
dfx identity use minter
export MINTER_ACCOUNT_ID=$(dfx ledger account-id)

Any transfer made from the minting account will make a Mint transaction, while transfers to the minting account will make a Burn transaction.

Now, switch back to your DevJourney identity and export its account ID as the environment variable DEFAULT_ACCOUNT_ID:

dfx identity use DevJourney
export DEFAULT_ACCOUNT_ID=$(dfx ledger account-id)

Deploying the canister

With these variables set, you can deploy the ledger canister. To deploy the canister with archiving options enabled, run the following command:

dfx deploy --specified-id ryjl3-tyaaa-aaaaa-aaaba-cai icp_ledger_canister --argument "
(variant {
Init = record {
minting_account = \"$MINTER_ACCOUNT_ID\";
initial_values = vec {
record {
\"$DEFAULT_ACCOUNT_ID\";
record {
e8s = 10_000_000_000 : nat64;
};
};
};
send_whitelist = vec {};
transfer_fee = opt record {
e8s = 10_000 : nat64;
};
token_symbol = opt \"LICP\";
token_name = opt \"Local ICP\";
}
})
"

In this command, you can deploy the ICP ledger canister locally and pass parameters that do the following:

  • Deploy the ICP ledger canister with the same canister ID as the mainnet ledger canister to simplify switching between local and mainnet deployments.

  • Set the minting account to the MINTING_ACCOUNT_ID environmental variable.

  • Mint 100 ICP tokens to the DEFAULT_ACCOUNT_ID value.

  • Set the transfer fee to 0.0001 ICP.

  • Name the token Local ICP / LICP.

Since you're using the ICP ledger locally, you will create a local token called LICP that you'll use for local integration testing. This LICP token does not have any value on the mainnet, and cannot be used in place of ICP for mainnet transactions.

Interacting with the ICP ledger canister

There are several ways to interact with the ICP ledger, such as:

  • Using the dfx ledger command, which is the dfx shortcut for interacting with the ledger.

  • Using the dfx canister command to interact directly with the ledger canister.

  • Using the Candid UI.

  • Using nns-js to interact with the ICP ledger from your web application.

  • Using the ic-cdk to make inter-canister calls from another canister to the ICP ledger canister.

Using dfx ledger

To get your local ledger account ID, you can use the command:

dfx ledger account-id

Then, to check the balance of that account, use the command:

dfx ledger --network ic balance ACCOUNT_ID

Replace ACCOUNT_ID with the output from the dfx ledger account-id command.

To use the mainnet ledger, use the flag --network ic with any dfx ledger command.

To transfer tokens from one account to another, use the command:

dfx ledger transfer --amount AMOUNT --memo MEMO RECEIVER_ACCOUNT_ID

Replace AMOUNT with the number of tokens to transfer, MEMO with a brief message to describe the reason for the transfer, and RECEIVER_ACCOUNT_ID with the account ID to receive the tokens.

Using dfx canister

The dfx canister command can be used to call additional methods of the ICP ledger canister, such as the token's name. To call the token's name, use the command:

dfx canister call icp_ledger_canister name

In our example, this command will return the following output:

("Local ICP")

Similarly, the following command can be used to return the token's symbol:

dfx canister call ryjl3-tyaaa-aaaaa-aaaba-cai symbol '()'

In our example, this command will return the following output:

(record { symbol = "LICP" })

To query the canister's archives, use the command:

dfx canister call ryjl3-tyaaa-aaaaa-aaaba-cai archives '()'

In our local example, there are no archives that have been created so far, resulting in the following output:

(record { archives = vec {} })

To transfer tokens to another account, you will need to get their AccountIdentifier. Their AccountIdentifier can be derived by getting the identity's principal with the command:

dfx identity get-principal --identity IDENTITY

For example, if you want to send LICP to the minting account you created earlier, you can get the principal with the command:

dfx identity get-principal --identity minter

This will return the principal:

sckqo-e2vyl-4rqqu-5g4wf-pqskh-iynjm-46ixm-awluw-ucnqa-4sl6j-mqe

Then, you can get the AccountIdentifier with the command:

dfx ledger account-id --of-principal sckqo-e2vyl-4rqqu-5g4wf-pqskh-iynjm-46ixm-awluw-ucnqa-4sl6j-mqe

This will return the following output:

d52f7f2b7277f025bcaa5c90b10d122274faba289

Then, let's combine the output of these commands to form a transfer transaction:

export TO_ACCOUNT = "d52f7f2b7277f025bcaa5c90b10d122274faba2891bea519105309ae1f0af91d"
dfx canister call ryjl3-tyaaa-aaaaa-aaaba-cai transfer '(record { to = $(python3 -c 'print("vec{" + ";".join([str(b) for b in bytes.fromhex("'$TO_ACCOUNT'")]) + "}")'); memo = 1:nat64; amount = record {e8s = 200_000_000 }; fee = record { e8s = 10_000 }; })'

The output of this command will return the block index that the transaction took place in. Since your example is deployed locally and you haven't run any other commands, your transaction takes place within the first block:

(variant { Ok = 1 : nat64 })

To confirm that the transaction was successful, query the balance of the AccountIdentifier with the command:

dfx canister call ryjl3-tyaaa-aaaaa-aaaba-cai account_balance '(record { account = '$(python3 -c 'print("vec{" + ";".join([str(b) for b in bytes.fromhex("'$TO_ACCOUNT'")]) + "}")')' })'

This should return a balance of 200_000_000:

(record { e8s = 200_000_000 : nat64 })

Using the Candid UI

Alternatively, the Candid UI can be used to interact with the local ICP ledger's methods by navigating to the URL provided when the canister was deployed, such as:

http://127.0.0.1:4943/?canisterId=bnz7o-iuaaa-aaaaa-qaaaa-cai&id=ryjl3-tyaaa-aaaaa-aaaba-cai

After navigating to this URL in a web browser, the Candid UI will resemble the following:

Candid UI

Resources

Learn how to use nns-js to interact with the ledger from the web application.

Learn how to use the ic-cdk to make inter-canister calls to the ICP ledger.

Need help?

Did you get stuck somewhere in this tutorial, or feel like you need additional help understanding some of the concepts? The ICP community has several resources available for developers, like working groups and bootcamps, along with our Discord community, forum, and events such as hackathons. Here are a few to check out:

Next steps

Next, you'll dive into ICRC-1 tokens and how to deploy your own token using the ICRC-1 standard.