# Code Examples (https://docs.ckbccc.com/en/docs/code-examples) Browse our collection of runnable code examples and interactive tools. Cards tagged **playground** open directly in the [CCC Playground](https://live.ckbccc.com/); cards tagged **app** link to the [CCC App](https://app.ckbccc.com/) — a mini-toolset for CKB that lets you perform common operations through a graphical interface, even if you're not a developer. Use the tag filter below to narrow down examples by category. ## Contribute your examples [#contribute-your-examples] We welcome community contributions! Whether it's a handy utility script, an interesting on-chain data analysis, or an integration example for a new protocol — it can be included here to help more people get started with CKB development. ### Via GitHub Pull Request [#via-github-pull-request] To have your example included in the official repository: **Fork and clone the repo** Go to [ckb-devrel/ccc](https://github.com/ckb-devrel/ccc), click **Fork**, then clone locally: ```bash git clone https://github.com//ccc.git cd ccc ``` **Write your script** Create a new `.ts` file in `packages/examples/src/`, using camelCase naming (e.g. `myNewExample.ts`). Your script should: * Import the SDK from `@ckb-ccc/ccc` * Import `render`, `signer`, etc. from `@ckb-ccc/playground` * Call `await render(tx)` at key steps to enable visualization in the Playground See [`transfer.ts`](https://github.com/ckb-devrel/ccc/blob/master/packages/examples/src/transfer.ts) for a reference. **Test locally** Paste your script into the [Playground](https://live.ckbccc.com/) and verify it runs correctly on Testnet. **Open a Pull Request** Push to your fork and open a PR against the `master` branch of `ckb-devrel/ccc`. Briefly describe the example's purpose and use case in the PR description. ### Quick share (no PR needed) [#quick-share-no-pr-needed] The Playground can load code from **any** publicly accessible URL via the `?src=` parameter — your GitHub repo, a Gist, or any raw file URL: ``` https://live.ckbccc.com/?src= ``` Share the generated link with colleagues or the community — they can open it and run your code immediately. The built-in **Share** button stores code on Nostr relay nodes. Nostr share links are great for quick collaboration, but relay nodes do not guarantee permanent data retention — links may expire over time. For **long-lived, stable share links**, host your script in a GitHub repository and load it via `?src=https://raw.githubusercontent.com/...` — the link will remain valid as long as the repository and file exist. # Address (https://docs.ckbccc.com/en/docs/concepts/address) A CKB address is a bech32m-encoded string that packages two things: 1. **A lock script** — the on-chain predicate that controls who can spend cells at this address (`codeHash`, `hashType`, and `args`). 2. **A network prefix** — `ckb` for mainnet, `ckt` for testnet. Because the lock script is embedded in the address, you can always recover the exact script needed to send funds — no additional metadata required. ## The `Address` class [#the-address-class] `ccc.Address` pairs a `Script` with a network prefix: ```ts export class Address { constructor( public script: Script, public prefix: string, // "ckb" | "ckt" ) {} } ``` Wherever an API accepts an `AddressLike`, you can pass an existing `Address` or a plain object — `Address.from()` normalizes both: ```ts const address = ccc.Address.from({ script: { codeHash: "0x...", hashType: "type", args: "0x..." }, prefix: "ckb", }); ``` ## Usage [#usage] ### Parse an address string [#parse-an-address-string] `Address.fromString` decodes an address string into a structured object: ```ts import { ccc } from "@ckb-ccc/ccc"; const client = new ccc.ClientPublicMainnet(); const address = await ccc.Address.fromString( "ckb1qzda0cr08m85hc8jlnfp3sdrlalyatuqvqdveld...", client, ); console.log(address.script.codeHash); // lock script code hash console.log(address.script.hashType); // "type" | "data" console.log(address.script.args); // lock script args console.log(address.prefix); // "ckb" ``` It validates that the address prefix matches the client's network and throws on a mismatch (e.g. passing a `ckt` address to a mainnet client). When you need to handle both networks in the same code path, pass a record of clients: ```ts const address = await ccc.Address.fromString(someAddress, { ckb: new ccc.ClientPublicMainnet(), ckt: new ccc.ClientPublicTestnet(), }); ``` ### Construct an address from a script [#construct-an-address-from-a-script] Build an `Address` directly from a lock script and a client: ```ts const address = ccc.Address.fromScript(lockScript, client); console.log(address.toString()); // "ckb1q..." ``` Or derive one from a built-in `KnownScript`: ```ts const address = await ccc.Address.fromKnownScript( client, ccc.KnownScript.Secp256k1Blake160, "0xPublicKeyHash...", ); ``` ### Get an address from a signer [#get-an-address-from-a-signer] In a dApp, the easiest way to get an address is directly from the connected signer: ```ts // Primary address as a string const addressStr = await signer.getRecommendedAddress(); // Primary address as an Address object (includes the lock script) const addressObj = await signer.getRecommendedAddressObj(); const { script: lock } = addressObj; // All addresses controlled by this signer const allAddresses = await signer.getAddresses(); // string[] ``` Prefer `getRecommendedAddressObj` when you need the lock script — for example, to set it as a transaction output recipient — rather than calling `Address.fromString` on the string result. ### Convert an address to a string [#convert-an-address-to-a-string] Call `toString()` on any `Address` instance to produce the bech32m string: ```ts const str = address.toString(); // "ckb1q..." | "ckt1q..." ``` `toString()` always emits the modern **Full** format. CCC can parse legacy formats for backwards compatibility, but never emits them. ## Address formats [#address-formats] CCC recognizes four on-the-wire formats, distinguished by their leading format byte. Only `Full` is produced by `toString()`; the rest exist for compatibility with pre-2021 addresses. | Format | Encoding | Payload layout | Notes | | ---------- | -------- | ----------------------------------------------------- | ------------------------------------------ | | `Full` | bech32m | `[0x00, codeHash(32), hashType(1), args]` | Current standard. Emitted by `toString()`. | | `FullData` | bech32 | `[0x02, codeHash(32), args]` — `hashType` is `"data"` | Legacy 2019 format. | | `FullType` | bech32 | `[0x04, codeHash(32), args]` — `hashType` is `"type"` | Legacy 2019 format. | | `Short` | bech32 | `[0x01, scriptIndex(1), args(20)]` | Legacy short form for well-known scripts. | The `Short` format encodes a script by index rather than a full code hash. Supported scripts: | `scriptIndex` | Script | | ------------- | ------------------------------- | | `0` | `KnownScript.Secp256k1Blake160` | | `1` | `KnownScript.Secp256k1Multisig` | | `2` | `KnownScript.AnyoneCanPay` | Decoding a short address requires a `Client` to look up the current `codeHash` and `cellDeps` for the indexed script. ## Network prefix [#network-prefix] | Prefix | Network | | ------ | ------- | | `ckb` | Mainnet | | `ckt` | Testnet | Read the prefix at runtime from the client: ```ts const client = new ccc.ClientPublicTestnet(); console.log(client.addressPrefix); // "ckt" ``` Never mix mainnet and testnet addresses. Passing a `ckt` address to a mainnet client will throw. Always pair `Address.fromString` with the correct client. ## Advanced: low-level parsing [#advanced-low-level-parsing] For most apps, `Address.fromString` and `Address.fromScript` are all you need. When you need to inspect or transform an address without a `Client` round-trip, CCC exposes the underlying steps via the `cccA` advanced namespace: ```ts import { cccA } from "@ckb-ccc/ccc/advanced"; ``` ### `addressPayloadFromString` [#addresspayloadfromstring] Decodes the bech32/bech32m envelope and returns the raw format byte and payload — without resolving any `KnownScript`: ```ts const { prefix, format, payload } = cccA.addressPayloadFromString( "ckb1qzda0cr08m85hc8jlnfp3sdrlalyatuqvqdveld...", ); // prefix: "ckb" // format: cccA.AddressFormat.Full // payload: number[] (codeHash + hashType + args bytes) ``` Tries bech32m first, falls back to legacy bech32. Throws `Unknown address format` if neither matches. ### `addressFromPayload` [#addressfrompayload] Turns a decoded payload back into an `AddressLike`, resolving short-format script-index lookups against the client: ```ts const addressLike = await cccA.addressFromPayload(prefix, format, payload, client); const address = ccc.Address.from(addressLike); ``` `Address.fromString` is exactly `addressPayloadFromString` + prefix validation + `addressFromPayload`. # CKB Cell Model (https://docs.ckbccc.com/en/docs/concepts/cell-model) ## What is the cell model? [#what-is-the-cell-model] CKB uses a generalized UTXO model called the **cell model**. Like Bitcoin UTXOs, cells are discrete units of state — consumed by transactions and replaced with new ones. Unlike Bitcoin UTXOs, each cell can store arbitrary data and be governed by programmable scripts. Every cell has four fields: | Field | Type | Description | | ------------ | --------------------- | ------------------------------------------------------- | | `capacity` | `Num` | Storage space in Shannon (1 CKB = 100,000,000 Shannon) | | `lock` | `Script` | Defines ownership — who can consume this cell | | `type` | `Script \| undefined` | Defines asset behavior — optional, enforced on transfer | | `outputData` | `Hex` | Arbitrary bytes stored in the cell | In CCC, `CellOutput` represents the structured output portion of a cell: ```typescript export class CellOutput { constructor( public capacity: Num, public lock: Script, public type?: Script, ) {} } ``` A full on-chain cell — with its location on the chain — is represented by `Cell`: ```typescript export class Cell extends CellAny { constructor( public outPoint: OutPoint, cellOutput: CellOutput, outputData: Hex, ) {} } ``` ## Scripts [#scripts] Scripts are the programmable logic attached to cells. CKB runs them in a RISC-V virtual machine to validate each transaction. ```typescript export class Script { constructor( public codeHash: Hex, // Identifies the script code public hashType: HashType, // "type" | "data" | "data1" | "data2" public args: Hex, // Arguments passed to the script at runtime ) {} } ``` Most commonly, you'll construct a `Script` from a known script: ```typescript const xudtScript = await ccc.Script.fromKnownScript( client, ccc.KnownScript.XUdt, "0xabcdef...", // args ); ``` If you need to construct a `Script` from a plain object, you can use: ```typescript const script = ccc.Script.from({ codeHash: "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", hashType: "type", args: "0xabcdef...", }); ``` ### Lock scripts [#lock-scripts] A lock script defines the ownership of a cell — who owns it. A transaction can only consume a cell if its lock conditions are satisfied — typically by providing a valid cryptographic signature. The most common lock on CKB is `Secp256k1Blake160`, which works similarly to Bitcoin's P2PKH. ### Type scripts [#type-scripts] A type script constrains how a cell can be transformed. It runs against both the consumed inputs and the created outputs, making it the right tool for enforcing asset rules — for example, preventing a UDT (User-Defined Token) supply from being inflated during a transfer. Type scripts are optional. A cell without one has no asset-level constraints beyond its lock. ## Capacity and Shannon [#capacity-and-shannon] Capacity is measured in **Shannon** — the smallest unit of CKB: ``` 1 CKB = 100,000,000 Shannon ``` Use `ccc.fixedPointFrom()` to convert human-readable CKB amounts to Shannon: ```typescript import { ccc } from "@ckb-ccc/ccc"; ccc.fixedPointFrom("10"); // → 1_000_000_000n ccc.fixedPointFrom(10); // → 1_000_000_000n ccc.fixedPointFrom("61.5"); // → 6_150_000_000n ``` `fixedPointFrom` defaults to 8 decimal places, matching CKB's Shannon denominator. The return type is `bigint`. ## OutPoint [#outpoint] An `OutPoint` uniquely identifies a cell on-chain by referencing the transaction that created it and its position in that transaction's output list: ```typescript export class OutPoint { constructor( public txHash: Hex, // Hash of the creating transaction public index: Num, // Output index within that transaction ) {} } // Usage: OutPoint.from({ txHash: "0x...", index: 0 }); ``` When a cell is spent as a transaction input, it is referenced via its `OutPoint` through `CellInput`: ```typescript export class CellInput { constructor( public previousOutput: OutPoint, // The cell being consumed public since: Num, // Time-lock constraint (0 = none) public cellOutput?: CellOutput, public outputData?: Hex, ) {} } ``` ## Addresses [#addresses] A CKB address is a bech32m encoding of a lock script. It combines the script's `codeHash`, `hashType`, and `args` with a network prefix — `ckb` for mainnet, `ckt` for testnet. ```typescript export class Address { constructor( public script: Script, public prefix: string, ) {} // Decode an address string back to its Address object static async fromString( address: string, clients: Client | Record, ): Promise
{} // Build an Address from a Script and a Client static fromScript(script: ScriptLike, client: Client): Address {} // Encode to a bech32m string toString(): string {} } ``` Two addresses that share the same lock script are identical. When you call `signer.getRecommendedAddress()`, CCC encodes the signer's lock script into the right format for the active network. ## How CCC abstracts the cell model [#how-ccc-abstracts-the-cell-model] You rarely need to construct raw `OutPoint` or `Script` objects by hand. CCC's helpers handle encoding, hashing, and on-chain lookups: * **`Transaction.from()`** — builds a transaction from a plain object, inferring capacities where omitted. * **`completeInputsByCapacity(signer)`** — selects input cells from the signer's address to cover the required capacity. * **`completeFeeBy(signer)`** — calculates the fee and adds a change output. * **`Script.fromKnownScript(client, KnownScript.Secp256k1Blake160, args)`** — resolves well-known script code hashes by name, no hardcoding required. # Client (https://docs.ckbccc.com/en/docs/concepts/client) ## What is a Client? [#what-is-a-client] A `Client` is an abstract class that connects CCC to a CKB node via JSON-RPC. It provides methods for querying chain state, searching cells and transactions, and broadcasting signed transactions. ```typescript // From packages/core/src/client/client.ts export abstract class Client { public cache: ClientCache; constructor(config?: { cache?: ClientCache }) { this.cache = config?.cache ?? new ClientCacheMemory(); } abstract get url(): string; abstract get addressPrefix(): string; abstract getKnownScript(script: KnownScript): Promise; } ``` ## Built-in clients [#built-in-clients] CCC ships with two ready-to-use client implementations: ```typescript import { ccc } from "@ckb-ccc/ccc"; const client = new ccc.ClientPublicMainnet(); ``` ```typescript import { ccc } from "@ckb-ccc/ccc"; const client = new ccc.ClientPublicTestnet(); ``` The default client in the React Provider is `ClientPublicTestnet`. Switch to `ClientPublicMainnet` for production deployments. ## Key methods [#key-methods] ### Chain state [#chain-state] ```typescript // Get the current tip block number abstract getTip(): Promise; // Get the current tip block header abstract getTipHeader(verbosity?: number | null): Promise; // Get a block by number (uses cache if available) async getBlockByNumber( blockNumber: NumLike, verbosity?: number | null, withCycles?: boolean | null, ): Promise; // Get a block by hash (uses cache if available) async getBlockByHash( blockHash: HexLike, verbosity?: number | null, withCycles?: boolean | null, ): Promise; ``` ### Transactions [#transactions] ```typescript // Fetch a transaction by hash async getTransaction( txHashLike: HexLike, ): Promise; // Broadcast a signed transaction; returns the tx hash async sendTransaction( transaction: TransactionLike, validator?: OutputsValidator, options?: { maxFeeRate?: NumLike }, ): Promise; // Wait until a transaction is confirmed async waitTransaction( txHash: HexLike, confirmations: number, // default 0 timeout: number, // default 60000 ms interval: number, // default 2000 ms ): Promise; ``` ### Cells [#cells] ```typescript // Get a cell by its OutPoint async getCell(outPointLike: OutPointLike): Promise; // Get a live (unspent) cell async getCellLive( outPointLike: OutPointLike, withData?: boolean | null, includeTxPool?: boolean | null, ): Promise; // Async generator yielding cells matching a search key async *findCells( keyLike: ClientCollectableSearchKeyLike, order?: "asc" | "desc", limit?: number, ): AsyncGenerator; // Convenience: find cells by lock script findCellsByLock( lock: ScriptLike, type?: ScriptLike | null, withData?: boolean, order?: "asc" | "desc", limit?: number, ): AsyncGenerator; // Convenience: find cells by type script findCellsByType( type: ScriptLike, withData?: boolean, order?: "asc" | "desc", limit?: number, ): AsyncGenerator; ``` ### Transactions search [#transactions-search] ```typescript // Async generator yielding transactions matching a search key async *findTransactions( key: ClientIndexerSearchKeyTransactionLike, order?: "asc" | "desc", limit?: number, ): AsyncGenerator<...>; // Convenience: find transactions by lock script findTransactionsByLock( lock: ScriptLike, type?: ScriptLike | null, groupByTransaction?: boolean | null, order?: "asc" | "desc", limit?: number, ): AsyncGenerator<...>; ``` ### Balance and fees [#balance-and-fees] ```typescript // Get the total capacity (in Shannon) of cells matching all given lock scripts async getBalance(locks: ScriptLike[]): Promise; // Get the current network fee rate (shannons per 1000 bytes) async getFeeRate( blockRange?: NumLike, options?: { maxFeeRate?: NumLike }, ): Promise; // Get fee rate statistics (mean and median) abstract getFeeRateStatistics( blockRange?: NumLike, ): Promise<{ mean?: Num; median?: Num }>; ``` ### Known scripts [#known-scripts] ```typescript // Resolve a well-known script to its on-chain ScriptInfo abstract getKnownScript(script: KnownScript): Promise; ``` ## KnownScript [#knownscript] The `KnownScript` enum lists pre-deployed scripts CCC knows how to resolve on both mainnet and testnet: ```typescript // From packages/core/src/client/knownScript.ts export enum KnownScript { NervosDao = "NervosDao", Secp256k1Blake160 = "Secp256k1Blake160", Secp256k1Multisig = "Secp256k1Multisig", Secp256k1MultisigV2 = "Secp256k1MultisigV2", AnyoneCanPay = "AnyoneCanPay", TypeId = "TypeId", XUdt = "XUdt", JoyId = "JoyId", COTA = "COTA", PWLock = "PWLock", OmniLock = "OmniLock", NostrLock = "NostrLock", UniqueType = "UniqueType", // ckb-proxy-locks AlwaysSuccess = "AlwaysSuccess", InputTypeProxyLock = "InputTypeProxyLock", OutputTypeProxyLock = "OutputTypeProxyLock", LockProxyLock = "LockProxyLock", SingleUseLock = "SingleUseLock", TypeBurnLock = "TypeBurnLock", EasyToDiscoverType = "EasyToDiscoverType", TimeLock = "TimeLock", } ``` Use `getKnownScript()` to fetch the `codeHash`, `hashType`, and `cellDeps` for a known script, or use `Script.fromKnownScript()` as a shortcut: ```typescript import { ccc } from "@ckb-ccc/ccc"; const client = new ccc.ClientPublicTestnet(); // Resolve to a Script instance directly const xudtScript = await ccc.Script.fromKnownScript( client, ccc.KnownScript.XUdt, "0xabcdef...", // args ); ``` ## ClientCache [#clientcache] Every client includes a `ClientCache` instance that stores previously fetched cells, transactions, and block headers in memory. This reduces redundant RPC calls and tracks spent cells locally after a transaction is sent. ```typescript // Access the cache on any client const cache: ccc.ClientCache = client.cache; ``` The default cache is `ClientCacheMemory`, which holds data in process memory. You can supply a custom cache implementation via the constructor: ```typescript const client = new ccc.ClientPublicTestnet({ cache: myCustomCache }); ``` ## Usage Examples [#usage-examples] ### Querying the chain [#querying-the-chain] ```typescript import { ccc } from "@ckb-ccc/ccc"; async function queryChain() { const client = new ccc.ClientPublicTestnet(); // Get the current tip block number const tip = await client.getTip(); console.log("Current tip:", tip.toString()); // Get fee rate const feeRate = await client.getFeeRate(); console.log("Fee rate (shannons/KB):", feeRate.toString()); // Look up a transaction const txResponse = await client.getTransaction("0xabc123..."); if (txResponse) { console.log("Tx status:", txResponse.status); } // Stream all cells locked by a script const lockScript = await ccc.Script.fromKnownScript( client, ccc.KnownScript.Secp256k1Blake160, "0x36c329ed630d6ce750712a477543672adab57f4c", ); for await (const cell of client.findCellsByLock(lockScript)) { console.log("Cell capacity:", cell.cellOutput.capacity.toString()); } } ``` ### Waiting for confirmation [#waiting-for-confirmation] ```typescript import { ccc } from "@ckb-ccc/ccc"; async function sendAndWait(signer: ccc.Signer, tx: ccc.Transaction) { const txHash = await signer.sendTransaction(tx); console.log("Sent:", txHash); // Wait up to 60 seconds for at least 1 confirmation const confirmed = await signer.client.waitTransaction(txHash, 1); if (confirmed) { console.log("Confirmed in block:", confirmed.blockNumber?.toString()); } } ``` # Core Concepts (https://docs.ckbccc.com/en/docs/concepts) Before writing serious CCC code, it helps to understand the primitives the SDK is organized around. They map directly to how CKB itself works. ## The core primitives [#the-core-primitives] * **Cells** are the units of state on CKB. A cell holds capacity, a lock script, an optional type script, and arbitrary data. Transactions consume old cells and create new cells — there are no accounts or balances. * **Transactions** describe a state transition: a list of input cells to consume and output cells to create. CCC composes them declaratively — you describe the outputs you want, and helpers fill in inputs and fees. * **Signers** represent a connected wallet. The same `Signer` interface works across CKB, EVM, BTC, Nostr, and Doge wallets, exposing a unified API for addresses, balance, signing, and broadcasting. * **Clients** are the link to a CKB node. They expose JSON-RPC methods for querying the chain, searching cells, and sending transactions. * **Addresses** are the user-facing encoding of a lock script plus a network prefix. Every address round-trips losslessly to the exact `Script` that controls its cells. ## How they fit together [#how-they-fit-together] A typical CCC flow uses all of them: 1. A **Client** connects to a CKB node. 2. A **Signer** (backed by a wallet) is created against that client and exposes the user's **Address**. 3. You build a **Transaction** by declaring its outputs — each output's `lock` is a script you typically obtained by parsing a recipient **Address**. 4. The transaction collects input **Cells** from the signer, gets signed, and is broadcast through the client. ## Pages in this section [#pages-in-this-section] The UTXO-style data model behind every CKB transaction: cells, scripts, capacity, and addresses. Compose transactions with `Transaction.from()`, auto-fill inputs by capacity, and let CCC handle fees and change. The unified signing interface that abstracts CKB, EVM, BTC, Nostr, and Doge wallets behind one API. Connect to mainnet or testnet, query chain state, and broadcast transactions through `ClientPublicMainnet` / `ClientPublicTestnet`. Parse and construct CKB addresses, understand the bech32m formats, and convert between addresses, scripts, and signers. # Signer (https://docs.ckbccc.com/en/docs/concepts/signer) ## What is a Signer? [#what-is-a-signer] `ccc.Signer` is the core abstraction that unifies signing across multiple blockchain ecosystems. It lets a single piece of CCC application code work with wallets from CKB, EVM, Bitcoin, Nostr, Doge, and more — all through the same API — while ultimately controlling CKB assets. ```typescript // From packages/core/src/signer/signer/index.ts export abstract class Signer { constructor(protected client_: Client) {} abstract get type(): SignerType; abstract get signType(): SignerSignType; get client(): Client { return this.client_; } } ``` * `type` — the originating blockchain ecosystem (CKB / EVM / BTC / Nostr / Doge). * `signType` — the concrete signing scheme (CkbSecp256k1, EvmPersonal, BtcEcdsa, NostrEvent, JoyId, DogeEcdsa). * `client` — the `ccc.Client` used to talk to a CKB node. Signers are normally **not constructed by hand**. They are produced by wallet connectors (e.g. `useCcc()` in the React connector, or `SignersController`). For server-side code you may instantiate one directly (see [Obtaining a signer](#obtaining-a-signer)). ## Signer hierarchy [#signer-hierarchy] ## Signer enums [#signer-enums] ### `SignerType` [#signertype] Identifies the originating blockchain ecosystem of the `signer`. ```typescript // From packages/core/src/signer/signer/index.ts export enum SignerType { EVM = "EVM", BTC = "BTC", CKB = "CKB", Nostr = "Nostr", Doge = "Doge", } ``` ### `SignerSignType` [#signersigntype] Identifies the specific signing scheme used by the `signer`. ```typescript // From packages/core/src/signer/signer/index.ts export enum SignerSignType { Unknown = "Unknown", BtcEcdsa = "BtcEcdsa", EvmPersonal = "EvmPersonal", JoyId = "JoyId", NostrEvent = "NostrEvent", CkbSecp256k1 = "CkbSecp256k1", DogeEcdsa = "DogeEcdsa", } ``` ## Key methods [#key-methods] ### Connection [#connection] ```typescript // Connect to the wallet abstract connect(): Promise; // Disconnect from the wallet async disconnect(): Promise; // Check whether the signer is currently connected abstract isConnected(): Promise; ``` ### Addresses [#addresses] ```typescript // Get the wallet's internal (native) address string abstract getInternalAddress(): Promise; // Get an identity used to verify signatures (typically address or pubkey) abstract getIdentity(): Promise; // Get the recommended CKB address as a string async getRecommendedAddress(preference?: unknown): Promise; // Get all CKB addresses as strings async getAddresses(): Promise; // Get all Address objects (includes the associated Script) abstract getAddressObjs(): Promise; // Get the recommended Address object async getRecommendedAddressObj(_preference?: unknown): Promise
; ``` ### Balance [#balance] ```typescript // Returns total balance (in Shannon) across all addresses async getBalance(): Promise; ``` ### Signing messages [#signing-messages] ```typescript // Sign a message and return a full Signature object async signMessage(message: string | BytesLike): Promise; // Sign a message and return only the signature string (implemented by subclasses) abstract signMessageRaw(message: string | BytesLike): Promise; // Verify a message signature async verifyMessage( message: string | BytesLike, signature: string | Signature, ): Promise; // Static verifier — dispatches to the algorithm implied by signature.signType static async verifyMessage( message: string | BytesLike, signature: Signature, ): Promise; ``` ### Transactions [#transactions] ```typescript // Prepare a transaction (adds cell deps, dummy witnesses, etc.) async prepareTransaction(tx: TransactionLike): Promise; // Sign a prepared transaction async signOnlyTransaction(_: TransactionLike): Promise; // Prepare + sign in one step async signTransaction(tx: TransactionLike): Promise; // Sign and broadcast — returns the transaction hash async sendTransaction(tx: TransactionLike): Promise; ``` ### Searching chain data [#searching-chain-data] ```typescript // Async generator yielding cells owned by this signer async *findCells( filter: ClientCollectableSearchKeyFilterLike, withData?: boolean | null, order?: "asc" | "desc", limit?: number, ): AsyncGenerator; // Async generator yielding transactions related to this signer async *findTransactions( filter: ClientCollectableSearchKeyFilterLike, groupByTransaction?: boolean | null, order?: "asc" | "desc", limit?: number, ): AsyncGenerator<...>; ``` ## Concrete implementations [#concrete-implementations] ### CKB signers [#ckb-signers] #### `SignerCkbPublicKey` [#signerckbpublickey] A read-only signer built from a 33-byte compressed public key. Supports discovery of AnyoneCanPay addresses. * `getAddressObjSecp256k1()` — returns the `Secp256k1Blake160` address. * `getAddressObjs()` — returns the primary address plus up to 10 discovered AnyoneCanPay addresses. #### `SignerCkbPrivateKey` [#signerckbprivatekey] Extends `SignerCkbPublicKey` and adds signing using a 32-byte private key. Signing flow: 1. Compute the message hash: `hashCkb("Nervos Message:" + message)`. 2. Produce a 65-byte ECDSA signature with a recovery id. 3. Place the signature in the witness `lock` field. ### EVM signers [#evm-signers] Allow Ethereum-style wallets to control CKB assets through OmniLock or PWLock scripts. Lock script `args` formats: * OmniLock (modern Ethereum auth flag `0x12`): `[0x12, ...evmAddress(20 bytes), 0x00]` * OmniLock (legacy Ethereum flag `0x01`): `[0x01, ...evmAddress(20 bytes), 0x00]` * PWLock: `evmAddress(20 bytes)` — direct address mapping. Signing flow: * OmniLock: uses `personal_sign` with message prefix `"CKB transaction: " + txHash`. * PWLock: uses Keccak256 instead of Blake2b for the transaction hash. ### BTC signers [#btc-signers] Bitcoin signers use the OmniLock Bitcoin auth flag (`0x04`). * Args: `[0x04, ...btcEcdsaPublicKeyHash(publicKey), 0x00]` * `btcEcdsaPublicKeyHash` = `RIPEMD160(SHA256(publicKey))`. Signing flow: * Message format: `"CKB (Bitcoin Layer) transaction: " + txHash`. * Use the wallet's `signMessage` (returns base64), then adjust the recovery flag. "Adjusting the recovery flag" means converting the recovery flag from the Bitcoin standard format returned by the Bitcoin wallet to the format required by CKB OmniLock. ### Nostr signers [#nostr-signers] Control CKB assets through Nostr event signatures (NIP-01). NostrLock script args: `[0x00, ...hashCkb(pubkey)[0:21]]`. Event structure: ```json { "pubkey": "nostr_public_key_hex", "created_at": 1234567890, "kind": 23334, "tags": [["ckb_sighash_all", "transaction_hash_hex"]], "content": "Signing a CKB transaction...", "id": "event_id", "sig": "event_signature" } ``` Constants: * `CKB_UNLOCK_EVENT_KIND` = `23334` * `CKB_SIG_HASH_ALL_TAG` = `"ckb_sighash_all"` ### JoyID signer [#joyid-signer] CKB signer backed by WebAuthn / passkeys. Sub-key wallet support: * Queries the JoyID Aggregator service for an unlock SMT proof. * Adds the COTA cell dependency. * Witness placeholder size is 1000 bytes (due to the WebAuthn signature format). ### Lock script generation summary [#lock-script-generation-summary] | Signer | Input | Lock script | Args format | Witness size | | ------ | ----------------- | ----------------- | ------------------------------- | ------------ | | CKB | 33-byte pubkey | Secp256k1Blake160 | `hashCkb(pubkey)[0:20]` | 65 bytes | | CKB | (same) | AnyoneCanPay | `hashCkb(pubkey)[0:20] + ...` | 65 bytes | | EVM | 20-byte address | OmniLock (modern) | `0x12 + address + 0x00` | 85 bytes | | EVM | (same) | OmniLock (legacy) | `0x01 + address + 0x00` | 85 bytes | | EVM | (same) | PWLock | `address` | 65 bytes | | BTC | 33/65-byte pubkey | OmniLock | `0x04 + hash160(pubkey) + 0x00` | 85 bytes | | Nostr | 32-byte pubkey | NostrLock | `0x00 + hashCkb(pubkey)[0:21]` | 572 bytes | | JoyID | WebAuthn pubkey | JoyID | `hashCkb(pubkey)[0:20]` | 1000 bytes | ## Wallet integrations [#wallet-integrations] ### EIP-6963 (MetaMask, OKX, ...) [#eip-6963-metamask-okx-] ```typescript import { Signer } from "@ckb-ccc/eip6963"; const signer = new Signer(client, provider); await signer.connect(); const address = await signer.getRecommendedAddress(); ``` ### NIP-07 (Nostr extensions) [#nip-07-nostr-extensions] ```typescript import { Signer } from "@ckb-ccc/nip07"; const signer = new Signer(client, window.nostr); await signer.connect(); ``` ### JoyID [#joyid] ```typescript import { getJoyIdSigners } from "@ckb-ccc/joy-id"; const signers = getJoyIdSigners(client, "My App", "icon.png"); ``` ## Usage examples [#usage-examples] ### Getting address and balance [#getting-address-and-balance] ```typescript import { ccc } from "@ckb-ccc/ccc"; async function printWalletInfo(signer: ccc.Signer) { if (!(await signer.isConnected())) { await signer.connect(); } const address = await signer.getRecommendedAddress(); console.log("CKB address:", address); const allAddresses = await signer.getAddresses(); console.log("All addresses:", allAddresses); const balance = await signer.getBalance(); console.log("Balance (Shannon):", balance.toString()); } ``` ### Signing a message [#signing-a-message] ```typescript import { ccc } from "@ckb-ccc/ccc"; async function signAndVerify(signer: ccc.Signer, message: string) { const sig = await signer.signMessage(message); console.log("Signature:", sig.signature); console.log("Sign type:", sig.signType); // Instance verify (uses this signer's algorithm) const valid = await signer.verifyMessage(message, sig); // Or static verify — dispatches by sig.signType, no signer instance needed const valid2 = await ccc.Signer.verifyMessage(message, sig); console.log("Valid:", valid, valid2); } ``` ### Obtaining a signer [#obtaining-a-signer] In practice, signers are provided by wallet connectors rather than instantiated directly. ```typescript import { useCcc } from "@ckb-ccc/connector-react"; function MyComponent() { const { signer } = useCcc(); if (!signer) { return

No wallet connected

; } // signer is a ccc.Signer } ```
```typescript import { ccc } from "@ckb-ccc/shell"; // For server-side usage, construct a signer directly const signer = new ccc.SignerCkbPrivateKey(client, privateKey); await signer.connect(); ```
# Transaction (https://docs.ckbccc.com/en/docs/concepts/transaction) ## Transaction structure [#transaction-structure] A CKB transaction consumes existing cells (inputs) and creates new cells (outputs): ```typescript export class Transaction { constructor( public version: Num, public cellDeps: CellDep[], // Script code dependencies public headerDeps: Hex[], // Block header dependencies public inputs: CellInput[], // Cells being consumed public outputs: CellOutput[], // New cells being created public outputsData: Hex[], // Data for each output cell public witnesses: Hex[], // Signatures and proofs ) {} } ``` ### `CellInput` [#cellinput] References an existing on-chain cell to consume: ```typescript export class CellInput { constructor( public previousOutput: OutPoint, // Which cell to consume public since: Num, // Time-lock (0 = none) public cellOutput?: CellOutput, // Populated by completeExtraInfos() public outputData?: Hex, // Populated by completeExtraInfos() ) {} } ``` ### `CellOutput` [#celloutput] Defines a new cell to create: ```typescript export class CellOutput { constructor( public capacity: Num, // Storage space in Shannon public lock: Script, // Ownership script public type?: Script, // Asset type script (optional) ) {} } ``` ### `CellDep` [#celldep] Tells the CKB VM where to find the script code that needs to run: ```typescript export type DepType = "depGroup" | "code"; export class CellDep { constructor( public outPoint: OutPoint, // Where the script code lives public depType: DepType, // "code" = raw bytecode; "depGroup" = group of deps ) {} } ``` ## Creating a transaction [#creating-a-transaction] Use `Transaction.from()` to build a transaction from a plain object. Omit `capacity` and CCC calculates it from the cell's occupied size: ```typescript import { ccc } from "@ckb-ccc/ccc"; const tx = ccc.Transaction.from({ outputs: [ { lock: recipientLockScript, capacity: ccc.fixedPointFrom("100"), // 100 CKB }, ], }); ``` For an empty transaction, use `Transaction.default()` and add outputs incrementally: ```typescript const tx = ccc.Transaction.default(); tx.addOutput({ lock: recipientLock }, "0x"); // capacity auto-calculated ``` ## Working with CKB amounts [#working-with-ckb-amounts] Capacity is always stored in **Shannon** (bigint). Use `fixedPointFrom()` to convert from human-readable CKB: ```typescript ccc.fixedPointFrom("61") // → 6_100_000_000n (61 CKB — minimum for a plain cell) ccc.fixedPointFrom(100) // → 10_000_000_000n ccc.Zero // → 0n ``` ## Completing a transaction [#completing-a-transaction] After declaring outputs, two method calls handle everything else: ### `completeInputsByCapacity(signer)` [#completeinputsbycapacitysigner] Searches the signer's cells and adds enough inputs to cover total output capacity: ```typescript async completeInputsByCapacity( from: Signer, capacityTweak?: NumLike, filter?: ClientCollectableSearchKeyFilterLike, ): Promise ``` ### `completeFeeBy(signer, feeRate?)` [#completefeebysigner-feerate] Calculates the fee from the serialized transaction size, adds more inputs if needed, and sends change back to the signer's address: ```typescript async completeFeeBy( from: Signer, feeRate?: NumLike, filter?: ClientCollectableSearchKeyFilterLike, options?: { feeRateBlockRange?: NumLike; maxFeeRate?: NumLike; shouldAddInputs?: boolean; }, ): Promise<[number, boolean]> ``` Omit `feeRate` and CCC fetches the current network rate from the client automatically. For most transfers, `completeInputsByCapacity` + `completeFeeBy` is all you need. They handle UTXO selection, fee estimation, and change output creation — no manual accounting required. ## Full transfer example [#full-transfer-example] ```typescript import { ccc } from "@ckb-ccc/ccc"; async function transferCkb( signer: ccc.Signer, toLock: ccc.Script, amount: string, // e.g. "100" for 100 CKB ) { // Declare what you want to create const tx = ccc.Transaction.from({ outputs: [{ lock: toLock, capacity: ccc.fixedPointFrom(amount) }], }); // CCC selects inputs to cover the outputs await tx.completeInputsByCapacity(signer); // CCC calculates the fee and adds a change output await tx.completeFeeBy(signer); // Sign and broadcast const txHash = await signer.sendTransaction(tx); console.log("Transaction sent:", txHash); return txHash; } ``` ## Transaction lifecycle [#transaction-lifecycle] A transaction moves through three stages before it hits the chain: ```typescript // 1. Prepare — adds cell deps and dummy witnesses const prepared = await signer.prepareTransaction(tx); // 2. Sign — replaces dummy witnesses with real signatures const signed = await signer.signTransaction(prepared); // 3. Broadcast — submits to the network, returns the tx hash const txHash = await signer.sendTransaction(tx); ``` In practice, `sendTransaction` calls `signTransaction` internally — you rarely need to call them separately. ## Computing hashes [#computing-hashes] ```typescript // Transaction hash (excludes witnesses) const txHash: ccc.Hex = tx.hash(); // Full transaction hash (includes witnesses) const fullHash: ccc.Hex = tx.hashFull(); // Raw CKB Blake2b hash of arbitrary bytes const hash = ccc.hashCkb(someBytes); ``` ## Advanced: custom change logic [#advanced-custom-change-logic] When you need full control over how change capacity is handled, use `completeFee()` directly: ```typescript const [addedInputs, hasChange] = await tx.completeFee( signer, (tx, capacity) => { // capacity = excess Shannon available for change const minCellCapacity = ccc.fixedPointFrom("61"); if (capacity >= minCellCapacity) { tx.addOutput({ capacity, lock: changeScript }); return 0; // 0 signals done } return minCellCapacity; // request more capacity }, ); ``` ## Advanced: Adding cell dependencies [#advanced-adding-cell-dependencies] ### Use cases [#use-cases] Adding cell dependencies is primarily needed for the following scenarios: * **Type scripts**: When a transaction uses a Type script (such as xUDT, NervosDao, Spore, etc.), you must add the corresponding cell deps to enable on-chain verification * **Special Lock scripts**: Certain special Lock scripts (such as TimeLock, SingleUseLock, etc.) require manual addition * **Custom scripts**: When using custom scripts not in the KnownScript list, you need to specify cell deps directly Note: The Signer's `prepareTransaction()` automatically adds KnownScript dependencies (such as OmniLock, NostrLock, etc.) related to that Signer, so manual addition is not required. ### Example [#example] For built-in scripts, use `addCellDepsOfKnownScripts()`. For custom scripts, add deps directly: ```typescript // Built-in script await tx.addCellDepsOfKnownScripts(client, ccc.KnownScript.XUdt); // Custom script tx.addCellDeps({ outPoint: { txHash: "0x...", index: 0 }, depType: "depGroup", }); ``` # Get Started (https://docs.ckbccc.com/en/docs/getting-started) New to CCC? Start here. This section walks you from "never heard of CKB" to a signed transaction broadcast on-chain — in three short pages. ## What you'll learn [#what-youll-learn] * **What CCC is** — the one-stop TypeScript / JavaScript SDK for the CKB blockchain, and the kinds of apps you can build with it. * **How to install it** — pick the right package for your environment: React, Node.js, Web Component, or a custom UI. * **How to send a transaction** — scaffold an app with `create-ccc-app`, connect a wallet, and broadcast a real transfer in under five minutes. ## Pages in this section [#pages-in-this-section] Learn what CCC is, what it does, and who is using it in production. Send your first CKB transaction on-chain with CCC — in under 5 minutes. Install CCC for React, Node.js, Web Component, or a custom UI. Want to skip the setup entirely? Try CCC directly in the [playground](https://live.ckbccc.com/) — no installation required. # Installation (https://docs.ckbccc.com/en/docs/getting-started/installation) Not sure where to start? The [Quick start](./quick-start) guide walks through a full React setup from scratch. Come back here when you need a specific package or TypeScript configuration details. CCC ships as several focused packages. Pick the one that matches your environment: | Package | Use case | | -------------------------- | ----------------------------------- | | `@ckb-ccc/connector-react` | React applications | | `@ckb-ccc/shell` | Node.js scripts and backends | | `@ckb-ccc/connector` | Web Component (framework-agnostic) | | `@ckb-ccc/ccc` | Custom UI, manual signer management | ```bash npm install @ckb-ccc/connector-react ``` ```bash yarn add @ckb-ccc/connector-react ``` ```bash pnpm add @ckb-ccc/connector-react ``` Wrap your app root with `ccc.Provider` to mount the wallet connector UI and make the CCC context available everywhere: Using Next.js App Router or another RSC setup? Add `"use client"` at the top of any file that imports `ccc.Provider` or `ccc.useCcc`. ```tsx title="app.tsx" "use client"; import { ccc } from "@ckb-ccc/connector-react"; export default function App({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` Then call `ccc.useCcc()` from any child component to open the connector and access the active signer: ```tsx title="component.tsx" "use client"; import { ccc } from "@ckb-ccc/connector-react"; export function ConnectButton() { const { open, wallet, signerInfo } = ccc.useCcc(); return signerInfo ? (

Connected: {wallet?.name}

) : ( ); } ```
```bash npm install @ckb-ccc/shell ``` ```bash yarn add @ckb-ccc/shell ``` ```bash pnpm add @ckb-ccc/shell ``` `@ckb-ccc/shell` bundles everything from `@ckb-ccc/core` — addresses, bytes, clients, transactions, signers — plus domain-specific modules for working with Spore digital objects, SSRI contracts, and UDT tokens. It's the right choice for scripts, backends, and CLI tools. ```typescript title="script.ts" import { ccc } from "@ckb-ccc/shell"; const client = new ccc.ClientPublicTestnet(); const signer = new ccc.SignerCkbPrivateKey(client, process.env.PRIVATE_KEY!); const address = await signer.getRecommendedAddress(); console.log("Address:", address); ``` ```bash npm install @ckb-ccc/connector ``` ```bash yarn add @ckb-ccc/connector ``` ```bash pnpm add @ckb-ccc/connector ``` Importing the package registers the `` custom element automatically — no extra setup needed: ```typescript title="main.ts" import { ccc } from "@ckb-ccc/connector"; ``` ```html title="index.html" ``` ```bash npm install @ckb-ccc/ccc ``` ```bash yarn add @ckb-ccc/ccc ``` ```bash pnpm add @ckb-ccc/ccc ``` Use `@ckb-ccc/ccc` when you want full control over the wallet connection UI. It includes all wallet integrations — EIP-6963 (EVM), JoyID, NIP-07 (Nostr), OKX, UniSat, UTXO Global, Xverse, and more — without any pre-built UI: ```typescript title="custom-ui.ts" import { ccc } from "@ckb-ccc/ccc"; const client = new ccc.ClientPublicTestnet(); const controller = new ccc.SignersController(); let wallets: ccc.WalletWithSigners[] | undefined; // Fetch all available signers await controller.refresh(client, (w) => (wallets = w)); if (!wallets) { throw new Error("Unexpected not wallets"); } wallets.forEach((wallet) => { console.log( wallet.name, wallet.signers.map(({ name }) => name), ); }); const signer = wallets[0].signers[0].signer; // Connect signer await signer.connect(); console.log("Connected"); // Sign message as test const signature = await signer.signMessage("Hello world"); console.log(signature); ``` For a detailed walkthrough, see the [Connect wallets](../guides/connect-wallets) guide.
## Import patterns [#import-patterns] Every CCC package exposes the same `ccc` namespace, so your import looks identical regardless of which package you're using: ```typescript import { ccc } from "@ckb-ccc/"; const tx = ccc.Transaction.from({ ... }); const amount = ccc.fixedPointFrom("100"); ``` For lower-level internals, the `/advanced` entry point exports `cccA`: ```typescript import { cccA } from "@ckb-ccc//advanced"; ``` `cccA` APIs are unstable and may change between versions without notice. Only reach for them when `ccc` doesn't cover your use case. ## TypeScript configuration [#typescript-configuration] CCC uses [Package Entry Points](https://nodejs.org/api/packages.html#packages_package_entry_points) for tree-shaking. Set `moduleResolution` to `node16`, `nodenext`, or `bundler` in your `tsconfig.json`, and don't disable `resolvePackageJsonExports`: ```json title="tsconfig.json" { "compilerOptions": { "moduleResolution": "bundler" } } ``` If you see `Property '*' does not exist on type 'typeof import(".../@ckb-ccc/...")'`, your `moduleResolution` is likely misconfigured. See the [TypeScript module resolution docs](https://www.typescriptlang.org/docs/handbook/modules/reference.html#packagejson-exports) for details. # Introduction (https://docs.ckbccc.com/en/docs/getting-started/introduction) CKB is a UTXO-based blockchain that natively supports Bitcoin and EVM wallets — making it uniquely suited for cross-chain applications. CCC is the TypeScript SDK that makes building on CKB approachable: it handles wallet connections, transaction composition, and cross-chain signing, so you can focus on your product instead of the plumbing. ## What you can do with CCC [#what-you-can-do-with-ccc] * **Explore CKB hands-on**: Run live code examples in the browser — no setup needed. * **Query the chain**: Fetch cells, track assets, and process blockchain data with a typed API. * **Compose transactions**: Build transactions with an intuitive API — CCC handles input selection and fee calculation for you. * **Sign messages**: Use a single `Signer` interface that works across EVM, Bitcoin, Nostr, and more — regardless of wallet type or chain. * **Connect wallets**: Drop in the connector component and go, or build a fully custom wallet UI for React or any framework. ## Who uses CCC [#who-uses-ccc] CCC powers production applications across the CKB ecosystem — including [NervDAO](https://nervdao.com/), [UTXO Global](https://utxo.global/), [Mobit](https://mobit.app/), [Omiga](https://omiga.io/), [Nervape](https://www.nervape.com/), [UTXOSwap](https://utxoswap.xyz/), [D.ID](https://d.id/), [Bool Network](https://bool.network/), [World3](https://world3.ai/), and more. If you're building something new on CKB, you're in good company. ## Ecosystem links [#ecosystem-links] * [CCC Playground](https://live.ckbccc.com/) — Run CCC code instantly in your browser, with live data and shareable snippets. * [CCC App](https://app.ckbccc.com/) — A mini-toolset showcasing common CKB scenarios. * [API Reference](https://api.ckbccc.com) — Full TypeScript API documentation. * [Nervos CKB Docs](https://docs.nervos.org/) — Background knowledge on the CKB blockchain. * [RGB++ SDK](https://github.com/rgbplusplus/rgbpp-sdk) — Issue assets on BTC L1 backed by CKB's VM. * [Spore SDK](https://github.com/sporeprotocol/spore-sdk) — On-chain digital objects (DOBs) protocol. [Lumos](https://github.com/ckb-js/lumos) is no longer actively maintained. We recommend CCC for all new projects. ## Next steps [#next-steps] Send your first CKB transaction on-chain with CCC — in under 5 minutes. Install CCC for React, Node.js, Web Component, or custom UI. Understand the building blocks behind every CKB app — and how they fit together. Find the recipe for what you're trying to build — step-by-step. # Quick start (https://docs.ckbccc.com/en/docs/getting-started/quick-start) Try CCC directly in the [Playground](https://live.ckbccc.com/) — run examples in your browser with no installation required. The fastest way to get started is with `create-ccc-app`. It scaffolds a new project with your preferred framework in one command: ```bash npx npx create-ccc-app@latest my-ccc-app ``` ```bash yarn yarn create ccc-app my-ccc-app ``` ```bash pnpm pnpm create ccc-app my-ccc-app ``` Follow the prompts to pick a framework template, then open the project directory. Already have a React app? Install the connector package directly: ```bash npm install @ckb-ccc/connector-react ``` ```bash yarn add @ckb-ccc/connector-react ``` ```bash pnpm add @ckb-ccc/connector-react ``` Using Node.js or a different framework? See the [Installation](./installation) guide for all available packages. Add `ccc.Provider` at the root of your app. It mounts the wallet connector UI and makes the CCC context available to all child components. Add `"use client"` at the top of any file that uses `ccc.Provider`. `ccc.Provider` connects to **testnet** by default. Pass a `defaultClient` prop to switch to mainnet before going to production. ```tsx title="app.tsx" "use client"; import { ccc } from "@ckb-ccc/connector-react"; export default function App({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` | Prop | Type | Description | | ------------------- | ------------------------- | ----------------------------------------------------- | | `name` | `string` | Your app's name, shown in the connector UI. | | `icon` | `string` | URL to your app's icon. | | `signerFilter` | `function` | Filter which wallets are available to the user. | | `defaultClient` | `ccc.Client` | Override the default CKB client (testnet by default). | | `preferredNetworks` | `ccc.NetworkPreference[]` | Preferred networks for wallet connections. | Use the `useCcc` hook inside any component to open the wallet modal and access the connected signer: ```tsx title="component.tsx" "use client"; import { ccc } from "@ckb-ccc/connector-react"; export function ConnectButton() { const { open, wallet, signerInfo, disconnect } = ccc.useCcc(); if (signerInfo) { return (

Connected: {wallet?.name}

); } return ; } ``` | Value | Type | Description | | ------------ | ----------------------------- | ------------------------------------- | | `open` | `() => void` | Opens the wallet connector modal. | | `close` | `() => void` | Closes the connector modal. | | `disconnect` | `() => void` | Disconnects the active wallet. | | `wallet` | `ccc.Wallet \| undefined` | The connected wallet object, if any. | | `signerInfo` | `ccc.SignerInfo \| undefined` | The active signer info, if connected. | | `client` | `ccc.Client` | The active CKB client. |
With a signer in hand, you're ready to send CKB. Describe what you want in the output — CCC handles input selection and fee calculation: ```typescript title="transfer.ts" import { ccc } from "@ckb-ccc/connector-react"; async function transfer(signer: ccc.Signer, toAddress: string, amount: string) { // Resolve the recipient's lock script from their address const { script: toLock } = await ccc.Address.fromString( toAddress, signer.client, ); // Declare what the transaction should produce const tx = ccc.Transaction.from({ outputs: [{ lock: toLock, capacity: ccc.fixedPointFrom(amount) }], }); // CCC selects input cells to cover the outputs await tx.completeInputsByCapacity(signer); // CCC calculates the fee and adjusts the change output await tx.completeFeeBy(signer); // Sign and broadcast — returns the transaction hash const txHash = await signer.sendTransaction(tx); console.log("Transaction sent:", txHash); return txHash; } ``` Three calls do all the heavy lifting: 1. `completeInputsByCapacity(signer)` — finds input cells that cover your outputs. 2. `completeFeeBy(signer)` — calculates the fee and adjusts inputs accordingly. 3. `signer.sendTransaction(tx)` — signs and broadcasts the transaction, returning its hash.
## What's next [#whats-next] * Explore the [Core Concepts](../concepts) to understand the building blocks behind every CKB app. * Check the [Installation](./installation) guide for all package options and TypeScript configuration. * Open the [CCC Playground](https://live.ckbccc.com/) to experiment with live examples in your browser. * Browse the [API Reference](https://api.ckbccc.com) for the full list of available types and methods. # Compose Transactions (https://docs.ckbccc.com/en/docs/guides/compose-transactions) Build and send CKB transactions without manually selecting inputs or computing fees. You declare the desired **outputs**, and CCC automatically finds matching inputs, calculates the fee, and creates change — all in a few method calls. ## What you'll get [#what-youll-get] After this guide you'll be able to: * **Transfer CKB** to any address with automatic input selection and fee calculation * **Sweep an entire wallet** into a single output * Understand the **declare → fill → fee → send** pattern that every CCC transaction follows All examples assume you already have a connected `signer`. If you don't, see [Connect Wallets](/docs/guides/connect-wallets) first. ## Core concepts [#core-concepts] On CKB, every output must declare its **capacity** in shannon (1 CKB = 10⁸ shannon). Use `ccc.fixedPointFrom(amount)` to convert a human-readable CKB amount to the internal representation: ```typescript ccc.fixedPointFrom(100) // 100 CKB → 10_000_000_000n shannon ccc.fixedPointFrom("0.01") // 0.01 CKB → 1_000_000n shannon ``` ## Transfer CKB [#transfer-ckb] **Use this when:** you want to send a specific amount of CKB to another address — the most common transaction type. The four steps below embody the **declare → fill → fee → send** pattern you'll reuse for every CKB transaction: Create a transaction skeleton with just the outputs you want. CCC will figure out which inputs are needed. ```typescript import { ccc } from "@ckb-ccc/ccc"; const receiver = await signer.getRecommendedAddress(); const { script: lock } = await ccc.Address.fromString(receiver, signer.client); const tx = ccc.Transaction.from({ outputs: [{ capacity: ccc.fixedPointFrom(100), lock }], }); ``` `completeInputsByCapacity` queries live cells owned by the signer and adds them as inputs until the total capacity covers all outputs. ```typescript await tx.completeInputsByCapacity(signer); ``` `completeFeeBy` adds a change output back to the signer and adjusts its capacity to cover the network fee. The fee rate is calculated automatically based on current network conditions. ```typescript await tx.completeFeeBy(signer); ``` ```typescript const txHash = await signer.sendTransaction(tx); console.log("Transaction hash:", txHash); // "0x..." — 32-byte hex hash ``` ## Transfer all CKB [#transfer-all-ckb] **Use this when:** you want to empty a wallet completely — migrate to a new address, consolidate cells, etc. Instead of specifying a fixed capacity, you collect *all* inputs first and deduct the fee from the output: ```typescript import { ccc } from "@ckb-ccc/ccc"; const receiver = await signer.getRecommendedAddress(); const { script: lock } = await ccc.Address.fromString(receiver, signer.client); // No capacity set — we'll fill it after collecting all inputs const tx = ccc.Transaction.from({ outputs: [{ lock }], }); // Collect every cell owned by the signer as inputs await tx.completeInputsAll(signer); // Deduct fee from output 0 and send all remaining CKB there await tx.completeFeeChangeToOutput(signer, 0); const txHash = await signer.sendTransaction(tx); ``` `completeFeeChangeToOutput(signer, 0)` adjusts the capacity of the output at index `0` downward to pay the fee. Make sure that output exists before calling this method. ## Fee calculation [#fee-calculation] CCC auto-fetches the current median fee rate from the node — you never need to set it manually. Both `completeFeeBy` and `completeFeeChangeToOutput` handle this for you. If you need fine-grained control (e.g. priority transactions), pass an explicit fee rate in **shannon per kilobyte**: ```typescript await tx.completeFeeBy(signer, 2000); // fixed rate of 2000 shannon/kB ``` ## API reference [#api-reference] | Method | Description | | ------------------------------------------------------- | ----------------------------------------------- | | `ccc.Transaction.from(skeleton)` | Create a transaction from a partial description | | `ccc.fixedPointFrom(amount)` | Convert CKB amount to shannon (`bigint`) | | `tx.completeInputsByCapacity(signer)` | Add inputs until capacity is sufficient | | `tx.completeInputsAll(signer)` | Collect all cells owned by the signer | | `tx.completeFeeBy(signer, feeRate?)` | Add change output and pay fee | | `tx.completeFeeChangeToOutput(signer, index, feeRate?)` | Deduct fee from a specific output | | `signer.sendTransaction(tx)` | Sign and broadcast; returns tx hash | ## Troubleshooting [#troubleshooting] **`completeInputsByCapacity` throws "not enough capacity"** The signer's cells don't have enough total CKB to cover the outputs plus minimum change cell (61 CKB). Fund the address or reduce the output amount. **Transaction is rejected with "InsufficientCellCapacity"** Every cell must hold at least enough CKB to pay for its own on-chain storage (the minimum is 61 CKB for a basic cell). Make sure each output's `capacity` meets this minimum. **I want to add custom cell deps or witnesses** `ccc.Transaction.from()` accepts a full `TransactionLike` — you can pass `cellDeps`, `headerDeps`, `witnesses`, and `inputs` alongside `outputs`. CCC's auto-fill methods append to, not replace, what you provide. ## Next steps [#next-steps] * [Sign messages](/docs/guides/sign-message) — prove address ownership without sending a transaction. * [UDT tokens](/docs/guides/udt-tokens) — issue and transfer fungible tokens on CKB. * [Spore Protocol](/docs/guides/spore-protocol) — create on-chain digital objects (DOBs). Try these examples interactively in the CCC playground at [live.ckbccc.com](https://live.ckbccc.com/). # Connect Wallets (https://docs.ckbccc.com/en/docs/guides/connect-wallets) Connect a user's wallet to your CKB app and obtain a `Signer` that can read addresses, fetch balances, sign messages, and broadcast transactions — across **CKB, EVM, BTC, Nostr, and Doge** ecosystems through a single unified API. ## What you'll get [#what-youll-get] After this guide you'll have: * A working **Connect Wallet button** wired to a multi-chain wallet modal * A `ccc.Signer` instance ready for use with the [transaction](/docs/guides/compose-transactions) and [signing](/docs/guides/sign-message) guides * Full control over which wallets appear, which network they connect to, and whether you use the built-in UI or your own ## Choose your integration path [#choose-your-integration-path] | Your situation | Use this approach | | ------------------------------------------------------------------- | ----------------------------------------------------------- | | Building a React app and want a polished modal out of the box | [`ccc.Provider` + `useCcc()`](#react-integration) | | Building a React app but want a custom UI for wallet selection | [`SignersController`](#custom-ui-with-signerscontroller) | | Hard-coding a single wallet (e.g. JoyID-only dApp, no selection UI) | [Direct signer instantiation](#direct-signer-instantiation) | | Server-side / Node.js with a private key (no user wallet) | See [Node.js Backend](/docs/guides/node-js-backend) | All examples below assume you've installed `@ckb-ccc/connector-react`. The non-React `SignersController` and direct-signer flows work with `@ckb-ccc/ccc` as well. ## Installation [#installation] ```bash npm npm install @ckb-ccc/connector-react ``` ```bash yarn yarn add @ckb-ccc/connector-react ``` ```bash pnpm pnpm add @ckb-ccc/connector-react ``` ## React Integration [#react-integration] The fastest way to ship a working Connect Wallet flow. Two pieces: 1. **`ccc.Provider`** — wraps your app and owns the connector modal + wallet state 2. **`ccc.useCcc()` / `ccc.useSigner()`** — read state and trigger connect/disconnect from any child component Place this at the root of your component tree (e.g. `app/layout.tsx` in Next.js, or your top-level `App.tsx`). For React Server Components (Next.js App Router), add `"use client"` at the top of the file. The connector UI is client-only. ```tsx "use client"; import { ccc } from "@ckb-ccc/connector-react"; export default function App({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` Anywhere inside the `Provider` tree, call `ccc.useCcc()` to open the modal and read the connection state. ```tsx "use client"; import { ccc } from "@ckb-ccc/connector-react"; export function ConnectButton() { const { open, disconnect, wallet, signerInfo } = ccc.useCcc(); return signerInfo ? ( ) : ( ); } ``` Once connected, `ccc.useSigner()` returns a ready-to-use `Signer` that you can pass to any CCC API. ```tsx "use client"; import { useEffect, useState } from "react"; import { ccc } from "@ckb-ccc/connector-react"; export function Balance() { const signer = ccc.useSigner(); const [balance, setBalance] = useState(); useEffect(() => { if (!signer) return; signer.getBalance().then(setBalance); // total CKB balance in shannon (bigint) }, [signer]); if (!signer) return

Not connected

; return

Balance: {ccc.fixedPointToString(balance ?? 0n)} CKB

; } ``` Next: use the same `signer` to [compose transactions](/docs/guides/compose-transactions) or [sign messages](/docs/guides/sign-message).
### `ccc.Provider` props [#cccprovider-props] | Prop | Type | Description | | ------------------- | ---------------------------------------------------------------------- | ------------------------------------------------ | | `children` | `ReactNode` | Child components | | `hideMark` | `boolean` | Hide the CCC watermark | | `name` | `string` | Your app name displayed in the connector | | `icon` | `string` | Your app icon URL displayed in the connector | | `signerFilter` | `(signerInfo: ccc.SignerInfo, wallet: ccc.Wallet) => Promise` | Filter which wallets/signers are shown | | `signersController` | `ccc.SignersController` | Custom signers controller instance | | `defaultClient` | `ccc.Client` | Default CKB client (testnet or mainnet) | | `clientOptions` | `{ icon?: string; client: ccc.Client; name: string }[]` | Network switching options shown in the connector | | `preferredNetworks` | `ccc.NetworkPreference[]` | Preferred networks per signer type | ## Hooks reference [#hooks-reference] Both hooks must be called from a component rendered inside `ccc.Provider`. ### `ccc.useCcc()` [#cccuseccc] Returns the full wallet state and control functions. Use it when you need to control the modal, switch networks, or render UI that depends on connection state. | Return value | Type | Description | | ------------ | ------------------------------ | ------------------------------------------------------ | | `isOpen` | `boolean` | Whether the connector modal is currently open | | `open` | `() => void` | Open the connector modal | | `close` | `() => void` | Close the connector modal | | `disconnect` | `() => void` | Disconnect the current wallet | | `setClient` | `(client: ccc.Client) => void` | Switch to a different CKB client (mainnet/testnet/RPC) | | `client` | `ccc.Client` | Current CKB client (defaults to `ClientPublicTestnet`) | | `wallet` | `ccc.Wallet \| undefined` | Currently connected wallet (`name`, `icon`) | | `signerInfo` | `ccc.SignerInfo \| undefined` | Current signer info (`name`, `signer`) | ### `ccc.useSigner()` [#cccusesigner] Shortcut for `useCcc().signerInfo?.signer`. Use it when all you need is the active `Signer`. Returns `ccc.Signer | undefined` (undefined when no wallet is connected). ```tsx const signer = ccc.useSigner(); const address = await signer?.getRecommendedAddress(); ``` ## Filter which wallets are shown [#filter-which-wallets-are-shown] **Use this when:** you only want to support certain chains or hide wallets that don't fit your app. `signerFilter` runs for every discovered `(wallet, signer)` pair. Return `true` to keep it, `false` to hide it. ```tsx { // Only show CKB-native wallets — hide BTC/EVM/Nostr/Doge return signerInfo.signer.type === ccc.SignerType.CKB; }} > {children} ``` `ccc.SignerType` values: `CKB`, `EVM`, `BTC`, `Nostr`, `Doge`. ## Force a specific network per wallet type [#force-a-specific-network-per-wallet-type] **Use this when:** your dApp targets, for example, CKB mainnet and you want users' BTC wallet to also be on Bitcoin mainnet (not testnet/signet). When the user's wallet is on the wrong network, CCC will prompt it to switch automatically. ```tsx {children} ``` `NetworkPreference` type: ```typescript type NetworkPreference = { addressPrefix: string; // "ckb" for mainnet, "ckt" for testnet signerType: SignerType; network: string; // BTC network values: "btc" | "btcTestnet" | "btcTestnet4" | "btcSignet" | "fractalBtc" }; ``` ## Custom UI with `SignersController` [#custom-ui-with-signerscontroller] **Use this when:** you want full control over the wallet picker UI (your own modal, button styles, ordering) without giving up CCC's wallet discovery. `SignersController` discovers every installed wallet extension and exposes them as a list of `WalletWithSigners`. You render the UI; CCC handles detection. ```typescript import { ccc } from "@ckb-ccc/ccc"; const controller = new ccc.SignersController(); let wallets: ccc.WalletWithSigners[] | undefined; // Discover all installed wallets and their available signers await controller.refresh(client, (w) => (wallets = w)); if (!wallets) throw new Error("No wallets discovered"); wallets.forEach((wallet) => { console.log( wallet.name, wallet.signers.map(({ name }) => name), ); }); // Pick the first signer (in real code: let the user choose) const signer = wallets[0].signers[0].signer; await signer.connect(); const signature = await signer.signMessage("Hello world"); console.log(signature); ``` ## Direct signer instantiation [#direct-signer-instantiation] **Use this when:** your dApp only ever uses one specific wallet (e.g. JoyID-only) and you want to skip wallet discovery entirely. ```typescript import { ccc } from "@ckb-ccc/ccc"; // Pass your app's name and icon — JoyID shows them in its sign-in popup const signer = new ccc.JoyId.CkbSigner(client, "CCC", "https://fav.farm/🇨"); await signer.connect(); const signature = await signer.signTransaction({}); // signs an empty tx as a smoke test console.log(signature); ``` Other directly-instantiable signers live under `ccc.JoyId`, `ccc.UniSat`, `ccc.Okx`, `ccc.Xverse`, `ccc.UtxoGlobal`, `ccc.Eip6963`, `ccc.Nip07`, `ccc.Rei`. ## Supported Ecosystems Matrix [#supported-ecosystems-matrix] CCC bridges multiple distinct chain cryptography types into CKB. Here is the support matrix: | Wallet | Signer Ecosystems | | ------------------- | ----------------------- | | JoyID | CKB / BTC / EVM / Nostr | | OKX | BTC / EVM / Nostr | | UniSat | BTC | | UTXO Global | CKB / BTC / DOGE | | Xverse | BTC | | MetaMask / EIP-6963 | EVM | | Nostr (NIP-07) | Nostr | | REI | CKB | ## Which package should I import from? [#which-package-should-i-import-from] ```typescript import { ccc } from "@ckb-ccc/connector-react"; // React apps (re-exports everything below + hooks/Provider) import { ccc } from "@ckb-ccc/ccc"; // Browser, custom UI, no React import { ccc } from "@ckb-ccc/shell"; // Node.js backend (no UI / no DOM-only signers) ``` ## Troubleshooting [#troubleshooting] **`useCcc` / `useSigner` returns `undefined` for the signer** The user hasn't connected yet, or the component is rendered outside `ccc.Provider`. Always render hooks inside the `Provider` tree, and gate signer-dependent code on `if (!signer) return`. **The connector modal renders blank or is missing styles in Next.js** You're likely missing `"use client"` on the file that uses `ccc.Provider` or any hook. CCC's UI is client-only. **A wallet I expect to see is missing from the modal** Check that: * The browser extension is installed and unlocked. * Your `signerFilter` (if any) isn't filtering it out. * For EVM wallets, the wallet must implement EIP-6963 (MetaMask and most modern wallets do). **The user's BTC/EVM wallet is on the wrong network** Configure [`preferredNetworks`](#force-a-specific-network-per-wallet-type) so CCC prompts the wallet to switch. **Connection state is lost on page reload** CCC persists the last connection in `localStorage` and restores it on `Provider` mount. If it isn't restoring, make sure the `Provider` is mounted on every page (typically in your root layout). ## Next steps [#next-steps] * [Compose transactions](/docs/guides/compose-transactions) — use the connected `signer` to send CKB and pay fees automatically. * [Sign messages](/docs/guides/sign-message) — prove address ownership for off-chain auth. * [Node.js backend](/docs/guides/node-js-backend) — sign with a private key on the server side. # Guides (https://docs.ckbccc.com/en/docs/guides) These guides assume you already understand the [core concepts](./concepts) and have CCC installed. Each guide walks through a single, real-world task end to end, with copy-pasteable code drawn from the CCC examples and packages. ## Pages in this section [#pages-in-this-section] Drop in the React connector, or wire up `SignersController` directly to support EVM, BTC, CKB, Nostr, and Doge wallets. Transfer CKB the canonical CCC way — declare outputs, auto-fill inputs by capacity, let `completeFeeBy` handle the fee. Sign and verify arbitrary messages through the unified `signer.signMessage` / `verifyMessage` API across all wallet types. Issue and transfer xUDT (and legacy sUDT) tokens with the `ccc.udt.Udt` class and SSRI execution. Create, transfer, and melt on-chain digital objects (DOBs), and group them with Spore Clusters using `@ckb-ccc/spore`. Use `@ckb-ccc/shell` for server-side scripts: connect to a node, build private-key signers, and run CCC without a browser. # Node.js Backend (https://docs.ckbccc.com/en/docs/guides/node-js-backend) Use CCC from a Node.js server, script, or CI job — with a **private key signer** instead of a browser wallet. The server-side package `@ckb-ccc/shell` gives you the full CCC API (transactions, UDT, Spore, SSRI) without any browser or DOM dependencies. ## What you'll get [#what-youll-get] After this guide you'll be able to: * **Connect to mainnet / testnet** from a Node.js process * **Sign transactions with a private key** for backend automation, airdrops, or indexing jobs * **Query balances and iterate cells** programmatically * Reuse the same [transaction composition](/docs/guides/compose-transactions) and [message signing](/docs/guides/sign-message) patterns from the frontend guides — the API is identical ## Installation [#installation] ```bash npm install @ckb-ccc/shell ``` ## What it exports [#what-it-exports] `@ckb-ccc/shell` re-exports the following: ```typescript export * from "@ckb-ccc/core/barrel"; // full CCC core (Transaction, Script, Address, ...) export { spore } from "@ckb-ccc/spore"; export { ssri } from "@ckb-ccc/ssri"; export { udt } from "@ckb-ccc/udt"; ``` Import everything through the `ccc` namespace: ```typescript import { ccc } from "@ckb-ccc/shell"; ``` ## Connect to a node [#connect-to-a-node] Use `ClientPublicMainnet` or `ClientPublicTestnet` for zero-config connectivity. Both default to a public RPC endpoint with automatic WebSocket / HTTP fallbacks — no setup required: ```typescript import { ccc } from "@ckb-ccc/shell"; const client = new ccc.ClientPublicMainnet(); // Defaults to wss://mainnet.ckb.dev/ws (Node.js with WebSocket) // Falls back to https://mainnet.ckb.dev/ and https://mainnet.ckbapp.dev/ ``` ```typescript import { ccc } from "@ckb-ccc/shell"; const client = new ccc.ClientPublicTestnet(); ``` ```typescript import { ccc } from "@ckb-ccc/shell"; const client = new ccc.ClientPublicMainnet({ url: "https://your-ckb-node.example.com/rpc", }); ``` ## Sign with a private key [#sign-with-a-private-key] **Use this when:** your backend needs to send transactions autonomously (e.g. airdrops, payouts, automated operations). `SignerCkbPrivateKey` provides a fully featured `Signer` backed by a raw secp256k1 private key. ```typescript import { ccc } from "@ckb-ccc/shell"; const client = new ccc.ClientPublicTestnet(); const signer = new ccc.SignerCkbPrivateKey( client, process.env.CKB_PRIVATE_KEY!, // 32-byte hex, loaded from environment ); await signer.connect(); const address = await signer.getRecommendedAddress(); console.log("Address:", address); // "ckt1qz..." (testnet) or "ckb1qz..." (mainnet) ``` Never hardcode private keys in source code. Load them from environment variables, a secrets manager, or an encrypted key file. A leaked private key means permanent, irrecoverable loss of funds. ## Query balance [#query-balance] **Use this when:** you need to check an account's CKB balance from a script or API handler. ```typescript import { ccc } from "@ckb-ccc/shell"; const client = new ccc.ClientPublicMainnet(); const signer = new ccc.SignerCkbPrivateKey(client, process.env.CKB_PRIVATE_KEY!); await signer.connect(); const balance = await signer.getBalance(); // balance is a bigint in shannon (1 CKB = 100_000_000 shannon) console.log(`Balance: ${ccc.fixedPointToString(balance)} CKB`); // e.g. "1234.56 CKB" ``` ## Complete example: query balance and send CKB [#complete-example-query-balance-and-send-ckb] ```typescript import { ccc } from "@ckb-ccc/shell"; async function main() { // 1. Connect to testnet const client = new ccc.ClientPublicTestnet(); const signer = new ccc.SignerCkbPrivateKey(client, process.env.CKB_PRIVATE_KEY!); await signer.connect(); const fromAddress = await signer.getRecommendedAddress(); console.log("From:", fromAddress); // 2. Query balance const balance = await signer.getBalance(); console.log(`Balance: ${ccc.fixedPointToString(balance)} CKB`); // 3. Resolve recipient address const recipientAddress = "ckt1qzda0cr08m85hc8jlnfp3sog3vczr9h9rqt4ykkqfzf3gcj…"; const { script: lock } = await ccc.Address.fromString( recipientAddress, signer.client, ); // 4. Build transaction const tx = ccc.Transaction.from({ outputs: [{ capacity: ccc.fixedPointFrom(100), lock }], }); // 5. Fill inputs and pay fee automatically await tx.completeInputsByCapacity(signer); await tx.completeFeeBy(signer); // 6. Sign and broadcast const txHash = await signer.sendTransaction(tx); console.log("Sent:", txHash); } main().catch(console.error); ``` ## Iterate cells [#iterate-cells] **Use this when:** you need to scan an address's live cells — for analytics, indexing, or building custom transaction logic. ```typescript for await (const cell of signer.findCellsOnChain( {}, // no type/data filter — return all cells true, // include cell data )) { console.log(cell.outPoint, cell.cellOutput.capacity); } ``` ## Advanced barrel [#advanced-barrel] The package also exports an `advanced` entry point with lower-level utilities under the `cccA` namespace: ```typescript import { cccA } from "@ckb-ccc/shell/advanced"; // Includes sporeA (advanced Spore helpers) and core advanced utilities ``` ## TypeScript configuration [#typescript-configuration] `@ckb-ccc/shell` ships ES modules. If you encounter `ERR_REQUIRE_ESM` or module resolution errors, add this to your `tsconfig.json`: ```json { "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext" } } ``` ## Troubleshooting [#troubleshooting] **`ERR_REQUIRE_ESM` when importing `@ckb-ccc/shell`** Your project is using CommonJS (`require`). Either switch to ESM (`"type": "module"` in `package.json`) or use dynamic `import()`. CCC does not ship a CJS build. **`SignerCkbPrivateKey` throws "invalid private key"** The key must be exactly 32 bytes (64 hex characters), without `0x` prefix. Double-check your environment variable. **`ClientPublicTestnet` connection times out** Public nodes may be rate-limited. For production, run your own CKB node and pass its URL to `ClientPublicMainnet({ url: "..." })`. **I want to use browser-wallet signers (JoyID, MetaMask) on the server** Those signers require a browser environment. On the server, use `SignerCkbPrivateKey`. If you need to verify wallet signatures on the server, use `ccc.Signer.verifyMessage()` — see [Sign Messages](/docs/guides/sign-message). ## Next steps [#next-steps] * [Compose transactions](/docs/guides/compose-transactions) — the same `declare → fill → fee → send` pattern works with `SignerCkbPrivateKey`. * [UDT tokens](/docs/guides/udt-tokens) — issue and transfer fungible tokens from a backend. * [Spore Protocol](/docs/guides/spore-protocol) — mint on-chain digital objects programmatically. # CCC Playground (https://docs.ckbccc.com/en/docs/guides/playground) The [CCC Playground](https://live.ckbccc.com/) is an in-browser IDE where you can write, execute, and visualize CKB transactions — no local setup required. It is the fastest way to learn CCC and experiment with CKB. ## What you'll get [#what-youll-get] After this guide you'll be able to: * Understand how the Playground works and how to use it * Use the Playground for debugging and visualizing CKB transactions * Write, run, and share CKB transaction scripts in the browser * Build and send transactions using the CCC SDK ## Interface overview [#interface-overview] Playground overview The Playground is split into two panels: * **Left panel** — a Monaco (VS Code) editor with full TypeScript IntelliSense for CCC types * **Right panel** — a Console that displays logs, errors, and interactive transaction visualizations ### Toolbar buttons [#toolbar-buttons] | Button | Description | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **Testnet / Mainnet** | Toggle between CKB testnet and mainnet | | **Format** | Auto-format your code with Prettier | | **Run** | Execute the code and auto-step through `render()` breakpoints | | **Step** | Execute in debug mode — pause at each `render()` and click **Continue** to advance | | **Share** | Publish your code to Nostr relays and generate a shareable URL | | **Connect** | Open the wallet connector to sign and send real transactions. Once connected, this button will display the CKB address of the connected wallet. | | **Console** | Show the Console tab | | **Clear** | Clear all console output | | **About** | Show links to docs, GitHub, faucet, and explorer | ## Available imports [#available-imports] Every script in the Playground runs in a sandboxed TypeScript environment. The following modules are available: ```typescript import { ccc } from "@ckb-ccc/ccc"; // Core CCC SDK import { render, signer, client } from "@ckb-ccc/playground"; // Playground helpers ``` ### `@ckb-ccc/playground` exports [#ckb-cccplayground-exports] | Export | Type | Description | | ----------------- | --------------- | --------------------------------------------------------------------------------- | | `signer` | `ccc.Signer` | The active signer — either the connected wallet or a built-in read-only key | | `client` | `ccc.Client` | The active CKB client (testnet or mainnet) | | `render(...args)` | `Promise` | Log values to the Console and pause execution (acts as a breakpoint in Step mode) | You can also use `console.log()` and `console.error()` — they output to the Console panel. If no wallet is connected, `signer` defaults to a built-in public key signer. You can read chain data and build transactions, but sending them requires a connected wallet. After understanding the basic interface and available tools, let's explore the core capabilities of the Playground through practical examples. ## Example 1 — Transfer CKB [#example-1--transfer-ckb] This is the default example that ships with the Playground. It demonstrates the **declare → fill → fee → send** pattern. Open the Playground and paste this code (or simply use the default): ```typescript import { ccc } from "@ckb-ccc/ccc"; import { render, signer } from "@ckb-ccc/playground"; console.log("Welcome to CCC Playground!"); // The receiver is the signer itself on mainnet const receiver = signer.client.addressPrefix === "ckb" ? await signer.getRecommendedAddress() : "ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqflz4emgssc6nqj4yv3nfv2sca7g9dzhscgmg28x"; console.log(receiver); // Parse the receiver script from an address const { script: lock } = await ccc.Address.fromString( receiver, signer.client, ); // Describe what we want const tx = ccc.Transaction.from({ outputs: [ { capacity: ccc.fixedPointFrom(100), lock }, ], }); await render(tx); // Complete missing parts: Fill inputs await tx.completeInputsByCapacity(signer); await render(tx); // Complete missing parts: Pay fee await tx.completeFeeBy(signer, 1000); await render(tx); ``` ### What happens when you click **Run** [#what-happens-when-you-click-run] 1. The code creates a transaction with one output of 100 CKB 2. `render(tx)` pauses and displays the transaction in the Console — you'll see the output Cell but no inputs yet 3. `completeInputsByCapacity` finds live cells to cover 100 CKB, then `render(tx)` shows the transaction again — now with inputs 4. `completeFeeBy` adds a change output and adjusts for the fee, then `render(tx)` shows the final transaction Click **Step** instead of **Run** to pause at each `render()` call. Click **Continue** to advance to the next breakpoint. This is useful for studying how CCC builds a transaction step by step. Now that we've mastered the basic transfer flow, let's explore a more complex scenario — issuing a token on chain. ## Example 2 — Issue a custom UDT token [#example-2--issue-a-custom-udt-token] This example shows how to issue a UDT (User Defined Token) — a fungible token on CKB: ```typescript import { ccc } from "@ckb-ccc/ccc"; import { render, signer } from "@ckb-ccc/playground"; // Get the signer's lock script — this will be the token owner const lock = (await signer.getRecommendedAddressObj()).script; console.log("Token owner lock:", lock); // Build a transaction with the UDT output const tx = ccc.Transaction.from({ outputs: [{ lock }], outputsData: [ccc.numLeToBytes(1000000, 16)], // initial supply: 1,000,000 tokens }); await render(tx); // The Type script for xUDT uses the first input's outPoint as a unique ID // We need to add inputs first, then set the Type script await tx.completeInputsByCapacity(signer); await render(tx); const firstInput = tx.inputs[0]; const typeScript = await ccc.Script.fromKnownScript( signer.client, ccc.KnownScript.XUdt, (await firstInput.getCell(signer.client)).cellOutput.lock.hash(), ); await render(tx); tx.outputs[0].type = typeScript; await render(tx); // Recalculate inputs (the Type script changes occupied capacity) await tx.completeFeeBy(signer, 1000); await render(tx); ``` **About sending transactions**: The above examples are for demonstration purposes only — they build the transaction to its final state without actually sending it. To broadcast the transaction to the chain, call `signer.sendTransaction(tx)` at the end: ```typescript // Sign and broadcast the transaction, returning the transaction hash const txHash = await signer.sendTransaction(tx); console.log("Transaction sent:", txHash); ``` Make sure you have connected a wallet and that the address has sufficient CKB balance. Playground's capabilities go far beyond building transactions. All methods provided by the CCC SDK can be run directly here, meaning you can write arbitrary scripts to perform various on-chain operations — for example, batch asset management, tracking large on-chain transactions, analyzing cell state distributions, and more. Below is a simple data query example. ## Example 3 — Query on-chain data [#example-3--query-on-chain-data] You can use the Playground to explore on-chain data without building any transaction: ```typescript import { ccc } from "@ckb-ccc/ccc"; import { signer, client } from "@ckb-ccc/playground"; // Get the current tip block number const tip = await client.getTip(); console.log("Current tip block:", tip.toString()); // Get the signer's address const address = await signer.getRecommendedAddress(); console.log("Signer address:", address); // Get the signer's balance const balance = await signer.getBalance(); console.log("Balance:", ccc.fixedPointToString(balance), "CKB"); ``` ## Understanding the Cell visualization [#understanding-the-cell-visualization] When `render()` receives a `ccc.Transaction`, the Console displays an interactive transaction diagram. Each Cell is drawn as a circular glyph with three visual layers, as shown in the following figure: Cell visualization ### Bagua — the outer and middle rings [#bagua--the-outer-and-middle-rings] Each Cell renders two concentric **Bagua** (eight-trigram) rings: * **Outer ring** — derived from the Cell's **Lock** Script * **Middle ring** — derived from the Cell's **Type** Script (if any) The trigram pattern is deterministic: `getScriptBagua(script)` extracts 24 bits from the script hash (`hash & 0xffffff000 >> 12`) and splits them into eight 3-bit trigrams. Each trigram draws three lines — a solid line for bit `0` and a broken line for bit `1` — following the classical trigram encoding (☰ Qian, ☷ Kun, etc.). The **color** of each ring is also derived from the script hash: `hash & 0xfff % 360` gives the HSL hue. This means: * Cells with the **same Lock** script share the same outer ring color and pattern * Cells with the **same Type** script share the same middle ring color and pattern * You can visually identify which Cells belong to the same owner or token type at a glance ### Taiji — the center circle [#taiji--the-center-circle] The center of each Cell displays a slowly spinning **Taiji** (yin-yang) symbol: * The **yang** (dark) side uses the Lock script color * The **yin** (light) side is translucent white * A small colored dot inside represents the **free capacity percentage** — how much capacity in this Cell is not occupied by data. Its diameter scales proportionally, and its color matches the Type script color This tells you at a glance how "full" a Cell is. ### Reading the numbers [#reading-the-numbers] Below the Taiji symbol you'll see: * **Capacity** in CKB (large number) * **DAO profit** (if the Cell is in NervosDAO, shown as `+ X CKB`) * A shortened **OutPoint** (`txHash:index`) linking to the CKB Explorer * **Data size** in bytes (if the Cell contains data) Click on any Cell to expand its full details: OutPoint, capacity, Lock script (address), Type script, and raw output data. ## Sharing code [#sharing-code] Click **Share** to publish your code to a set of Nostr relays. The Playground generates a `nevent` identifier and redirects to a URL like: ``` https://live.ckbccc.com/?src=nostr:nevent1... ``` Anyone who opens this URL will see your exact code loaded in the editor. Code is stored on decentralized Nostr relays — no centralized server involved. You can also load code from any URL by passing a `src` query parameter: ``` https://live.ckbccc.com/?src=https://raw.githubusercontent.com/sporeprotocol/dob-cookbook/refs/heads/main/examples/dob0/1.colorful-loot.ts ``` ## Tips [#tips] * **Use `render()` liberally** — it's your primary debugging tool. Pass a transaction at each stage to see how CCC mutates it. * **Step mode is your friend** — click **Step** to pause at each `render()`, inspect the state, then click **Continue**. Great for understanding the transaction lifecycle. * **Code persists in localStorage** — if you don't use a `?src=` URL, your code is saved automatically and restored when you return. * **Get testnet CKB** — click **About** → **CKB Testnet Faucet** to fund your testnet address, or visit [faucet.nervos.org](https://faucet.nervos.org/) directly. * **Full IntelliSense** — the editor provides autocomplete and type-checking for all CCC types, plus `@noble/curves`, `@noble/hashes`, and `@nervina-labs/dob-render`. * **Share to collaborate** — after writing a script, click **Share** to generate a link that others can open to reproduce your code environment. ## Important Notes [#important-notes] Before running any script, **make sure the network environment shown in the bottom-left corner** (Testnet or Mainnet) is correct. Operations on mainnet assets are irreversible. * **Testnet first, then Mainnet** — any script should be run and confirmed to behave as expected on the testnet before switching to the mainnet. Never operate on mainnet assets directly. * **Check connection status** — make sure the correct wallet and network are connected in the bottom-right corner before sending transactions. * **Minimum CKB capacity** — each Cell must have at least 61 CKB to cover on-chain storage fees. If the output capacity is set too low, the transaction will be rejected by the node. * **Transactions are irreversible** — once a transaction is confirmed on-chain, it cannot be undone. When operating on mainnet assets, it is recommended to carefully review the inputs and outputs of the transaction using `render()` first. * **Browser refresh** — code is automatically saved in localStorage, so refreshing the page will not lose your work. However, if a `?src=` URL is used, the code comes from an external source, and local modifications will not override the URL parameter. * **Execution timeout** — scripts in the Playground run in the browser's main thread. If you write long loops or make many RPC calls, it may cause the page to freeze. It is recommended to execute in steps. ## Next steps [#next-steps] * [Compose Transactions](/docs/guides/compose-transactions) — the full guide to building and sending transactions. * [UDT Tokens](/docs/guides/udt-tokens) — issue and transfer fungible tokens. * [Spore Protocol](/docs/guides/spore-protocol) — create on-chain digital objects. * [Connect Wallets](/docs/guides/connect-wallets) — integrate wallet connections into your own app. # Sign Messages (https://docs.ckbccc.com/en/docs/guides/sign-message) Sign an arbitrary message with the user's wallet and verify the signature later — useful for **off-chain authentication** ("Sign-In with Wallet"), **proving address ownership**, or **creating verifiable attestations**. The API is identical regardless of whether the user connected a CKB, EVM, BTC, JoyID, Nostr, or Doge wallet. ## What you'll get [#what-youll-get] After this guide you'll be able to: * **Sign a message** with any connected wallet (one unified call) * **Verify a signature** statically — no connected wallet required * Understand the `Signature` object and how `signType` enables **cross-chain verification** All examples assume you already have a connected `signer`. If you don't, see [Connect Wallets](/docs/guides/connect-wallets) first. ## The `Signature` type [#the-signature-type] `signer.signMessage` returns a `Signature` object defined in `packages/core/src/signer/signer/index.ts`: ```typescript class Signature { signature: string; // the raw signature hex identity: string; // signer identity (usually their address) signType: SignerSignType; } ``` The `signType` field tells verifiers which cryptographic scheme was used, enabling scheme-specific verification without the caller needing to know the wallet type ahead of time. ## `SignerSignType` enum [#signersigntype-enum] | Value | Wallet / scheme | | ----------------------------- | --------------------------------------- | | `SignerSignType.CkbSecp256k1` | CKB native secp256k1 wallets | | `SignerSignType.EvmPersonal` | EVM wallets (MetaMask, OKX EVM, etc.) | | `SignerSignType.BtcEcdsa` | Bitcoin wallets (UniSat, OKX BTC, etc.) | | `SignerSignType.JoyId` | JoyID passkey wallet | | `SignerSignType.NostrEvent` | Nostr clients | | `SignerSignType.DogeEcdsa` | Dogecoin wallets | | `SignerSignType.Unknown` | Unknown / unsupported type | ## Sign a message [#sign-a-message] One call — works with every wallet type CCC supports: ```typescript import { ccc } from "@ckb-ccc/ccc"; const message = "Hello world"; const signature = await signer.signMessage(message); console.log(signature.signature); // "0x..." — raw signature bytes as hex console.log(signature.identity); // signer's address or public key console.log(signature.signType); // e.g. "CkbSecp256k1", "EvmPersonal", "BtcEcdsa" ``` `signMessage` accepts either a plain `string` or a `BytesLike` (`Uint8Array` / hex string), so you can sign raw bytes when needed. ## Verify a signature [#verify-a-signature] Verification is a **static method** — you do not need a connected wallet. CCC dispatches to the correct cryptographic scheme automatically based on `signature.signType`: ```typescript const isValid = await ccc.Signer.verifyMessage(message, signature); // true — message + signature match const isFail = await ccc.Signer.verifyMessage("Wrong message", signature); // false — message doesn't match ``` This means your backend can verify signatures from *any* CCC-supported wallet without knowing which wallet the user used. ## Full sign + verify example [#full-sign--verify-example] End-to-end example from `packages/examples/src/sign.ts`. It handles the playground's default `SignerCkbPublicKey` (which cannot sign messages) by substituting a private-key signer for demonstration: ```typescript import { ccc } from "@ckb-ccc/ccc"; import { client, signer as playgroundSigner } from "@ckb-ccc/playground"; // The default playground signer cannot sign messages. // In a real app the connected wallet signer is always usable. const signer: ccc.Signer = playgroundSigner instanceof ccc.SignerCkbPublicKey ? new ccc.SignerCkbPrivateKey(client, "01".repeat(32)) : playgroundSigner; const message = "Hello world"; // Sign const signature = await signer.signMessage(message); console.log(signature); // Verify — passes console.log( `Verification should pass: ${await ccc.Signer.verifyMessage(message, signature)}`, ); // Verify — fails with wrong message console.log( `Verification should fail: ${await ccc.Signer.verifyMessage("Wrong message", signature)}`, ); ``` ## Instance-level verification (identity check) [#instance-level-verification-identity-check] **Use this when:** you not only want to verify the signature is valid, but also confirm it was produced by the *currently connected* wallet. The instance method additionally checks that `signature.identity` matches the signer: ```typescript // Returns false if the signature was produced by a different signer const ok = await signer.verifyMessage(message, signature); ``` **Static vs. instance verification:** `ccc.Signer.verifyMessage()` (static) verifies any `Signature` regardless of who produced it. `signer.verifyMessage()` (instance) additionally enforces that `signature.identity` matches the current signer's identity. ## Sign raw bytes [#sign-raw-bytes] **Use this when:** you need to sign arbitrary binary data (e.g. a hash, a serialized protobuf, or a binary payload): ```typescript const bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]); const signature = await signer.signMessage(bytes); // Hex strings also work: const sigFromHex = await signer.signMessage("0xdeadbeef"); ``` ## Troubleshooting [#troubleshooting] **`signer.signMessage` throws "not implemented" or "unsupported"** Some signer types (e.g. `SignerCkbPublicKey`) represent a read-only public key with no signing capability. This happens in the CCC playground default signer. In production apps with real wallet connections, `signMessage` always works. **Verification returns `false` even though I signed the same message** Make sure you're passing the *exact* same `message` value (including encoding). If you signed a `string`, verify with the same `string` — not a `Uint8Array` version of it, and vice versa. **I need to verify on a backend that doesn't have CCC** The `Signature` object is JSON-serializable. Send it to your backend, install `@ckb-ccc/shell`, and call `ccc.Signer.verifyMessage(message, signature)` there. ## Next steps [#next-steps] * [Compose transactions](/docs/guides/compose-transactions) — send CKB or tokens on-chain. * [Node.js backend](/docs/guides/node-js-backend) — verify signatures and send transactions from the server. # Spore Protocol (https://docs.ckbccc.com/en/docs/guides/spore-protocol) Create permanent, ownable **Digital Objects (DOBs)** directly on the CKB blockchain. A Spore encodes arbitrary content (images, text, JSON, etc.) into a cell — ownership is enforced by a lock script, and transfers are atomic on-chain operations without any marketplace or indexer dependency. ## What you'll get [#what-youll-get] After this guide you'll be able to: * **Create a Spore** — store content (text, image, JSON, …) permanently on-chain * **Transfer and melt** Spores — change ownership or destroy and reclaim CKB * **Organize with Clusters** — group related Spores into named collections * **Query Spores** — iterate owned Spores and filter by cluster All examples assume you already have a connected `signer`. See [Connect Wallets](/docs/guides/connect-wallets) (browser) or [Node.js Backend](/docs/guides/node-js-backend) (server) first. ## Key concepts [#key-concepts] * **Spore** — a cell containing arbitrary content + a type script that gives it a unique ID. Content is permanent; ownership is transferable. * **Cluster** — an optional collection cell with a name and description. Any Spore can reference a Cluster ID to declare membership. * **Melt** — destroy a Spore cell and reclaim its locked CKB capacity. Irreversible. ## Installation [#installation] ```bash npm install @ckb-ccc/spore ``` ```typescript import { createSpore, transferSpore, meltSpore } from "@ckb-ccc/spore"; import { createSporeCluster, transferSporeCluster } from "@ckb-ccc/spore"; ``` ## Create a Spore [#create-a-spore] `createSpore` encodes your content into a new Spore cell. You provide a MIME content type and raw content bytes; CCC handles the on-chain data packing: ```typescript import { ccc } from "@ckb-ccc/ccc"; import { createSpore } from "@ckb-ccc/spore"; const { tx, id } = await createSpore({ signer, data: { contentType: "text/plain", content: new TextEncoder().encode("Hello, Spore!"), }, // Optional: send the Spore to a different address // to: recipientLockScript, }); // Balance capacity and pay fee await tx.completeInputsByCapacity(signer); await tx.completeFeeBy(signer); const txHash = await signer.sendTransaction(tx); console.log("Spore ID:", id); // "0x..." — unique type script args, store this! console.log("Transaction hash:", txHash); // "0x..." — 32-byte tx hash ``` The returned `id` is the sporeId — the `args` field of the Spore's type script. Store it; you will need it for transfers and melts. ### Spore inside a Cluster [#spore-inside-a-cluster] If you want the Spore to belong to a Cluster, include `clusterId` in the data and set `clusterMode`: ```typescript const { tx, id } = await createSpore({ signer, data: { contentType: "image/png", content: pngBytes, clusterId: "0xabc123...", // id returned by createSporeCluster }, clusterMode: "lockProxy", // or "clusterCell" }); ``` | `clusterMode` | Behaviour | | --------------- | ------------------------------------------------------------------------------ | | `"lockProxy"` | Puts a cell with the Cluster's lock in both inputs and outputs — lower cost | | `"clusterCell"` | Puts the Cluster cell itself in inputs and outputs — proves ownership directly | | `"skip"` | Skips cluster logic entirely — use only if you handle it yourself | ## Transfer a Spore [#transfer-a-spore] **Use this when:** you want to change ownership of an existing Spore (e.g. sell, gift, or move to a different address). ```typescript import { transferSpore } from "@ckb-ccc/spore"; const { script: newOwner } = await ccc.Address.fromString( recipientAddress, signer.client, ); const { tx } = await transferSpore({ signer, id: sporeId, // "0x..." to: newOwner, }); await tx.completeInputsByCapacity(signer); await tx.completeFeeBy(signer); const txHash = await signer.sendTransaction(tx); ``` ## Melt (destroy) a Spore [#melt-destroy-a-spore] **Use this when:** you want to permanently destroy a Spore and reclaim its locked CKB capacity: ```typescript import { meltSpore } from "@ckb-ccc/spore"; const { tx } = await meltSpore({ signer, id: sporeId, }); await tx.completeFeeBy(signer); const txHash = await signer.sendTransaction(tx); ``` Melting is irreversible. The Spore's content is removed from the chain and its CKB is released. There is no undo. ## Clusters [#clusters] Clusters are optional named collections. Create a Cluster first, then reference its ID when creating Spores. ### Create a Cluster [#create-a-cluster] ```typescript import { createSporeCluster } from "@ckb-ccc/spore"; const { tx, id: clusterId } = await createSporeCluster({ signer, data: { name: "My Collection", description: "A collection of on-chain art.", }, }); await tx.completeInputsByCapacity(signer); await tx.completeFeeBy(signer); const txHash = await signer.sendTransaction(tx); console.log("Cluster ID:", clusterId); // "0x..." — pass this as clusterId when creating Spores ``` ### Transfer a Cluster [#transfer-a-cluster] ```typescript import { transferSporeCluster } from "@ckb-ccc/spore"; const { script: newOwner } = await ccc.Address.fromString( recipientAddress, signer.client, ); const { tx } = await transferSporeCluster({ signer, id: clusterId, to: newOwner, }); await tx.completeInputsByCapacity(signer); await tx.completeFeeBy(signer); await signer.sendTransaction(tx); ``` ## Query Spores and Clusters [#query-spores-and-clusters] All query functions return **async generators** — they stream results lazily so you can process large collections without loading everything into memory. ```typescript import { findSporesBySigner, findSporeClusters } from "@ckb-ccc/spore"; // Iterate all Spores owned by the signer for await (const { spore, sporeData } of findSporesBySigner({ signer })) { console.log(spore.outPoint, sporeData.contentType); } // Filter: only Spores in a specific Cluster for await (const { sporeData } of findSporesBySigner({ signer, clusterId: "0xabc123...", })) { console.log(sporeData); } // Filter: only public Spores (not in any cluster) for await (const { spore } of findSporesBySigner({ signer, clusterId: "", // empty string = public spores only })) { console.log(spore.outPoint); } ``` ## API reference [#api-reference] | Function | Description | | ----------------------------------- | ----------------------------------- | | `createSpore(params)` | Create a new Spore cell | | `transferSpore(params)` | Transfer a Spore to a new owner | | `meltSpore(params)` | Destroy a Spore and reclaim CKB | | `findSpore(client, id)` | Look up a single Spore by ID | | `findSpores(params)` | Query Spores by lock and/or cluster | | `findSporesBySigner(params)` | Query Spores owned by a signer | | `createSporeCluster(params)` | Create a new Cluster cell | | `transferSporeCluster(params)` | Transfer a Cluster to a new owner | | `findCluster(client, id)` | Look up a single Cluster by ID | | `findSporeClusters(params)` | Query Clusters by lock | | `findSporeClustersBySigner(params)` | Query Clusters owned by a signer | ## Troubleshooting [#troubleshooting] **`createSpore` fails with "not enough capacity"** Spore cells store content on-chain, so they require more CKB capacity than a basic transfer. A text Spore needs \~200+ CKB; an image Spore can need thousands. Make sure the signer has sufficient balance. **Spore ID is lost after creation** The `id` returned by `createSpore` is the **type script args** of the Spore cell. It is deterministic (derived from the first input + output index), but you should store it immediately. You can also recover it via `findSporesBySigner`. **`clusterMode` — which one should I use?** * `"lockProxy"` (recommended) — lower cost, proves cluster ownership via a proxy cell with the same lock. * `"clusterCell"` — higher cost, puts the actual cluster cell in the transaction. Use this when the cluster's lock is different from the signer's. * `"skip"` — only if you are manually handling cluster inputs/outputs yourself. ## Next steps [#next-steps] * [Compose transactions](/docs/guides/compose-transactions) — understand the declare → fill → fee → send pattern used in every Spore operation. * [UDT tokens](/docs/guides/udt-tokens) — issue fungible tokens on CKB. * [Node.js backend](/docs/guides/node-js-backend) — mint Spores from a server-side script. # UDT Tokens (https://docs.ckbccc.com/en/docs/guides/udt-tokens) Issue, transfer, and query **fungible tokens** on CKB. UDT (User Defined Token) amounts are stored in the `data` field of cells; CCC's `ccc.udt.Udt` class handles encoding, SSRI execution, and legacy xUDT fallback automatically. ## What you'll get [#what-youll-get] After this guide you'll be able to: * **Transfer UDT tokens** with automatic input selection and change handling * **Mint new tokens** (requires owner-mode authority) * **Read on-chain metadata** (name, symbol, decimals, icon) for SSRI-compliant tokens * Understand how xUDT and sUDT relate All examples assume you already have a connected `signer`. See [Connect Wallets](/docs/guides/connect-wallets) (browser) or [Node.js Backend](/docs/guides/node-js-backend) (server) first. ## xUDT vs sUDT [#xudt-vs-sudt] | | sUDT | xUDT | | ------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | Standard | [CKB RFC 25](https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0025-simple-udt/0025-simple-udt.md) | [CKB RFC 52](https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0052-extensible-udt/0052-extensible-udt.md) | | Extension support | No | Yes (owner-mode, RCE rules) | | `KnownScript` value | — (not in CCC) | `ccc.KnownScript.XUdt` | | Production use | Legacy | Recommended | The `Udt` class in CCC supports both. By default it uses the **SSRI** execution model (on-chain script execution), and falls back gracefully to off-chain construction for legacy xUDT tokens. ## Installation [#installation] ```bash npm install @ckb-ccc/ccc ``` UDT support is bundled in the main package — no extra install is required. ## Import [#import] ```typescript import { ccc } from "@ckb-ccc/ccc"; ``` The `Udt` class is available as `ccc.udt.Udt`. ## Construct a `Udt` instance [#construct-a-udt-instance] Before you can transfer or mint, you need a `Udt` object. It takes two pieces of information: * **`code`** — the `OutPoint` of the cell that holds the UDT script code (cell dep) * **`script`** — the type script that uniquely identifies this token (its `args` is typically the issuer's lock hash) ```typescript const type = await ccc.Script.fromKnownScript( signer.client, ccc.KnownScript.XUdt, // The args uniquely identify this token (issuer lock hash) "0xf8f94a13dfe1b87c10312fb9678ab5276eefbe1e0b2c62b4841b1f393494eff2", ); const code = ( await signer.client.getCellDeps( (await signer.client.getKnownScript(ccc.KnownScript.XUdt)).cellDeps, ) )[0].outPoint; const udt = new ccc.udt.Udt(code, type); // Optional 3rd arg: { executor } for SSRI execution. Omit for legacy xUDT. ``` ## Transfer tokens [#transfer-tokens] **Use this when:** you want to send UDT tokens from the signer to another address. The four-step pattern mirrors CKB transfers, with an extra UDT-specific input-filling step: `udt.transfer` creates a transaction with the desired output cells. It does **not** yet balance inputs — that's the next two steps. ```typescript const receiver = await signer.getRecommendedAddress(); const { script: lock } = await ccc.Address.fromString(receiver, signer.client); let { res: tx } = await udt.transfer(signer, [ { to: lock, amount: ccc.fixedPointFrom(1) }, // 1 token unit ]); ``` `udt.completeBy` collects the signer's UDT cells until there is enough token balance to cover the outputs. Any surplus UDT is returned as a change cell automatically. ```typescript tx = await udt.completeBy(tx, signer); ``` UDT cells also need CKB capacity to exist on-chain. This step adds CKB inputs to cover all output cells: ```typescript await tx.completeInputsByCapacity(signer); ``` ```typescript await tx.completeFeeBy(signer); const txHash = await signer.sendTransaction(tx); console.log("UDT transfer hash:", txHash); // "0x..." — 32-byte tx hash ``` ## Mint tokens [#mint-tokens] **Use this when:** you are the token issuer and want to create new token supply. The signer must have **mint authority** (typically `owner-mode` in xUDT — the signer's lock hash matches the type script args). Minting creates new token outputs without requiring existing UDT inputs: ```typescript const { script: to } = await ccc.Address.fromString(receiver, signer.client); let { res: tx } = await udt.mint(signer, [ { to, amount: ccc.fixedPointFrom(1000) }, ]); await tx.completeInputsByCapacity(signer); await tx.completeFeeBy(signer); const txHash = await signer.sendTransaction(tx); ``` ## Read token metadata (SSRI tokens) [#read-token-metadata-ssri-tokens] **Use this when:** you want to display token name, symbol, decimals, or icon in your UI. Only works for tokens that implement the SSRI `UDT` interface on-chain: ```typescript const { res: name } = await udt.name(); const { res: symbol } = await udt.symbol(); const { res: decimals } = await udt.decimals(); const { res: icon } = await udt.icon(); console.log(`${name} (${symbol}), ${decimals} decimals`); ``` These methods return `undefined` for legacy sUDT/xUDT tokens that do not implement the SSRI `UDT` interface. Check the return value before using it. ## API reference [#api-reference] | Method | Description | | -------------------------------------------- | ------------------------------------------- | | `new ccc.udt.Udt(code, script)` | Construct a UDT instance | | `udt.transfer(signer, transfers, tx?)` | Build transfer outputs | | `udt.mint(signer, mints, tx?)` | Build mint outputs | | `udt.completeBy(tx, signer)` | Fill UDT inputs and add change | | `udt.completeChangeToLock(tx, signer, lock)` | Fill UDT inputs with a specific change lock | | `udt.name()` | Token name (SSRI only) | | `udt.symbol()` | Token symbol (SSRI only) | | `udt.decimals()` | Token decimals (SSRI only) | | `udt.icon()` | Token icon URI (SSRI only) | ## Troubleshooting [#troubleshooting] **`udt.completeBy` throws "not enough UDT balance"** The signer doesn't own enough UDT cells to cover the transfer amount. Check the signer's UDT balance or fund the address with more tokens. **`udt.transfer` returns `{ res: tx }` — what is `res`?** All UDT methods return an `ssri.ExecutorResponse` wrapper. The actual `Transaction` is in the `.res` property. This enables CCC to attach SSRI execution metadata alongside the result. **`udt.name()` / `udt.symbol()` returns `undefined`** The token does not implement the SSRI `UDT` metadata interface. This is expected for legacy sUDT/xUDT tokens. You'll need to source metadata from an off-chain registry. **I want to transfer UDT to an address that has never held this token** That works out of the box. `udt.transfer` creates a new output cell with the token type script at the recipient's lock. The recipient just needs enough CKB capacity (occupied by the new cell) — which the signer pays for via `completeInputsByCapacity`. ## Next steps [#next-steps] * [Compose transactions](/docs/guides/compose-transactions) — the declare → fill → fee → send pattern that UDT builds on. * [Spore Protocol](/docs/guides/spore-protocol) — create non-fungible digital objects on CKB. * [Node.js backend](/docs/guides/node-js-backend) — issue and transfer tokens from a server-side script. # CCC Package Guide (https://docs.ckbccc.com/en/docs/packages) > CCC is a monorepo of focused packages — pick what you need. CCC ships as a collection of focused npm packages. Install only what your project requires. Most projects only need one entry-point package. Use `@ckb-ccc/shell` for Node.js, `@ckb-ccc/connector-react` for React apps, or `@ckb-ccc/ccc` when you need a fully custom wallet UI. ## At a glance [#at-a-glance] CCC offers multiple packages to serve different development scenarios. Choosing the right package based on your runtime (frontend/backend), framework (React/other), and feature needs (wallet integration/protocol support) can dramatically speed up development. * **Core packages** — `@ckb-ccc/core` and `@ckb-ccc/shell` provide foundational functionality, ideal for deep customization or backend development. * **Wallet integration** — `@ckb-ccc/ccc` aggregates all wallets for custom UIs; `@ckb-ccc/connector-react` provides pre-built components for rapid React integration. * **Protocol SDKs** — `@ckb-ccc/spore`, `@ckb-ccc/udt`, and `@ckb-ccc/ssri` support specific CKB protocol standards. * **Individual wallet packages** — Standalone packages for specific wallets (JoyID, OKX, UniSat, etc.), installed as needed. Core SDK — provides Transaction, Client, Signer abstractions and cryptographic utilities. Best for advanced scenarios requiring deep customization. Backend / Node.js aggregate — bundles the core SDK with protocol extensions (Spore, UDT, etc.), purpose-built for server environments. Ideal for Node.js backends, CLI tools, or scripts. Wallet integration aggregate — includes every wallet integration (EVM, BTC, Nostr, JoyID, etc.) in a single import. The best choice when you need a custom wallet-connection UI without pre-built components. Web Components wallet connector — a framework-agnostic Lit-based component usable in any frontend framework. Ideal for non-React projects or cross-framework reuse. React wallet connector — pre-built React Hooks and components for quickly adding wallet-connection features. Ideal for React apps with no manual connection logic required. Spore NFT Protocol SDK — create, transfer, and melt on-chain Digital Objects (DOBs) and Clusters. Ideal for CKB apps that need NFT capabilities. UDT/xUDT token SDK — issue, transfer, and mint User Defined Tokens on CKB. Ideal for CKB apps that need fungible token functionality. SSRI (Script-Sourced Rich Information) protocol support for interacting with CKB smart contracts. Ideal for CKB apps that call on-chain contract methods. Lumos compatibility patches — enable existing Lumos apps to work with JoyID, Nostr, and Portal Wallet. Ideal for legacy Lumos projects migrating to new wallets. JoyID Passkey wallet — WebAuthn-based passwordless signing with multi-chain support (CKB, EVM, BTC, Nostr). Ideal for apps that need biometric or passwordless experiences. EVM wallet discovery — auto-detects MetaMask, Rabby, and other EVM wallets via the EIP-6963 standard. Ideal for frontend apps supporting multiple EVM wallets. Nostr wallet integration — signs via NIP-07 browser extensions, supporting the Nostr protocol. Ideal for Nostr ecosystem apps or those requiring Nostr signatures. UTXO Global wallet integration — signs via the UTXO Global browser extension, supporting the BTC, CKB, and DOGE ecosystems. REI wallet integration — signs via the REI browser extension. Xverse wallet integration — BTC ecosystem browser extension signing. OKX multi-protocol wallet — supports EVM, Nostr, and BTC signing via the OKX browser extension. UniSat wallet integration — BTC ecosystem browser extension signing. ## NPM package reference [#npm-package-reference] The tables below list all CCC packages with their latest version and weekly download count. Use version numbers to gauge update freshness and download counts to assess community adoption and maintenance activity. When choosing a package, prefer recently updated ones for better community support and bug fixes. ### Core packages [#core-packages] | Package (click for docs) | Purpose | Latest version & weekly downloads | | -------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------- | | [`@ckb-ccc/core`](./packages/core-packages/core) | Core SDK — Transaction, Client, Signer abstractions and cryptographic utilities | | | [`@ckb-ccc/shell`](./packages/core-packages/shell) | Backend / Node.js aggregate — core SDK + protocol extensions | | ### Aggregates & UI components [#aggregates--ui-components] | Package (click for docs) | Purpose | Latest version & weekly downloads | | ---------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------ | | [`@ckb-ccc/ccc`](./packages/core-packages/ccc) | Wallet integration aggregate — all wallets in a single import | | | [`@ckb-ccc/connector`](./packages/core-packages/connector) | Web Components wallet connector — framework-agnostic | | | [`@ckb-ccc/connector-react`](./packages/core-packages/connector-react) | React wallet connector — pre-built Hooks and components | | ### Protocol SDKs [#protocol-sdks] | Package (click for docs) | Purpose | Latest version & weekly downloads | | ------------------------------------------------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------- | | [`@ckb-ccc/spore`](./packages/protocol-sdks/spore) | Spore NFT Protocol SDK — DOB/Cluster management | | | [`@ckb-ccc/udt`](./packages/protocol-sdks/udt) | UDT/xUDT token SDK — issuance, transfer, minting | | | [`@ckb-ccc/ssri`](./packages/protocol-sdks/ssri) | SSRI protocol support — smart contract interaction and metadata queries | | | [`@ckb-ccc/lumos-patches`](./packages/protocol-sdks/lumos-patches) | Lumos compatibility patches — JoyID, Nostr, Portal Wallet support | | ### Wallet integrations [#wallet-integrations] | Package (click for docs) | Purpose | Latest version & weekly downloads | | -------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------- | | [`@ckb-ccc/joy-id`](./packages/wallet-integrations/joy-id) | JoyID Passkey wallet — WebAuthn passwordless signing, multi-chain | | | [`@ckb-ccc/eip6963`](./packages/wallet-integrations/eip6963) | EVM wallet discovery — EIP-6963 standard, MetaMask, Rabby, etc. | | | [`@ckb-ccc/nip07`](./packages/wallet-integrations/nip07) | Nostr wallet integration — NIP-07 browser extension signing | | | [`@ckb-ccc/utxo-global`](./packages/wallet-integrations/utxo-global) | UTXO Global wallet — BTC/CKB/DOGE ecosystem support | | | [`@ckb-ccc/rei`](./packages/wallet-integrations/rei) | REI wallet — REI browser extension signing | | | [`@ckb-ccc/xverse`](./packages/wallet-integrations/xverse) | Xverse wallet — BTC ecosystem browser extension signing | | | [`@ckb-ccc/okx`](./packages/wallet-integrations/okx) | OKX multi-protocol wallet — EVM/Nostr/BTC signing | | | [`@ckb-ccc/uni-sat`](./packages/wallet-integrations/uni-sat) | UniSat wallet — BTC ecosystem browser extension signing | | ## Import pattern [#import-pattern] All packages expose their public API on a single `ccc` namespace object: ```typescript import { ccc } from "@ckb-ccc/"; ``` Advanced (unstable) APIs are available via `cccA`: ```typescript import { cccA } from "@ckb-ccc//advanced"; ``` Your `tsconfig.json` must set `moduleResolution` to `node16`, `nodenext`, or `bundler` and must not disable `resolvePackageJsonExports`. CCC uses [Package Entry Points](https://nodejs.org/api/packages.html#packages_package_entry_points) for tree shaking. # @ckb-ccc/ccc (https://docs.ckbccc.com/en/docs/packages/core-packages/ccc) `@ckb-ccc/ccc` is the all-in-one package for browser environments. It bundles `@ckb-ccc/core` with every wallet signer and protocol package into a single `ccc` namespace. Use it when you want full wallet discovery but prefer to build your own connection UI instead of using the built-in connector modal. ## When to use this package [#when-to-use-this-package] | Scenario | Package | | ---------------------------------------- | --------------------------------- | | React app with built-in wallet modal | `@ckb-ccc/connector-react` | | Non-React frontend with built-in modal | `@ckb-ccc/connector` | | **Custom wallet UI (no built-in modal)** | **`@ckb-ccc/ccc` ← you are here** | | Node.js backend or script | `@ckb-ccc/shell` | ## Installation [#installation] ```bash npm install @ckb-ccc/ccc ``` ```bash yarn add @ckb-ccc/ccc ``` ```bash pnpm add @ckb-ccc/ccc ``` ## What's included [#whats-included] `@ckb-ccc/ccc` re-exports everything from: | Sub-package | Namespace | Contents | | ---------------------- | --------- | -------------------------------------------- | | `@ckb-ccc/joy-id` | ccc.\* | JoyID wallet signer | | `@ckb-ccc/eip6963` | ccc.\* | MetaMask, Rabby, and any EIP-6963 wallet | | `@ckb-ccc/uni-sat` | ccc.\* | UniSat Bitcoin wallet signer | | `@ckb-ccc/okx` | ccc.\* | OKX wallet signer (BTC + Nostr) | | `@ckb-ccc/xverse` | ccc.\* | Xverse / SATS Connect wallet signer | | `@ckb-ccc/nip07` | ccc.\* | NIP-07 Nostr extension signer | | `@ckb-ccc/rei` | ccc.\* | REI CKB-native wallet signer | | `@ckb-ccc/utxo-global` | ccc.\* | UTXO Global wallet signer (CKB + BTC + DOGE) | It also adds one package-level export: * **`SignersController`** — discovers and manages the lifecycle of all available wallet signers automatically. ## Usage [#usage] ```typescript import { ccc } from "@ckb-ccc/ccc"; const client = new ccc.ClientPublicTestnet(); // Discover all wallets available in the current browser environment const controller = new ccc.SignersController(client); controller.subscribeSigners((signerInfo) => { console.log(`Found wallet: ${signerInfo.name}`); }); // Or instantiate a specific signer directly const signer = new ccc.SignerCkbPrivateKey(client, "0x..."); await signer.connect(); const balance = await signer.getBalance(); ``` ## Advanced exports [#advanced-exports] ```typescript import { cccA } from "@ckb-ccc/ccc/advanced"; ``` `cccA` exposes internal APIs that may change between minor versions without notice. Only reach for them when the stable `ccc` API doesn't cover your use case. # @ckb-ccc/connector-react (https://docs.ckbccc.com/en/docs/packages/core-packages/connector-react) `@ckb-ccc/connector-react` is the recommended integration layer for React and Next.js applications. It provides a context `Provider`, a `useCcc` hook that exposes the full connector state, and a `useSigner` hook that returns the active signer whenever a wallet is connected. ## Installation [#installation] ```bash npm install @ckb-ccc/connector-react ``` ```bash yarn add @ckb-ccc/connector-react ``` ```bash pnpm add @ckb-ccc/connector-react ``` ## Quick start [#quick-start] Place `Provider` at the root of your application (above any component that needs wallet access). ```tsx import { ccc } from "@ckb-ccc/connector-react"; export default function App({ children }) { return ( {children} ); } ``` Call `useCcc()` inside any descendant component to open the wallet picker and read the connected wallet state. ```tsx import { ccc } from "@ckb-ccc/connector-react"; export function ConnectButton() { const { open, wallet, signerInfo } = ccc.useCcc(); return (
{signerInfo &&
}
); } function Address() { const { signerInfo } = ccc.useCcc(); const [address, setAddress] = React.useState(""); React.useEffect(() => { signerInfo?.signer.getRecommendedAddress().then(setAddress); }, [signerInfo]); return

{address}

; } ```
## Provider props [#provider-props] ```tsx true} defaultClient={new ccc.ClientPublicTestnet()} clientOptions={[ { name: "Testnet", client: new ccc.ClientPublicTestnet() }, { name: "Mainnet", client: new ccc.ClientPublicMainnet() }, ]} preferredNetworks={[{ addressPrefix: "ckb", signerType: ccc.SignerType.BTC, network: "btc" }]} > {children} ``` | Prop | Type | Description | | ------------------- | ------------------------------------------ | -------------------------------------------------- | | `children` | `ReactNode` | Your application tree | | `connectorProps` | `HTMLAttributes<{}>?` | Additional props to pass to the connector element | | `hideMark` | `boolean?` | Hide the "Powered by CCC" mark in the connector UI | | `name` | `string?` | App name shown in the wallet picker | | `icon` | `string?` | App icon URL shown in the wallet picker | | `signerFilter` | `(signerInfo, wallet) => Promise` | Filter which wallet/signer combinations appear | | `signersController` | `ccc.SignersController?` | Custom signers controller (advanced) | | `defaultClient` | `ccc.Client?` | The initial network client | | `clientOptions` | `{ icon?, client, name }[]?` | Network options to display in the connector | | `preferredNetworks` | `ccc.NetworkPreference[]?` | Networks to prefer when a wallet supports multiple | ## `useCcc()` hook [#useccc-hook] ```typescript const { isOpen, // boolean — whether the wallet picker modal is open open, // () => void — open the wallet picker close, // () => void — close the wallet picker disconnect, // () => void — disconnect the current wallet setClient, // (client: ccc.Client) => void — switch network client, // ccc.Client — current network client wallet, // ccc.Wallet | undefined — connected wallet signerInfo, // ccc.SignerInfo | undefined — connected signer } = ccc.useCcc(); ``` The hook throws if called outside a `` tree. ## Full example [#full-example] ```tsx "use client"; // Required for Next.js App Router import { ccc } from "@ckb-ccc/connector-react"; import { useState, useEffect } from "react"; function Layout({ children }: { children: React.ReactNode }) { return (
{children} ); } function Header() { const { open, disconnect, wallet, signerInfo, client } = ccc.useCcc(); const [address, setAddress] = useState(""); useEffect(() => { if (!signerInfo) { setAddress(""); return; } signerInfo.signer.getRecommendedAddress().then(setAddress); }, [signerInfo]); return (
{wallet ? ( <> {address} ) : ( )}
); } ``` ## Filtering wallets [#filtering-wallets] Use `signerFilter` to show only specific wallet types: ```tsx import { ccc } from "@ckb-ccc/connector-react"; // Show only CKB-native wallets { return signerInfo.signer.type === ccc.SignerType.CKB; }} > {children} ``` ## Next.js (App Router) [#nextjs-app-router] CCC's connector uses React context and browser APIs, so it only works on the client side. Add `"use client"` to any file that imports from `@ckb-ccc/connector-react` or renders ``. ```tsx "use client"; import { ccc } from "@ckb-ccc/connector-react"; ``` Forgetting `"use client"` in Next.js App Router will cause a runtime error: `TypeError: (0, react....createContext) is not a function`. # @ckb-ccc/connector (https://docs.ckbccc.com/en/docs/packages/core-packages/connector) `@ckb-ccc/connector` provides a native [Web Component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) that renders CCC's wallet-selection UI without requiring any JavaScript framework. It is the foundation that `@ckb-ccc/connector-react` is built on top of. ## When to use this package [#when-to-use-this-package] Use `@ckb-ccc/connector` when you need wallet connectivity in a plain HTML page, vanilla JS project, or a framework that is not React. If you are building a React application, prefer [`@ckb-ccc/connector-react`](./connector-react.mdx) which wraps this package with React bindings. ## Installation [#installation] ```bash npm install @ckb-ccc/connector ``` ```bash yarn add @ckb-ccc/connector ``` ```bash pnpm add @ckb-ccc/connector ``` ## The `WebComponentConnector` class [#the-webcomponentconnector-class] The central export is `ccc.WebComponentConnector` — the underlying custom element class. When you register the element and add it to the DOM, it renders the wallet picker UI. ```typescript import { ccc } from "@ckb-ccc/connector"; // The connector element const connector: ccc.WebComponentConnector; connector.client; // current ccc.Client connector.wallet; // connected ccc.Wallet | undefined connector.signer; // ccc.SignerInfo | undefined connector.disconnect(); // disconnect the current wallet connector.setClient(newClient); // switch networks ``` ## Usage in plain HTML [#usage-in-plain-html] ```html

``` ## Usage with a bundler (vanilla JS/TS) [#usage-with-a-bundler-vanilla-jsts] ```typescript import { ccc } from "@ckb-ccc/connector"; // The custom element is auto-registered when you import the package. const connector = document.querySelector("ccc-connector") as ccc.WebComponentConnector; // Show the wallet picker connector.style.display = ""; // Listen for events connector.addEventListener("close", () => { connector.style.display = "none"; }); connector.addEventListener("willUpdate", () => { console.log("Connected wallet:", connector.wallet?.name); console.log("Signer:", connector.signer); }); // Switch to mainnet connector.setClient(new ccc.ClientPublicMainnet()); ``` ## Comparison with `@ckb-ccc/connector-react` [#comparison-with-ckb-cccconnector-react] | Feature | `@ckb-ccc/connector` | `@ckb-ccc/connector-react` | | ----------------- | -------------------- | ---------------------------- | | Framework | None (Web Component) | React | | Integration style | DOM events | `Provider` + `useCcc()` hook | | State management | Manual DOM listeners | React context, reactive | | Peer dependency | — | `react >= 16` | `@ckb-ccc/connector-react` uses `@lit/react` to wrap this Web Component. All wallet integrations (JoyID, MetaMask, Nostr, BTC wallets, etc.) are wired through the same underlying connector element. # @ckb-ccc/core (https://docs.ckbccc.com/en/docs/packages/core-packages/core) `@ckb-ccc/core` is the base layer of CCC. It contains all CKB data types, encoders, hashers, and the abstract `Signer` / `Client` interfaces used by every higher-level package. Most projects do not need to install `@ckb-ccc/core` directly. Use `@ckb-ccc/shell` for Node.js or `@ckb-ccc/connector-react` for React — both re-export everything from core. ## Installation [#installation] ```bash npm install @ckb-ccc/core ``` ```bash yarn add @ckb-ccc/core ``` ```bash pnpm add @ckb-ccc/core ``` ## Imports [#imports] ```typescript import { ccc } from "@ckb-ccc/core"; ``` ## Exported modules [#exported-modules] `@ckb-ccc/core` re-exports the following sub-modules under the `ccc` namespace: | Module | Contents | | ------------ | ------------------------------------------------------------------------------------- | | `address` | `Address`, `AddressLike`, address parsing and formatting | | `bytes` | `bytesFrom`, `bytesTo`, byte array utilities | | `ckb` | `Transaction`, `Script`, `Cell`, `CellInput`, `CellOutput`, `OutPoint`, `WitnessArgs` | | `client` | `Client` (abstract), `ClientPublicMainnet`, `ClientPublicTestnet` | | `fixedPoint` | `fixedPointFrom`, `fixedPointToString` — CKB capacity in shannons | | `hasher` | `hashCkb`, `Hasher` — CKB Blake2b hashing | | `hex` | `hexFrom`, `HexLike`, hex encoding helpers | | `jsonRpc` | `RequestorJsonRpc` — low-level JSON-RPC client | | `keystore` | `KeyStore` — encrypted key storage | | `molecule` | Molecule codec primitives | | `num` | `numFrom`, `numFromBytes`, `numToBytes`, `numLeToBytes` — numeric conversions | | `signer` | `Signer` (abstract), `SignerInfo`, `SignerType`, `SignerSignType` | | `utils` | Miscellaneous helpers | ## Key classes [#key-classes] ### Transaction [#transaction] The primary type for building CKB transactions: ```typescript // Build a CKB transfer transaction const tx = ccc.Transaction.from({ outputs: [{ lock: toLock, capacity: ccc.fixedPointFrom(amount) }], }); await tx.completeInputsByCapacity(signer); await tx.completeFeeBy(signer); const txHash = await signer.sendTransaction(tx); ``` Important methods: | Method | Description | | ------------------------------------- | ------------------------------------ | | `Transaction.from(like)` | Create from a plain object | | `tx.addInput(cell)` | Add a cell input | | `tx.addOutput(output, data?)` | Add a cell output | | `tx.completeInputsByCapacity(signer)` | Auto-select inputs to cover capacity | | `tx.completeFeeBy(signer, feeRate?)` | Add a change output and compute fee | | `tx.addCellDepInfos(client, deps)` | Resolve and add cell dependencies | | `signer.sendTransaction(tx)` | Sign and broadcast | ### Script [#script] ```typescript const script = ccc.Script.from({ codeHash: "0x...", hashType: "type", args: "0x...", }); script.eq(otherScript); // structural equality ``` ### Address [#address] ```typescript const addr = await ccc.Address.fromString( "ckb1qzda0cr08m85hc8jlnfp3gog...", client, ); const { script, prefix } = addr; ``` ### Signer (abstract) [#signer-abstract] The `Signer` abstract class is implemented by every wallet integration. Key members: ```typescript abstract class Signer { readonly client: Client; abstract getInternalAddress(): Promise; abstract getRecommendedAddressObj(preference?: unknown): Promise
; abstract signTransaction(tx: Transaction): Promise; async sendTransaction(tx: Transaction): Promise; async findCells(filter, withData?, order?, limit?): AsyncGenerator; } ``` ### Client (abstract) [#client-abstract] ```typescript // Connect to public testnet const client = new ccc.ClientPublicTestnet(); // Connect to public mainnet const client = new ccc.ClientPublicMainnet(); // Connect to a custom node const client = new ccc.ClientPublicTestnet("https://my-node.example.com/rpc"); ``` ## Key functions [#key-functions] ```typescript // Convert any value to a fixed-point CKB capacity (shannons) const capacity = ccc.fixedPointFrom("100"); // 100 CKB = 10_000_000_000n shannons // Hash data with CKB's Blake2b const hash = ccc.hashCkb(data); // Encode to hex const hex = ccc.hexFrom(bytes); // Numeric conversion const n = ccc.numFrom("0xff"); // BigInt const bytes = ccc.numToBytes(n, 8); // little-endian Uint8Array // Byte conversions const arr = ccc.bytesFrom("0xdeadbeef"); const str = ccc.bytesTo(arr, "utf8"); ``` # Core Packages (https://docs.ckbccc.com/en/docs/packages/core-packages) The Core Packages are the backbone of CCC. They provide the CKB primitives every higher-level package depends on, plus the aggregated entry points and connectors most applications consume directly. Most projects only need **one** of these packages as their entry point. Pick by environment: `@ckb-ccc/shell` for Node.js, `@ckb-ccc/connector-react` for React apps, `@ckb-ccc/connector` for any other browser framework, or `@ckb-ccc/ccc` when you want every wallet bundled and plan to build a fully custom UI. | Package | Environment | Includes | Use when | | ------------------------------------------------------------- | --------------- | ----------------------------------------- | ---------------------------------------------------- | | [`@ckb-ccc/core`](./core-packages/core) | Any | CKB primitives only | You're authoring a library or want minimal footprint | | [`@ckb-ccc/shell`](./core-packages/shell) | Node.js | core + spore + udt + ssri | Backend scripts, indexers, server-side transactions | | [`@ckb-ccc/ccc`](./core-packages/ccc) | Browser | core + all wallet signers + protocol SDKs | Custom wallet UI in any browser app | | [`@ckb-ccc/connector`](./core-packages/connector) | Browser | Web Component connector UI | Vanilla JS / Vue / Svelte / Angular apps | | [`@ckb-ccc/connector-react`](./core-packages/connector-react) | Browser (React) | `Provider`, `useCcc`, `useSigner` | React or Next.js apps | ## Layering [#layering] Every package re-exports its dependencies on the same `ccc` namespace, so application code only ever imports from a single entry point: ```typescript import { ccc } from "@ckb-ccc/connector-react"; // or shell / ccc / core ``` ## Choosing an entry point [#choosing-an-entry-point] * **Building a React dApp?** Start with [`@ckb-ccc/connector-react`](./core-packages/connector-react) — it gives you a ready-made wallet selection modal plus hooks. * **Building with another browser framework?** Use [`@ckb-ccc/connector`](./core-packages/connector) and drop the `` Web Component into your page. * **Need a custom wallet UI?** Use [`@ckb-ccc/ccc`](./core-packages/ccc) for full control over connection flow. * **Running in Node.js?** Use [`@ckb-ccc/shell`](./core-packages/shell) — it ships CommonJS / ESM builds without browser-only wallet code. * **Authoring a library?** Depend on [`@ckb-ccc/core`](./core-packages/core) only and let consumers pick their own entry point. # @ckb-ccc/shell (https://docs.ckbccc.com/en/docs/packages/core-packages/shell) `@ckb-ccc/shell` is the recommended entry point for **Node.js / server-side** code. It bundles `@ckb-ccc/core`, `@ckb-ccc/spore`, `@ckb-ccc/udt`, and `@ckb-ccc/ssri` into a single package with Node.js-compatible builds (CommonJS and ESM). `@ckb-ccc/shell` re-exports everything from `@ckb-ccc/core` so you get a single `ccc` namespace without pulling in browser-only wallet signer code. ## Installation [#installation] ```bash npm install @ckb-ccc/shell ``` ```bash yarn add @ckb-ccc/shell ``` ```bash pnpm add @ckb-ccc/shell ``` ## Imports [#imports] ```typescript import { ccc } from "@ckb-ccc/shell"; ``` ## When to use this package [#when-to-use-this-package] * Server-side scripts or automation * Data analysis and blockchain indexing * Backend transaction signing with a private key * Any Node.js environment where there is no browser wallet For React apps, use [`@ckb-ccc/connector-react`](./connector-react) instead. ## What it exports [#what-it-exports] The `barrel` entry point (`@ckb-ccc/shell/barrel`) re-exports: * Everything from `@ckb-ccc/core/barrel` — all CKB primitives * `spore` — the Spore Protocol namespace * `ssri` — the SSRI protocol namespace * `udt` — the UDT token namespace ```typescript import { ccc } from "@ckb-ccc/shell"; // ccc.Transaction, ccc.Script, ccc.Address, ccc.Client... // ccc.ClientPublicMainnet, ccc.ClientPublicTestnet // ccc.SignerCkbPrivateKey — for signing with a raw private key ``` ## Usage examples [#usage-examples] ### Connect to a CKB node [#connect-to-a-ckb-node] ```typescript import { ccc } from "@ckb-ccc/shell"; // Public testnet RPC const client = new ccc.ClientPublicTestnet(); // Public mainnet RPC const client = new ccc.ClientPublicMainnet(); // Custom node URL const client = new ccc.ClientPublicTestnet("http://localhost:8114"); ``` ### Sign transactions with a private key [#sign-transactions-with-a-private-key] ```typescript import { ccc } from "@ckb-ccc/shell"; const client = new ccc.ClientPublicTestnet(); const signer = new ccc.SignerCkbPrivateKey( client, "0xYOUR_PRIVATE_KEY_HEX", ); const myAddress = await signer.getRecommendedAddress(); console.log("Address:", myAddress); ``` ### Build and send a transaction [#build-and-send-a-transaction] ```typescript import { ccc } from "@ckb-ccc/shell"; const client = new ccc.ClientPublicTestnet(); const signer = new ccc.SignerCkbPrivateKey(client, "0x..."); const { script: toLock } = await ccc.Address.fromString( "ckt1qzda0cr08m85hc8jlnfp3gog...", client, ); const tx = ccc.Transaction.from({ outputs: [{ lock: toLock, capacity: ccc.fixedPointFrom("100") }], }); await tx.completeInputsByCapacity(signer); await tx.completeFeeBy(signer); const txHash = await signer.sendTransaction(tx); console.log("Sent:", txHash); ``` ### Query cells [#query-cells] ```typescript import { ccc } from "@ckb-ccc/shell"; const client = new ccc.ClientPublicTestnet(); for await (const cell of client.findCells({ script: { codeHash: "0x...", hashType: "type", args: "0x" }, scriptType: "type", scriptSearchMode: "prefix", withData: true, })) { console.log(cell.outPoint.txHash, cell.cellOutput.capacity); } ``` `@ckb-ccc/shell` does not include wallet connectors (JoyID, MetaMask, etc.). For browser wallet connectivity, use `@ckb-ccc/connector-react` or `@ckb-ccc/ccc`. # Protocol Support Layer (https://docs.ckbccc.com/en/docs/packages/protocol-sdks) The Protocol Support Layer adds higher-level abstractions over `@ckb-ccc/core` for the CKB ecosystem's most-used protocols. Each SDK composes transactions through the same declarative pattern as core, so you can mix them freely with your own custom logic. `@ckb-ccc/spore`, `@ckb-ccc/udt`, and `@ckb-ccc/ssri` are already bundled in `@ckb-ccc/shell` and `@ckb-ccc/ccc`. You only need to install them individually if you depend on `@ckb-ccc/core` directly. ## Architecture [#architecture] ## Package Overview [#package-overview] | Package | Purpose | Built on | Use when | | ------------------------------------------- | ----------------------------------------------------------------------- | ----------- | -------------------------------------------- | | [`@ckb-ccc/spore`](./spore) | Create, transfer, and melt on-chain Digital Objects (DOBs) and Clusters | core | Building NFT-like assets or on-chain media | | [`@ckb-ccc/udt`](./udt) | Issue, mint, and transfer User Defined Tokens (xUDT / sUDT) | core + ssri | Issuing or moving fungible tokens | | [`@ckb-ccc/ssri`](./ssri) | Call named methods on SSRI-compliant CKB scripts | core | Calling custom CKB scripts with SSRI methods | | [`@ckb-ccc/lumos-patches`](./lumos-patches) | Add JoyID / Nostr / Portal lock support to legacy Lumos apps | Lumos SDK | Migrating a Lumos app gradually | ## Quick Start [#quick-start] All protocol SDKs follow the core declarative pattern — describe the desired outputs, then let CCC complete inputs, fees, and change: ```typescript import { ccc } from "@ckb-ccc/shell"; // Example: transfer xUDT const udt = new ccc.udt.Udt(udtCodeOutPoint, udtType); const { res: tx } = await udt.transfer(signer, [ { to: receiverLock, amount: 1000n }, ]); await tx.completeInputsByUdt(signer, udtType); await tx.completeFeeBy(signer); const txHash = await signer.sendTransaction(tx); ``` The same pattern applies to Spore creation, SSRI method calls, and any custom transaction you compose alongside them. # @ckb-ccc/lumos-patches (https://docs.ckbccc.com/en/docs/packages/protocol-sdks/lumos-patches) `@ckb-ccc/lumos-patches` adds lock script support for JoyID, Nostr (NostrLock), and Portal (PWLock) wallets to the [Lumos](https://github.com/ckb-js/lumos) SDK. If you have an existing Lumos-based application and want to support these wallets without migrating to CCC, apply these patches. ## Who needs it [#who-needs-it] Install `@ckb-ccc/lumos-patches` if you have an existing Lumos-based codebase and want to: * Sign transactions with **JoyID** (no `@ckb-lumos/joyid` required). * Sign transactions with **Nostr** wallets. * Sign transactions with **Portal** wallet. For new projects, CCC natively supports all these wallets. Use `@ckb-ccc/lumos-patches` only when you already depend on Lumos and cannot migrate. ## Installation [#installation] ```bash npm install @ckb-ccc/lumos-patches ``` ```bash yarn add @ckb-ccc/lumos-patches ``` ```bash pnpm add @ckb-ccc/lumos-patches ``` ## Exports [#exports] | Export | Description | | ---------------------------- | ----------------------------------------------------------------------------------- | | `generateDefaultScriptInfos` | Returns `LockScriptInfo[]` for JoyID, Nostr, and Portal on both mainnet and testnet | | `generateScriptInfo` | Build a custom `LockScriptInfo` for any code hash | ## Applying the patches [#applying-the-patches] Call `generateDefaultScriptInfos()` and register the result with Lumos before composing any transactions: ```typescript import { generateDefaultScriptInfos } from "@ckb-ccc/lumos-patches"; import { registerCustomLockScriptInfos } from "@ckb-lumos/common-scripts/lib/common"; // Apply patches once at app startup — before using Lumos. // You no longer need @ckb-lumos/joyid after this. registerCustomLockScriptInfos(generateDefaultScriptInfos()); ``` After registration, Lumos will be able to collect cells and set up witnesses for the following lock scripts on both mainnet and testnet: | Script | Wallet | Dummy lock length | | ----------- | -------------------- | ----------------- | | `JoyId` | JoyID (passkey) | 1000 bytes | | `NostrLock` | Nostr NIP-07 wallets | 572 bytes | | `PWLock` | Portal Wallet | 65 bytes | ## Custom script info [#custom-script-info] If you need to add a different custom lock, use `generateScriptInfo` directly: ```typescript import { generateScriptInfo } from "@ckb-ccc/lumos-patches"; import { ccc } from "@ckb-ccc/core"; const myLockInfo = generateScriptInfo( "0xYOUR_CODE_HASH", [ // array of ccc.CellDepInfoLike { cellDep: { outPoint: { txHash: "0x...", index: 0 }, depType: "depGroup", }, }, ], 65, // dummy lock length in bytes for fee estimation ); ``` ## How it works [#how-it-works] `generateDefaultScriptInfos` returns a `LockScriptInfo` for each supported script on both mainnet and testnet. Each info object: 1. Provides a `CellCollector` that filters cells matching the lock's code hash. 2. Implements `setupInputCell` to add the correct cell deps and a dummy witness of the right size (used by Lumos for fee estimation). The lock scripts are looked up from `@ckb-ccc/core`'s built-in `MAINNET_SCRIPTS` and `TESTNET_SCRIPTS` constants, so you don't need to hardcode any hashes. ## Lumos version compatibility [#lumos-version-compatibility] This package is tested against: ``` @ckb-lumos/base 0.24.0-next.2 @ckb-lumos/codec 0.24.0-next.2 @ckb-lumos/common-scripts 0.24.0-next.2 @ckb-lumos/config-manager 0.24.0-next.2 @ckb-lumos/helpers 0.24.0-next.2 ``` **Lumos is no longer actively maintained. We recommend using CCC for new projects.** # @ckb-ccc/spore (https://docs.ckbccc.com/en/docs/packages/protocol-sdks/spore) ## What is Spore? [#what-is-spore] `@ckb-ccc/spore` implements the [Spore Protocol](https://spore.pro/) — CKB's on-chain digital object (DOB) standard. Each Spore is a Cell that holds its content (image, text, etc.) **fully on-chain**. Spores can optionally belong to a Cluster (similar to an NFT collection). ## Installation [#installation] ```bash npm install @ckb-ccc/spore ``` ```bash yarn add @ckb-ccc/spore ``` ```bash pnpm add @ckb-ccc/spore ``` If you are using `@ckb-ccc/shell`, spore is already included as `ccc.spore`. ## Exports [#exports] The package exports the `spore` namespace from the barrel: ```typescript import { spore } from "@ckb-ccc/spore"; // or, when using @ckb-ccc/shell: import { ccc } from "@ckb-ccc/shell"; // ccc.spore.createSpore(...) ``` Top-level exports: | Export | Description | | --------------------------- | ------------------------------------- | | `createSpore` | Create a new Spore cell | | `transferSpore` | Transfer a Spore to a new owner | | `meltSpore` | Destroy a Spore and recover capacity | | `findSpore` | Look up a single Spore cell by ID | | `findSpores` | Search Spore cells by lock or cluster | | `findSporesBySigner` | Find all Spores owned by a signer | | `createSporeCluster` | Create a new Cluster | | `transferSporeCluster` | Transfer a Cluster to a new owner | | `findCluster` | Look up a Cluster by ID | | `findSporeClusters` | Search Cluster cells | | `findSporeClustersBySigner` | Find all Clusters owned by a signer | | `dob` | Digital object encoding utilities | ## Key functions [#key-functions] ### `createSpore` [#createspore] ```typescript async function createSpore(params: { signer: ccc.Signer; data: SporeDataView; to?: ccc.ScriptLike; clusterMode?: "lockProxy" | "clusterCell" | "skip"; tx?: ccc.TransactionLike; scriptInfo?: SporeScriptInfoLike; scriptInfoHash?: ccc.HexLike; }): Promise<{ tx: ccc.Transaction; id: ccc.Hex }> ``` ### `transferSpore` [#transferspore] ```typescript async function transferSpore(params: { signer: ccc.Signer; id: ccc.HexLike; to: ccc.ScriptLike; tx?: ccc.TransactionLike; scripts?: SporeScriptInfoLike[]; scriptInfoHash?: ccc.HexLike; }): Promise<{ tx: ccc.Transaction }> ``` ### `meltSpore` [#meltspore] ```typescript async function meltSpore(params: { signer: ccc.Signer; id: ccc.HexLike; tx?: ccc.TransactionLike; scripts?: SporeScriptInfoLike[]; scriptInfoHash?: ccc.HexLike; }): Promise<{ tx: ccc.Transaction }> ``` ### `createSporeCluster` [#createsporecluster] ```typescript async function createSporeCluster(params: { signer: ccc.Signer; data: ClusterDataView; to?: ccc.ScriptLike; tx?: ccc.TransactionLike; scriptInfo?: SporeScriptInfoLike; scriptInfoHash?: ccc.HexLike; }): Promise<{ tx: ccc.Transaction; id: ccc.Hex }> ``` ### `transferSporeCluster` [#transfersporecluster] ```typescript async function transferSporeCluster(params: { signer: ccc.Signer; id: ccc.HexLike; to: ccc.ScriptLike; tx?: ccc.TransactionLike; scripts?: SporeScriptInfoLike[]; scriptInfoHash?: ccc.HexLike; }): Promise<{ tx: ccc.Transaction }> ``` ## Usage examples [#usage-examples] ### Create a Spore [#create-a-spore] ```typescript import { spore } from "@ckb-ccc/spore"; import { ccc } from "@ckb-ccc/shell"; const { tx, id } = await spore.createSpore({ signer, data: { contentType: "text/plain", content: new TextEncoder().encode("Hello, Spore!"), }, }); await tx.completeFeeBy(signer); const txHash = await signer.sendTransaction(tx); console.log("Spore ID:", id); ``` ### Create a Spore in a Cluster [#create-a-spore-in-a-cluster] ```typescript const { tx: clusterTx, id: clusterId } = await spore.createSporeCluster({ signer, data: { name: "My Collection", description: "A collection of digital objects", }, }); await clusterTx.completeFeeBy(signer); await signer.sendTransaction(clusterTx); // Create a Spore that belongs to the cluster const { tx, id: sporeId } = await spore.createSpore({ signer, data: { contentType: "image/png", content: pngBytes, clusterId, }, clusterMode: "lockProxy", }); await tx.completeFeeBy(signer); await signer.sendTransaction(tx); ``` ### Find a Spore by ID [#find-a-spore-by-id] ```typescript import { findSpore } from "@ckb-ccc/spore"; const result = await findSpore(client, "0xSPORE_ID..."); if (result) { console.log("Content type:", result.sporeData.contentType); console.log("Content:", ccc.bytesTo(result.sporeData.content, "utf8")); } ``` ### Transfer a Spore [#transfer-a-spore] ```typescript import { ccc } from "@ckb-ccc/core"; import { transferSpore } from "@ckb-ccc/spore"; // Transfer a Spore to a new owner const { script: newOwner } = await ccc.Address.fromString(receiverAddress, client); let { tx } = await transferSpore({ signer, id: "0xSPORE_ID...", to: newOwner, }); await tx.completeFeeBy(signer); const txHash = await signer.sendTransaction(tx); ``` ### Search Spores by Owner [#search-spores-by-owner] ```typescript import { findSporesBySigner } from "@ckb-ccc/spore"; for await (const { sporeData, cell } of findSporesBySigner({ signer })) { console.log("Spore:", cell.outPoint.txHash); console.log("Content type:", sporeData.contentType); } ``` ### Melt (Destroy) a Spore [#melt-destroy-a-spore] ```typescript import { meltSpore } from "@ckb-ccc/spore"; let { tx } = await meltSpore({ signer, id: "0xSPORE_ID...", }); await tx.completeFeeBy(signer); const txHash = await signer.sendTransaction(tx); ``` ## DOB (digital object) encoding [#dob-digital-object-encoding] The `dob` sub-namespace provides utilities for encoding Spore data following the DOB0/DOB1 specifications: ```typescript import { spore } from "@ckb-ccc/spore"; const encoded = spore.dob.encodeNativeRendererDob0({ /* DOB0 content */ }); ``` ## Predefined script configurations [#predefined-script-configurations] The package ships with predefined script info for both mainnet and testnet. These are selected automatically based on your `client`. You can also pass a custom `scriptInfo` to every function to target a different deployment. # @ckb-ccc/ssri (https://docs.ckbccc.com/en/docs/packages/protocol-sdks/ssri) `@ckb-ccc/ssri` implements the **Script-Sourced Rich Information** (SSRI) protocol. SSRI defines a standard way to call named methods on CKB scripts and receive structured responses — enabling smart contracts to expose rich metadata and operations via an off-chain RPC server. ## Installation [#installation] If you use `@ckb-ccc/shell`, the `ssri` namespace is already available as `ccc.ssri`. ```bash npm install @ckb-ccc/ssri ``` ```bash yarn add @ckb-ccc/ssri ``` ```bash pnpm add @ckb-ccc/ssri ``` ## What SSRI is [#what-ssri-is] CKB scripts (lock/type scripts) are pure validation functions — by default they have no way to expose metadata or provide callable interfaces. SSRI solves this by defining a protocol where: 1. A smart contract implements named methods following a naming convention (e.g. `UDT.name`, `SSRI.version`). 2. An off-chain **SSRI server** executes those methods by running the script in a sandboxed environment. 3. Client code calls the SSRI server via JSON-RPC, passing the script's code `OutPoint`, method name, and arguments. `@ckb-ccc/ssri` provides the TypeScript abstractions for this pattern. ## Exports [#exports] | Export | Description | | ------------------------------ | ------------------------------------------------------------------------ | | `Trait` | Base class for all SSRI traits (extend this to implement a custom trait) | | `Executor` | Abstract base class for SSRI execution backends | | `ExecutorJsonRpc` | Concrete executor that calls an SSRI server via JSON-RPC | | `ExecutorResponse` | Response wrapper carrying a result and resolved cell dependencies | | `ExecutorErrorUnknown` | Error: unknown server error | | `ExecutorErrorExecutionFailed` | Error: script execution failed | | `ExecutorErrorDecode` | Error: failed to decode the response | | `ContextCode` | Execution context: no context (code-level) | | `ContextScript` | Execution context: script-level | | `ContextCell` | Execution context: cell-level | | `ContextTransaction` | Execution context: transaction-level | | `getMethodPath` | Compute the 8-byte method path hash for a method name | ## Key classes [#key-classes] ### `Trait` [#trait] The base class for any SSRI-compatible contract interface. Extend it to build typed wrappers around on-chain scripts: ```typescript import { ssri } from "@ckb-ccc/ssri"; import { ccc } from "@ckb-ccc/core"; class MyContract extends ssri.Trait { async myMethod(): Promise> { const res = await this.assertExecutor().runScript( this.code, "MyContract.my_method", [], ); return res.map((bytes) => ccc.bytesTo(bytes, "utf8")); } } ``` Built-in `Trait` methods: ```typescript // List methods exposed by the script trait.getMethods(offset?, limit?): Promise> // Check which method names exist trait.hasMethods(methodNames: string[]): Promise> // Query the SSRI version of the script trait.version(): Promise> // Safely run a call, returning undefined instead of throwing on execution failure trait.tryRun(call): Promise> ``` ### `ExecutorJsonRpc` [#executorjsonrpc] Connects to an SSRI server over HTTP JSON-RPC: ```typescript const executor = new ssri.ExecutorJsonRpc("https://your-ssri-server.example.com"); ``` ### `ExecutorResponse` [#executorresponset] All SSRI calls return an `ExecutorResponse`. It carries the decoded result and any cell dependencies that must be included in the transaction: ```typescript const response: ssri.ExecutorResponse = await trait.myMethod(); response.res; // the decoded value response.cellDeps; // ccc.OutPoint[] — cell deps to add to your transaction response.map(fn); // transform the result ``` ## Usage example [#usage-example] ```typescript import { ssri } from "@ckb-ccc/ssri"; import { ccc } from "@ckb-ccc/core"; const executor = new ssri.ExecutorJsonRpc("https://ssri.example.com"); const trait = new ssri.Trait( { txHash: "0x...", index: 0 }, // code OutPoint executor, ); // Check the SSRI version const { res: version, cellDeps } = await trait.version(); console.log("Version:", version); // List available methods const { res: methods } = await trait.getMethods(); console.log("Methods:", methods); ``` ## Execution contexts [#execution-contexts] SSRI methods can be called at four levels of context, controlling what chain data is available to the script: | Context type | When to use | | ----------------------- | ----------------------------------------------------------------- | | `ContextCode` (default) | Pure code-level queries — no cell or transaction data | | `ContextScript` | Pass a script to the executor (e.g. to query per-script metadata) | | `ContextCell` | Pass a full cell (output + data) for cell-level script execution | | `ContextTransaction` | Pass a full transaction for transaction-level script execution | ```typescript const res = await executor.runScript( codeOutPoint, "UDT.balance", [encodedArgs], { script: { codeHash: "0x...", hashType: "type", args: "0x..." }, }, ); ``` `@ckb-ccc/udt` is built on top of `@ckb-ccc/ssri` and demonstrates a full working implementation of the SSRI pattern. ## References [#references] * [Script-Sourced Rich Information (SSRI)](https://talk.nervos.org/t/en-cn-script-sourced-rich-information-script/8256/2) * [SSRI Executor on WASM: Implementing SSRI Protocol in Browsers](https://talk.nervos.org/t/en-cn-ssri-executor-on-wasm-implementing-ssri-protocol-in-browsers-ssri/8732) # @ckb-ccc/udt (https://docs.ckbccc.com/en/docs/packages/protocol-sdks/udt) `@ckb-ccc/udt` provides a TypeScript SDK for interacting with User Defined Tokens (UDT) and the xUDT standard on CKB. It is built on top of the SSRI protocol, supporting both SSRI-compliant contracts and legacy sUDT/xUDT tokens. ## Installation [#installation] If you are using `@ckb-ccc/shell`, the `udt` namespace is already available as `ccc.udt`. ```bash npm install @ckb-ccc/udt ``` ```bash yarn add @ckb-ccc/udt ``` ```bash pnpm add @ckb-ccc/udt ``` ## Exports [#exports] | Export | Description | | ------------- | -------------------------------------- | | `Udt` | Main class for interacting with a UDT | | `UdtPausable` | Extended class for pausable UDT tokens | ## The `Udt` class [#the-udt-class] `Udt` extends `ssri.Trait` and provides high-level methods for all token operations. ### Constructor [#constructor] ```typescript const udt = new Udt( code, // ccc.OutPointLike — the script code cell OutPoint script, // ccc.ScriptLike — the type script identifying this token config?, // { executor?: ssri.Executor | null } ); ``` ### Read methods [#read-methods] ```typescript // Token metadata const { res: name } = await udt.name(); // string | undefined const { res: symbol } = await udt.symbol(); // string | undefined const { res: decimals } = await udt.decimals(); // ccc.Num | undefined const { res: icon } = await udt.icon(); // string (data URI) | undefined ``` ### `transfer` [#transfer] Transfer tokens to one or more recipients: ```typescript async transfer( signer: ccc.Signer, transfers: { to: ccc.ScriptLike; amount: ccc.NumLike }[], tx?: ccc.TransactionLike | null, ): Promise> ``` ### `mint` [#mint] Mint new tokens (issuer only): ```typescript async mint( signer: ccc.Signer, mints: { to: ccc.ScriptLike; amount: ccc.NumLike }[], tx?: ccc.TransactionLike | null, ): Promise> ``` ### `completeBy` [#completeby] Balance UDT inputs/outputs automatically using the signer's address as change: ```typescript async completeBy( tx: ccc.TransactionLike, from: ccc.Signer, ): Promise ``` ### `completeChangeToLock` [#completechangetolock] Balance UDT inputs/outputs with a custom change lock script: ```typescript async completeChangeToLock( txLike: ccc.TransactionLike, signer: ccc.Signer, change: ccc.ScriptLike, ): Promise ``` ## Usage examples [#usage-examples] ### Issue xUDT [#issue-xudt] The following example shows how to issue xUDT tokens using CCC (from CCC demo application): ```typescript // Get the lock script from signer's recommended address, Set token metadata const { script } = await signer.getRecommendedAddressObj(); const decimals = "8"; const symbol = "SUS"; const name = "Sample Token"; // Create transaction with one output const susTx = ccc.Transaction.from({ outputs: [{ lock: script }], }); // Add inputs to satisfy capacity requirement automatically await susTx.completeInputsByCapacity(signer); // Calculate and add transaction fee automatically await susTx.completeFeeBy(signer); // Sign and broadcast transaction, return transaction hash const susTxHash = await signer.sendTransaction(susTx); ``` ### Use Type ID to issue xUDT [#use-type-id-to-issue-xudt] ```typescript // 1. Create or use existing Type ID const typeId = await ccc.Script.fromKnownScript( signer.client, ccc.KnownScript.TypeId, typeIdArgs, ); // 2. Create output type proxy lock const outputTypeLock = await ccc.Script.fromKnownScript( signer.client, ccc.KnownScript.OutputTypeProxyLock, typeId.hash(), ); // 3. Create owner cell const lockTx = ccc.Transaction.from({ outputs: [{ lock: outputTypeLock }], }); await lockTx.completeInputsByCapacity(signer); await lockTx.completeFeeBy(signer); const lockTxHash = await signer.sendTransaction(lockTx); // 4. Mint tokens const mintTx = ccc.Transaction.from({ inputs: [ { previousOutput: typeIdCell.outPoint }, { previousOutput: { txHash: lockTxHash, index: 0 } }, ], outputs: [ typeIdCell.cellOutput, { lock: script, type: await ccc.Script.fromKnownScript( signer.client, ccc.KnownScript.XUdt, outputTypeLock.hash(), ), }, ], }); ``` ### Transfer tokens [#transfer-tokens] ```typescript import { udt } from "@ckb-ccc/udt"; import { ccc } from "@ckb-ccc/shell"; const myToken = new udt.Udt( { txHash: "0x4e2e832e0b1e7b5994681b621b00c1e65f577ee4b440ef95fa07db9bb3d50269", index: 0, }, { codeHash: "0xcc9dc33ef234e14bc788c43a4848556a5fb16401a04662fc55db9bb201987037", hashType: "type", args: "0x71fd1985b2971a9903e4d8ed0d59e6710166985217ca0681437883837b86162f", }, ); const { script: toScript } = await ccc.Address.fromString( "ckb1qzda0cr08m85hc8jlnfp3gog...", signer.client, ); // Build the transfer transaction const { res: tx } = await myToken.transfer(signer, [ { to: toScript, amount: 100n }, ]); // Balance UDT change and capacity const completedTx = await myToken.completeBy(tx, signer); await completedTx.completeInputsByCapacity(signer); await completedTx.completeFeeBy(signer); const txHash = await signer.sendTransaction(completedTx); ``` ### Read token metadata [#read-token-metadata] ```typescript const { res: name } = await myToken.name(); const { res: symbol } = await myToken.symbol(); const { res: decimals } = await myToken.decimals(); console.log(`${name} (${symbol}), ${decimals} decimal places`); ``` ### Mint tokens [#mint-tokens] ```typescript const { res: tx } = await myToken.mint(signer, [ { to: recipientScript, amount: 1_000_000n }, ]); const completedTx = await myToken.completeBy(tx, signer); await completedTx.completeInputsByCapacity(signer); await completedTx.completeFeeBy(signer); await signer.sendTransaction(completedTx); ``` `@ckb-ccc/udt` depends on `@ckb-ccc/ssri`. An `ssri.Executor` is optional — without one, the `Udt` class falls back to constructing transactions directly without querying SSRI metadata from the chain. # @ckb-ccc/eip6963 (https://docs.ckbccc.com/en/docs/packages/wallet-integrations/eip6963) `@ckb-ccc/eip6963` exposes any [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) compatible browser wallet (MetaMask, Rabby, OKX EVM, etc.) as a CCC `Signer`. It signs CKB transactions via `personal_sign` EVM signatures and derives CKB addresses from the user's Ethereum account. If you're using `@ckb-ccc/connector-react` or `@ckb-ccc/ccc`, EIP-6963 wallets are already included — no separate installation needed. ## Installation [#installation] ```bash npm install @ckb-ccc/eip6963 ``` ```bash yarn add @ckb-ccc/eip6963 ``` ```bash pnpm add @ckb-ccc/eip6963 ``` **Dependencies:** | Package | Description | | --------------- | -------------------------------------------------------- | | `@ckb-ccc/core` | Base types — `Signer`, `Client`, `Transaction`, and more | ## Architecture [#architecture] Unlike other wallet packages that detect a single global provider, `@ckb-ccc/eip6963` uses the EIP-6963 **multi-injected provider discovery** standard. This means it can discover and create signers for *all* EVM wallets installed in the browser simultaneously. ### Entry point: `SignerFactory` [#entry-point-signerfactory] `SignerFactory` is the main entry point. It listens for `eip6963:announceProvider` events and creates a `Signer` for each unique wallet discovered: ## The `Signer` class [#the-signer-class] `Signer` extends `ccc.SignerEvm` and adapts any EIP-1193 compatible provider to CKB signing. ### Key methods [#key-methods] | Method | Description | | ------------------------- | ----------------------------------------------------------- | | `connect()` | Calls `eth_requestAccounts` to prompt wallet connection | | `isConnected()` | Checks if `eth_accounts` returns any account | | `getEvmAccount()` | Returns the first EVM address from the provider | | `signMessageRaw(message)` | Signs via `personal_sign` — used for CKB witness generation | | `onReplaced(listener)` | Fires on `accountsChanged` or `disconnect` events | ### Signing flow [#signing-flow] CKB transactions are signed by deriving a CKB address from the EVM account and using `personal_sign` to produce a witness signature: ## Account change detection [#account-change-detection] `Signer` implements `onReplaced()` to handle account or wallet disconnection: * Listens for `"accountsChanged"` — user switched account in wallet * Listens for `"disconnect"` — wallet disconnected When either fires, the application callback is invoked and the listener is automatically cleaned up. ## Provider interface (EIP-1193) [#provider-interface-eip-1193] The package uses a minimal subset of the EIP-1193 provider interface: | Method | Purpose | | --------------------- | ---------------------------------------- | | `eth_requestAccounts` | Prompt user to connect | | `eth_accounts` | Get connected accounts (no prompt) | | `personal_sign` | Sign a message with the selected account | ## Integration pattern [#integration-pattern] `@ckb-ccc/eip6963` follows the same integration contract as other wallet packages in CCC: * **Factory class** — `SignerFactory` discovers wallets and creates signers dynamically. * **Provider detection** — uses EIP-6963 events with `window.ethereum` fallback. * **Deduplication** — tracks provider UUIDs to avoid duplicate signers. * **Graceful degradation** — if no EVM wallets are installed, no signers are created. This means `SignersController` can dynamically discover all EVM wallets without any configuration. # Wallet Integrations (https://docs.ckbccc.com/en/docs/packages/wallet-integrations) CCC supports multiple wallet ecosystems through dedicated signer packages. Each package implements the unified `Signer` interface so application code stays the same regardless of which wallet the user picks. All wallet packages below are already bundled in `@ckb-ccc/ccc` and `@ckb-ccc/connector-react`. You only need to install them individually if you're building a custom integration. | Package | Chain | Wallets | Provider Detection | | ----------------------------------------------------------- | ----------------------- | --------------------------------------------- | --------------------------------- | | [`@ckb-ccc/joy-id`](./wallet-integrations/joy-id) | CKB / BTC / EVM / Nostr | JoyID | WebAuthn / iframe | | [`@ckb-ccc/eip6963`](./wallet-integrations/eip6963) | EVM | MetaMask, Rabby, OKX EVM, any EIP-6963 wallet | `eip6963:announceProvider` events | | [`@ckb-ccc/nip07`](./wallet-integrations/nip07) | Nostr | nos2x, Alby, any NIP-07 extension | `window.nostr` | | [`@ckb-ccc/utxo-global`](./wallet-integrations/utxo-global) | CKB / BTC / DOGE | UTXO Global | `window.utxoGlobal` | | [`@ckb-ccc/rei`](./wallet-integrations/rei) | CKB | REI Wallet | `window.rei.ckb` | | [`@ckb-ccc/okx`](./wallet-integrations/okx) | BTC / Nostr | OKX Wallet | `window.okxwallet` | | [`@ckb-ccc/uni-sat`](./wallet-integrations/uni-sat) | BTC | UniSat | `window.unisat` | | [`@ckb-ccc/xverse`](./wallet-integrations/xverse) | BTC | Xverse, any SATS Connect wallet | `window.btc_providers` | ## Integration contract [#integration-contract] Every wallet package follows the same pattern: 1. **Factory function** — e.g. `getOKXSigners(client)` — returns `SignerInfo[]` or a single `Signer`. 2. **Provider detection** — checks for a browser-injected object before creating signers. 3. **Graceful degradation** — returns an empty array (or `undefined`) when the wallet isn't installed. 4. **`onReplaced()` listener** — notifies the app when the user switches accounts in the wallet UI. This means `SignersController` can treat all wallets uniformly with zero special-casing in your application code. # @ckb-ccc/joy-id (https://docs.ckbccc.com/en/docs/packages/wallet-integrations/joy-id) `@ckb-ccc/joy-id` is the Protocol Support Layer package in the CCC responsible for integrating the JoyID wallet. JoyID is a wallet based on WebAuthn/Passkey — no seed phrases required, keys are managed via device biometrics (fingerprint, Face ID, etc.). This package communicates with the JoyID application via browser popups and provides unified `Signer` interface implementations for four chain types: CKB, Bitcoin, EVM, and Nostr. ## Installation [#installation] ```bash npm install @ckb-ccc/joy-id ``` ```bash yarn add @ckb-ccc/joy-id ``` ```bash pnpm add @ckb-ccc/joy-id ``` **Dependencies:** | Package | Description | | --------------- | ----------------------------------------------------------------------------------------------- | | `@ckb-ccc/core` | CCC core layer — provides `Signer`, `Client`, `Transaction`, and other base types | | `@joyid/ckb` | JoyID CKB SDK — provides `Aggregator` (COTA aggregator) | | `@joyid/common` | JoyID common utilities — provides `buildJoyIDURL`, `createBlockDialog`, `DappRequestType`, etc. | If you are using `@ckb-ccc/connector-react` or `@ckb-ccc/ccc`, JoyID is already included — no separate installation needed. ## Architecture Overview [#architecture-overview] ## Public API Summary [#public-api-summary] All public exports come from `src/barrel.ts`: | Export | Source File | Kind | | ----------------- | ------------------------ | -------- | | `CkbSigner` | `ckb/index.ts` | class | | `BitcoinSigner` | `btc/index.ts` | class | | `EvmSigner` | `evm/index.ts` | class | | `NostrSigner` | `nostr/index.ts` | class | | `getJoyIdSigners` | `signerFactory/index.ts` | function | ## Core Classes [#core-classes] ### 1. `CkbSigner` [#1-ckbsigner] Native CKB chain signer based on JoyID Passkey technology. Supports both main key (`main_key`) and sub key (`sub_key`, which depends on the COTA protocol). **Constructor Parameters:** ```typescript new CkbSigner( client: ccc.Client, // CKB node client name: string, // DApp name (shown in JoyID popup) icon: string, // DApp icon URL _appUri?: string, // Custom JoyID App URL (optional, auto-selected by network) _aggregatorUri?: string, // Custom COTA aggregator URL (optional) connectionsRepo?: ConnectionsRepo // Connection storage (defaults to localStorage) ) ``` **Properties:** | Property | Value | | ---------- | -------------------------- | | `type` | `ccc.SignerType.CKB` | | `signType` | `ccc.SignerSignType.JoyId` | **Default Endpoints:** | Network | JoyID App URL | COTA Aggregator URL | | --------------- | --------------------------- | --------------------------------------------- | | Mainnet (`ckb`) | `https://app.joy.id` | `https://cota.nervina.dev/mainnet-aggregator` | | Testnet (`ckt`) | `https://testnet.joyid.dev` | `https://cota.nervina.dev/aggregator` | **Key Methods:** * `connect()` — Opens a JoyID popup for authentication (`/auth`), retrieves address, public key, and keyType, then persists to localStorage. * `disconnect()` — Clears in-memory connection state and removes from localStorage. * `isConnected()` — Checks in-memory connection; if absent, attempts to restore from localStorage. * `getInternalAddress()` — Returns the JoyID CKB address string. * `getIdentity()` — Returns a JSON string `{ address, keyType, publicKey }` used for signature verification. Note: `publicKey` has the first byte (prefix/format identifier) removed to adapt to signature verification requirements. * `prepareTransaction(txLike)` — Adds JoyId script cell deps; for `sub_key` accounts, additionally adds COTA cell deps and SMT unlock data. * `signOnlyTransaction(txLike)` — Opens a JoyID popup (`/sign-ckb-raw-tx`) to complete transaction signing. * `signMessageRaw(message)` — Opens a JoyID popup (`/sign-message`) to sign a message; returns a JSON string `{ signature, alg, message }`. **Sub Key Mechanism:** When `keyType === "sub_key"`, `prepareTransaction` will: 1. Call `generateSubkeyUnlockSmt` on the COTA aggregator to generate an SMT unlock entry. 2. Write the unlock data into the witness `outputType` field. 3. Prepend COTA cell deps to the transaction. ### 2. `BitcoinSigner` [#2-bitcoinsigner] Bitcoin chain signer supporting both P2WPKH (Native SegWit) and P2TR (Taproot) address types. **Constructor Parameters:** ```typescript new BitcoinSigner( client: ccc.Client, name: string, icon: string, preferredNetworks?: ccc.NetworkPreference[], // defaults to btc/btcTestnet addressType?: "auto" | "p2wpkh" | "p2tr", // defaults to "auto" _appUri?: string, connectionsRepo?: ConnectionsRepo ) ``` **`addressType` Behavior:** | Value | Behavior | | ---------- | ------------------------------------------------------------------------ | | `"auto"` | Automatically selects based on the user's JoyID account `btcAddressType` | | `"p2wpkh"` | Forces Native SegWit address | | `"p2tr"` | Forces Taproot address | **Key Methods:** * `connect()` — Popup authentication; selects `nativeSegwit` or `taproot` from the response based on `addressType`. * `getBtcAccount()` — Returns the Bitcoin address string. * `getBtcPublicKey()` — Returns the Bitcoin public key (`ccc.Hex`). * `signMessageRaw(message)` — Signs a message using ECDSA (`signMessageType: "ecdsa"`). ### 3. `EvmSigner` [#3-evmsigner] EVM chain signer that retrieves an Ethereum address and signs via JoyID. **Constructor Parameters:** ```typescript new EvmSigner( client: ccc.Client, name: string, icon: string, _appUri?: string, connectionsRepo?: ConnectionsRepo ) ``` **Key Methods:** * `connect()` — Popup authentication; takes `ethAddress` from the response as the EVM account address. * `getEvmAccount()` — Returns the Ethereum address (`ccc.Hex`). * `signMessageRaw(message)` — Signs a message; returns a `ccc.Hex` signature. ### 4. `NostrSigner` [#4-nostrsigner] Nostr protocol signer that retrieves a Nostr public key and signs events via JoyID. **Constructor Parameters:** ```typescript new NostrSigner( client: ccc.Client, name: string, icon: string, _appUri?: string, connectionsRepo?: ConnectionsRepo ) ``` **Key Methods:** * `connect()` — Popup authentication; takes `nostrPubkey` from the response. * `getNostrPublicKey()` — Returns the Nostr public key (`ccc.Hex`). * `signNostrEvent(event)` — Opens a popup (`/sign-nostr-event`) to sign a Nostr event; returns a complete `Required`. ### 5. `getJoyIdSigners()` Factory Function [#5-getjoyidsigners-factory-function] The recommended integration entry point — returns all JoyID-supported signers in a single call. **Signature:** ```typescript function getJoyIdSigners( client: ccc.Client, name: string, icon: string, preferredNetworks?: ccc.NetworkPreference[], ): ccc.SignerInfo[] ``` **Returned Signers (standard browser environment):** | name | Signer Type | | ---------------- | ------------------------ | | `"CKB"` | `CkbSigner` | | `"BTC"` | `BitcoinSigner` (auto) | | `"Nostr"` | `NostrSigner` | | `"EVM"` | `EvmSigner` | | `"BTC (P2WPKH)"` | `BitcoinSigner` (p2wpkh) | | `"BTC (P2TR)"` | `BitcoinSigner` (p2tr) | **WebView / Standalone Browser Fallback:** In WebView or PWA standalone mode, JoyID cannot open popups. The factory returns `ccc.SignerAlwaysError` instances for CKB, EVM, and BTC — any method call will throw `"JoyID can only be used with standard browsers"`. ## Connection Storage [#connection-storage] ### `ConnectionsRepo` Interface [#connectionsrepo-interface] ### `ConnectionsRepoLocalStorage` (Default Implementation) [#connectionsrepolocalstorage-default-implementation] Stores connection information as a JSON array under the `"ccc-joy-id-signer"` key in `localStorage`. **`Connection` Type:** ```typescript type Connection = { readonly address: string; // Chain address readonly publicKey: ccc.Hex; // Public key (hex format) readonly keyType: string; // "main_key" | "sub_key" }; ``` **`AccountSelector` Type:** ```typescript type AccountSelector = { uri: string; // JoyID App URL addressType: string; // "ckb" | "btc-auto" | "btc-p2wpkh" | "btc-p2tr" | "ethereum" | "nostr" }; ``` **Custom Storage:** Implement the `ConnectionsRepo` interface to replace the default localStorage backend — e.g., with IndexedDB or a server-side store. ## Popup Communication Mechanism [#popup-communication-mechanism] `createPopup()` is the underlying function used by all signers to communicate with the JoyID App. **Flow:** **Error Types:** | Error Class | Trigger Condition | | ------------------------ | ------------------------------------------ | | `PopupNotSupportedError` | Standalone browser does not support popups | | `PopupCancelledError` | User manually closes the popup | | `PopupTimeoutError` | Operation times out (default 3000 seconds) | ## Integration with `@ckb-ccc/ccc` [#integration-with-ckb-cccccc] In `@ckb-ccc/ccc`'s `SignersController`, JoyID is registered under the name `"JoyID Passkey"`: When using `@ckb-ccc/connector-react`, JoyID automatically appears in the wallet selection UI — no manual integration required. ## Usage Examples [#usage-examples] ### Example 0: Using `@ckb-ccc/connector-react`(Recommended) [#example-0-using-ckb-cccconnector-reactrecommended] JoyID is automatically registered when using the CCC connector: ```tsx import { ccc } from "@ckb-ccc/connector-react"; export default function App({ children }) { return ( {children} ); } ``` When the user opens the wallet selector, JoyID appears as an option. No additional configuration is required. ### Example 1: Factory Function Integration [#example-1-factory-function-integration] ```typescript import { ccc } from "@ckb-ccc/core"; import { getJoyIdSigners } from "@ckb-ccc/joy-id"; const client = new ccc.ClientPublicTestnet(); const signers = getJoyIdSigners( client, "My DApp", "https://my-dapp.com/icon.png", ); // Find the CKB signer const ckbSignerInfo = signers.find((s) => s.name === "CKB"); const signer = ckbSignerInfo!.signer; await signer.connect(); const address = await signer.getRecommendedAddress(); console.log("CKB Address:", address); ``` ### Example 2: Direct `CkbSigner` Usage [#example-2-direct-ckbsigner-usage] ```typescript import { ccc } from "@ckb-ccc/core"; import { CkbSigner } from "@ckb-ccc/joy-id"; const client = new ccc.ClientPublicMainnet(); const signer = new CkbSigner(client, "My DApp", "https://my-dapp.com/icon.png"); await signer.connect(); // Build and send transaction const tx = ccc.Transaction.from({ outputs: [{ lock: await signer.getAddressObj().then((a) => a.script), capacity: ccc.fixedPointFrom(100) }], outputsData: ["0x"], }); await tx.completeInputsByCapacity(signer); await tx.completeFeeBy(signer, 1000); const txHash = await signer.sendTransaction(tx); console.log("TxHash:", txHash); ``` ### Example 3: Custom COTA Aggregator (for sub\_key accounts) [#example-3-custom-cota-aggregator-for-sub_key-accounts] ```typescript import { CkbSigner } from "@ckb-ccc/joy-id"; const signer = new CkbSigner( client, "My DApp", "https://my-dapp.com/icon.png", undefined, // use default JoyID App URL "https://my-custom-aggregator.com/aggregator" // custom COTA aggregator ); ``` ### Example 4: Sign a Message and Verify [#example-4-sign-a-message-and-verify] ```typescript import { ccc } from "@ckb-ccc/core"; import { CkbSigner } from "@ckb-ccc/joy-id"; const signer = new CkbSigner(client, "My DApp", "https://icon.url"); await signer.connect(); const message = "Hello, CKB!"; const rawSig = await signer.signMessageRaw(message); // rawSig is a JSON string: { signature, alg, message } const identity = await signer.getIdentity(); const isValid = await ccc.Signer.verifyMessage(message, { signType: ccc.SignerSignType.JoyId, signature: rawSig, identity, }); ``` ### Example 5: Custom Connection Storage [#example-5-custom-connection-storage] ```typescript import { CkbSigner, ConnectionsRepo, AccountSelector, Connection } from "@ckb-ccc/joy-id"; class MyCustomStorage implements ConnectionsRepo { async get(selector: AccountSelector): Promise { // Read from IndexedDB or server } async set(selector: AccountSelector, connection: Connection | undefined): Promise { // Write to IndexedDB or server } } const signer = new CkbSigner( client, "My DApp", "https://icon.url", undefined, undefined, new MyCustomStorage() ); ``` ## Lumos compatibility [#lumos-compatibility] JoyID is supported via Lumos patches if you are using the Lumos SDK: ```typescript import { generateDefaultScriptInfos } from "@ckb-ccc/lumos-patches"; // Call before using Lumos — no @ckb-lumos/joyid needed registerCustomLockScriptInfos(generateDefaultScriptInfos()); ``` See the [@ckb-ccc/lumos-patches](../protocol-sdks/lumos-patches) page for details. ## Caveats and Limitations [#caveats-and-limitations] 1. **Browser-only**: All signers depend on `window.open`, `window.localStorage`, and `window.postMessage`. Node.js server-side environments are not supported. 2. **No WebView support**: In WebView (e.g., WeChat in-app browser) or PWA standalone mode, `getJoyIdSigners` returns `SignerAlwaysError` instances — JoyID cannot be used. 3. **Popup blocking**: Browsers may block popups not triggered by a user gesture. `connect()` and signing methods should be called inside user event handlers (e.g., button click). 4. **Sub Key requires COTA**: When using a sub key account, the account must hold a COTA cell; otherwise `prepareTransaction` throws `"No COTA cells for sub key wallet"`. 5. **Message signature format**: `CkbSigner.signMessageRaw` returns a JSON string (containing `signature`, `alg`, and `message` fields), not raw signature bytes. Use `ccc.Signer.verifyMessage` with `SignerSignType.JoyId` for verification. ## References [#references] * [JoyID Website](https://joy.id) # @ckb-ccc/nip07 (https://docs.ckbccc.com/en/docs/packages/wallet-integrations/nip07) `@ckb-ccc/nip07` lets any [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md) compatible Nostr browser extension act as a CCC `Signer`. It derives a CKB address from the user's Nostr public key and signs CKB transactions via Nostr event signing. If you're using `@ckb-ccc/connector-react` or `@ckb-ccc/ccc`, NIP-07 support is already included — no separate installation needed. ## Installation [#installation] ```bash npm install @ckb-ccc/nip07 ``` ```bash yarn add @ckb-ccc/nip07 ``` ```bash pnpm add @ckb-ccc/nip07 ``` **Dependencies:** | Package | Description | | --------------- | -------------------------------------------------------- | | `@ckb-ccc/core` | Base types — `Signer`, `Client`, `Transaction`, and more | ## Architecture [#architecture] `@ckb-ccc/nip07` is a thin wrapper around the `window.nostr` object injected by NIP-07 extensions (e.g. nos2x, Alby). ### Entry point: `getNip07Signer` [#entry-point-getnip07signer] `getNip07Signer(client)` checks for `window.nostr` and returns a `Signer` instance — or `undefined` if no NIP-07 extension is available: ## The `Signer` class [#the-signer-class] `Signer` extends `ccc.SignerNostr` and wraps a NIP-07 provider with public key caching. ### Key methods [#key-methods] | Method | Description | | ----------------------- | ---------------------------------------------------------------------------- | | `connect()` | Retrieves and caches the Nostr public key | | `isConnected()` | Always returns `true` (NIP-07 extensions are always available once detected) | | `getNostrPublicKey()` | Returns the cached Nostr public key (hex-encoded) | | `signNostrEvent(event)` | Signs a Nostr event via `provider.signEvent()` | ### Public key caching [#public-key-caching] The Nostr public key is fetched once via `provider.getPublicKey()` and cached as a `Promise`. If the call fails, the cache is cleared so the next attempt retries: ```typescript async getNostrPublicKey(): Promise { if (!this.publicKeyCache) { this.publicKeyCache = this.provider.getPublicKey().catch((e) => { this.publicKeyCache = undefined; throw e; }); } return ccc.hexFrom(await this.publicKeyCache); } ``` ### Signing flow [#signing-flow] CKB transactions are signed by constructing a Nostr event and delegating to the extension: ## Provider interface [#provider-interface] The NIP-07 provider interface is minimal: | Method | Returns | Description | | ------------------ | --------------------- | ------------------------------------------------------- | | `getPublicKey()` | `Promise` | Returns the user's Nostr public key (hex) | | `signEvent(event)` | `Promise` | Signs a Nostr event and returns it with the `sig` field | ## Integration pattern [#integration-pattern] `@ckb-ccc/nip07` follows the same integration contract as other wallet packages in CCC: * **Factory function** — `getNip07Signer` returns a `Signer` or `undefined`. * **Provider detection** — checks for `window.nostr` before creating the signer. * **Graceful degradation** — returns `undefined` when no NIP-07 extension is installed. This package is also used as a dependency by `@ckb-ccc/okx` for its Nostr signing support. # @ckb-ccc/okx (https://docs.ckbccc.com/en/docs/packages/wallet-integrations/okx) `@ckb-ccc/okx` integrates OKX Wallet into CCC, providing `Signer` implementations for Bitcoin and Nostr protocols. It communicates with the wallet via the browser-injected `window.okxwallet` object and reuses protocol implementations from `@ckb-ccc/uni-sat` and `@ckb-ccc/nip07` rather than reimplementing them from scratch. If you're using `@ckb-ccc/connector-react` or `@ckb-ccc/ccc`, OKX is already included — no separate installation needed. ## Installation [#installation] ```bash npm install @ckb-ccc/okx ``` ```bash yarn add @ckb-ccc/okx ``` ```bash pnpm add @ckb-ccc/okx ``` **Dependencies:** | Package | Description | | ------------------ | -------------------------------------------------------- | | `@ckb-ccc/core` | Base types — `Signer`, `Client`, `Transaction`, and more | | `@ckb-ccc/nip07` | NIP-07 protocol support — Nostr signer implementation | | `@ckb-ccc/uni-sat` | UniSat protocol support — Bitcoin provider interface | ## Architecture [#architecture] `@ckb-ccc/okx` composes existing single-protocol packages rather than reimplementing them: ### Entry point: `getOKXSigners` [#entry-point-getokxsigners] `getOKXSigners(client)` is the main entry point. It checks for `window.okxwallet` and returns a `SignerInfo[]` array — empty if the wallet isn't available: ## Bitcoin protocol [#bitcoin-protocol] `BitcoinSigner` extends `ccc.SignerBtc` and adapts the UniSat provider interface for OKX. ### Network support [#network-support] | Network key | Chain | Network type | Provider access | | ----------- | --------- | ------------ | ------------------------ | | `btc` | `bitcoin` | Mainnet | `this.providers.bitcoin` | OKX only supports Bitcoin mainnet. While the code includes mappings for `btcTestnet` and `btcSignet`, OKX no longer provides providers for these networks, so they are not actually available. See [the announcement](https://www.okx.com/help/okx-wallet-to-cease-support-for-the-btc-testnet) for details. ### Account and public key retrieval [#account-and-public-key-retrieval] `BitcoinSigner` supports two methods to accommodate different OKX provider versions: * **`getAccounts()`** — returns an array of addresses. * **`getSelectedAccount()`** — returns the currently selected account. ## Nostr protocol [#nostr-protocol] `NostrSigner` wraps the provider interfaces from `@ckb-ccc/nip07` with two additions: * **Public key caching** — `publicKeyCache` avoids redundant RPC calls. * **Event signing** — delegates to `this.provider.signEvent` after ensuring the public key is attached. ## Account change detection [#account-change-detection] Both `BitcoinSigner` and `NostrSigner` implement `onReplaced()` to keep connection state in sync when the user switches accounts in the wallet UI: `onReplaced()` registers a listener for `"accountChanged"`, invokes the application callback when triggered, and returns a cleanup function that removes the listener. ## Integration pattern [#integration-pattern] `@ckb-ccc/okx` follows the same integration contract as every other wallet package in CCC: * **Factory function** — `getOKXSigners` returns a `SignerInfo[]` array. * **Provider detection** — checks for `window.okxwallet` before creating any signers. * **Graceful degradation** — returns an empty array when the wallet is unavailable or incompatible. This means `SignersController` can treat OKX exactly like any other signer source, with no special-casing required in application code. # @ckb-ccc/rei (https://docs.ckbccc.com/en/docs/packages/wallet-integrations/rei) `@ckb-ccc/rei` integrates [REI Wallet](https://reiwallet.io/) into CCC, providing a native CKB `Signer` implementation. REI is a CKB-native browser extension wallet that supports direct Secp256k1-Blake160 signing — no cross-chain address derivation needed. If you're using `@ckb-ccc/connector-react` or `@ckb-ccc/ccc`, REI is already included — no separate installation needed. ## Installation [#installation] ```bash npm install @ckb-ccc/rei ``` ```bash yarn add @ckb-ccc/rei ``` ```bash pnpm add @ckb-ccc/rei ``` **Dependencies:** | Package | Description | | --------------- | -------------------------------------------------------- | | `@ckb-ccc/core` | Base types — `Signer`, `Client`, `Transaction`, and more | ## Architecture [#architecture] `@ckb-ccc/rei` directly extends `ccc.Signer` (not a cross-chain signer) since REI is a CKB-native wallet. ### Entry point: `getReiSigners` [#entry-point-getreisigners] `getReiSigners(client)` checks for `window.rei.ckb` and returns a `SignerInfo[]` array — empty if the wallet isn't available: ## The `ReiSigner` class [#the-reisigner-class] `ReiSigner` extends `ccc.Signer` directly and communicates with the REI extension via its injected provider. ### Signer properties [#signer-properties] | Property | Value | | ---------- | ----------------------------- | | `type` | `SignerType.CKB` | | `signType` | `SignerSignType.CkbSecp256k1` | ### Key methods [#key-methods] | Method | Description | | ------------------------- | ----------------------------------------------------------------------- | | `connect()` | Switches REI to the correct network (mainnet/testnet) to match `client` | | `isConnected()` | Returns `true` if connected AND on the matching network | | `getInternalAddress()` | Calls `ckb_requestAccounts` to get the CKB address | | `getIdentity()` | Calls `ckb_getPublicKey` to get the public key | | `signMessageRaw(message)` | Signs via `ckb_signMessage` | | `signOnlyTransaction(tx)` | Signs via `ckb_signTransaction` | | `prepareTransaction(tx)` | Adds Secp256k1-Blake160 cell deps and prepares witnesses | | `onReplaced(listener)` | Fires on `accountsChanged` or `chainChanged` events | ### Network auto-switching [#network-auto-switching] On `connect()`, `ReiSigner` automatically switches the wallet to match the `client`'s network: ### Transaction signing [#transaction-signing] REI handles full transaction signing natively — no witness pre-computation needed on the CCC side: ## Account change detection [#account-change-detection] `ReiSigner` implements `onReplaced()` to keep state in sync: * Listens for `"accountsChanged"` — user switched account * Listens for `"chainChanged"` — user switched network When either fires, the application callback is invoked and the listener is cleaned up. ## Provider interface [#provider-interface] | Method | Description | | --------------------- | ------------------------------ | | `ckb_requestAccounts` | Get the current CKB address | | `ckb_getPublicKey` | Get the account's public key | | `ckb_signMessage` | Sign an arbitrary message | | `ckb_signTransaction` | Sign a full CKB transaction | | `ckb_switchNetwork` | Switch between mainnet/testnet | | `isConnected()` | Check wallet connection status | ## Integration pattern [#integration-pattern] `@ckb-ccc/rei` follows the same integration contract as every other wallet package in CCC: * **Factory function** — `getReiSigners` returns a `SignerInfo[]` array. * **Provider detection** — checks for `window.rei.ckb` before creating signers. * **Graceful degradation** — returns an empty array when the wallet is unavailable. # @ckb-ccc/uni-sat (https://docs.ckbccc.com/en/docs/packages/wallet-integrations/uni-sat) `@ckb-ccc/uni-sat` integrates [UniSat Wallet](https://unisat.io/) into CCC, providing a `SignerBtc` implementation for Bitcoin signing. It communicates with the wallet via the browser-injected `window.unisat` object and supports automatic network switching between Bitcoin mainnet and testnet. If you're using `@ckb-ccc/connector-react` or `@ckb-ccc/ccc`, UniSat is already included — no separate installation needed. ## Installation [#installation] ```bash npm install @ckb-ccc/uni-sat ``` ```bash yarn add @ckb-ccc/uni-sat ``` ```bash pnpm add @ckb-ccc/uni-sat ``` **Dependencies:** | Package | Description | | --------------- | -------------------------------------------------------- | | `@ckb-ccc/core` | Base types — `Signer`, `Client`, `Transaction`, and more | ## Architecture [#architecture] ### Entry point: `getUniSatSigners` [#entry-point-getunisatsigners] `getUniSatSigners(client, preferredNetworks?)` checks for `window.unisat` and returns a `SignerInfo[]` array — empty if the wallet isn't available: ## The `Signer` class [#the-signer-class] `Signer` extends `ccc.SignerBtc` and adapts the UniSat provider interface to CKB signing. ### Key methods [#key-methods] | Method | Description | | ------------------------- | -------------------------------------------------------------- | | `connect()` | Calls `requestAccounts()` then ensures the correct BTC network | | `isConnected()` | Returns `true` if accounts exist and network matches | | `getBtcAccount()` | Returns the first BTC address from the provider | | `getBtcPublicKey()` | Returns the BTC public key (hex-encoded) | | `signMessageRaw(message)` | Signs via `signMessage(msg, "ecdsa")` | | `onReplaced(listener)` | Fires on `accountsChanged` or `networkChanged` events | ### Network management [#network-management] UniSat supports multiple Bitcoin networks. On `connect()`, the signer automatically switches the wallet to the correct network based on `preferredNetworks` configuration: | CKB Network | Default BTC Network | | --------------- | ---------------------- | | Mainnet (`ckb`) | `btc` (livenet) | | Testnet (`ckt`) | `btcTestnet` (testnet) | The signer also supports Fractal Bitcoin via the `switchChain` API (when available). ### Signing flow [#signing-flow] ## Account change detection [#account-change-detection] `Signer` implements `onReplaced()` to handle account or network changes: * Listens for `"accountsChanged"` — user switched BTC account * Listens for `"networkChanged"` — user switched BTC network When either fires, the application callback is invoked and the listener is cleaned up. ## Provider interface [#provider-interface] | Method | Description | | ------------------------ | -------------------------------------------------- | | `requestAccounts()` | Prompt user to connect and return accounts | | `getAccounts()` | Get connected accounts (no prompt) | | `getPublicKey()` | Get the BTC public key | | `getNetwork()` | Get current network (`"livenet"` or `"testnet"`) | | `switchNetwork(network)` | Switch between livenet/testnet | | `getChain()` | Get current chain (extended API) | | `switchChain(chain)` | Switch chain (extended API, supports Fractal) | | `signMessage(msg, type)` | Sign a message with `"ecdsa"` or `"bip322-simple"` | ## Integration pattern [#integration-pattern] `@ckb-ccc/uni-sat` follows the same integration contract as every other wallet package in CCC: * **Factory function** — `getUniSatSigners` returns a `SignerInfo[]` array. * **Provider detection** — checks for `window.unisat` before creating signers. * **Graceful degradation** — returns an empty array when the wallet is unavailable. This package is also used as a dependency by `@ckb-ccc/okx` for its Bitcoin signing support. # @ckb-ccc/utxo-global (https://docs.ckbccc.com/en/docs/packages/wallet-integrations/utxo-global) `@ckb-ccc/utxo-global` integrates [UTXO Global](https://utxo.global/) into CCC, providing `Signer` implementations for CKB, Bitcoin, and Dogecoin. UTXO Global is a multi-chain browser extension wallet that supports native CKB transaction signing as well as cross-chain signing from BTC and DOGE. If you're using `@ckb-ccc/connector-react` or `@ckb-ccc/ccc`, UTXO Global is already included — no separate installation needed. ## Installation [#installation] ```bash npm install @ckb-ccc/utxo-global ``` ```bash yarn add @ckb-ccc/utxo-global ``` ```bash pnpm add @ckb-ccc/utxo-global ``` **Dependencies:** | Package | Description | | --------------- | -------------------------------------------------------- | | `@ckb-ccc/core` | Base types — `Signer`, `Client`, `Transaction`, and more | ## Architecture [#architecture] `@ckb-ccc/utxo-global` provides three separate signers from a single provider injection at `window.utxoGlobal`: ### Entry point: `getUtxoGlobalSigners` [#entry-point-getutxoglobalsigners] `getUtxoGlobalSigners(client, preferredNetworks?)` checks for `window.utxoGlobal` and returns a `SignerInfo[]` array with all three signers — or an empty array if the wallet isn't available: ## Supported signer types [#supported-signer-types] | Signer Class | Base Type | Chain | SignerType | | ------------ | ---------------- | -------- | ---------- | | `SignerCkb` | `ccc.Signer` | CKB | `CKB` | | `SignerBtc` | `ccc.SignerBtc` | Bitcoin | `BTC` | | `SignerDoge` | `ccc.SignerDoge` | Dogecoin | `Doge` | ### CKB signer [#ckb-signer] `SignerCkb` provides native CKB signing — no cross-chain address derivation needed. It calls `signTransaction()` on the provider directly. ### BTC and Doge signers [#btc-and-doge-signers] `SignerBtc` and `SignerDoge` extend their respective cross-chain base classes. They derive CKB addresses from Bitcoin/Dogecoin public keys and sign CKB transaction witnesses using the respective chain's signing scheme. ## Account change detection [#account-change-detection] All three signers implement `onReplaced()`: * Listens for `"accountsChanged"` — user switched account * Listens for `"networkChanged"` — user switched network When either fires, the application callback is invoked and listeners are cleaned up. ## Provider interface [#provider-interface] All three sub-providers (`ckbSigner`, `bitcoinSigner`, `dogeSigner`) share the same interface: | Method | Description | | --------------------------- | --------------------------------------------- | | `requestAccounts()` | Prompt user to connect and return accounts | | `getAccount()` | Get connected accounts | | `getPublicKey()` | Get address and public key pairs | | `connect()` | Establish connection | | `isConnected()` | Check connection status | | `signMessage(msg, address)` | Sign a message | | `signTransaction(tx)` | Sign a full CKB transaction (CKB signer only) | | `getNetwork()` | Get current network | | `switchNetwork(network)` | Switch network | ## Integration pattern [#integration-pattern] `@ckb-ccc/utxo-global` follows the same integration contract as every other wallet package in CCC: * **Factory function** — `getUtxoGlobalSigners` returns a `SignerInfo[]` array. * **Provider detection** — checks for `window.utxoGlobal` before creating signers. * **Graceful degradation** — returns an empty array when the wallet is unavailable. ## References [#references] * [UTXO Global Website](https://utxo.global/) # @ckb-ccc/xverse (https://docs.ckbccc.com/en/docs/packages/wallet-integrations/xverse) `@ckb-ccc/xverse` integrates [Xverse Wallet](https://www.xverse.app/) and any [SATS Connect](https://docs.xverse.app/sats-connect) compatible Bitcoin wallet into CCC. It provides a `SignerBtc` implementation using the SATS Connect RPC protocol, supporting multi-wallet discovery via `window.btc_providers`. If you're using `@ckb-ccc/connector-react` or `@ckb-ccc/ccc`, Xverse is already included — no separate installation needed. ## Installation [#installation] ```bash npm install @ckb-ccc/xverse ``` ```bash yarn add @ckb-ccc/xverse ``` ```bash pnpm add @ckb-ccc/xverse ``` **Dependencies:** | Package | Description | | --------------- | -------------------------------------------------------- | | `@ckb-ccc/core` | Base types — `Signer`, `Client`, `Transaction`, and more | ## Architecture [#architecture] Unlike other wallet packages that detect a single global provider, `@ckb-ccc/xverse` uses the `window.btc_providers` array to discover multiple SATS Connect wallets simultaneously. ### Entry point: `getXverseSigners` [#entry-point-getxversesigners] `getXverseSigners(client, preferredNetworks?)` reads from `window.btc_providers` and returns a `{ wallet, signerInfo }[]` array — one entry per discovered wallet: Each provider entry includes wallet metadata (`name`, `icon`) allowing `SignersController` to display distinct wallet entries. ## The `Signer` class [#the-signer-class] `Signer` extends `ccc.SignerBtc` and uses the SATS Connect RPC protocol for all wallet interactions. ### Key methods [#key-methods] | Method | Description | | ------------------------- | ---------------------------------------------------------- | | `connect()` | Calls `wallet_requestPermissions` if not already connected | | `disconnect()` | Clears the cached address | | `isConnected()` | Attempts `getBalance` — returns `true` on success | | `getBtcAccount()` | Returns the payment address via `getAddresses` | | `getBtcPublicKey()` | Returns the public key from the payment address | | `signMessageRaw(message)` | Signs via `signMessage` with ECDSA protocol | | `onReplaced(listener)` | Fires on `accountChange` or `networkChange` events | ### Connection and address caching [#connection-and-address-caching] The signer caches the resolved address to avoid redundant RPC calls. The cache is invalidated on `disconnect()`: ### Network preferences [#network-preferences] | CKB Network | Default BTC Network | | --------------- | ------------------- | | Mainnet (`ckb`) | `btc` | | Testnet (`ckt`) | `btcTestnet` | ### Signing flow [#signing-flow] ## Account change detection [#account-change-detection] `Signer` implements `onReplaced()` via the SATS Connect event API: * Listens for `"accountChange"` — user switched BTC account * Listens for `"networkChange"` — user switched BTC network Returns cleanup functions from `provider.addListener()` for proper teardown. ## SATS Connect RPC methods [#sats-connect-rpc-methods] | Method | Description | | --------------------------- | ----------------------------------------------------------- | | `wallet_requestPermissions` | Request wallet connection permissions | | `getAddresses` | Get addresses filtered by purpose (Payment, Ordinals, etc.) | | `getBalance` | Get wallet balance (used for connection check) | | `signMessage` | Sign a message with ECDSA or BIP-322 | ## Integration pattern [#integration-pattern] `@ckb-ccc/xverse` follows the same integration contract as other wallet packages in CCC: * **Factory function** — `getXverseSigners` returns `{ wallet, signerInfo }[]` for multi-wallet support. * **Provider detection** — reads `window.btc_providers` array. * **Multi-wallet discovery** — creates separate signer entries per provider with distinct wallet names and icons. * **Graceful degradation** — returns an empty array when no SATS Connect wallets are available.