View and Transfer a CKB Balance
Tutorial Overview
- An IDE/Editor that supports TypeScript
- CKB dev environment: OffCKB
CKB is based on a UTXO-like Cell Model. Every Cell has a capacity limit, which represents both the CKB balance and how much data can be stored in the Cell simultaneously.
Transfering balance in CKB involves consuming some input Cells from the sender's account and producing new output Cells which can be unlocked by the receiver's account. The amount transferred is equal to the total capacities of the coverting Cells.
In this tutorial, we will learn how to write a simple dApp to transfer CKB balance from one account to another.
Setup Devnet & Run Example
Step 1: Clone the Repository
To get started with the tutorial dApp, clone the repository and navigate to the appropriate directory using the following commands:
git clone https://github.com/nervosnetwork/docs.nervos.org.git
cd docs.nervos.org/examples/simple-transfer
Step 2: Start the Devnet
To interact with the dApp, ensure that your Devnet is up and running. After installing @offckb/cli, open a terminal and start the Devnet with the following command:
- Command
- Response
offckb node
/bin/sh: /Users/nervosDocs/.nvm/versions/node/v18.12.1/lib/node_modules/@offckb/cli/target/ckb/ckb: No such file or directory
/Users/nervosDocs/.nvm/versions/node/v18.12.1/lib/node_modules/@offckb/cli/target/ckb/ckb not found, download and install the new version 0.113.1..
CKB installed successfully.
init Devnet config folder: /Users/nervosDocs/.nvm/versions/node/v18.12.1/lib/node_modules/@offckb/cli/target/devnet
modified /Users/nervosDocs/.nvm/versions/node/v18.12.1/lib/node_modules/@offckb/cli/target/devnet/ckb-miner.toml
CKB output: 2024-03-20 07:56:44.765 +00:00 main INFO sentry sentry is disabled
CKB output: 2024-03-20 07:56:44.766 +00:00 main INFO ckb_bin::helper raise_fd_limit newly-increased limit: 61440
CKB output: 2024-03-20 07:56:44.854 +00:00 main INFO ckb_bin::subcommand::run ckb version: 0.113.1 (95ad24b 2024-01-31)
CKB output: 2024-03-20 07:56:45.320 +00:00 main INFO ckb_db_migration Init database version 20230206163640
CKB output: 2024-03-20 07:56:45.329 +00:00 main INFO ckb_launcher Touch chain spec hash: Byte32(0x3036c73473a371f3aa61c588c38924a93fb8513e481fa7c8d884fc4cf5fd368a)
You might want to check pre-funded accounts and copy private keys for later use. Open another terminal and execute:
- Command
- Response
offckb accounts
Print account list, each account is funded with 42_000_000_00000000 capacity in the genesis block.
[
{
privkey: '0x6109170b275a09ad54877b82f7d9930f88cab5717d484fb4741ae9d1dd078cd6',
pubkey: '0x02025fa7b61b2365aa459807b84df065f1949d58c0ae590ff22dd2595157bffefa',
lockScript: {
codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8',
hashType: 'type',
args: '0x8e42b1999f265a0078503c4acec4d5e134534297'
},
address: 'ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqvwg2cen8extgq8s5puft8vf40px3f599cytcyd8',
args: '0x8e42b1999f265a0078503c4acec4d5e134534297'
},
{
privkey: '0x9f315d5a9618a39fdc487c7a67a8581d40b045bd7a42d83648ca80ef3b2cb4a1',
pubkey: '0x026efa0579f09cc7c1129b78544f70098c90b2ab155c10746316f945829c034a2d',
lockScript: {
codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8',
hashType: 'type',
args: '0x758d311c8483e0602dfad7b69d9053e3f917457d'
},
address: 'ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqt435c3epyrupszm7khk6weq5lrlyt52lg48ucew',
args: '0x758d311c8483e0602dfad7b69d9053e3f917457d'
},
#...
]
Step 3: Run the Example
Navigate to your project, install the node dependencies, and start running the example:
- Command
- Response
yarn && NETWORK=devnet yarn start
$ parcel index.html
Server running at http://localhost:1234
✨ Built in 66ms
Now, the app is running in http://localhost:1234
Behind the Scene
Open the lib.ts
file in your project and check out the generateAccountFromPrivateKey
function:
export const generateAccountFromPrivateKey = (privKey: string): Account => {
const pubKey = hd.key.privateToPublic(privKey);
const args = hd.key.publicKeyToBlake160(pubKey);
const template = lumosConfig.SCRIPTS["SECP256K1_BLAKE160"]!;
const lockScript = {
codeHash: template.CODE_HASH,
hashType: template.HASH_TYPE,
args: args,
};
const address = helpers.encodeToAddress(lockScript, { config: lumosConfig });
return {
lockScript,
address,
pubKey,
};
};
What this function does is generate the account's public key and address via a private key. Here, we need to construct and encode a Lock Script to obtain the corresponding address of this account. A Lock Script ensures that only the owner can consume their Live Cells.
Here, we use the CKB standard Lock Script template, combining the SECP256K1 signing algorithm with the BLAKE160 hashing algorithm, to build such a Lock Script. Note that different templates will yield different addresses when encoding the address, corresponding to different types of guard for the assets.
Once we have the Lock Script of an account, we can determine how much balance the account has. The calculation is straightforward: we query and find all the Cells that use the same Lock Script and sum all these Cells' capacities; the sum is the balance.
export async function capacityOf(address: string): Promise<BI> {
const collector = indexer.collector({
lock: helpers.parseAddress(address, { config: lumosConfig }),
});
let balance = BI.from(0);
for await (const cell of collector.collect()) {
balance = balance.add(cell.cellOutput.capacity);
}
return balance;
}
In Nervos CKB, Shannon is the smallest currency unit, with 1 CKB = 10^8 Shannons. This unit system is similar to Bitcoin's Satoshis, where 1 Bitcoin = 10^8 Satoshis. In this tutorial, we use only the Shannon unit.
Next, we can start to transfer balance. Check out the transfer function in lib.ts
:
export async function transfer(
fromAddress: string,
toAddress: string,
amountInShannon: string,
signerPrivateKey: string
): Promise<string>;
The transfer
function accepts parameters such as fromAddress
, toAddress
, amountInShannon
, and signerPrivateKey
to sign the transfer transaction.
This transfer transaction collects and consumes as many capacities as needed using some Live Cells as the input Cells and produce some new output Cells. The Lock Script of all these new Cells is set to the new owner's Lock Script. In this way, the CKB balance is transferred from one account to another, marking the transition of Cells from old to new.
Thanks to the Lumos SDK, we can use high-level helper function commons.common.transfer
to perform the transfer transaction, which wraps the above logic.
export async function transfer(
fromAddress: string,
toAddress: string,
amountInShannon: string,
signerPrivateKey: string
): Promise<string> {
let txSkeleton = helpers.TransactionSkeleton({ cellProvider: indexer });
txSkeleton = await commons.common.transfer(
txSkeleton,
[fromAddress],
toAddress,
amountInShannon
);
// https://github.com/nervosnetwork/ckb/blob/develop/util/app-config/src/legacy/tx_pool.rs#L9
// const DEFAULT_MIN_FEE_RATE: FeeRate = FeeRate::from_u64(1000);
txSkeleton = await commons.common.payFeeByFeeRate(
txSkeleton,
[fromAddress],
1000 /*fee_rate*/
);
//...
}
Next, we need to sign the transaction. We will generate signingEntries
for the transaction and use the private key to sign the message recoverably.
txSkeleton = commons.common.prepareSigningEntries(txSkeleton);
const signatures = txSkeleton
.get("signingEntries")
.map((entry) => hd.key.signRecoverable(entry.message, signerPrivateKey))
.toArray();
Now let's seal our transaction with the txSkeleton
and the just-generated signature for subsequent signature verification processes.
const tx = helpers.sealTransaction(txSkeleton, [Sig]);
Send the transaction
const txHash = await rpc.sendTransaction(signedTx);
You can open the console on the browser to see the full transaction to confirm the process.
Congratulations!
By following this tutorial this far, you have mastered how balance transfers work on CKB. Here's a quick recap:
- The capacity of a Cell indicates both the CKB balance and the amount of data that can be stored in the Cell simultaneously.
- Transferring CKB balance involves transferring some Cells from the sender to the receiver.
- We use
commons.common.transfer
from the Lumos SDK to build the transfer transaction.
Next Step
So now your dApp works great on the local blockchain, you might want to switch it to different environments like Testnet and Mainnet.
To do that, just change the environment variable NETWORK
to testnet
:
export NETWORK=testnet
For more details, check out the README.md.
Additional Resources
- CKB transaction structure: RFC-0022-transaction-structure
- CKB data structure basics: RFC-0019-data-structure