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 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, 1000);
// Sign and send the transaction
log(
"Transaction sent:",
explorerTransaction(await signer.sendTransaction(tx))
);
}}
>
Transfer
</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
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, 1000);
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, 1000);
// Sign and send the transaction
log(
"Transaction sent:",
explorerTransaction(await signer.sendTransaction(tx)),
);
}}
>
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";
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.
</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, 1000);
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, 1000);
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, 1000);
log(
"Transaction sent:",
explorerTransaction(await signer.sendTransaction(mintTx)),
);
}}
>
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";
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, 1000);
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, 1000);
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, 1000);
log(
"Transaction sent:",
explorerTransaction(await signer.sendTransaction(mintTx)),
);
}}
>
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, 1000);
// Sign and send the transaction
log(
"Transaction sent:",
explorerTransaction(await signer.sendTransaction(tx)),
);
}}
>
Transfer
</Button>
</ButtonsPanel>
</div>
);
}
Manage NervosDAO
Toggle to view code
"use client";
import React, { useEffect, useMemo, useState } from "react";
import { TextInput } from "@/src/components/Input";
import { Button } from "@/src/components/Button";
import { ccc } from "@ckb-ccc/connector-react";
import { useGetExplorerLink } from "@/src/utils";
import { useApp } from "@/src/context";
import { ButtonsPanel } from "@/src/components/ButtonsPanel";
import { BigButton } from "@/src/components/BigButton";
function parseEpoch(epoch: ccc.Epoch): ccc.FixedPoint {
return (
ccc.fixedPointFrom(epoch[0].toString()) +
(ccc.fixedPointFrom(epoch[1].toString()) * ccc.fixedPointFrom(1)) /
ccc.fixedPointFrom(epoch[2].toString())
);
}
function getProfit(
dao: ccc.Cell,
depositHeader: ccc.ClientBlockHeader,
withdrawHeader: ccc.ClientBlockHeader,
): ccc.Num {
const occupiedSize = ccc.fixedPointFrom(
dao.cellOutput.occupiedSize + ccc.bytesFrom(dao.outputData).length,
);
const profitableSize = dao.cellOutput.capacity - occupiedSize;
return (
(profitableSize * withdrawHeader.dao.ar) / depositHeader.dao.ar -
profitableSize
);
}
function getClaimEpoch(
depositHeader: ccc.ClientBlockHeader,
withdrawHeader: ccc.ClientBlockHeader,
): ccc.Epoch {
const depositEpoch = depositHeader.epoch;
const withdrawEpoch = withdrawHeader.epoch;
const intDiff = withdrawEpoch[0] - depositEpoch[0];
// deposit[1] withdraw[1]
// ---------- <= -----------
// deposit[2] withdraw[2]
if (
intDiff % ccc.numFrom(180) !== ccc.numFrom(0) ||
depositEpoch[1] * withdrawEpoch[2] <= depositEpoch[2] * withdrawEpoch[1]
) {
return [
depositEpoch[0] +
(intDiff / ccc.numFrom(180) + ccc.numFrom(1)) * ccc.numFrom(180),
depositEpoch[1],
depositEpoch[2],
];
}
return [
depositEpoch[0] + (intDiff / ccc.numFrom(180)) * ccc.numFrom(180),
depositEpoch[1],
depositEpoch[2],
];
}
function DaoButton({ dao }: { dao: ccc.Cell }) {
const { signer, createSender } = useApp();
const { log, error } = createSender("Transfer");
const { explorerTransaction } = useGetExplorerLink();
const [tip, setTip] = useState<ccc.ClientBlockHeader | undefined>();
const [infos, setInfos] = useState<
| [
ccc.Num,
ccc.ClientTransactionResponse,
ccc.ClientBlockHeader,
[undefined | ccc.ClientTransactionResponse, ccc.ClientBlockHeader],
]
| undefined
>();
const isNew = useMemo(() => dao.outputData === "0x0000000000000000", [dao]);
useEffect(() => {
if (!signer) {
return;
}
(async () => {
const tipHeader = await signer.client.getTipHeader();
setTip(tipHeader);
const previousTx = await signer.client.getTransaction(
dao.outPoint.txHash,
);
if (!previousTx?.blockHash) {
return;
}
const previousHeader = await signer.client.getHeaderByHash(
previousTx.blockHash,
);
if (!previousHeader) {
return;
}
const claimInfo = await (async (): Promise<typeof infos> => {
if (isNew) {
return;
}
const depositTxHash =
previousTx.transaction.inputs[Number(dao.outPoint.index)]
.previousOutput.txHash;
const depositTx = await signer.client.getTransaction(depositTxHash);
if (!depositTx?.blockHash) {
return;
}
const depositHeader = await signer.client.getHeaderByHash(
depositTx.blockHash,
);
if (!depositHeader) {
return;
}
return [
getProfit(dao, depositHeader, previousHeader),
depositTx,
depositHeader,
[previousTx, previousHeader],
];
})();
if (claimInfo) {
setInfos(claimInfo);
} else {
setInfos([
getProfit(dao, previousHeader, tipHeader),
previousTx,
previousHeader,
[undefined, tipHeader],
]);
}
})();
}, [dao, signer, isNew]);
return (
<BigButton
key={ccc.hexFrom(dao.outPoint.toBytes())}
size="sm"
iconName="Vault"
onClick={() => {
if (!signer || !infos) {
return;
}
(async () => {
const [profit, depositTx, depositHeader] = infos;
if (!depositTx.blockHash || !depositTx.blockNumber) {
error(
"Unexpected empty block info for",
explorerTransaction(dao.outPoint.txHash),
);
return;
}
const { blockHash, blockNumber } = depositTx;
let tx;
if (isNew) {
tx = ccc.Transaction.from({
headerDeps: [blockHash],
inputs: [{ previousOutput: dao.outPoint }],
outputs: [dao.cellOutput],
outputsData: [ccc.numLeToBytes(blockNumber, 8)],
});
await tx.addCellDepsOfKnownScripts(
signer.client,
ccc.KnownScript.NervosDao,
);
await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer, 1000);
} else {
if (!infos[3]) {
error("Unexpected no found deposit info");
return;
}
const [withdrawTx, withdrawHeader] = infos[3];
if (!withdrawTx?.blockHash) {
error("Unexpected empty withdraw tx block info");
return;
}
if (!depositTx.blockHash) {
error("Unexpected empty deposit tx block info");
return;
}
tx = ccc.Transaction.from({
headerDeps: [withdrawTx.blockHash, blockHash],
inputs: [
{
previousOutput: dao.outPoint,
since: {
relative: "absolute",
metric: "epoch",
value: ccc.numLeFromBytes(
ccc.epochToHex(
getClaimEpoch(depositHeader, withdrawHeader),
),
),
},
},
],
outputs: [
{
lock: (await signer.getRecommendedAddressObj()).script,
},
],
witnesses: [
ccc.WitnessArgs.from({
inputType: ccc.numLeToBytes(1, 8),
}).toBytes(),
],
});
await tx.addCellDepsOfKnownScripts(
signer.client,
ccc.KnownScript.NervosDao,
);
await tx.completeInputsByCapacity(signer);
await tx.completeFeeChangeToOutput(signer, 0, 1000);
tx.outputs[0].capacity += profit;
}
// Sign and send the transaction
log(
"Transaction sent:",
explorerTransaction(await signer.sendTransaction(tx)),
);
})();
}}
className={`align-center ${isNew ? "text-yellow-400" : "text-orange-400"}`}
>
<div className="text-md flex flex-col">
<span>
{ccc.fixedPointToString(
(dao.cellOutput.capacity / ccc.fixedPointFrom("0.01")) *
ccc.fixedPointFrom("0.01"),
)}
</span>
{infos ? (
<span className="-mt-1 text-sm">
+
{ccc.fixedPointToString(
(infos[0] / ccc.fixedPointFrom("0.0001")) *
ccc.fixedPointFrom("0.0001"),
)}
</span>
) : undefined}
</div>
<div className="flex flex-col text-sm">
{infos && tip ? (
<div className="flex whitespace-nowrap">
{ccc.fixedPointToString(
((parseEpoch(getClaimEpoch(infos[2], infos[3][1])) -
parseEpoch(tip.epoch)) /
ccc.fixedPointFrom("0.001")) *
ccc.fixedPointFrom("0.001"),
)}{" "}
epoch
</div>
) : undefined}
<span>{isNew ? "Withdraw" : "Claim"}</span>
</div>
</BigButton>
);
}
export default function Transfer() {
const { signer, createSender } = useApp();
const { log, error } = createSender("Transfer");
const { explorerTransaction } = useGetExplorerLink();
const [amount, setAmount] = useState<string>("");
const [daos, setDaos] = useState<ccc.Cell[]>([]);
useEffect(() => {
if (!signer) {
return;
}
(async () => {
const daos = [];
for await (const cell of signer.findCells(
{
script: await ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.NervosDao,
"0x",
),
scriptLenRange: [33, 34],
outputDataLenRange: [8, 9],
},
true,
)) {
daos.push(cell);
setDaos(daos);
}
})();
}, [signer]);
return (
<div className="flex w-full flex-col items-stretch">
<TextInput
label="Amount"
placeholder="Amount to deposit"
state={[amount, setAmount]}
/>
<div className="mt-4 flex flex-wrap justify-center gap-2">
{daos.map((dao) => (
<DaoButton key={ccc.hexFrom(dao.outPoint.toBytes())} dao={dao} />
))}
</div>
<ButtonsPanel>
<Button
onClick={async () => {
if (!signer) {
return;
}
const { script: lock } = await signer.getRecommendedAddressObj();
const tx = ccc.Transaction.from({
outputs: [
{
lock,
type: await ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.NervosDao,
"0x",
),
},
],
outputsData: ["00".repeat(8)],
});
await tx.addCellDepsOfKnownScripts(
signer.client,
ccc.KnownScript.NervosDao,
);
await tx.completeInputsAll(signer);
await tx.completeFeeChangeToOutput(signer, 0, 1000);
const amount = ccc.fixedPointToString(tx.outputs[0].capacity);
log("You can deposit at most", amount, "CKB");
setAmount(amount);
}}
>
Max Amount
</Button>
<Button
className="ml-2"
onClick={async () => {
if (!signer) {
return;
}
const { script: lock } = await signer.getRecommendedAddressObj();
const tx = ccc.Transaction.from({
outputs: [
{
lock,
type: await ccc.Script.fromKnownScript(
signer.client,
ccc.KnownScript.NervosDao,
"0x",
),
},
],
outputsData: ["00".repeat(8)],
});
await tx.addCellDepsOfKnownScripts(
signer.client,
ccc.KnownScript.NervosDao,
);
if (tx.outputs[0].capacity > ccc.fixedPointFrom(amount)) {
error(
"Insufficient capacity at output, min",
ccc.fixedPointToString(tx.outputs[0].capacity),
"CKB",
);
return;
}
tx.outputs[0].capacity = ccc.fixedPointFrom(amount);
await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer, 1000);
// Sign and send the transaction
log(
"Transaction sent:",
explorerTransaction(await signer.sendTransaction(tx)),
);
}}
>
Deposit
</Button>
</ButtonsPanel>
</div>
);
}
Generate Mnemonics & Keypairs and Encrypt to a Keystore
Toggle to view code
"use client";
import { ccc } from "@ckb-ccc/connector-react";
import React, { useEffect, useMemo, useState } from "react";
import { Button } from "@/src/components/Button";
import { TextInput } from "@/src/components/Input";
import * as bip39 from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english";
import { HDKey } from "@scure/bip32";
import { useApp } from "@/src/context";
import { ButtonsPanel } from "@/src/components/ButtonsPanel";
export default function Mnemonic() {
const { client } = ccc.useCcc();
const { createSender } = useApp();
const { log } = createSender("Mnemonic");
const [mnemonic, setMnemonic] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [countStr, setCountStr] = useState<string>("10");
const [accounts, setAccount] = useState<
{
publicKey: string;
privateKey: string;
address: string;
path: string;
}[]
> ([]);
> const isValid = useMemo(
() => bip39.validateMnemonic(mnemonic, wordlist),
[mnemonic],
);
useEffect(() => setAccount([]), [mnemonic]);
useEffect(() => {
(async () => {
let modified = false;
const newAccounts = await Promise.all(
accounts.map(async (acc) => {
const address = await new ccc.SignerCkbPublicKey(
client,
acc.publicKey,
).getRecommendedAddress();
if (address !== acc.address) {
modified = true;
}
acc.address = address;
return acc;
}),
);
if (modified) {
setAccount(newAccounts);
}
})();
}, [client, accounts]);
return (
<div className="mb-1 flex w-9/12 flex-col items-stretch">
<TextInput
label="Mnemonic"
placeholder="Mnemonic"
state={[mnemonic, setMnemonic]}
/>
<TextInput
label="Accounts count"
placeholder="Accounts count"
state={[countStr, setCountStr]}
/>
<TextInput
label="Password"
placeholder="Password"
state={[password, setPassword]}
/>
{accounts.length !== 0 ? (
<div className="mt-1 w-full overflow-scroll whitespace-nowrap bg-white">
<p>path, address, private key</p>
{accounts.map(({ privateKey, address, path }) => (
<p key={path}>
{path}, {address}, {privateKey}
</p>
))}
</div>
) : undefined}
<ButtonsPanel>
<Button
onClick={() => {
setMnemonic(bip39.generateMnemonic(wordlist));
}} >
Random Mnemonic
</Button>
<Button
className="ml-2"
onClick={async () => {
const count = parseInt(countStr, 10);
const seed = await bip39.mnemonicToSeed(mnemonic);
const hdKey = HDKey.fromMasterSeed(seed);
setAccount([
...accounts,
...Array.from(new Array(count), (_, i) => {
const path = `m/44'/309'/0'/0/${i}`;
const derivedKey = hdKey.derive(path);
return {
publicKey: ccc.hexFrom(derivedKey.publicKey!),
privateKey: ccc.hexFrom(derivedKey.privateKey!),
path,
address: "",
};
}),
]);
}}
disabled={!isValid || Number.isNaN(parseInt(countStr, 10))} >
More accounts
</Button>
<Button
className="ml-2"
onClick={async () => {
const seed = await bip39.mnemonicToSeed(mnemonic);
const hdKey = HDKey.fromMasterSeed(seed);
log(
JSON.stringify(
await ccc.keystoreEncrypt(
hdKey.privateKey!,
hdKey.chainCode!,
password,
),
),
);
}}
disabled={!isValid} >
To Keystore
</Button>
{accounts.length !== 0 ? (
<Button
as="a"
className="ml-2"
href={`data:application/octet-stream,path%2C%20address%2C%20private%20key%0A${accounts .map(({ privateKey, address, path }) => encodeURIComponent(`${path}, ${address}, ${privateKey}`),
)
.join("\n")}`}
download={`ckb_accounts_${Date.now()}.csv`} >
Save as CSV
</Button>
) : undefined}
</ButtonsPanel>
</div>
);
}
Decrypt a Keystore
Toggle to view code
"use client";
import { ccc } from "@ckb-ccc/connector-react";
import React, { useEffect, useState } from "react";
import { Button } from "@/src/components/Button";
import { TextInput } from "@/src/components/Input";
import { HDKey } from "@scure/bip32";
import { Textarea } from "@/src/components/Textarea";
import { useApp } from "@/src/context";
import { ButtonsPanel } from "@/src/components/ButtonsPanel";
export default function Keystore() {
const { client } = ccc.useCcc();
const { createSender } = useApp();
const { log, error } = createSender("Keystore");
const [keystore, setKeystore] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [countStr, setCountStr] = useState<string>("10");
const [accounts, setAccount] = useState<
{
publicKey: string;
privateKey: string;
address: string;
path: string;
}[]
>([]);
const [hdKey, setHdKey] = useState<HDKey | undefined>(undefined);
useEffect(() => {
setAccount([]);
setHdKey(undefined);
}, [keystore, password]);
useEffect(() => {
(async () => {
let modified = false;
const newAccounts = await Promise.all(
accounts.map(async (acc) => {
const address = await new ccc.SignerCkbPublicKey(
client,
acc.publicKey,
).getRecommendedAddress();
if (address !== acc.address) {
modified = true;
}
acc.address = address;
return acc;
}),
);
if (modified) {
setAccount(newAccounts);
}
})();
}, [client, accounts]);
return (
<div className="flex w-full flex-col items-stretch">
<Textarea
label="keystore"
placeholder="Keystore"
state={[keystore, setKeystore]}
/>
<TextInput
label="Accounts count"
placeholder="Accounts count"
state={[countStr, setCountStr]}
/>
<TextInput
label="Password"
placeholder="Password"
state={[password, setPassword]}
/>
{accounts.length !== 0 ? (
<div className="mt-1 w-full overflow-scroll whitespace-nowrap">
<p>path, address, private key</p>
{accounts.map(({ privateKey, address, path }) => (
<p key={path}>
{path}, {address}, {privateKey}
</p>
))}
</div>
) : undefined}
<ButtonsPanel>
<Button
onClick={async () => {
try {
const { privateKey, chainCode } = await ccc.keystoreDecrypt(
JSON.parse(keystore),
password,
);
setHdKey(new HDKey({ privateKey, chainCode }));
} catch (err) {
error("Invalid");
throw err;
}
log("Valid");
}}
>
Verify Keystore
</Button>
<Button
className="ml-2"
onClick={async () => {
if (!hdKey) {
return;
}
const count = parseInt(countStr, 10);
setAccount([
...accounts,
...Array.from(new Array(count), (_, i) => {
const path = `m/44'/309'/0'/0/${i}`;
const derivedKey = hdKey.derive(path);
return {
publicKey: ccc.hexFrom(derivedKey.publicKey!),
privateKey: ccc.hexFrom(derivedKey.privateKey!),
path,
address: "",
};
}),
]);
}}
disabled={!hdKey || Number.isNaN(parseInt(countStr, 10))}
>
More accounts
</Button>
{accounts.length !== 0 ? (
<Button
as="a"
className="mt-2"
href={`data:application/octet-stream,path%2C%20address%2C%20private%20key%0A${accounts
.map(({ privateKey, address, path }) =>
encodeURIComponent(`${path}, ${address}, ${privateKey}`),
)
.join("\n")}`}
download={`ckb_accounts_${Date.now()}.csv`}
>
Save as CSV
</Button>
) : undefined}
</ButtonsPanel>
</div>
);
}