Skip to main content

View and Transfer a CKB Balance

Tutorial Overview

⏰ Estimated Time: 2 - 5 min
🔧 Tools You Need:

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:

offckb node

You might want to check pre-funded accounts and copy private keys for later use. Open another terminal and execute:

offckb accounts

Step 3: Run the Example

Navigate to your project, install the node dependencies, and start running the example:

yarn && NETWORK=devnet yarn start

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;
}
tip

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