> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usewraith.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# CKB Primitives

> Stealth address crypto for Nervos CKB using secp256k1 and the Cell model

Low-level stealth address functions for Nervos CKB using secp256k1. Import from `@wraith-protocol/sdk/chains/ckb`.

Most developers should use the [Agent Client](/sdk/agent-client) instead. These primitives are for power users building custom stealth address integrations on CKB.

## The Cell Model

CKB is fundamentally different from account-based chains like EVM or Solana. CKB uses a **UTXO-based Cell model** where all state is stored in Cells.

A Cell has four fields:

| Field      | Purpose                                                        |
| ---------- | -------------------------------------------------------------- |
| `capacity` | Amount of CKB stored (like a UTXO value)                       |
| `lock`     | Script that must be satisfied to spend (like an address/owner) |
| `type`     | Optional script for additional validation                      |
| `data`     | Arbitrary data                                                 |

### Cells Are Announcements

On EVM chains, stealth address announcements are separate events emitted by an Announcer contract. On CKB, **the Cell itself is the announcement**. There is no separate announcer.

The stealth lock script `args` contain both the ephemeral public key and the stealth address hash:

```
lock.args = ephemeral_pubkey (33 bytes) || blake160(stealth_pubkey) (20 bytes)
```

This is 53 bytes total. The announcement data is embedded directly in the Cell's lock script — no separate transaction or event needed.

### Scanning = Querying Live Cells

Instead of querying a subgraph or parsing event logs, scanning on CKB means querying all live Cells that use the `stealth-lock` code hash. CKB's built-in indexer RPC (`get_cells`) supports filtering by lock script code hash, so only stealth Cells are returned.

### Spending = Consuming Cells

To withdraw from a stealth address, you consume the stealth Cell and create a new Cell at the destination. The stealth lock script verifies the secp256k1 signature against the pubkey hash in the args.

## Installation

```bash theme={null}
npm install @wraith-protocol/sdk
```

No additional peer dependencies required. CKB RPC calls use native `fetch`. Address hashing uses `@noble/hashes` (blake2b), which is already a direct dependency.

## Import

```typescript theme={null}
import {
  deriveStealthKeys,
  generateStealthAddress,
  checkStealthCell,
  scanStealthCells,
  deriveStealthPrivateKey,
  encodeStealthMetaAddress,
  decodeStealthMetaAddress,
  blake160,
  fetchStealthCells,
  hashName,
  buildRegisterName,
  buildResolveName,
  metaAddressFromNameData,
  getDeployment,
  DEPLOYMENTS,
  STEALTH_SIGNING_MESSAGE,
  SCHEME_ID,
  META_ADDRESS_PREFIX,
} from "@wraith-protocol/sdk/chains/ckb";
```

## Types

```typescript theme={null}
type HexString = `0x${string}`;

interface StealthKeys {
  spendingKey: HexString;       // 32-byte private key
  viewingKey: HexString;        // 32-byte private key
  spendingPubKey: HexString;    // 33-byte compressed secp256k1
  viewingPubKey: HexString;     // 33-byte compressed secp256k1
}

interface GeneratedStealthAddress {
  stealthPubKey: HexString;     // 33-byte compressed stealth public key
  stealthPubKeyHash: HexString; // 20-byte blake160 hash
  ephemeralPubKey: HexString;   // 33-byte compressed ephemeral public key
  lockArgs: HexString;          // 53 bytes: ephemeral_pub || blake160(stealth_pub)
}

// CKB doesn't have separate announcements — Cells ARE the announcements
interface StealthCell {
  txHash: HexString;
  index: number;
  capacity: bigint;              // in shannons (1 CKB = 10^8 shannons)
  lockArgs: HexString;           // 53 bytes
  ephemeralPubKey: HexString;    // extracted from lockArgs[0:33]
  stealthPubKeyHash: HexString;  // extracted from lockArgs[33:53]
}

interface MatchedStealthCell extends StealthCell {
  stealthPrivateKey: HexString;
}
```

## Key Differences from EVM

CKB uses the same secp256k1 curve as EVM but different hash functions and a completely different state model.

| Aspect             | EVM                               | CKB                                  |
| ------------------ | --------------------------------- | ------------------------------------ |
| Model              | Account-based                     | UTXO (Cell)                          |
| Curve              | secp256k1                         | secp256k1 (same)                     |
| Announcement       | Separate Announcer contract event | Embedded in Cell lock script args    |
| Address hash       | `keccak256(pubkey)[12:32]`        | `blake2b(pubkey)[0:20]` (blake160)   |
| Shared secret hash | `keccak256(ECDH_shared)`          | `SHA-256(ECDH_shared)`               |
| Scanning           | Query subgraph for events         | Query live Cells via `get_cells` RPC |
| Address format     | `0x` + 20-byte hex                | bech32m CKB address                  |
| Min balance        | None (EOA)                        | 61 CKB for cell capacity             |
| Spending           | `sendTransaction`                 | Consume Cell, create new Cell        |
| View tags          | Yes (fast rejection)              | No (check every Cell)                |

## Constants

```typescript theme={null}
const STEALTH_SIGNING_MESSAGE = "Sign this message to generate your Wraith stealth keys.\n\nChain: CKB\nNote: This signature is used for key derivation only and does not authorize any transaction.";
const SCHEME_ID = 1;
const META_ADDRESS_PREFIX = "st:ckb:";
```

***

## Functions

### `deriveStealthKeys(signature)`

Derive spending and viewing key pairs from a 65-byte ECDSA signature. Identical to the EVM module.

```typescript theme={null}
const signature = await wallet.signMessage(STEALTH_SIGNING_MESSAGE);
const keys = deriveStealthKeys(signature as HexString);

console.log(keys.spendingKey);    // "0x..." (32-byte private key)
console.log(keys.viewingKey);     // "0x..." (32-byte private key)
console.log(keys.spendingPubKey); // "0x02..." (33-byte compressed)
console.log(keys.viewingPubKey);  // "0x03..." (33-byte compressed)
```

**Algorithm:** Same as EVM — split signature `r`/`s`, `keccak256` each to get spending and viewing keys.

### `generateStealthAddress(spendingPubKey, viewingPubKey, ephemeralKey?)`

Generate a one-time stealth address with lock script args for CKB.

```typescript theme={null}
const result = generateStealthAddress(
  keys.spendingPubKey,
  keys.viewingPubKey
);

console.log(result.stealthPubKey);     // "0x02..." (33-byte stealth public key)
console.log(result.stealthPubKeyHash); // "0x..." (20-byte blake160)
console.log(result.ephemeralPubKey);   // "0x03..." (33-byte ephemeral key)
console.log(result.lockArgs);          // "0x..." (53 bytes: ephemeral || blake160)
```

**Algorithm:**

1. Generate random ephemeral key pair `(r, R = r * G)`
2. Compute ECDH shared secret `S = r * viewingPubKey` (compressed)
3. `hashedSecret = SHA-256(S)` — **not** keccak256 like EVM
4. `stealthPubKey = spendingPubKey + hashedSecret * G`
5. `stealthPubKeyHash = blake160(stealthPubKey)` — blake2b with "ckb-default-hash", first 20 bytes
6. `lockArgs = ephemeralPubKey || stealthPubKeyHash`

### `blake160(data)`

CKB's address hashing function. blake2b with `"ckb-default-hash"` personalization, truncated to 20 bytes.

```typescript theme={null}
const hash = blake160(publicKeyBytes);
// Uint8Array (20 bytes)
```

The personalization string is critical — without it, hashes won't match CKB's on-chain verification.

### `checkStealthCell(cell, viewingKey, spendingPubKey)`

Check if a stealth Cell belongs to you.

```typescript theme={null}
const result = checkStealthCell(cell, keys.viewingKey, keys.spendingPubKey);

if (result.isMatch) {
  console.log("This cell is ours");
}
```

**Algorithm:**

1. Extract `ephemeralPubKey = cell.lockArgs[0:33]`
2. Compute ECDH shared secret `S = viewingKey * ephemeralPubKey`
3. `hashedSecret = SHA-256(S)`
4. `expectedPubKey = spendingPubKey + hashedSecret * G`
5. `expectedHash = blake160(expectedPubKey)`
6. Compare `expectedHash` with `cell.lockArgs[33:53]`

No view tag optimization — every Cell is fully checked. This is acceptable because CKB's `get_cells` RPC already filters to only stealth-lock Cells.

### `scanStealthCells(cells, viewingKey, spendingPubKey, spendingKey)`

Scan an array of stealth Cells and return the ones that belong to you.

```typescript theme={null}
const cells = await fetchStealthCells("ckb");

const matched = scanStealthCells(
  cells,
  keys.viewingKey,
  keys.spendingPubKey,
  keys.spendingKey
);

for (const m of matched) {
  console.log(m.txHash);           // Cell outpoint
  console.log(m.capacity);         // CKB amount in shannons
  console.log(m.stealthPrivateKey); // private key to spend this Cell
}
```

### `deriveStealthPrivateKey(spendingKey, ephemeralPubKey, viewingKey)`

Compute the private key that controls a specific stealth Cell.

```typescript theme={null}
const privateKey = deriveStealthPrivateKey(
  keys.spendingKey,
  cell.ephemeralPubKey,
  keys.viewingKey
);
// Use this key to sign the transaction that consumes the stealth Cell
```

**Algorithm:**

1. `S = viewingKey * ephemeralPubKey` (shared secret)
2. `hashedSecret = SHA-256(S)` — **not** keccak256
3. `stealthPrivateKey = (spendingKey + hashedSecret) mod n`

### `encodeStealthMetaAddress(spendingPubKey, viewingPubKey)`

Encode two public keys into a CKB stealth meta-address string.

```typescript theme={null}
const metaAddress = encodeStealthMetaAddress(
  keys.spendingPubKey,
  keys.viewingPubKey
);
// "st:ckb:02abc...03def..."
```

Format: `st:ckb:{spendingPubKeyHex}{viewingPubKeyHex}` — 132 hex chars (two 33-byte compressed secp256k1 keys), same structure as EVM.

### `decodeStealthMetaAddress(metaAddress)`

Decode a CKB meta-address back into its component public keys.

```typescript theme={null}
const { spendingPubKey, viewingPubKey } = decodeStealthMetaAddress(
  "st:ckb:02abc...03def..."
);
```

***

## End-to-End Flow

```typescript theme={null}
import {
  deriveStealthKeys,
  generateStealthAddress,
  scanStealthCells,
  deriveStealthPrivateKey,
  encodeStealthMetaAddress,
  decodeStealthMetaAddress,
  fetchStealthCells,
  STEALTH_SIGNING_MESSAGE,
} from "@wraith-protocol/sdk/chains/ckb";
import type { HexString } from "@wraith-protocol/sdk/chains/ckb";

// 1. Recipient: derive keys from wallet signature
const sig = await wallet.signMessage(STEALTH_SIGNING_MESSAGE);
const keys = deriveStealthKeys(sig as HexString);

// 2. Recipient: publish stealth meta-address
const metaAddress = encodeStealthMetaAddress(keys.spendingPubKey, keys.viewingPubKey);
// Share "st:ckb:..." or register as a .wraith name via buildRegisterName()

// 3. Sender: generate stealth address from meta-address
const { spendingPubKey, viewingPubKey } = decodeStealthMetaAddress(metaAddress);
const stealth = generateStealthAddress(spendingPubKey, viewingPubKey);

// 4. Sender: create a Cell with stealth-lock
//    lock.code_hash = stealth-lock code hash
//    lock.args = stealth.lockArgs (53 bytes: ephemeral_pub || blake160)
//    capacity = payment amount (minimum 61 CKB)

// 5. Recipient: scan stealth Cells
const cells = await fetchStealthCells("ckb");
const matched = scanStealthCells(
  cells,
  keys.viewingKey,
  keys.spendingPubKey,
  keys.spendingKey
);

// 6. Recipient: spend from stealth Cell
for (const m of matched) {
  // Build a CKB transaction:
  //   Input: the matched stealth Cell
  //   Output: new Cell at destination with regular lock
  //   Witness: signature with m.stealthPrivateKey
  // Submit via CKB RPC
}
```

***

## Names

`.wraith` name registration on CKB uses the Cell model. Each name is a live Cell whose type script identifies the name and whose data holds the stealth meta-address. Ownership is proven by the Cell's lock script — whoever can spend the Cell owns the name. This is fundamentally different from EVM and Solana, where ownership is proven by a signature from the spending key.

### `hashName(name)`

Compute the blake2b hash of a name string. The result is used as the type script `args` to identify the name Cell.

```typescript theme={null}
const nameHash = hashName("alice");
// "0x..." (32-byte blake2b hash with "ckb-default-hash" personalization)
```

### `buildRegisterName({ name, spendingPubKey, viewingPubKey })`

Build the type script and Cell data for creating a `.wraith` name Cell. Returns the type script object and the 66-byte data payload.

```typescript theme={null}
import {
  buildRegisterName,
  deriveStealthKeys,
  getDeployment,
  STEALTH_SIGNING_MESSAGE,
} from "@wraith-protocol/sdk/chains/ckb";
import type { HexString } from "@wraith-protocol/sdk/chains/ckb";

const sig = await wallet.signMessage(STEALTH_SIGNING_MESSAGE);
const keys = deriveStealthKeys(sig as HexString);
const deployment = getDeployment("ckb");

const { typeScript, data } = buildRegisterName({
  name: "alice",
  spendingPubKey: keys.spendingPubKey,
  viewingPubKey: keys.viewingPubKey,
});

// Create a Cell with:
//   lock: your wallet's lock script (you are the owner)
//   type: typeScript (wraith-names-type with args = blake2b("alice"))
//   data: 66 bytes (spending_pub || viewing_pub)
//   capacity: enough to cover the Cell size (minimum ~130 CKB)
```

The type script returned uses the `wraith-names-type` code hash from the deployment config. The `args` field is `hashName(name)`.

### `buildResolveName({ name })`

Build the type script for querying a name Cell. Use this with the CKB `get_cells` RPC to find the Cell that holds a name's meta-address.

```typescript theme={null}
import { buildResolveName, metaAddressFromNameData } from "@wraith-protocol/sdk/chains/ckb";

const { typeScript } = buildResolveName({ name: "alice" });

// Query CKB for live Cells with this type script
const result = await fetch(rpcUrl, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    id: 0,
    jsonrpc: "2.0",
    method: "get_cells",
    params: [{
      script: {
        code_hash: typeScript.codeHash,
        hash_type: typeScript.hashType,
        args: typeScript.args,
      },
      script_type: "type",
    }, "asc", "0x1"],
  }),
});

const data = await result.json();
const cell = data.result?.objects?.[0];

if (cell) {
  const metaAddress = metaAddressFromNameData(cell.output_data);
  // { spendingPubKey: "0x02...", viewingPubKey: "0x03..." }
}
```

### `metaAddressFromNameData(data)`

Parse the 66-byte Cell data from a name Cell into its component public keys.

```typescript theme={null}
const { spendingPubKey, viewingPubKey } = metaAddressFromNameData(cellData);
// spendingPubKey: "0x02..." (33-byte compressed secp256k1)
// viewingPubKey: "0x03..." (33-byte compressed secp256k1)
```

The data format is `spendingPubKey (33 bytes) || viewingPubKey (33 bytes)` = 66 bytes total.

### Registering a Name

```typescript theme={null}
import {
  deriveStealthKeys,
  buildRegisterName,
  getDeployment,
  STEALTH_SIGNING_MESSAGE,
} from "@wraith-protocol/sdk/chains/ckb";
import type { HexString } from "@wraith-protocol/sdk/chains/ckb";

const sig = await wallet.signMessage(STEALTH_SIGNING_MESSAGE);
const keys = deriveStealthKeys(sig as HexString);
const deployment = getDeployment("ckb");

const { typeScript, data } = buildRegisterName({
  name: "alice",
  spendingPubKey: keys.spendingPubKey,
  viewingPubKey: keys.viewingPubKey,
});

// Build a CKB transaction:
//   Output: {
//     capacity: "0x" + (130_00000000n).toString(16),
//     lock: walletLockScript,       // your wallet's lock = you own the name
//     type: {
//       codeHash: typeScript.codeHash,
//       hashType: typeScript.hashType,
//       args: typeScript.args,       // blake2b("alice")
//     },
//   }
//   Output data: data               // 66 bytes meta-address
//   Cell dep: deployment.cellDeps.namesType
// Submit via CKB RPC
```

Ownership is determined by the Cell's lock script. Whoever can spend the Cell controls the name. To transfer a name, consume the Cell and create a new one with a different lock script. No signature verification against the spending key is needed — the lock script handles authorization.

### Resolving a Name

```typescript theme={null}
import {
  buildResolveName,
  metaAddressFromNameData,
  generateStealthAddress,
  getDeployment,
} from "@wraith-protocol/sdk/chains/ckb";

const deployment = getDeployment("ckb");
const { typeScript } = buildResolveName({ name: "alice" });

// Query for the name Cell
const response = await fetch(deployment.rpcUrl, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    id: 0,
    jsonrpc: "2.0",
    method: "get_cells",
    params: [{
      script: {
        code_hash: typeScript.codeHash,
        hash_type: typeScript.hashType,
        args: typeScript.args,
      },
      script_type: "type",
    }, "asc", "0x1"],
  }),
});

const result = await response.json();
const cell = result.result?.objects?.[0];

if (cell) {
  const { spendingPubKey, viewingPubKey } = metaAddressFromNameData(cell.output_data);
  const stealth = generateStealthAddress(spendingPubKey, viewingPubKey);
  // Send a stealth payment to alice.wraith on CKB
}
```

### CKB Names vs Other Chains

| Aspect          | EVM / Solana                               | CKB                                     |
| --------------- | ------------------------------------------ | --------------------------------------- |
| Ownership proof | Spending key signature                     | Cell lock script (whoever can spend)    |
| Storage         | Contract state / PDA                       | Live Cell                               |
| Lookup          | `resolve(name)` contract call              | `get_cells` RPC filtered by type script |
| Transfer        | Not supported (re-register)                | Consume Cell, create with new lock      |
| Data size       | 64 bytes (ed25519) or 66 bytes (secp256k1) | 66 bytes (secp256k1)                    |
| Name hash       | keccak256 (EVM), SHA-256 (Solana/Stellar)  | blake2b with "ckb-default-hash"         |

### CKB-Specific Considerations

* **Minimum capacity:** A stealth-lock Cell requires at least 61 CKB due to the 53-byte args. Senders must send at least this amount.
* **No view tags:** Every Cell must be fully checked. CKB's `get_cells` RPC already filters by lock script code hash, so only stealth Cells are examined.
* **blake2b personalization:** CKB uses `"ckb-default-hash"` as the blake2b personalization parameter. This **must** be included or hashes won't match on-chain verification.
* **UTXO spending:** Withdrawing means consuming the Cell and creating a new one at the destination. The transaction fee is deducted from the Cell's capacity.
* **Name ownership:** On CKB, name ownership is proven by the Cell's lock script, not by a signature from the spending key like on EVM. Whoever can spend the name Cell controls the name.

## Chain Deployments

### `getDeployment(chain)`

```typescript theme={null}
const deployment = getDeployment("ckb");
// {
//   network: "testnet",
//   rpcUrl: "https://testnet.ckbapp.dev",
//   explorerUrl: "https://pudge.explorer.nervos.org",
//   contracts: {
//     stealthLockCodeHash: "0x...",
//     namesTypeCodeHash: "0xc133817d433f72ea16a2404adaf961524e9572c8378829a21968710d6182e20d",
//   },
//   cellDeps: {
//     stealthLock: { txHash: "0x...", index: 0 },
//     namesType: { txHash: "0x9acd640d35eadd893b358dddd415f4061fe81cb249e8ace51a866fee314141b8", index: 0 },
//   },
// }
```

### Supported Networks

| Network             | Status |
| ------------------- | ------ |
| CKB Testnet (Pudge) | Live   |

## Fetching Stealth Cells

### `fetchStealthCells(chain?)`

Fetches all live stealth Cells from CKB using the `get_cells` RPC method, filtered by the stealth-lock code hash. Handles pagination automatically.

```typescript theme={null}
const cells = await fetchStealthCells("ckb");
// Returns StealthCell[] — ready to pass to scanStealthCells()
```

This replaces the subgraph/event-based scanning used on other chains. CKB's native indexer makes this efficient without external infrastructure.
