Tutorial: Run JavaScript Code on CKB
- CKB dev environment: OffCKB (β₯v0.3.0)
- JavaScript SDK: CCC (β₯v0.1.0-alpha.4)
Tutorial Overviewβ
As you have learned before, it's possible to use any programming language to write a Script (Smart contract) for CKB.
But how practical is it in reality? In this tutorial, you will see a full example of using JavaScript to write and execute Scripts within the CKB-VM
The process is as follows:
- Port a JavaScript Engine: You will learn how to port a JavaScript engine as a base Script to run on CKB.
- Write Business Logic in JavaScript: You will learn how to build and execute JavaScript business logic within this base Script on the CKB-VM.
This might sound complex, but thanks to the CKB-VM team, we already have a fully runnable JavaScript engine called ckb-js-vm, which was ported from quick.js and optimized for CKB-VM. We just need to take With ckb-js-vm, you simply deploy the engine on-chain and use it to run JavaScript-based Scripts on CKB.
Follow the step-by-step guide below, or check out the full code example on Github.
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.
You will need Clang β₯v18
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β
Install CKB-Debugger using cargo. We recommend using β₯v0.113.0.
cargo install --git https://github.com/nervosnetwork/ckb-standalone-debugger ckb-debugger
On MacOS, the protoc
binary must be available to compile ckb-vm-pprof-converter
. This can be installed via Homebrew:
brew install protobuf
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:
- Command
- Response
ckb-debugger --read-file tests/examples/hello.js --bin build/ckb-js-vm -- -r
Run from file, local access enabled. For testing only.
hello, world
Run result: 0
Total cycles consumed: 30081070(2.9m)
Transfer cycles: 125121(122.2k), running cycles: 2955949(2.8m)
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. Make sure you have the following dependencies installed:
- Make (β₯v4.3), Sed (β₯v4.7), Bash (β₯v5.0), sha256sum (β₯v9.0)
- Rust (β₯v.1.71.1) and
riscv64
target - Clang (β₯v18)
- cargo-generate (β₯0.17.0)
For detailed installation steps, refer to our Installation Guide
Init a Script Projectβ
Now let's run the command to generate a new Script project called my-first-script-workspace
:
- Command
- Response
alias create-ckb-scripts="cargo generate gh:cryptape/ckb-script-templates workspace"
create-ckb-scripts
β οΈ Favorite `gh:cryptape/ckb-script-templates` not found in config, using it as a git repository: https://github.com/cryptape/ckb-script-templates.git
π€· Project Name: my-first-script-workspace
π§ Destination: /tmp/my-first-script-workspace ...
π§ project-name: my-first-script-workspace ...
π§ Generating template ...
π§ Moving generated files into: `/tmp/my-first-script-workspace`...
π§ Initializing a fresh Git repository
β¨ Done! New project created /tmp/my-first-script-workspace
Create a New Scriptβ
Letβs create a new Script called run-js
.
- Command
- Response
cd my-first-script-workspace
make generate
π€· Project Name: run-js
π§ Destination: /tmp/my-first-script-workspace/contracts/run-js ...
π§ project-name: carrot ...
π§ Generating template ...
π§ Moving generated files into: `/tmp/my-first-script-workspace/contracts/run-js`...
π§ Initializing a fresh Git repository
β¨ Done! New project created /tmp/my-first-script-workspace/contracts/run-js
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>
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:
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.
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:
- Command
- Response
cargo test -- --nocapture
running 1 test
[contract debug] hello, ckb-js-script!
consume cycles: 3070458
test tests::hello_script ... ok
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:
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β
#[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β
#![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:
[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
sUDTrun-js
Scripts.
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/sudt.js --bin deps/ckb-js-vm -- -c | awk '/Run result: 0/{exit} {print}' | xxd -r -p > js/build/sudt.bc
Add a new test to the tests
file:
#[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 performingckb_spawn
syscall to callckb-js-vm
. - Build args for the Script to carry the reference info to the JavaScript code Cell and its arguments.
Additional Resourcesβ
- Full source code of this tutorial: ckb-js-script
- More about
ckb-js-vm
: ckb-js-vm docs - CKB syscalls specs: RFC-0009
- Script templates: ckb-script-templates
- CKB transaction structure: RFC-0022-transaction-structure
- CKB data structure basics: RFC-0019-data-structure