4.1 Using the ICP ledger
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 AccountIdentifier
s instead of just using principal IDs since AccountIdentifier
s 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 ofryjl3-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:
- dfx v0.17.0 or newer
- dfx v0.16.1 or older
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
dfx start --clean --background
dfx new icp_ledger_canister
cd icp_ledger_canister
Remember, by default dfx new
creates a new project in the Motoko language. If you'd like to create a Rust project, use the flag --type=rust
.
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 thedfx
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:
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:
Developer Discord community, which is a large chatroom for ICP developers to ask questions, get help, or chat with other developers asynchronously via text chat.
Motoko Bootcamp - The DAO Adventure - Discover the Motoko language in this 7 day adventure and learn to build a DAO on the Internet Computer.
Motoko Bootcamp - Discord community - A community for and by Motoko developers to ask for advice, showcase projects and participate in collaborative events.
Weekly developer office hours to ask questions, get clarification, and chat with other developers live via voice chat. This is hosted on our developer Discord group.
Submit your feedback to the ICP Developer feedback board.
Next steps
Next, you'll dive into ICRC-1 tokens and how to deploy your own token using the ICRC-1 standard.