View and Transfer a CKB Balance
- CKB dev environment: OffCKB (≥v0.3.0)
- JavaScript SDK: CCC (≥v0.1.0-alpha.4)
Tutorial Overview
CKB is based on a UTXO-like Cell Model
Transferring 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 capacity of the consumed Cells.
In this tutorial, you 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 = async (
privKey: string
): Promise<Account> => {
const signer = new ccc.SignerCkbPrivateKey(cccClient, privKey);
const lock = await signer.getAddressObjSecp256k1();
return {
lockScript: lock.script,
address: lock.toString(),
pubKey: signer.publicKey,
};
};
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
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<bigint> {
const addr = await ccc.Address.fromString(address, cccClient);
let balance = await cccClient.getBalance([addr.script]);
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 CCC SDK, the value handle are mostly done in the Shannon unit.
Next, we can start to transfer balance. Check out the transfer function in lib.ts
:
export async function transfer(
toAddress: string,
amountInCKB: string,
signerPrivateKey: string
): Promise<string>;
The transfer
function accepts parameters such as 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 CCC SDK, we can use high-level helper function ccc.Transaction.from
to perform the transfer transaction, which wraps the above logic.
export async function transfer(
toAddress: string,
amountInCKB: string,
signerPrivateKey: string
): Promise<string> {
const signer = new ccc.SignerCkbPrivateKey(cccClient, signerPrivateKey);
const { script: toLock } = await ccc.Address.fromString(toAddress, cccClient);
// Build the full transaction to estimate the fee
const tx = ccc.Transaction.from({
outputs: [{ lock: toLock }],
outputsData: [],
});
// CCC transactions are easy to be edited
tx.outputs.forEach((output, i) => {
if (output.capacity > ccc.fixedPointFrom(amountInCKB)) {
alert(`Insufficient capacity at output ${i} to store data`);
return;
}
output.capacity = ccc.fixedPointFrom(amountInCKB);
});
// ....
}
Next, we need to complete the inputs of the transaction.
//....
// Complete missing parts for transaction
await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer, 1000);
Now we can use signer to sign and send the CKB transaction
// ...
const txHash = await signer.sendTransaction(tx);
console.log(
`Go to explorer to check the sent transaction https://pudge.explorer.nervos.org/transaction/${txHash}`
);
return txHash;
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
ccc.Transaction.from
from the CCC 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