Skip to main content

Tutorial: Run JavaScript Code on CKB

Tutorial Overview

⏰ Estimated Time: 5 - 7 min
πŸ’‘ Topics: Script, CKB-VM, Cell Model, Transaction
πŸ”§ Tools You Need:
  • git,make,bash,sha256sum and others Unix utilities.
  • Rust and riscv64 target: rustup target add riscv64imac-unknown-none-elf
  • Clang 16+ sample installtion steps for selected platforms are:
    • Debian / Ubuntu: wget https://apt.llvm.org/llvm.sh && chmod +x llvm.sh && sudo ./llvm.sh 16 && rm llvm.sh
    • Fedora 39+: sudo dnf -y install clang
    • Archlinux: sudo pacman --noconfirm -Syu clang
    • macOS: brew install llvm@16
    • Windows(with [Scoop](scoop install llvm yasm)): scoop install llvm yasm

The High-Level Idea​

As we have learned before, you can use any programming language to write a Script (Smart contract) for CKB. But does it really work in reality? This tutorial will show a full example of using JavaScript to write Scripts and execute them in the CKB-VM.

The process is as follows: first we port a JavaScript engine as a base Script to CKB. Then, we write the business logic in JavaScript and execute this JS-powered Script within the base Script on top of CKB-VM.

It sounds like a of work. But thanks to the CKB VM team, we already have a fully runnable JavaScript engine called ckb-js-vm. It is ported from quick.js so that it is compatible with running on CKB-VM. We just need to take the ckb-js-vm and deploy it on-chain before we can run our JS Script.

Below is a step-by-step guide, and you can also clone the full code example from the Github repo.

Get ckb-js-vm Binary​

The ckb-js-vm is a binary that can be used both in the CLI and in the on-chain CKB-VM. Let's first build the binary and give it a try to see if it works as expected.

info

You will need clang 16+ to build the ckb-js-vm binary:

git clone https://github.com/nervosnetwork/ckb-js-vm
cd ckb-js-vm
git submodule update --init
make all

Now, the binary is in the build/ folder. Without writing any codes, we can use the CKB-Debugger(another CLI tool that enables off-chain Script development, as the name suggests) to run the ckb-js-vm binary for a quick test.

Install CKB-Debugger​

info

To install, you need Rust and cargo

cargo install --git https://github.com/nervosnetwork/ckb-standalone-debuggger ckb-debugger

Quick Test with CKB-Debugger​

Now let's run the ckb-js-vm with some JS test codes.

Make sure you are in the root of the ckb-vm-js project folder:

ckb-debugger --read-file tests/examples/hello.js --bin build/ckb-js-vm -- -r

With the -r option, ckb-js-vm will read a local JS file via CKB-Debugger. This function is intended for testing purposes and does not function in a production environment. However, we can see the running output, which includes a hello, world message. The run result is 0, indicating that the hello.js Script executes successfully. Also, you can see how many cycles(the overhead required to execute a Script) are needed to run the JS Script in the output as well.

Integrate ckb-js-vm​

ckb-js-vm offers different ways to be integrated into your own Scripts. In the next step, we will set up a project and writing codes to integrate ckb-js-vm with JavaScript code to gain a deeper understanding.

The first step is to create a new Script project. We use ckb-script-templates for this purpose. You will need the following dependencies:

  • git, make, sed, bash, sha256sum and others Unix utilities
  • Rust with riscv64 target installed: rustup target add riscv64imac-unknown-none-elf
  • Clang 16+
  • cargo-generate: You can install this via cargo install cargo-generate

If you encounter any problems with these dependencies, refer to the ReadMe for installation details.

Init a Script Project​

Now let's run the command to generate a new Script project called my-first-script-workspace:

alias create-ckb-scripts="cargo generate gh:cryptape/ckb-script-templates workspace"
create-ckb-scripts

Create a New Script​

Let’s create a new Script called run-js.

cd my-first-script-workspace
make generate

Our project relies on ckb-js-vm, so we need to include it in the project. Create a new folder named deps in the root of our Script workspace:

cd my-first-script-workspace
mkdir deps

Copy the ckb-js-vm binary we built before into the deps folder. When you're done, it should look like this:

--build
--contracts
--deps
--ckb-js-vm
...

Everything looks good now!

Integrate via Script​

The simplest way to run JavaScript code using ckb-js-vm is via a Script. A ckb-js-vm Script has the following structure:

code_hash: <code_hash to ckb-js-vm cell>
hash_type: <hash_type>
args: <ckb-js-vm args, 2 bytes> <code_hash to JavaScript code cell, 32 bytes> <hash_type to
javascript code cell, 1 byte> <javascript code args, variable length>
note

2 bytes ckb-js-vm args are reserved for further use

Now let's get our hands dirty to integrate ckb-js-vm in this way.

Write a simple hello.js Script​

cd my-first-script-workspace
mkdir js/build
touch js/hello.js

Fill the hello.js with the following code:

my-first-script-workspace/js/hello.js
console.log("hello, ckb-js-script!");

Compile the hello.js into binary with CKB-Debugger​

ckb-debugger --read-file js/hello.js --bin deps/ckb-js-vm -- -c | awk '/Run result: 0/{exit} {print}' | xxd -r -p > js/build/hello.bc 

Write tests for the hello.js Script​

Now let's assemble all the Scripts and run them in a single CKB transaction. We will use the built-in test module from ckb-script-templates, which allows us to test without actually running a blockchain.

my-first-script-workspace/tests/src/tests.rs
use super::*;
use ckb_testtool::{
builtin::ALWAYS_SUCCESS,
ckb_types::{bytes::Bytes, core::TransactionBuilder, packed::*, prelude::*},
context::Context,
};

const MAX_CYCLES: u64 = 10_000_000;

#[test]
fn hello_script() {
// deploy contract
let mut context = Context::default();
let loader = Loader::default();
let js_vm_bin = loader.load_binary("../../deps/ckb-js-vm");
let js_vm_out_point = context.deploy_cell(js_vm_bin);
let js_vm_cell_dep = CellDep::new_builder()
.out_point(js_vm_out_point.clone())
.build();

let js_script_bin = loader.load_binary("../../js/build/hello.bc");
let js_script_out_point = context.deploy_cell(js_script_bin.clone());
let js_script_cell_dep = CellDep::new_builder()
.out_point(js_script_out_point.clone())
.build();

// prepare scripts
let always_success_out_point = context.deploy_cell(ALWAYS_SUCCESS.clone());
let lock_script = context
.build_script(&always_success_out_point.clone(), Default::default())
.expect("script");
let lock_script_dep = CellDep::new_builder()
.out_point(always_success_out_point)
.build();

// prepare cell deps
let cell_deps: Vec<CellDep> = vec![lock_script_dep, js_vm_cell_dep, js_script_cell_dep];

// prepare cells
let input_out_point = context.create_cell(
CellOutput::new_builder()
.capacity(1000u64.pack())
.lock(lock_script.clone())
.build(),
Bytes::new(),
);
let input = CellInput::new_builder()
.previous_output(input_out_point.clone())
.build();

// args: <ckb-js-vm args, 2 bytes> <code_hash to JavaScript code cell, 32 bytes> <hash_type to JavaScript code cell, 1 byte> <JavaScript code args, variable length>
let mut type_script_args: [u8; 35] = [0u8; 35];
let reserved = [0u8; 2];
let (js_cell, _) = context.get_cell(&js_script_out_point.clone()).unwrap();
let js_type_script = js_cell.type_().to_opt().unwrap();
let code_hash = js_type_script.calc_script_hash();
let hash_type = js_type_script.hash_type();
type_script_args[..2].copy_from_slice(&reserved);
type_script_args[2..34].copy_from_slice(code_hash.as_slice());
type_script_args[34..35].copy_from_slice(&hash_type.as_slice());

let type_script = context
.build_script(&js_vm_out_point, type_script_args.to_vec().into())
.expect("script");

let outputs = vec![
CellOutput::new_builder()
.capacity(500u64.pack())
.lock(lock_script.clone())
.type_(Some(type_script.clone()).pack())
.build(),
CellOutput::new_builder()
.capacity(500u64.pack())
.lock(lock_script)
.build(),
];

// prepare output cell data
let outputs_data = vec![Bytes::new()), Bytes::new()];

// build transaction
let tx = TransactionBuilder::default()
.cell_deps(cell_deps)
.input(input)
.outputs(outputs)
.outputs_data(outputs_data.pack())
.build();

let tx = tx.as_advanced_builder().build();

// run
let cycles = context
.verify_tx(&tx, MAX_CYCLES)
.expect("pass verification");
println!("consume cycles: {}", cycles);
}

Let's break down the code provided:

First, We deploy the ckb-js-vm, hello.bc and ALWAYS_SUCCESS binaries to the blockchain, resulting in 3 Scripts in Live Cells. The ALWAYS_SUCCESS is used solely to simplify the Lock Script in our test flow.

Then, we build an output Cell that carries a special Type Script to execute the hello.js codes. The code_hash and hash_type in the Type Script reference the ckb-js-vm Script Cell. It is automatically done by this line of code:

    let type_script = context
.build_script(&js_vm_out_point, type_script_args.to_vec().into())
.expect("script");

The key here is the args of the Type Script. We locate the Cell that carries our hello.js codes and insert the reference informationβ€”which includes code_hash and hash_type–of that Cell into the args, following the args structure of ckb-js-vm.

  // args: <ckb-js-vm args, 2 bytes> <code_hash to JavaScript code cell, 32 bytes> <hash_type to JavaScript code cell, 1 byte> <JavaScript code args, variable length>
let mut type_script_args: [u8; 35] = [0u8; 35];
let reserved = [0u8; 2];
let (js_cell, _) = context.get_cell(&js_script_out_point.clone()).unwrap();
let js_type_script = js_cell.type_().to_opt().unwrap();
let code_hash = js_type_script.calc_script_hash();
let hash_type = js_type_script.hash_type();
type_script_args[..2].copy_from_slice(&reserved);
type_script_args[2..34].copy_from_slice(code_hash.as_slice());
type_script_args[34..35].copy_from_slice(&hash_type.as_slice());

Finally, don't forget to add all the Live Cells containing the related Scripts in the cellDeps in the transaction:

  // prepare cell deps
let cell_deps: Vec<CellDep> = vec![lock_script_dep, js_vm_cell_dep, js_script_cell_dep];

// build transaction
let tx = TransactionBuilder::default()
.cell_deps(cell_deps)
.input(input)
.outputs(outputs)
.outputs_data(outputs_data.pack())
.build();

let tx = tx.as_advanced_builder().build();

// run
let cycles = context
.verify_tx(&tx, MAX_CYCLES)
.expect("pass verification");
println!("consume cycles: {}", cycles);

Run the Test to See If It Passes​

make build
make test

By default, the test output does not display the executing logs of the Scripts. To view them, you can use the following alternative command:

cargo test -- --nocapture

The logs show hello, ckb-js-script!, indicating our JavaScript code executed successfully.

Write a fib.js Script​

We can try a different JavaScript example. Let's write a fib.js in the js folder:

my-first-script-workspace/js/fib.js
console.log("testing fib");
function fib(n) {
if (n <= 0)
return 0;
else if (n == 1)
return 1;
else
return fib(n - 1) + fib(n - 2);
};
var value = fib(10);
console.assert(value == 55, 'fib(10) = 55');

Compile the fib.js into Binary with CKB-Debugger​

ckb-debugger --read-file js/fib.js --bin deps/ckb-js-vm -- -c | awk '/Run result: 0/{exit} {print}' | xxd -r -p > js/build/fib.bc

Add a New Test for The fib.js Script​

my-first-script-workspace/tests/src/tests.rs
#[test]
fn fib_script() {
// deploy contract
let mut context = Context::default();
let loader = Loader::default();
let js_vm_bin = loader.load_binary("../../deps/ckb-js-vm");
let js_vm_out_point = context.deploy_cell(js_vm_bin);
let js_vm_cell_dep = CellDep::new_builder()
.out_point(js_vm_out_point.clone())
.build();

let js_script_bin = loader.load_binary("../../js/build/fib.bc");
let js_script_out_point = context.deploy_cell(js_script_bin.clone());
let js_script_cell_dep = CellDep::new_builder()
.out_point(js_script_out_point.clone())
.build();

// prepare scripts
let always_success_out_point = context.deploy_cell(ALWAYS_SUCCESS.clone());
let lock_script = context
.build_script(&always_success_out_point.clone(), Default::default())
.expect("script");
let lock_script_dep = CellDep::new_builder()
.out_point(always_success_out_point)
.build();

// prepare cell deps
let cell_deps: Vec<CellDep> = vec![lock_script_dep, js_vm_cell_dep, js_script_cell_dep];

// prepare cells
let input_out_point = context.create_cell(
CellOutput::new_builder()
.capacity(1000u64.pack())
.lock(lock_script.clone())
.build(),
Bytes::new(),
);
let input = CellInput::new_builder()
.previous_output(input_out_point.clone())
.build();

// args: <ckb-js-vm args, 2 bytes> <code_hash to JavaScript code cell, 32 bytes> <hash_type to JavaScript code cell, 1 byte> <JavaScript code args, variable length>
let mut type_script_args: [u8; 35] = [0u8; 35];
let reserved = [0u8; 2];
let (js_cell, _) = context.get_cell(&js_script_out_point.clone()).unwrap();
let js_type_script = js_cell.type_().to_opt().unwrap();
let code_hash = js_type_script.calc_script_hash();
let hash_type = js_type_script.hash_type();
type_script_args[..2].copy_from_slice(&reserved);
type_script_args[2..34].copy_from_slice(code_hash.as_slice());
type_script_args[34..35].copy_from_slice(&hash_type.as_slice());

let type_script = context
.build_script(&js_vm_out_point, type_script_args.to_vec().into())
.expect("script");

let outputs = vec![
CellOutput::new_builder()
.capacity(500u64.pack())
.lock(lock_script.clone())
.type_(Some(type_script.clone()).pack())
.build(),
CellOutput::new_builder()
.capacity(500u64.pack())
.lock(lock_script)
.build(),
];

// prepare output cell data
let outputs_data = vec![Bytes::new(), Bytes::new()];

// build transaction
let tx = TransactionBuilder::default()
.cell_deps(cell_deps)
.input(input)
.outputs(outputs)
.outputs_data(outputs_data.pack())
.build();

let tx = tx.as_advanced_builder().build();

// run
let cycles = context
.verify_tx(&tx, MAX_CYCLES)
.expect("pass verification");
println!("consume cycles: {}", cycles);
}

Run the test for fib.js smart contract​

make build
make test

Integrate via Spawn Syscall​

Another way to integrate ckb-js-vm is by calling it from your own Scripts. This approach is useful when you have more complex custom logic to handle and still want to execute some JavaScript code. In this example, we use ckb_spawn syscall to call Script from another Script. ckb_spawn is the recommended way to call ckb-js-vm, here is why.

We will use Rust to write a new Script called run-js. In this Script, you can add custom logics and validations before calling the ckb-js-vm Script to execute JS codes.

Write run-js Script​

my-first-script-workspace/contracts/run-js/src/main.rs
#![no_std]
#![cfg_attr(not(test), no_main)]

#[cfg(test)]
extern crate alloc;

#[cfg(not(test))]
use ckb_std::default_alloc;
use ckb_std::syscalls;
#[cfg(not(test))]
ckb_std::entry!(program_entry);
#[cfg(not(test))]
default_alloc!();

pub fn program_entry() -> i8 {
ckb_std::debug!("This is a sample run JS code contract!");

let mut spgs_exit_code: i8 = -1;

let mut spgs_content = [0u8; 80];
let mut spgs_content_length: u64 = 80;
let spgs = syscalls::SpawnArgs {
memory_limit: 8,
exit_code: &mut spgs_exit_code as *mut i8,
content: &mut spgs_content as *mut u8,
content_length: &mut spgs_content_length as *mut u64,
};

// we supposed the first cell in cellDeps is the ckb-js-vm cell
// we then call ckb-js-vm script using spawn syscall to execute the js code in the script args
let result =
ckb_std::syscalls::spawn(0, ckb_std::ckb_constants::Source::CellDep, 0, &[], &spgs);
ckb_std::debug!("spawn result: {:?}", result);

if result != 0 {
return 1;
}

if spgs_exit_code != 0 {
return 1;
}

0
}

The most important code in the Script is the usage of ckb_std library to perform the spawn syscall to call the ckb-js-vm:

    // we supposed the first cell in cellDeps is the ckb-js-vm cell
// we then call ckb-js-vm script using spawn syscall to execute the js code in the script args
let result =
ckb_std::syscalls::spawn(0, ckb_std::ckb_constants::Source::CellDep, 0, &[], &spgs);

In order to use ckb_std::syscalls::spawn, you need to enable the ckb2023 feature in the ckb-std deps:

my-first-script-workspace/contracts/run-js/Cargo.toml
[dependencies]
ckb-std = {version = "0.15.1", features = ["ckb2023"]}

For simplicity, we supposed the ckb-js-vm Script is in the first position of the cell deps in the transaction.

We can check the return result from the spawn syscall to see if the code executes successfully.

Write Test for Run-JS Script​

We have our custom Script run-js that can execute JS codes and customize validations. Now let's write some tests for our Script.

This time, let's use a more realistic JS Script to test. We will utilize the ckb-syscall JS binding to write a sudt example in JavaScript and verify that it executes successfully in our run-js Scripts.

my-first-script-workspace/js/sudt.js
const CKB_INDEX_OUT_OF_BOUND = 1;
const ERROR_AMOUNT = -52;

function assert(cond, obj1) {
if (!cond) {
throw Error(obj1);
}
}

function compare_array(a, b) {
if (a.byteLength != b.byteLength) {
return false;
}
for (let i = 0; i < a.byteLength; i++) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}

function unpack_script(buf) {
let script = new Uint32Array(buf);
let raw_data = new Uint8Array(buf);

let full_size = script[0];
assert(full_size == buf.byteLength, 'full_size == buf.byteLength');
let code_hash_offset = script[1];
let code_hash = buf.slice(code_hash_offset, code_hash_offset + 32);
let hash_type_offset = script[2];
let hash_type = raw_data[hash_type_offset];
let args_offset = script[3];
let args = buf.slice(args_offset + 4);
return {'code_hash': code_hash, 'hash_type': hash_type, 'args': args};
}

function* iterate_field(source, field) {
let index = 0;
while (true) {
try {
let ret = ckb.load_cell_by_field(index, source, field);
yield ret;
index++;
} catch (e) {
if (e.error_code == CKB_INDEX_OUT_OF_BOUND) {
break;
} else {
throw e;
}
}
}
}

function* iterate_cell_data(source) {
let index = 0;
while (true) {
try {
let ret = ckb.load_cell_data(index, source);
yield ret;
index++;
} catch (e) {
if (e.error_code == CKB_INDEX_OUT_OF_BOUND) {
break;
} else {
throw e;
}
}
}
}

function main() {
console.log('simple UDT ...');
let buf = ckb.load_script();
let script = unpack_script(buf);
let owner_mode = false;
// ckb-js-vm has leading 35 bytes args
let real_args = script.args.slice(35);
for (let lock_hash of iterate_field(ckb.SOURCE_INPUT, ckb.CELL_FIELD_LOCK_HASH)) {
if (compare_array(lock_hash, real_args)) {
owner_mode = true;
}
}
if (owner_mode) {
return 0;
}
let input_amount = 0n;

for (let data of iterate_cell_data(ckb.SOURCE_GROUP_INPUT)) {
if (data.byteLength != 16) {
throw `Invalid data length: ${data.byteLength}`;
}
let n = new BigUint64Array(data);
let current_amount = n[0] | (n[1] << 64n);
input_amount += current_amount;
}
let output_amount = 0n;
for (let data of iterate_cell_data(ckb.SOURCE_GROUP_OUTPUT)) {
if (data.byteLength != 16) {
throw `Invalid data length: ${data.byteLength}`;
}
let n = new BigUint64Array(data);
let current_amount = n[0] | (n[1] << 64n);
output_amount += current_amount;
}
console.log(`verifying amount: ${input_amount} and ${output_amount}`);
if (input_amount < output_amount) {
return ERROR_AMOUNT;
}
console.log('Simple UDT quit successfully');
return 0;
}

let exit_code = main();
if (exit_code != 0) {
ckb.exit(exit_code);
}

Compile this sudt.js into binaries with CKB-Debugger:

ckb-debugger --read-file js/fib.js --bin deps/ckb-js-vm -- -c | awk '/Run result: 0/{exit} {print}' | xxd -r -p > js/build/fib.bc

Add a new test to the tests file:

my-first-script-workspace/tests/src/tests.rs
#[test]
fn sudt_script() {
// deploy contract
let mut context = Context::default();
let loader = Loader::default();
let js_vm_bin = loader.load_binary("../../deps/ckb-js-vm");
let js_vm_out_point = context.deploy_cell(js_vm_bin);
let js_vm_cell_dep = CellDep::new_builder()
.out_point(js_vm_out_point.clone())
.build();

let run_js_bin = loader.load_binary("run-js");
let run_js_out_point = context.deploy_cell(run_js_bin);
let run_js_cell_dep = CellDep::new_builder()
.out_point(run_js_out_point.clone())
.build();

let js_script_bin = loader.load_binary("../../js/build/sudt.bc");
let js_script_out_point = context.deploy_cell(js_script_bin.clone());
let js_script_cell_dep = CellDep::new_builder()
.out_point(js_script_out_point.clone())
.build();

// prepare scripts
let always_success_out_point = context.deploy_cell(ALWAYS_SUCCESS.clone());
let lock_script = context
.build_script(&always_success_out_point.clone(), Default::default())
.expect("script");
let lock_script_dep = CellDep::new_builder()
.out_point(always_success_out_point)
.build();

// prepare cell deps
let cell_deps: Vec<CellDep> = vec![
js_vm_cell_dep,
run_js_cell_dep,
lock_script_dep,
js_script_cell_dep,
];

// prepare cells
let input_out_point = context.create_cell(
CellOutput::new_builder()
.capacity(1000u64.pack())
.lock(lock_script.clone())
.build(),
Bytes::new(),
);
let input = CellInput::new_builder()
.previous_output(input_out_point.clone())
.build();

// args: <ckb-js-vm args, 2 bytes> <code_hash to JavaScript code cell, 32 bytes> <hash_type to JavaScript code cell, 1 byte> <JavaScript code args, variable length>
let mut type_script_args: [u8; 67] = [0u8; 67];
let reserved = [0u8; 2];
let (js_cell, _) = context.get_cell(&js_script_out_point.clone()).unwrap();
let js_type_script = js_cell.type_().to_opt().unwrap();
let code_hash = js_type_script.calc_script_hash();
let hash_type = js_type_script.hash_type();
let owner_lock_script_hash = lock_script.clone().calc_script_hash();

type_script_args[..2].copy_from_slice(&reserved);
type_script_args[2..34].copy_from_slice(code_hash.as_slice());
type_script_args[34..35].copy_from_slice(&hash_type.as_slice());
type_script_args[35..].copy_from_slice(owner_lock_script_hash.as_slice());

let type_script = context
.build_script(&run_js_out_point, type_script_args.to_vec().into())
.expect("script");

let outputs = vec![
CellOutput::new_builder()
.capacity(500u64.pack())
.lock(lock_script.clone())
.type_(Some(type_script.clone()).pack())
.build(),
CellOutput::new_builder()
.capacity(500u64.pack())
.lock(lock_script)
.build(),
];

// prepare output cell data
let sudt_amount: u128 = 10; // issue 10 tokens
let outputs_data = vec![
Bytes::from(sudt_amount.to_le_bytes().to_vec()),
Bytes::new(),
];

// build transaction
let tx = TransactionBuilder::default()
.cell_deps(cell_deps)
.input(input)
.outputs(outputs)
.outputs_data(outputs_data.pack())
.build();

let tx = tx.as_advanced_builder().build();

// run
let cycles = context
.verify_tx(&tx, MAX_CYCLES)
.expect("pass verification");
println!("consume cycles: {}", cycles);
}

Some explanation for this test:

Just like the previous tests, we deploy all the Scripts we need, including ckb-js-vm, run-js, sudt.js and so on. We then assemble a transaction that produce an output Cell that carries our run-js Script as its Type Script. In the args of this Type Script, we follow the ckb-js-vm args data structure. The difference this time is that we also include the arguments for the sudt.js within the Type Script args. This allows our sudt.js code can read its own arguments and get executed as expected. The arguments for sudt.js include a Lock Script hash, which is used to determine if it is under owner_mode to perform different validations.

  // args: <ckb-js-vm args, 2 bytes> <code_hash to JavaScript code cell, 32 bytes> <hash_type to JavaScript code cell, 1 byte> <JavaScript code args, variable length>
let mut type_script_args: [u8; 67] = [0u8; 67];
let reserved = [0u8; 2];
let (js_cell, _) = context.get_cell(&js_script_out_point.clone()).unwrap();
let js_type_script = js_cell.type_().to_opt().unwrap();
let code_hash = js_type_script.calc_script_hash();
let hash_type = js_type_script.hash_type();
let owner_lock_script_hash = lock_script.clone().calc_script_hash();

type_script_args[..2].copy_from_slice(&reserved);
type_script_args[2..34].copy_from_slice(code_hash.as_slice());
type_script_args[34..35].copy_from_slice(&hash_type.as_slice());
type_script_args[35..].copy_from_slice(owner_lock_script_hash.as_slice());

Lastly, we put the token amount in the data field of the output Cell containing our run-js Script and then assemble the transaction for submission on-chain:

    // prepare output cell data
let sudt_amount: u128 = 10; // issue 10 tokens
let outputs_data = vec![
Bytes::from(sudt_amount.to_le_bytes().to_vec()),
Bytes::new(),
];

// build transaction
let tx = TransactionBuilder::default()
.cell_deps(cell_deps)
.input(input)
.outputs(outputs)
.outputs_data(outputs_data.pack())
.build();

let tx = tx.as_advanced_builder().build();

// run
let cycles = context
.verify_tx(&tx, MAX_CYCLES)
.expect("pass verification");
println!("consume cycles: {}", cycles);

Run test for sudt.js​

make build
cargo test -- --nocapture sudt_script

You can see the output contains the spawn result and other information:

running 1 test
[contract debug] This is a sample run js code contract!
[contract debug] simple UDT ...
[contract debug] checking failed on quickjs/ckb_module.c:123, code = 1
[contract debug] spawn result: 0
consume cycles: 3775332
test tests::sudt_script ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.04s

Congratulations!​

By following this tutorial so far, you have mastered how to write Scripts that integrates ckb-js-vm to execute JavaScript codes on CKB. Here's a quick recap:

  • Use ckb-script-templates to init a Script project
  • Use ckb_std to leverage CKB syscalls for performing ckb_spawn syscall to call ckb-js-vm.
  • Build args for the Script to carry the reference info to the JavaScript code Cell and its arguments.

Additional Resources​