CKB KeyWayDocs

Signing transactions

Use the KeyWay account as a CCC signer to send CKB, move tokens and NFTs, and sign messages.

useCkbWallet().signer is a CCC signer for the signed-in account. Write the CCC code you already know. KeyWay shows the user what each transaction does and signs only after they approve.

import { ccc } from "@ckb-ccc/core";
import { useCkbWallet } from "@ckb-keyway/react";

function SendButton({ to }: { to: string }) {
  const { signer } = useCkbWallet();

  async function send() {
    if (!signer) return;
    const { script } = await ccc.Address.fromString(to, signer.client);
    const tx = ccc.Transaction.from({ outputs: [{ lock: script, capacity: ccc.fixedPointFrom(100) }] });
    await tx.completeInputsByCapacity(signer);
    await tx.completeFeeBy(signer);
    const hash = await signer.sendTransaction(tx); // approval dialog, then broadcast
  }

  return <button onClick={send} disabled={!signer}>Send 100 CKB</button>;
}

signer is undefined until the account is ready. It doesn't need Fiber. getAddresses, getBalance, findCells, signMessage and verifyMessage work as they do with any CCC signer. Tokens (xUDT, sUDT, including RUSD on testnet), Spore NFTs and Nervos DAO transactions work too: CCC builds them, and KeyWay signs the user's part.

What the user approves

KeyWay's backend reads every input from the chain and decodes what the transaction does to the user's account. The approval dialog shows:

  • the CKB they send or receive, and the network fee;
  • token amounts by token, and NFTs sent, received, minted or burned;
  • Nervos DAO deposits and withdrawals;
  • each recipient, labelled as a wallet, a contract or a Fiber channel;
  • any script KeyWay doesn't recognize, as a warning;
  • inputs that other people must sign.

Token amounts are shown with the token's name and decimals when KeyWay knows the token (for example, "Send 25 RUSD?"), and as a short token ID with the raw amount otherwise.

The backend signs only the exact transaction it previewed, and only the user's own account. A changed transaction is refused. To use your own approval UI, pass approveCkbTransaction to KeyWayProvider. It receives the same preview (TransactionPreview) and returns true to approve.

Signing messages

const signature = await signer.signMessage("Log in to Acme Pay");
const valid = await signer.verifyMessage("Log in to Acme Pay", signature);

Messages use CKB's standard Nervos Message: format and are shown to the user word for word. Pass approveCkbMessage to replace the dialog.

Email confirmation

Users can require a code from their inbox once what leaves their account within 24 hours reaches a daily amount they choose. Each asset has its own amount: one for CKB, and one for each token. Everything signed in the last 24 hours counts toward it, so splitting a transfer into smaller ones doesn't avoid the code. With a hosted Fiber node, Fiber payments count toward the same totals.

It's on by default. Before an account's first send, the signer shows a "Protect your account" dialog with a daily CKB limit of 100,000 CKB, which the user can change before continuing. Continuing emails a code that turns the limit on, then the send goes ahead as usual. Cancelling cancels that send, and the dialog comes back on the next one. The dialog is skipped, and the send goes out as before, for an account with less than about 236 CKB of plain CKB (the cell below needs 174) and for an account with recovery on, which can't hold the cell yet. Pass approveDailyLimit to KeyWayProvider to replace the dialog: it receives { ckb } (the amount offered, in shannons) and returns the amount to use, or null to cancel the send.

Only assets with an amount of their own are limited. A token without one has no limit and never needs the code, unless the user sets an amount for that token; the same goes for NFTs and any other cell KeyWay can't read an amount from (each cell counts as one, so an amount for its type script hash limits how many cells of it can leave in 24 hours). CKB keeps its daily amount as set.

The amounts are enforced by KeyWay's Lit Action, not only by KeyWay's server. Setting the first amount writes a small send-limit cell to the user's account (about 174 CKB that stays theirs) holding the amounts and a keyed fingerprint of their email, never the address; that first change asks for a code, which binds the address. From then on, every send spends that cell and creates its successor, which the signer does for you in prepareTransaction (CCC calls it before computing the fee). The Lit Action counts the last 24 hours from that chain and emails its own 8-digit code once the amount is reached. Opening a Fiber channel always needs the code while an amount is set: Fiber chooses the funding transaction's inputs, so it can't join the chain.

Sends right after each other work from any tab, device or backend: KeyWay lists the account's transactions that haven't confirmed yet, and the signer loads them before it picks inputs, so it never spends a cell another client already spent.

import { ccc } from "@ckb-ccc/core";

// 500 CKB a day, in shannons.
await signer.setDailyConfirmationAmount("ckb", ccc.fixedPointFrom(500));

// 100 RUSD a day, in the token's smallest unit (RUSD has 8 decimals).
// A token is identified by its type script hash: ccc.Script.from(typeScript).hash().
const RUSD = "0x45b32a2bc4285d0a09678eb11960ddc8707bc2779887a09d482e9bfe9a2cdf52";
await signer.setDailyConfirmationAmount(RUSD, 100n * 100_000_000n);

await signer.getDailyConfirmationAmounts(); // { ckb: 50000000000n, "0x45b3…df52": 10000000000n }
await signer.setDailyConfirmationAmount("ckb", null); // remove the CKB amount

Changes that make confirmation stricter, such as lowering an amount, apply at once. Changes that loosen it, and turning it on the first time, email the user a code first: raising or removing an amount. Giving a token its own amount makes it stricter, so it applies at once. An account whose send-limit cell was written before tokens became unlimited by default keeps needing the code for tokens without an amount until its next settings change, which asks for the code once to update the cell. The code prompt is always the SDK's own, even when you replace the approval dialogs. TransactionPreview.requiresEmailCode tells your own dialog whether a code will follow.

Silent signing

Each approval prop replaces KeyWay's dialog with your app's answer. An app that returns true without asking signs silently:

<KeyWayProvider
  appId="keyway_..."
  approveCkbTransaction={() => true}
  approveCkbMessage={() => true}
>
  <App />
</KeyWayProvider>

The same works for the Fiber approvals (approveFiberChannelOpen, approveFiberChannelClose, approveFiberPeerConnect, approveFiberChannelSettings). Fiber payments have no dialog by default; see Fiber node.

This removes the user's confirmation of each action, so your app signs for them. Sign-in still appears, and so does the emailed code whenever an action needs one. See Silent signing for every approval prop, what always still appears and the trade-off.

Limits

  • Testnet only.
  • The network fee may be at most 100 shannons per byte of the transaction (about 100 times the normal rate), and never less than 0.01 CKB. Normal transactions pay far less.
  • Each user can start up to 10 signing requests a minute (transactions and messages together).
  • Email confirmation codes: up to 5 every 10 minutes per user. Each code lasts 10 minutes and allows 5 wrong attempts. An approval waits 5 minutes for its first code; once a code is sent, the approval lasts as long as the code, and each new code gives both another 10 minutes. After that the prompt says "This request expired. Start again."

The approval dialog runs inside your app's page. If your app has an XSS bug, or a user's session token leaks, an attacker could request and approve transactions until the user's 24-hour email-confirmation amount is reached. Protect your app with a strict Content Security Policy, and encourage users to set a confirmation amount.

On this page