Skip to content

Commit

Permalink
docs: added a tutorial to mint and transfer xudt from scratch (#704)
Browse files Browse the repository at this point in the history
  • Loading branch information
homura authored Jun 5, 2024
1 parent 6aa1d4d commit 4110f0d
Show file tree
Hide file tree
Showing 2 changed files with 360 additions and 1 deletion.
359 changes: 359 additions & 0 deletions website/docs/00-tutorial-issue-and-transfer-xudt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
---
title: "Tutorial: Mint and Transfer xUDT From Scratch"
sidebar_position: 1
---

# Tutorial: Mint and Transfer xUDT From Scratch

Extensible User Defined Token ([xUDT](https://blog.cryptape.com/enhance-sudts-programmability-with-xudt)) is a programmable token standard on the CKB blockchain. This tutorial guides you through creating and transferring xUDT tokens from scratch.

## Pre-requirement

- Basic TypeScript syntax knowledge, including defining variables with type declaration
- Basic CKB knowledge, including Cell and RPC

### Initialize Project

The code in this tutorial is written in TypeScript,
so it is recommended to use a CLI tool that supports running TypeScript directly without transpiling, such as [Bun](https://bun.sh/).

### Create Project Directory and Initialize

```sh
mkdir xudt-from-scratch
cd xudt-from-scratch
bun init -y
# try running the ts file created by default
bun index.ts
# based on the @ckb-lumos/[email protected]
bun install @ckb-lumos/lumos@canary
```

### Generate a Private Key

We'll need a private key to act as the owner of the minted xUDT. Run the following command to generate a random private key:

```shell
openssl rand -hex 32
```

Save the displayed key. You'll need it later.

### Setup `index.ts`

Let's set up the demo for configuration and importing library used later

```ts
import type { Cell, Script, CellDep } from "@ckb-lumos/lumos"
import { config, hd, Indexer, RPC } from "@ckb-lumos/lumos"
import { bytes, BytesLike, Uint128 } from "@ckb-lumos/lumos/codec"
import { common } from "@ckb-lumos/lumos/common-scripts"
import { ScriptConfig } from "@ckb-lumos/lumos/config"
import { addCellDep, cellHelper, encodeToAddress, sealTransaction, TransactionSkeleton } from "@ckb-lumos/lumos/helpers"
import { computeScriptHash } from "@ckb-lumos/lumos/utils"

// to work with the testnet
config.initializeConfig(config.TESTNET)
// indexer for cell provider
const indexer = new Indexer("https://testnet.ckb.dev")
// rpc to interact with the CKB node
const rpc = new RPC("https://testnet.ckb.dev")

// paste the generated key for the owner
const ownerPrivateKey = "<0x paste the key here>"

// script config that will be used later
const { XUDT, SECP256K1_BLAKE160 } = config.TESTNET.SCRIPTS

const ownerLockScript = createScript(SECP256K1_BLAKE160, hd.key.privateKeyToBlake160(ownerPrivateKey))
const ownerAddress = encodeToAddress(ownerLockScript)

// a helper to create a Script from a ScriptConfig
function createScript(config: ScriptConfig, args: BytesLike): Script {
return { codeHash: config.CODE_HASH, hashType: config.HASH_TYPE, args: bytes.hexify(args) }
}

// a helper to crete a CellDep from a ScriptConfig
function createCellDep(config: ScriptConfig): CellDep {
return { depType: config.DEP_TYPE, outPoint: { txHash: config.TX_HASH, index: config.INDEX } }
}
```

## Mint

To mint xUDT, a lock script is required to be treated an owner, and the xUDT can be minted when the owner script in the `inputs` (or in the `witness.outputType`).

The mint transaction is like the following

```yaml
inputs:
- owner:
lock: unlimited
outputs:
- minted_cell:
lock: receiver_lock
type:
code_hash: xudt_code_hash
type: xudt_type
args: owner_lock_code_hash(32 bytes)
data: xudt_amount_u128
- owner_change_cell
```
Let's start coding to build the above mint transaction
```ts
async function mint() {
console.log("Please Claim some testnet CKB first from https://faucet.nervos.org")
console.log("Your owner address:", ownerAddress)

// 1. Create the xUDT Type Script
// This script defines the structure of the xUDT token.
const xudtTypeScript = createScript(XUDT, computeScriptHash(ownerLockScript))

// 2. Define Cell Provider (Optional)
// This helps filter out unnecessary cells during transaction building.
const cellProvider: TransactionSkeletonType["cellProvider"] = {
collector: (query) => indexer.collector({ type: "empty", data: "0x", ...query }),
}

// 3. Create Transaction Skeleton
// This is the base structure for our transaction.
let txSkeleton = TransactionSkeleton({ cellProvider })

// 4. Create Minted Cell with Amount
// This defines the cell that will hold the minted xUDT tokens.
const mintCell = cellHelper.create({
lock: ownerLockScript, // The owner (you) controls this cell.
type: xudtTypeScript, // This cell holds xUDT tokens.
data: Uint128.pack(10000), // Set the initial amount of xUDT to mint (10000).
})

// 5. Add xUDT Script Dependency
txSkeleton = addCellDep(txSkeleton, createCellDep(XUDT))

// 6. Inject Capacity for Minted Cell
txSkeleton = await common.injectCapacity(txSkeleton, [ownerAddress], mintCell.cellOutput.capacity)

// 7. Add Minted Cell to Outputs
// Specify the minted cell as part of the transaction outputs.
txSkeleton = txSkeleton.update("outputs", (outputs) => outputs.push(mintCell))

// 8. Pay Transaction Fee
// Allocate CKB for transaction fees.
// see also https://github.com/nervosnetwork/ckb/blob/31e02872b3a55ca7558073cb781971d8bc8f29b2/util/app-config/src/legacy/tx_pool.rs#L8-L9
txSkeleton = await common.payFeeByFeeRate(txSkeleton, [ownerAddress], 1000)

// 9. Prepare Signing Entries and Sign
// Prepare transaction data for signing and sign it with your private key.
txSkeleton = common.prepareSigningEntries(txSkeleton)
const signatures = txSkeleton
.get("signingEntries")
.map(({ message }) => hd.key.signRecoverable(message, ownerPrivateKey))
.toArray()

// 10. Broadcast Transaction
// Send the signed transaction to the CKB node.
const signedTransaction = sealTransaction(txSkeleton, signatures)
const txHash = await rpc.sendTransaction(signedTransaction)
console.log(`https://pudge.explorer.nervos.org/transaction/${txHash}`)
}
```

After broadcasting successfully, the transaction can be found in the explorer,
and here is a [mint example](https://pudge.explorer.nervos.org/transaction/0x4467a0e8179b2044b2cf1aaa6f0b4d375f38b625519c174bee9cc73b28cce6e0) on the CKB testnet explorer

## Transfer

Let's transfer the minted xUDT cell to Alice. The transaction is also pretty straightforward

```yaml
inputs:
- minted_cell:
lock: owner_lock
type: xudt
data: amount
- fee_cell:
lock: unlimited
outputs:
- minted_cell:
lock: alice
type: xudt
data: amount
- fee_cell_change:
lock: unlimited
```
```ts
async function transfer() {
const xudtTypeScript = createScript(XUDT, computeScriptHash(ownerLockScript))

const cellProvider: TransactionSkeletonType["cellProvider"] = {
collector: (query) => indexer.collector({ type: "empty", data: "0x", ...query }),
}

const alicePrivateKey = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
const aliceLock = createScript(SECP256K1_BLAKE160, hd.key.privateKeyToBlake160(alicePrivateKey))

// 1. Collect Minted xUDT Cell
// Find the xUDT cell owned by you (based on owner lock script).
const xudtCollector = indexer.collector({ type: xudtTypeScript, lock: ownerLockScript })

let transferCell: Cell | undefined

for await (const cell of xudtCollector.collect()) {
transferCell = cell
// Collect only one (assuming you have only one minted xUDT cell).
break
}

if (!transferCell) {
throw new Error("Owner do not have an xUDT cell yet, please call mint first")
}

const transferAmount = Uint128.unpack(transferCell.data)
console.log("Transfer to Alice", transferAmount.toNumber(), "xUDT")

// 2. Create Transaction Skeleton
let txSkeleton = TransactionSkeleton({ cellProvider })

// 3. Add xUDT Script Dependency
txSkeleton = addCellDep(txSkeleton, createCellDep(XUDT))

// 4. Set Up Input Cell (Transfer Cell)
// Include the minted xUDT cell as both input and output (for transfer).
txSkeleton = await common.setupInputCell(txSkeleton, transferCell)

// 5. Update Output Cell Lock to Alice's Lock
// Change the ownership of the minted xUDT cell to Alice's lock.
txSkeleton = txSkeleton.update("outputs", (outputs) =>
outputs.update(0, (cell) => ({ ...cell!, cellOutput: { ...cell!.cellOutput, lock: aliceLock } }))
)

// the following process is the same with mint to broadcast the transaction
txSkeleton = await common.payFeeByFeeRate(txSkeleton, [ownerAddress], 1000)
txSkeleton = common.prepareSigningEntries(txSkeleton)

const signatures = txSkeleton
.get("signingEntries")
.map(({ message }) => hd.key.signRecoverable(message, ownerPrivateKey))
.toArray()

const signed = sealTransaction(txSkeleton, signatures)
const txHash = await rpc.sendTransaction(signed)
console.log(txHash)
}
```

Here is the [transfer example](https://pudge.explorer.nervos.org/transaction/0xc00d5c18cecf7b436ebc8735961d8c9b383f56ceb03d6b0dcc5e43e4eebd4341) in the explorer

## Mint with Extension Script

xUDT introduces a new way to mint token.
The new way allows the owner script as an extension script in the witness instead of in the `inputs`.
This is useful to avoid cell congestion, so users are able to mint tokens in parallel.
The mint transaction structure looks like this:

```yaml
inputs:
- capacity_provider_cell:
lock: unlimited
outputs:
- capacity_change_cell:
lock: unlimited
- minted_cell:
lock: receiver_lock
type:
code_hash: xudt_code_hash
type: xudt_type
args: owner_lock_code_hash(32 bytes)
data: xudt_amount_u128
witnesses:
- witness_args:
output_type: owner_script
```
```ts
async function mintViaExtensionScript() {
// An always-success extension script.
// You can compile it yourself from https://github.com/nervosnetwork/ckb-production-scripts/blob/410b16c499a8888781d9ab03160eeef93182d8e6/tests/xudt_rce/extension_script_0.c
// we use the script to demonstrate how to use Lumos to mint xUDT with the extension script
const ALWAYS_SUCCESS_EXTENSION: ScriptConfig = {
CODE_HASH: "0xea8ee0b1e932802224f6462f57b34110907357ac35d0952383d550820e1205d1",
HASH_TYPE: "type",
TX_HASH: "0x5681d80387af77a096aa1386dbe3ba7b44a3302e62cc0f832ea51869bc5a614c",
INDEX: "0x0",
DEP_TYPE: "code",
}

// Reuse the owner defined above
const receiverLock = ownerLockScript
const receiverAddress = encodeToAddress(receiverLock)

// 🌟 1. Define the schema for extension script
// https://github.com/nervosnetwork/ckb-production-scripts/blob/410b16c499a8888781d9ab03160eeef93182d8e6/c/xudt_rce.mol#L3-L11
const ScriptVec = vector(blockchain.Script)
const ScriptVecOpt = option(ScriptVec)
const XudtWitnessInput = table(
{
ownerScript: blockchain.ScriptOpt,
ownerSignature: blockchain.BytesOpt,
rawExtensionData: ScriptVecOpt,
extensionData: blockchain.BytesVec,
},
["ownerScript", "ownerSignature", "rawExtensionData", "extensionData"]
)

// 🌟2. Create the Owner Lock Script
// The owner script will be set into witness instead of inputs
const extensionOwnerScript = createScript(ALWAYS_SUCCESS_EXTENSION, "0x")

// 🌟3. Create the xUDT Type Script
const xudtTypeScript = createScript(XUDT, computeScriptHash(extensionOwnerScript))

// 4. Create Minted Cell with Amount
const mintCell: Cell = cellHelper.create({
lock: receiverLock,
type: xudtTypeScript,
data: Uint128.pack(10000),
})

// 5. Create Transaction Skeleton
const cellProvider: TransactionSkeletonType["cellProvider"] = {
collector: (query) => indexer.collector({ type: "empty", data: "0x", ...query }),
}
let txSkeleton = TransactionSkeleton({ cellProvider })

// 6. Add xUDT Script and Owner Extension Script Dependency
txSkeleton = addCellDep(txSkeleton, createCellDep(XUDT))
txSkeleton = addCellDep(txSkeleton, createCellDep(ALWAYS_SUCCESS_EXTENSION))

// 7. Inject Capacity for Minted Cell
txSkeleton = await common.injectCapacity(txSkeleton, [receiverAddress], mintCell.cellOutput.capacity)

// 8. Add Minted Cell to Outputs
txSkeleton = txSkeleton.update("outputs", (outputs) => outputs.push(mintCell))

// 🌟9. Set the Extension Owner Script into witnesses
txSkeleton = txSkeleton.update("witnesses", (witnesses) => {
const witnessOutputType = XudtWitnessInput.pack({ ownerScript: extensionOwnerScript, extensionData: [] })
const mintCellIndex = txSkeleton.get("outputs").size - 1
return witnesses.set(mintCellIndex, hexify(WitnessArgs.pack({ outputType: witnessOutputType })))
})

// the following process is the same with mint to broadcast the transaction
txSkeleton = await common.payFeeByFeeRate(txSkeleton, [ownerAddress], 1000)
txSkeleton = common.prepareSigningEntries(txSkeleton)

const signatures = txSkeleton
.get("signingEntries")
.map(({ message }) => hd.key.signRecoverable(message, ownerPrivateKey))
.toArray()

const signed = sealTransaction(txSkeleton, signatures)
const txHash = await rpc.sendTransaction(signed)
console.log(txHash)
}
```

You can check the [mint via extension script example](https://pudge.explorer.nervos.org/transaction/0x2118ad6e7d1e56a2f295f1d0f9e8718c457b9e76282e651ce7abb40fef249b2d) in the explorer.
2 changes: 1 addition & 1 deletion website/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const config = {
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
additionalLanguages: ["diff"],
additionalLanguages: ["diff", "yaml"],
},
}),

Expand Down

1 comment on commit 4110f0d

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 New canary release: 0.0.0-canary-4110f0d-20240605105355

npm install @ckb-lumos/[email protected]

Please sign in to comment.