Skip to main content

Create a Fungible Token

⏰ Estimated Time: 5 - 10 min
🔧 What You Will Need:
For detailed installation steps, refer to our Installation Guide

Tutorial Overview

Unlike ERC20(Ethereum) and BRC20(Bitcoin), CKB uses a unique way to build custom tokens based on its UTXO-like Cell Model

.

In CKB, custom tokens are called User-Defined Tokens (UDTs). CKB's core team has proposed a minimal standard for UDT called xUDT(extensible UDT). In this tutorial, you will learn how to issue custom tokens using the pre-deployed xUDT Script.

Steps to Issue a Custom Token with xUDT:

  1. Create a Special Cell: When you issue tokens, you create a special Cell representing a balance of your custom token, similar to how physical cash represents a balance of currency.
  2. Configure the Cell's Data: This Cell’s data field will store the token amount, while its Type Script will be the xUDT Script. The script’s args field will contain the Lock Script Hash of the issuer.
  3. Establish a Unique Token ID: The issuer’s Lock Script hash serves as the unique identifier for each custom token. Different Lock Script hashes represent different tokens, enabling secure and distinct transactions for each token type.

While xUDT includes more advanced features, this tutorial focuses on its core concept. For more details on xUDT’s capabilities, you can explore the full xUDT spec.

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/xudt

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

Issuing Custom Token

Open the lib.ts file in your project and check out the IssueToken function:

export async function issueToken(privKey: string, amount: string) {
const signer = new ccc.SignerCkbPrivateKey(cccClient, privKey);
const lockScript = (await signer.getAddressObjSecp256k1()).script;
const xudtArgs = lockScript.hash() + "00000000";

const typeScript = await ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.XUdt,
xudtArgs
);
...
}

This function accepts two parameters:

  • privKey: The private key of the issuer
  • amount: The amount of token

Note that we aim to create an output Cell whose Type Script

is an xUDT Script. The args of this xUDT Script are the issuer's Lock Script Hash, which is why we include the following lines of code:

const lockScript = (await signer.getAddressObjSecp256k1()).script;
const xudtArgs = lockScript.hash() + "00000000";

Also, note that the 00000000 here is just a placeholder. To unlock more capabilities of the xUDT Script, this placeholder can contain specific data. However, we don't need to concern ourselves with this detail at the moment.

Further down in the function, you'll see that the complete target output Cell of our custom token appears as follows:

const tx = ccc.Transaction.from({
outputs: [{ lock: lockScript, type: typeScript }],
outputsData: [ccc.numLeToBytes(amount, 16)],
});

Note that the outputsData field is the amount of the custom token.

Next, to complete our issueToken function, we just use the helpers.TransactionSkeleton to build the transaction with our desired output Cells.

  await tx.addCellDepsOfKnownScripts(signer.client, ccc.KnownScript.XUdt);
// additional 0.001 ckb for tx fee
// Complete missing parts for transaction
await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer, 1000);

...

Lastly, we do the signing and sending transaction:

const txHash = await signer.sendTransaction(tx);
console.log("The transaction hash is", hash);

Token Info & Holders

Since we have issued a custom token, the next step will be checking out this token and viewing its holders. To do that, we write a queryIssuedTokenCells in the lib.ts file:

export async function queryIssuedTokenCells(xudtArgs: HexString) {
const typeScript = await ccc.Script.fromKnownScript(
cccClient,
ccc.KnownScript.XUdt,
xudtArgs
);

const collected: ccc.Cell[] = [];
const collector = cccClient.findCellsByType(typeScript, true);
for await (const cell of collector) {
collected.push(cell);
}
return collected;
}

Note that to query a custom token Cell, we must know its xUDTArgs. As explained in the high-level ideas for xUDT Scripts, this xUDTArgs functions like the unique ID for the token you issued.

Thus, queryIssuedTokenCells will accept only one parameter: xudtArgs. We then construct a Type Script with this xudtArgs and use cccClient.findCellsByType(typeScript, true); to query the Live Cells that possess such a Type Script.

By identifying the Lock Scripts of these Live Cells, we can determine that those custom tokens now belong to the individual who can unlock this Lock Script. Consequently, we know who the token holders are.

Transfer Custom Token

The next step you want to do is probably sending your tokens to someone else. To do that, you will replace the Lock Script of the custom token Cell with the receiver's Lock Script. Therefore, the receiver can unlock the custom token Cell. In this way, the token is transferred from you to other people.

Check out the transferTokenToAddress function in lib.ts file.

export async function transferTokenToAddress(
udtIssuerArgs: string,
senderPrivKey: string,
amount: string,
receiverAddress: string,
){
...
}

The function use udtIssuerArgs to build the Type Script from the custom token. It then collects Live Cells which match the Type Script and the Lock Script of the senderLockScript, effectively saying, "give me the custom token Cells that belong to the sender (the sender can unlock the Lock Script).".

With all these Live Cells, we can build the transaction to produce custom token Cells with the required amount and the receiver's Lock Scripts from the input Cells.

const signer = new ccc.SignerCkbPrivateKey(cccClient, senderPrivKey);
const senderLockScript = (await signer.getAddressObjSecp256k1()).script;
const receiverLockScript = (
await ccc.Address.fromString(receiverAddress, cccClient)
).script;

const xudtArgs = udtIssuerArgs;
const xUdtType = await ccc.Script.fromKnownScript(
cccClient,
ccc.KnownScript.XUdt,
xudtArgs
);

const tx = ccc.Transaction.from({
outputs: [{ lock: receiverLockScript, type: xUdtType }],
outputsData: [ccc.numLeToBytes(amount, 16)],
});
await tx.completeInputsByUdt(signer, xUdtType);

Notice that If there is any token amount remaining, we need to return the change amount along with change capacities to the sender.

const balanceDiff =
(await tx.getInputsUdtBalance(signer.client, xUdtType)) -
tx.getOutputsUdtBalance(xUdtType);
console.log("balanceDiff: ", balanceDiff);
if (balanceDiff > ccc.Zero) {
tx.addOutput(
{
lock: senderLockScript,
type: xUdtType,
},
ccc.numLeToBytes(balanceDiff, 16)
);
}
await tx.addCellDepsOfKnownScripts(signer.client, ccc.KnownScript.XUdt);

// Complete missing parts for transaction
await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer, 1000);

const txHash = await signer.sendTransaction(tx);

Congratulations!

By following this tutorial this far, you have mastered how custom tokens work on CKB. Here's a quick recap:

  • Create a CKB transaction containing a xUDT Cell in the outputs
  • The data of the xUDT Cell contains the amount number of the token
  • Query the custom token Cell by passing the Lock Script Hash of the token issuer
  • Transfer tokens to another account by replacing the Lock Script.

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