UDT
Learn how to issue, transfer, and manage xUDT tokens using the RGB++ SDK through practical examples.
Prerequisites
npm i ckb-ccc@0.0.0-canary-20250710073207
https://api.testnet.rgbpp.io
https://api.signet.rgbpp.io
https://api.rgbpp.io
UDT Token Operations
In CKB, custom tokens are implemented as User-Defined Tokens (UDTs). The CKB core team has established a minimal standard for UDTs called xUDT (extensible UDT). In this section, we demonstrate the RGB++ protocol by issuing a RGB++ token using the pre-deployed xUDT Script.
For a comprehensive guide on issuing fungible tokens on CKB, please refer to the tutorial: Create a Fungible Token. The following discussion will focus on the RGB++-specific aspects of the token issuance process.
The complete implementation is available in the RGB++ SDK repository.
1. Issuance
The process begins by selecting a UTXO (or automatically creating one with the dust limit value of 546 satoshis if not provided) to serve as the initial single-use seal.
Subsequently, we create a CKB cell with its lock script set to the RGB++ lock script, using the UTXO as its argument. This configuration represents the user's intent to issue a RGB++ xUDT token, which will only be fulfilled after the initial UTXO is spent.
Next, a partial CKB transaction is constructed using the CKB cell and xUDT script information. Based on this, the commitment is calculated and the BTC transaction is assembled, which is then submitted to the network.
The BTC transaction's confirmation status is periodically checked through the SPV service. Upon confirmation, we acquire the new single-use seal - a UTXO also with the dust limit value of 546 satoshis - which represents ownership of the issued RGB++ xUDT token. The transaction ID of this UTXO is used to replace the placeholder value in the RGB++ lock script, enabling the assembly of the final CKB transaction.
The final CKB transaction is then submitted to the network, completing the token issuance process.
The following is the code for the issuance process.
Code Example: Token Issuance
async function issueUdt({
udtScriptInfo,
utxoSeal,
}: {
udtScriptInfo: ScriptInfo;
utxoSeal?: UtxoSeal;
}) {
// Initialize the RGB++ environment
const {
rgbppBtcWallet,
rgbppUdtClient,
utxoBasedAccountAddress,
ckbRgbppUnlockSinger,
} = initializeRgbppEnv();
// Prepare the initial single-use seal and corresponding RGB++ cells
if (!utxoSeal) {
utxoSeal = await rgbppBtcWallet.prepareUtxoSeal();
}
const rgbppIssuanceCells = await prepareRgbppCells(utxoSeal, rgbppUdtClient);
// Construct the partial CKB transaction
const ckbPartialTx = await rgbppUdtClient.issuanceCkbPartialTx({
token: udtToken,
amount: issuanceAmount,
rgbppLiveCells: rgbppIssuanceCells,
udtScriptInfo,
});
// Build and submit the BTC transaction
const { psbt, indexedCkbPartialTx } = await rgbppBtcWallet.buildPsbt({
ckbPartialTx,
ckbClient,
rgbppUdtClient,
btcChangeAddress: utxoBasedAccountAddress,
receiverBtcAddresses: [utxoBasedAccountAddress],
});
const btcTxId = await rgbppBtcWallet.signAndSendTx(psbt);
const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx(
indexedCkbPartialTx,
btcTxId,
);
// Polling the SPV service to wait for the BTC transaction to be confirmed to construct the witness
const rgbppSignedCkbTx = await ckbRgbppUnlockSinger.signTransaction(ckbPartialTxInjected);
// Build and submit the final CKB transaction
await rgbppSignedCkbTx.completeFeeBy(ckbSigner);
const ckbFinalTx = await ckbSigner.signTransaction(rgbppSignedCkbTx);
const txHash = await ckbSigner.client.sendTransaction(ckbFinalTx);
await ckbRgbppUnlockSinger.client.waitTransaction(txHash);
}
issueUdt({
udtScriptInfo: {
name: ccc.KnownScript.XUdt,
script: await ccc.Script.fromKnownScript(
ckbClient,
ccc.KnownScript.XUdt,
"",
),
cellDep: (await ckbClient.getKnownScript(ccc.KnownScript.XUdt)).cellDeps[0]
.cellDep,
},
});
2. Transfer on BTC
The process of transferring RGB++ xUDT tokens on BTC follows a similar pattern to the issuance process. Key points to note:
- The unique ID of issued xUDT token, obtained during issuance, is used to construct the xUDT script that identifies the token.
- The partial CKB transaction assembly is simplified through ccc: by providing the xUDT script, ccc automatically handles the input construction.
The following is the code for the transfer process.
Code Example: Token Transfer on BTC
async function transferUdt({
udtScriptInfo,
receivers,
}: {
udtScriptInfo: ScriptInfo;
receivers: RgbppBtcReceiver[];
}) {
// ...
const udt = new ccc.udt.Udt(
udtScriptInfo.cellDep.outPoint,
udtScriptInfo.script,
);
// Complete the outputs
let { res: tx } = await udt.transfer(
ckbSigner as unknown as ccc.Signer,
receivers.map((receiver) => ({
to: rgbppUdtClient.buildPseudoRgbppLockScript(),
amount: ccc.fixedPointFrom(receiver.amount),
})),
);
// Auto complete the xUDT inputs
const txWithInputs = await udt.completeChangeToLock(
tx,
ckbRgbppUnlockSinger,
rgbppUdtClient.buildPseudoRgbppLockScript(),
);
// the rest is the same as the issuance process...
}
transferUdt({
udtScriptInfo: {
name: ccc.KnownScript.XUdt,
script: await ccc.Script.fromKnownScript(
ckbClient,
ccc.KnownScript.XUdt,
"<unique id of issued xUDT token>",
),
cellDep: (await ckbClient.getKnownScript(ccc.KnownScript.XUdt)).cellDeps[0]
.cellDep,
},
receivers: [
{
amount: "<amount of xUDT token to transfer>",
to: "<receiver's BTC address>",
},
],
});
3. Leap to CKB
The process of leaping xUDT from BTC to CKB follows the same pattern as regular transfer, with one key distinction: after the leap, the lock script changes from RGBPP_Lock
to BTC_TIME_lock
.
The following is the code for the leap process.
Code Example: Leap to CKB
async function btcUdtToCkb({
udtScriptInfo,
receivers,
}: {
udtScriptInfo: ScriptInfo;
receivers: { address: string; amount: bigint }[];
}) {
// ...
const udt = new ccc.udt.Udt(
udtScriptInfo.cellDep.outPoint,
udtScriptInfo.script,
);
let { res: tx } = await udt.transfer(
ckbSigner as unknown as ccc.Signer,
await Promise.all(
receivers.map(async (receiver) => ({
// build the BTC_TIME_lock script
to: await rgbppUdtClient.buildBtcTimeLockScript(receiver.address),
amount: ccc.fixedPointFrom(receiver.amount),
})),
),
);
// the rest is the same as the transfer process...
}
btcUdtToCkb({
udtScriptInfo: {
name: ccc.KnownScript.XUdt,
script: await ccc.Script.fromKnownScript(
ckbClient,
ccc.KnownScript.XUdt,
"<unique id of issued xUDT token>",
),
cellDep: (await ckbClient.getKnownScript(ccc.KnownScript.XUdt)).cellDeps[0]
.cellDep,
},
receivers: [
{
address: "<receiver's CKB address>",
amount: "<amount of xUDT token to leap>",
},
],
});
4. Unlocking BTC_TIME_lock
This process is relatively straightforward. We wait for the required number of confirmations (default is 6) before unlocking the BTC_TIME_lock
. After unlocking, the xUDT becomes a standard CKB asset, with its ownership logic governed by the lock script specified in the BTC_TIME_lock
arguments. Notably, this process does not require any Bitcoin transaction.
The following is the code for the unlock process.
Code Example: Unlock BTC_TIME_lock
async function unlockBtcTimeLock(btcTimeLockArgs: string) {
// ...
const tx = ccc.Transaction.default();
const btcTimeLockCells = await collectBtcTimeLockCells(
btcTimeLockArgs,
rgbppUdtClient,
);
// Complete the inputs and outputs
btcTimeLockCells.forEach((cell) => {
const cellInput = ccc.CellInput.from({
previousOutput: cell.outPoint,
});
cellInput.completeExtraInfos(ckbClient);
tx.inputs.push(cellInput);
tx.addOutput(
{
lock: parseBtcTimeLockArgs(cell.cellOutput.lock.args).lock,
type: cell.cellOutput.type,
capacity: cell.cellOutput.capacity,
},
cell.outputData,
);
});
// Monitor the SPV service until the Bitcoin transaction achieves the required confirmation threshold for witness construction
for await (const btcTimeLockCell of btcTimeLockCells) {
const { btcTxId, confirmations } = parseBtcTimeLockArgs(
btcTimeLockCell.cellOutput.lock.args,
);
const spvProof = await pollForSpvProof(
rgbppBtcWallet,
btcTxId,
confirmations,
);
tx.cellDeps.push(
ccc.CellDep.from({
outPoint: spvProof.spvClientOutpoint,
depType: "code",
}),
);
tx.witnesses.push(buildBtcTimeUnlockWitness(spvProof.proof));
}
// Build and submit the final CKB transaction
await tx.completeFeeBy(ckbSigner);
const signedTx = await ckbSigner.signTransaction(tx);
const txHash = await ckbSigner.client.sendTransaction(signedTx);
await ckbSigner.client.waitTransaction(txHash);
}
unlockBtcTimeLock(
"<btc time lock args>",
)