@ckb-ccc/joy-id
JoyID wallet integration — passkey-based, no seed phrase required.
@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
npm install @ckb-ccc/joy-idyarn add @ckb-ccc/joy-idpnpm add @ckb-ccc/joy-idDependencies:
| 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
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
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:
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; forsub_keyaccounts, 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:
- Call
generateSubkeyUnlockSmton the COTA aggregator to generate an SMT unlock entry. - Write the unlock data into the witness
outputTypefield. - Prepend COTA cell deps to the transaction.
2. BitcoinSigner
Bitcoin chain signer supporting both P2WPKH (Native SegWit) and P2TR (Taproot) address types.
Constructor Parameters:
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; selectsnativeSegwitortaprootfrom the response based onaddressType.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
EVM chain signer that retrieves an Ethereum address and signs via JoyID.
Constructor Parameters:
new EvmSigner(
client: ccc.Client,
name: string,
icon: string,
_appUri?: string,
connectionsRepo?: ConnectionsRepo
)Key Methods:
connect()— Popup authentication; takesethAddressfrom the response as the EVM account address.getEvmAccount()— Returns the Ethereum address (ccc.Hex).signMessageRaw(message)— Signs a message; returns accc.Hexsignature.
4. NostrSigner
Nostr protocol signer that retrieves a Nostr public key and signs events via JoyID.
Constructor Parameters:
new NostrSigner(
client: ccc.Client,
name: string,
icon: string,
_appUri?: string,
connectionsRepo?: ConnectionsRepo
)Key Methods:
connect()— Popup authentication; takesnostrPubkeyfrom 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 completeRequired<ccc.NostrEvent>.
5. getJoyIdSigners() Factory Function
The recommended integration entry point — returns all JoyID-supported signers in a single call.
Signature:
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
ConnectionsRepo Interface
ConnectionsRepoLocalStorage (Default Implementation)
Stores connection information as a JSON array under the "ccc-joy-id-signer" key in localStorage.
Connection Type:
type Connection = {
readonly address: string; // Chain address
readonly publicKey: ccc.Hex; // Public key (hex format)
readonly keyType: string; // "main_key" | "sub_key"
};AccountSelector Type:
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
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
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
Example 0: Using @ckb-ccc/connector-react(Recommended)
JoyID is automatically registered when using the CCC connector:
import { ccc } from "@ckb-ccc/connector-react";
export default function App({ children }) {
return (
<ccc.Provider name="My App" icon="/icon.png">
{children}
</ccc.Provider>
);
}When the user opens the wallet selector, JoyID appears as an option. No additional configuration is required.
Example 1: Factory Function Integration
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
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)
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
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
import { CkbSigner, ConnectionsRepo, AccountSelector, Connection } from "@ckb-ccc/joy-id";
class MyCustomStorage implements ConnectionsRepo {
async get(selector: AccountSelector): Promise<Connection | undefined> {
// Read from IndexedDB or server
}
async set(selector: AccountSelector, connection: Connection | undefined): Promise<void> {
// Write to IndexedDB or server
}
}
const signer = new CkbSigner(
client, "My DApp", "https://icon.url",
undefined, undefined,
new MyCustomStorage()
);Lumos compatibility
JoyID is supported via Lumos patches if you are using the Lumos SDK:
import { generateDefaultScriptInfos } from "@ckb-ccc/lumos-patches";
// Call before using Lumos — no @ckb-lumos/joyid needed
registerCustomLockScriptInfos(generateDefaultScriptInfos());See the @ckb-ccc/lumos-patches page for details.
Caveats and Limitations
- Browser-only: All signers depend on
window.open,window.localStorage, andwindow.postMessage. Node.js server-side environments are not supported. - No WebView support: In WebView (e.g., WeChat in-app browser) or PWA standalone mode,
getJoyIdSignersreturnsSignerAlwaysErrorinstances — JoyID cannot be used. - 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). - Sub Key requires COTA: When using a sub key account, the account must hold a COTA cell; otherwise
prepareTransactionthrows"No COTA cells for sub key wallet". - Message signature format:
CkbSigner.signMessageRawreturns a JSON string (containingsignature,alg, andmessagefields), not raw signature bytes. Useccc.Signer.verifyMessagewithSignerSignType.JoyIdfor verification.