JavaScript/TypeScript (CCC)
Introduction
Common Chain Connector (CCC) is a JavaScript/Typescript SDK tailored for CKB. Highly recommended as the primary CKB development tool, CCC offers advantages over alternatives such as Lumos and ckb-js-sdk.
CCC also serves as a wallet connector enhancing interoperability between wallets across different blockchains. Explore by checking out the CCC Demo.
Install Packages
CCC is designed for both front-end and back-end developers. It streamlines the development process by offering a single package that caters to a variety of requirements:
- NodeJS
- Custom UI
- Web Component
- React
npm install @ckb-ccc/core
npm install @ckb-ccc/ccc
npm install @ckb-ccc/connector
npm install @ckb-ccc/connector-react
To use CCC, import the desired package:
import { ccc } from "@ckb-ccc/<package-name>";
CCC encapsulates all functionalities within the ccc
object, providing a unified interface.
For advanced developers, CCC introduces cccA
object that offers a comprehensive set of advanced features:
import { cccA } from "@ckb-ccc/<package-name>/advanced";
Please notice that these advanced interfaces are subject to change and may not be as stable as the core API.
Transaction Composing
Below is a example demonstrating how to compose a transaction for transferring CKB:
const tx = ccc.Transaction.from({
outputs: [{ lock: toLock, capacity: ccc.fixedPointFrom(amount) }],
});
// Instruct CCC to complete the transaction
await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer, 1000); // Specify the transaction fee rate
const txHash = await signer.sendTransaction(tx); // Send and get the transaction hash
Examples
Sign and Verify Message
Toggle to view code
"use client";
import { ccc } from "@ckb-ccc/connector-react";
import React, { useState } from "react";
import { Button } from "@/src/components/Button";
import { TextInput } from "@/src/components/Input";
import { useApp } from "@/src/context";
import { ButtonsPanel } from "@/src/components/ButtonsPanel";
export default function Sign() {
const { signer, createSender } = useApp();
const { log, error } = createSender("Sign");
const [messageToSign, setMessageToSign] = useState<string>("");
const [signature, setSignature] = useState<string>("");
return (
<div className="flex w-full flex-col items-stretch">
<TextInput
label="Message"
placeholder="Message to sign and verify"
state={[messageToSign, setMessageToSign]}
/>
<ButtonsPanel>
<Button
onClick={async () => {
if (!signer) {
return;
}
const sig = JSON.stringify(await signer.signMessage(messageToSign));
setSignature(sig);
log("Signature:", sig);
}}
>
Sign
</Button>
<Button
className="ml-2"
onClick={async () => {
if (
!(await ccc.Signer.verifyMessage(
messageToSign,
JSON.parse(signature)
))
) {
error("Invalid");
return;
}
log("Valid");
}}
>
Verify
</Button>
</ButtonsPanel>
</div>
);
}
Calculate CKB Hash of Any Message
Toggle to view code
"use client";
import { ccc } from "@ckb-ccc/connector-react";
import React, { useState } from "react";
import { Button } from "@/src/components/Button";
import { TextInput } from "@/src/components/Input";
import { useApp } from "@/src/context";
import { ButtonsPanel } from "@/src/components/ButtonsPanel";
export default function Hash() {
const { createSender } = useApp();
const { log } = createSender("Hash");
const [messageToHash, setMessageToHash] = useState<string>("");
return (
<div className="flex w-full flex-col items-stretch">
<TextInput
label="Message"
placeholder="Message to hash"
state={[messageToHash, setMessageToHash]}
/>
<ButtonsPanel>
<Button
onClick={async () => {
log("Hash:", ccc.hashCkb(ccc.bytesFrom(messageToHash, "utf8")));
}}
>
Hash as UTF-8
</Button>
<Button
className="ml-2"
onClick={async () => {
log("Hash:", ccc.hashCkb(messageToHash));
}}
>
Hash as hex
</Button>
</ButtonsPanel>
</div>
);
}
Transfer CKB Tokens ⭐️
Toggle to view code
"use client";
import React, { useState } from "react";
import { TextInput } from "@/src/components/Input";
import { Button } from "@/src/components/Button";
import { Textarea } from "@/src/components/Textarea";
import { ccc } from "@ckb-ccc/connector-react";
import { bytesFromAnyString, useGetExplorerLink } from "@/src/utils";
import { useApp } from "@/src/context";
import { ButtonsPanel } from "@/src/components/ButtonsPanel";
export default function Transfer() {
const { signer, createSender } = useApp();
const { log, error } = createSender("Transfer");
const { explorerTransaction } = useGetExplorerLink();
const [transferTo, setTransferTo] = useState<string>("");
const [amount, setAmount] = useState<string>("");
const [data, setData] = useState<string>("");
return (
<div className="flex w-full flex-col items-stretch">
<Textarea
label="Address"
placeholder="Addresses to transfer to, separated by lines"
state={[transferTo, setTransferTo]}
/>
<TextInput
label="Amount"
placeholder="Amount to transfer for each"
state={[amount, setAmount]}
/>
<Textarea
label="Output Data(Options)"
state={[data, setData]}
placeholder="Leave empty if you don't know what this is. Data in the first output. Hex string will be parsed."
/>
<ButtonsPanel>
<Button
variant="info"
onClick={async () => {
if (!signer) {
return;
}
if (transferTo.split("\n").length !== 1) {
error("Only one destination is allowed for max amount");
return;
}
log("Calculating the max amount...");
// Verify destination address
const { script: toLock } = await ccc.Address.fromString(
transferTo,
signer.client
);
// Build the full transaction to estimate the fee
const tx = ccc.Transaction.from({
outputs: [{ lock: toLock }],
outputsData: [bytesFromAnyString(data)],
});
// Complete missing parts for transaction
await tx.completeInputsAll(signer);
// Change all balance to the first output
await tx.completeFeeChangeToOutput(signer, 0, 2000);
const amount = ccc.fixedPointToString(tx.outputs[0].capacity);
log("You can transfer at most", amount, "CKB");
setAmount(amount);
}}
>
Max Amount
</Button>
<Button
className="ml-2"
onClick={async () => {
if (!signer) {
return;
}
// Verify destination addresses
const toAddresses = await Promise.all(
transferTo
.split("\n")
.map((addr) => ccc.Address.fromString(addr, signer.client))
);
const tx = ccc.Transaction.from({
outputs: toAddresses.map(({ script }) => ({ lock: script })),
outputsData: [bytesFromAnyString(data)],
});
// CCC transactions are easy to be edited
tx.outputs.forEach((output, i) => {
if (output.capacity > ccc.fixedPointFrom(amount)) {
error(`Insufficient capacity at output ${i} to store data`);
return;
}
output.capacity = ccc.fixedPointFrom(amount);
});
// Complete missing parts for transaction
await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer, 2000);
// Sign and send the transaction
const txHash = await signer.sendTransaction(tx);
log("Transaction sent:", explorerTransaction(txHash));
await signer.client.waitTransaction(txHash);
log("Transaction committed:", explorerTransaction(txHash));
}}
>
Transfer
</Button>
</ButtonsPanel>
</div>
);
}
Transfer Native CKB Tokens With Lumos SDK
Toggle to view code
"use client";
import React, { useState } from "react";
import { TextInput } from "@/src/components/Input";
import { Button } from "@/src/components/Button";
import { ccc } from "@ckb-ccc/connector-react";
import common, {
registerCustomLockScriptInfos,
} from "@ckb-lumos/common-scripts/lib/common";
import { generateDefaultScriptInfos } from "@ckb-ccc/lumos-patches";
import { Indexer } from "@ckb-lumos/ckb-indexer";
import { TransactionSkeleton } from "@ckb-lumos/helpers";
import { predefined } from "@ckb-lumos/config-manager";
import { Textarea } from "@/src/components/Textarea";
import { useGetExplorerLink } from "@/src/utils";
import { useApp } from "@/src/context";
import { ButtonsPanel } from "@/src/components/ButtonsPanel";
export default function TransferLumos() {
const { signer, createSender } = useApp();
const { log, error } = createSender("Transfer with Lumos");
const { explorerTransaction } = useGetExplorerLink();
const [transferTo, setTransferTo] = useState<string>("");
const [amount, setAmount] = useState<string>("");
const [data, setData] = useState<string>("");
return (
<div className="flex w-full flex-col items-stretch">
<TextInput
label="Address"
placeholder="Address to transfer to"
state={[transferTo, setTransferTo]}
/>
<TextInput
label="Amount"
placeholder="Amount to transfer"
state={[amount, setAmount]}
/>
<Textarea
label="Output Data(options)"
state={[data, setData]}
placeholder="Data in the cell. Hex string will be parsed."
/>
<ButtonsPanel>
<Button
className="self-center"
onClick={async () => {
if (!signer) {
return;
}
// Verify destination address
await ccc.Address.fromString(transferTo, signer.client);
const fromAddresses = await signer.getAddresses();
// === Composing transaction with Lumos ===
registerCustomLockScriptInfos(generateDefaultScriptInfos());
const indexer = new Indexer(
signer.client.url
.replace("wss://", "https://")
.replace("ws://", "http://")
.replace(new RegExp("/ws/?$"), "/")
);
let txSkeleton = new TransactionSkeleton({
cellProvider: indexer,
});
txSkeleton = await common.transfer(
txSkeleton,
fromAddresses,
transferTo,
ccc.fixedPointFrom(amount),
undefined,
undefined,
{
config:
signer.client.addressPrefix === "ckb"
? predefined.LINA
: predefined.AGGRON4,
}
);
txSkeleton = await common.payFeeByFeeRate(
txSkeleton,
fromAddresses,
BigInt(3600),
undefined,
{
config:
signer.client.addressPrefix === "ckb"
? predefined.LINA
: predefined.AGGRON4,
}
);
// ======
const tx = ccc.Transaction.fromLumosSkeleton(txSkeleton);
// CCC transactions are easy to be edited
const dataBytes = (() => {
try {
return ccc.bytesFrom(data);
} catch (e) {}
return ccc.bytesFrom(data, "utf8");
})();
if (tx.outputs[0].capacity < ccc.fixedPointFrom(dataBytes.length)) {
error("Insufficient capacity to store data");
return;
}
tx.outputsData[0] = ccc.hexFrom(dataBytes);
// Sign and send the transaction
log(
"Transaction sent:",
explorerTransaction(await signer.sendTransaction(tx))
);
}}
>
Transfer
</Button>
</ButtonsPanel>
</div>
);
}
Issue xUDT Tokens With Single-Use Lock ⭐️
Toggle to view code
"use client";
import { useState } from "react";
import { TextInput } from "@/src/components/Input";
import { Button } from "@/src/components/Button";
import { ccc } from "@ckb-ccc/connector-react";
import { tokenInfoToBytes, useGetExplorerLink } from "@/src/utils";
import { Message } from "@/src/components/Message";
import React from "react";
import { useApp } from "@/src/context";
import { ButtonsPanel } from "@/src/components/ButtonsPanel";
import Link from "next/link";
export default function IssueXUdtSul() {
const { signer, createSender } = useApp();
const { log, error } = createSender("Issue xUDT (SUS)");
const { explorerTransaction } = useGetExplorerLink();
const [amount, setAmount] = useState<string>("");
const [decimals, setDecimals] = useState<string>("");
const [name, setName] = useState<string>("");
const [symbol, setSymbol] = useState<string>("");
return (
<>
<div className="flex w-full flex-col items-stretch">
<Message title="Hint" type="info">
You will need to sign two or three transactions.
<br />
Learn more on{" "}
<Link
className="underline"
href="https://talk.nervos.org/t/en-cn-misc-single-use-seals/8279"
target="_blank"
>
[EN/CN] Misc: Single-Use-Seals - 杂谈:一次性密封
</Link>
</Message>
<TextInput
label="Amount"
placeholder="Amount to issue"
state={[amount, setAmount]}
/>
<TextInput
label="Decimals"
placeholder="Decimals of the token"
state={[decimals, setDecimals]}
/>
<TextInput
label="Symbol"
placeholder="Symbol of the token"
state={[symbol, setSymbol]}
/>
<TextInput
label="Name"
placeholder="Name of the token, same as symbol if empty"
state={[name, setName]}
/>
<ButtonsPanel>
<Button
className="self-center"
onClick={async () => {
if (!signer) {
return;
}
if (decimals === "" || symbol === "") {
error("Invalid token info");
return;
}
const { script } = await signer.getRecommendedAddressObj();
const susTx = ccc.Transaction.from({
outputs: [
{
lock: script,
},
],
});
await susTx.completeInputsByCapacity(signer);
await susTx.completeFeeBy(signer);
const susTxHash = await signer.sendTransaction(susTx);
log("Transaction sent:", explorerTransaction(susTxHash));
await signer.client.cache.markUnusable({
txHash: susTxHash,
index: 0,
});
const singleUseLock = await ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.SingleUseLock,
ccc.OutPoint.from({
txHash: susTxHash,
index: 0,
}).toBytes()
);
const lockTx = ccc.Transaction.from({
outputs: [
// Owner cell
{
lock: singleUseLock,
},
],
});
await lockTx.completeInputsByCapacity(signer);
await lockTx.completeFeeBy(signer);
const lockTxHash = await signer.sendTransaction(lockTx);
log("Transaction sent:", explorerTransaction(lockTxHash));
const mintTx = ccc.Transaction.from({
inputs: [
// SUS
{
previousOutput: {
txHash: susTxHash,
index: 0,
},
},
// Owner cell
{
previousOutput: {
txHash: lockTxHash,
index: 0,
},
},
],
outputs: [
// Issued xUDT
{
lock: script,
type: await ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.XUdt,
singleUseLock.hash()
),
},
// xUDT Info
{
lock: script,
type: await ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.UniqueType,
"00".repeat(32)
),
},
],
outputsData: [
ccc.numLeToBytes(amount, 16),
tokenInfoToBytes(decimals, symbol, name),
],
});
await mintTx.addCellDepsOfKnownScripts(
signer.client,
ccc.KnownScript.SingleUseLock,
ccc.KnownScript.XUdt,
ccc.KnownScript.UniqueType
);
await mintTx.completeInputsByCapacity(signer);
if (!mintTx.outputs[1].type) {
error("Unexpected disappeared output");
return;
}
mintTx.outputs[1].type!.args = ccc.hexFrom(
ccc.bytesFrom(ccc.hashTypeId(mintTx.inputs[0], 1)).slice(0, 20)
);
await mintTx.completeFeeBy(signer);
const mintTxHash = await signer.sendTransaction(mintTx);
log("Transaction sent:", explorerTransaction(mintTxHash));
await signer.client.waitTransaction(mintTxHash);
log("Transaction committed:", explorerTransaction(mintTxHash));
}}
>
Issue
</Button>
</ButtonsPanel>
</div>
</>
);
}
Issue xUDT Tokens Controlled by a Type ID Cell ⭐️
Toggle to view code
"use client";
import React, { useState } from "react";
import { TextInput } from "@/src/components/Input";
import { Button } from "@/src/components/Button";
import { ccc } from "@ckb-ccc/connector-react";
import { tokenInfoToBytes, useGetExplorerLink } from "@/src/utils";
import { Message } from "@/src/components/Message";
import { useApp } from "@/src/context";
import { ButtonsPanel } from "@/src/components/ButtonsPanel";
import Link from "next/link";
export default function IssueXUdtTypeId() {
const { signer, createSender } = useApp();
const { log, error } = createSender("Issue xUDT (Type ID)");
const { explorerTransaction } = useGetExplorerLink();
const [typeIdArgs, setTypeIdArgs] = useState<string>("");
const [amount, setAmount] = useState<string>("");
const [decimals, setDecimals] = useState<string>("");
const [name, setName] = useState<string>("");
const [symbol, setSymbol] = useState<string>("");
return (
<div className="flex w-full flex-col items-stretch">
<Message title="Hint" type="info">
You will need to sign two or three transactions.
</Message>
<TextInput
label="Type ID(options)"
placeholder="Type ID args, empty to create new"
state={[typeIdArgs, setTypeIdArgs]}
/>
<TextInput
label="Amount"
placeholder="Amount to issue"
state={[amount, setAmount]}
/>
<TextInput
label="Decimals"
placeholder="Decimals of the token"
state={[decimals, setDecimals]}
/>
<TextInput
label="Symbol"
placeholder="Symbol of the token"
state={[symbol, setSymbol]}
/>
<TextInput
label="Name (options)"
placeholder="Name of the token, same as symbol if empty"
state={[name, setName]}
/>
<ButtonsPanel>
<Button
className="self-center"
onClick={async () => {
if (!signer) {
return;
}
const { script } = await signer.getRecommendedAddressObj();
if (decimals === "" || symbol === "") {
error("Invalid token info");
return;
}
const typeId = await (async () => {
if (typeIdArgs !== "") {
return ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.TypeId,
typeIdArgs
);
}
const typeIdTx = ccc.Transaction.from({
outputs: [
{
lock: script,
type: await ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.TypeId,
"00".repeat(32)
),
},
],
});
await typeIdTx.completeInputsByCapacity(signer);
if (!typeIdTx.outputs[0].type) {
error("Unexpected disappeared output");
return;
}
typeIdTx.outputs[0].type.args = ccc.hashTypeId(
typeIdTx.inputs[0],
0
);
await typeIdTx.completeFeeBy(signer);
log(
"Transaction sent:",
explorerTransaction(await signer.sendTransaction(typeIdTx))
);
log("Type ID created: ", typeIdTx.outputs[0].type.args);
return typeIdTx.outputs[0].type;
})();
if (!typeId) {
return;
}
const outputTypeLock = await ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.OutputTypeProxyLock,
typeId.hash()
);
const lockTx = ccc.Transaction.from({
outputs: [
// Owner cell
{
lock: outputTypeLock,
},
],
});
await lockTx.completeInputsByCapacity(signer);
await lockTx.completeFeeBy(signer);
const lockTxHash = await signer.sendTransaction(lockTx);
log("Transaction sent:", explorerTransaction(lockTxHash));
const typeIdCell = await signer.client.findSingletonCellByType(
typeId
);
if (!typeIdCell) {
error("Type ID cell not found");
return;
}
const mintTx = ccc.Transaction.from({
inputs: [
// Type ID
{
previousOutput: typeIdCell.outPoint,
},
// Owner cell
{
previousOutput: {
txHash: lockTxHash,
index: 0,
},
},
],
outputs: [
// Keep the Type ID cell
typeIdCell.cellOutput,
// Issued xUDT
{
lock: script,
type: await ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.XUdt,
outputTypeLock.hash()
),
},
// xUDT Info
{
lock: script,
type: await ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.UniqueType,
"00".repeat(32)
),
},
],
outputsData: [
typeIdCell.outputData,
ccc.numLeToBytes(amount, 16),
tokenInfoToBytes(decimals, symbol, name),
],
});
await mintTx.addCellDepsOfKnownScripts(
signer.client,
ccc.KnownScript.OutputTypeProxyLock,
ccc.KnownScript.XUdt,
ccc.KnownScript.UniqueType
);
await mintTx.completeInputsByCapacity(signer);
if (!mintTx.outputs[2].type) {
throw new Error("Unexpected disappeared output");
}
mintTx.outputs[2].type!.args = ccc.hexFrom(
ccc.bytesFrom(ccc.hashTypeId(mintTx.inputs[0], 2)).slice(0, 20)
);
await mintTx.completeFeeBy(signer);
const mintTxHash = await signer.sendTransaction(mintTx);
log("Transaction sent:", explorerTransaction(mintTxHash));
await signer.client.waitTransaction(mintTxHash);
log("Transaction committed:", explorerTransaction(mintTxHash));
}}
>
Issue
</Button>
</ButtonsPanel>
</div>
);
}
Transfer xUDT Tokens ⭐️
Toggle to view code
"use client";
import React, { useState } from "react";
import { TextInput } from "@/src/components/Input";
import { Button } from "@/src/components/Button";
import { ccc } from "@ckb-ccc/connector-react";
import { Textarea } from "@/src/components/Textarea";
import { useGetExplorerLink } from "@/src/utils";
import { useApp } from "@/src/context";
import { ButtonsPanel } from "@/src/components/ButtonsPanel";
export default function TransferXUdt() {
const { signer, createSender } = useApp();
const { log } = createSender("Transfer xUDT");
const { explorerTransaction } = useGetExplorerLink();
const [xUdtArgs, setXUdtArgs] = useState<string>("");
const [transferTo, setTransferTo] = useState<string>("");
const [amount, setAmount] = useState<string>("");
return (
<div className="flex w-full flex-col items-stretch">
<TextInput
label="Args"
placeholder="xUdt args to transfer"
state={[xUdtArgs, setXUdtArgs]}
/>
<Textarea
label="Address"
placeholder="Addresses to transfer to, separated by lines"
state={[transferTo, setTransferTo]}
/>
<TextInput
label="amount"
placeholder="Amount to transfer for each"
state={[amount, setAmount]}
/>
<ButtonsPanel>
<Button
className="self-center"
onClick={async () => {
if (!signer) {
return;
}
const toAddresses = await Promise.all(
transferTo
.split("\n")
.map((addr) => ccc.Address.fromString(addr, signer.client))
);
const { script: change } = await signer.getRecommendedAddressObj();
const xUdtType = await ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.XUdt,
xUdtArgs
);
const tx = ccc.Transaction.from({
outputs: toAddresses.map(({ script }) => ({
lock: script,
type: xUdtType,
})),
outputsData: Array.from(Array(toAddresses.length), () =>
ccc.numLeToBytes(amount, 16)
),
});
await tx.completeInputsByUdt(signer, xUdtType);
const balanceDiff =
(await tx.getInputsUdtBalance(signer.client, xUdtType)) -
tx.getOutputsUdtBalance(xUdtType);
if (balanceDiff > ccc.Zero) {
tx.addOutput(
{
lock: change,
type: xUdtType,
},
ccc.numLeToBytes(balanceDiff, 16)
);
}
await tx.addCellDepsOfKnownScripts(
signer.client,
ccc.KnownScript.XUdt
);
await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer, 2000);
// Sign and send the transaction
log(
"Transaction sent:",
explorerTransaction(await signer.sendTransaction(tx))
);
}}
>
Transfer
</Button>
</ButtonsPanel>
</div>
);
}