ckb-session-kit 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ import { ccc } from "@ckb-ccc/core";
2
+ import { TESTNET_LEGACY_DEPLOYMENTS } from "./lock.js";
3
+ import { SessionPolicyError } from "./session.js";
4
+ /**
5
+ * Checks run before any new cell goes under the session lock (audit round 2, 31 Oct).
6
+ *
7
+ * Only paths that CREATE cells under a deployment call this: `openSession`,
8
+ * `openSubscription`. Spending, closing, recovering, charging and cancelling
9
+ * still accept any deployment, legacy ones included, so funds already under an
10
+ * old lock can always come back.
11
+ */
12
+ /** Successful checks, per client and deployment: one lookup per process, not per call. */
13
+ const verified = new WeakMap();
14
+ const keyOf = (d) => {
15
+ const op = ccc.OutPoint.from(d.cellDep.outPoint);
16
+ return [ccc.hexFrom(d.codeHash), ccc.hashTypeFrom(d.hashType), op.txHash, op.index, ccc.depTypeFrom(d.cellDep.depType)].join(":");
17
+ };
18
+ /**
19
+ * Throws a `SessionPolicyError` unless new cells may go under `deployment` on
20
+ * `client`'s network:
21
+ *
22
+ * - it is not one of `TESTNET_LEGACY_DEPLOYMENTS` (older code with fewer rules;
23
+ * the 13 Oct one cannot even spend a token session's 128/160-byte args);
24
+ * - its `hashType` is not `data`: under CKB-VM version 0 the lock faults on
25
+ * load, owner mode included, so such cells could never be spent;
26
+ * - its cell dep is live on this network and holds the code: the code cell's
27
+ * data hashes to `codeHash` (`data1`, `data2`) or its type script hashes to
28
+ * it (`type`). A `depGroup` cell dep is resolved and any member may match.
29
+ * This catches a testnet deployment on a mainnet client, a typo in
30
+ * `codeHash` and a wrong out point, any of which would leave funds under a
31
+ * lock no code can run.
32
+ *
33
+ * A passing check is remembered for that client and deployment; a failing one
34
+ * is not, so the next call looks again.
35
+ */
36
+ export async function assertCurrentDeployment(client, deployment) {
37
+ const codeHash = ccc.hexFrom(deployment.codeHash);
38
+ if (TESTNET_LEGACY_DEPLOYMENTS.some((d) => ccc.hexFrom(d.codeHash) === codeHash)) {
39
+ throw new SessionPolicyError("this is an old deployment of the session lock: new sessions and subscriptions go under the current one " +
40
+ "(old deployments are still accepted to spend, close, recover and cancel)");
41
+ }
42
+ if (ccc.hashTypeFrom(deployment.hashType) === "data") {
43
+ throw new SessionPolicyError("the session lock cannot run under hash_type data (CKB-VM version 0): use data1 or data2 (or type)");
44
+ }
45
+ let byKey = verified.get(client);
46
+ if (!byKey)
47
+ verified.set(client, (byKey = new Map()));
48
+ const key = keyOf(deployment);
49
+ let check = byKey.get(key);
50
+ if (!check) {
51
+ check = checkCode(client, deployment);
52
+ byKey.set(key, check);
53
+ check.catch(() => byKey.delete(key));
54
+ }
55
+ await check;
56
+ }
57
+ async function checkCode(client, d) {
58
+ const codeHash = ccc.hexFrom(d.codeHash);
59
+ const byType = ccc.hashTypeFrom(d.hashType) === "type";
60
+ const outPoint = ccc.OutPoint.from(d.cellDep.outPoint);
61
+ const where = `${outPoint.txHash}:${Number(outPoint.index)}`;
62
+ const dep = await client.getCellLive(outPoint, true);
63
+ if (!dep) {
64
+ throw new SessionPolicyError(`the session lock's cell dep ${where} is not a live cell on this network: a deployment for another ` +
65
+ "network (TESTNET_DEPLOYMENT on mainnet?) or a wrong out point; no funds go under a lock with no code");
66
+ }
67
+ let candidates = [dep];
68
+ if (ccc.depTypeFrom(d.cellDep.depType) === "depGroup") {
69
+ const members = depGroupMembers(dep.outputData);
70
+ if (!members)
71
+ throw new SessionPolicyError(`the session lock's dep group ${where} does not hold a list of out points`);
72
+ candidates = (await Promise.all(members.map((m) => client.getCellLive(m, true)))).filter((c) => !!c);
73
+ }
74
+ const holds = (c) => (byType ? c.cellOutput.type?.hash() === codeHash : ccc.hashCkb(c.outputData) === codeHash);
75
+ if (!candidates.some(holds)) {
76
+ throw new SessionPolicyError(`the session lock's cell dep ${where} does not hold code whose ${byType ? "type hash" : "data hash"} is ` +
77
+ `${codeHash}: a typo in codeHash or a wrong out point; no funds go under a lock with no code`);
78
+ }
79
+ }
80
+ /** A dep group cell's data: a molecule OutPointVec (u32 LE count, then 36-byte out points). */
81
+ function depGroupMembers(data) {
82
+ const b = ccc.bytesFrom(data);
83
+ if (b.length < 4)
84
+ return undefined;
85
+ const n = Number(ccc.numLeFromBytes(b.slice(0, 4)));
86
+ if (b.length !== 4 + 36 * n)
87
+ return undefined;
88
+ return Array.from({ length: n }, (_, i) => {
89
+ const at = 4 + 36 * i;
90
+ return ccc.OutPoint.from({ txHash: b.slice(at, at + 32), index: ccc.numLeFromBytes(b.slice(at + 32, at + 36)) });
91
+ });
92
+ }
package/dist/index.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  export { checkRequest, createSession, isActive, SessionPolicyError } from "./session.js";
2
- export type { CreateOptions, ScopeCheck, Session, SessionPolicy, SessionScope } from "./session.js";
2
+ export type { CreateOptions, ScopeCheck, Session, SessionPolicy, SessionPolicyReason, SessionScope } from "./session.js";
3
3
  export { deserializeSession, indexedDbStore, memoryStore, serializeSession, SessionStoreConflict } from "./store.js";
4
4
  export type { SessionStore } from "./store.js";
5
5
  export { ScopeError, sendInScope, sessionAddress, sessionSigner } from "./transfer.js";
6
6
  export { relativeBlocksSince, SESSION_LOCK_ERRORS, sessionLockArgs, sessionLockCellDep, sessionLockErrorFrom, sessionLockScript, TESTNET_DEPLOYMENT, TESTNET_LEGACY_DEPLOYMENTS, } from "./lock.js";
7
7
  export type { SessionLockDeployment, SessionLockParams } from "./lock.js";
8
- export { assertPlainXudt, assertSigningLock, closeSession, findOwnedSessionCells, findSessionCells, keyLockOf, matureCells, onchainRecipient, openSession, recoverSessions, SessionBalanceError, sessionLockOf, spendInSession, tokenAmount, } from "./onchain.js";
9
- export type { OnChainBinding, SessionCells } from "./onchain.js";
10
- export { blocksForSeconds, cancelSubscription, chargeSubscription, findSubscriptionCells, openSubscription, subscriptionLock, subscriptionMemo, subscriptionStatus, verifySubscription, } from "./subscription.js";
8
+ export { assertCurrentDeployment } from "./deployment.js";
9
+ export { assertPlainXudt, assertSigningLock, closeSession, createPayoutCells, DEFAULT_RETRY, findOwnedSessionCells, findSessionCells, keyLockOf, matureCells, MAX_PAYOUT_CELLS, MAX_POOL_CHECKS, onchainRecipient, openSession, pickPayoutCell, poolConflict, recoverSessions, SendOutcomeUnknownError, SessionBalanceError, sessionLockOf, spendInSession, tokenAmount, } from "./onchain.js";
10
+ export type { OnChainBinding, RetryOptions, SessionBalanceReason, SessionCells, SessionWarning, SigningLockOptions, } from "./onchain.js";
11
+ export { blocksForSeconds, cancelSubscription, chargeSubscription, clearPlantedCells, findSubscriptionCells, MAX_SUBSCRIPTION_CELLS, openSubscription, subscriptionLock, subscriptionMemo, subscriptionStatus, verifySubscription, } from "./subscription.js";
11
12
  export type { Subscription, SubscriptionCheck, SubscriptionStatus } from "./subscription.js";
12
13
  export { paymentMemo, signAccess, verifyAccess } from "./paywall.js";
13
14
  export type { AccessCheck, AccessProof } from "./paywall.js";
package/dist/index.js CHANGED
@@ -2,7 +2,8 @@ export { checkRequest, createSession, isActive, SessionPolicyError } from "./ses
2
2
  export { deserializeSession, indexedDbStore, memoryStore, serializeSession, SessionStoreConflict } from "./store.js";
3
3
  export { ScopeError, sendInScope, sessionAddress, sessionSigner } from "./transfer.js";
4
4
  export { relativeBlocksSince, SESSION_LOCK_ERRORS, sessionLockArgs, sessionLockCellDep, sessionLockErrorFrom, sessionLockScript, TESTNET_DEPLOYMENT, TESTNET_LEGACY_DEPLOYMENTS, } from "./lock.js";
5
- export { assertPlainXudt, assertSigningLock, closeSession, findOwnedSessionCells, findSessionCells, keyLockOf, matureCells, onchainRecipient, openSession, recoverSessions, SessionBalanceError, sessionLockOf, spendInSession, tokenAmount, } from "./onchain.js";
6
- export { blocksForSeconds, cancelSubscription, chargeSubscription, findSubscriptionCells, openSubscription, subscriptionLock, subscriptionMemo, subscriptionStatus, verifySubscription, } from "./subscription.js";
5
+ export { assertCurrentDeployment } from "./deployment.js";
6
+ export { assertPlainXudt, assertSigningLock, closeSession, createPayoutCells, DEFAULT_RETRY, findOwnedSessionCells, findSessionCells, keyLockOf, matureCells, MAX_PAYOUT_CELLS, MAX_POOL_CHECKS, onchainRecipient, openSession, pickPayoutCell, poolConflict, recoverSessions, SendOutcomeUnknownError, SessionBalanceError, sessionLockOf, spendInSession, tokenAmount, } from "./onchain.js";
7
+ export { blocksForSeconds, cancelSubscription, chargeSubscription, clearPlantedCells, findSubscriptionCells, MAX_SUBSCRIPTION_CELLS, openSubscription, subscriptionLock, subscriptionMemo, subscriptionStatus, verifySubscription, } from "./subscription.js";
7
8
  export { paymentMemo, signAccess, verifyAccess } from "./paywall.js";
8
9
  export { applyPlacements, checkPlacement, decodePlacement, encodePlacement, PLACE_BOARD, placementMemo, placementPrice, PlaceMemoError, } from "./place.js";
package/dist/onchain.d.ts CHANGED
@@ -33,8 +33,37 @@ export interface SessionCells {
33
33
  /** Token units in the session cells (token sessions; 0 otherwise). */
34
34
  tokenBalance: bigint;
35
35
  }
36
+ /**
37
+ * Why a `SessionBalanceError` was thrown, where a caller may need to tell it apart
38
+ * without matching the message (which may change):
39
+ * - `no-fee-cell`: the paying key (a session's key cell, a subscription's charger)
40
+ * has no cell that can pay the fee.
41
+ * - `no-payout-cell`: the recipient (`payTo`) has no cell to top up.
42
+ * - `payout-busy`: every payout cell checked is being spent by a transaction in
43
+ * the node's pool (other payers at the same moment). Nothing was sent; try
44
+ * again after a block, or give the recipient more cells (`createPayoutCells`).
45
+ *
46
+ * Neither is about the session's or the subscription's own balance or timing: the
47
+ * missing cells belong to the key's holder or the recipient (in a subscription,
48
+ * both are the creator's). Other balance errors have no `reason`.
49
+ */
50
+ export type SessionBalanceReason = "no-fee-cell" | "no-payout-cell" | "payout-busy";
36
51
  export declare class SessionBalanceError extends Error {
37
- constructor(message: string);
52
+ readonly reason?: SessionBalanceReason;
53
+ constructor(message: string, reason?: SessionBalanceReason);
54
+ }
55
+ /**
56
+ * Something about a session's terms that the lock will enforce as written but that
57
+ * the owner may not have meant. `openSession` reports it before anything is signed
58
+ * (`opts.onWarning`, or `console.warn` without one).
59
+ * - `no-rate-limit`: `minInterval` is 0 (or 1, almost the same: review W6 I3). Each
60
+ * spend is held to `maxPerTx`, but the change of one spend can be spent again in
61
+ * the next block (with 1, a block later), so a leaked key can repeat spends until the
62
+ * session is empty.
63
+ */
64
+ export interface SessionWarning {
65
+ code: "no-rate-limit";
66
+ message: string;
38
67
  }
39
68
  /**
40
69
  * The smallest key cell that can still be returned to the owner: a cell under the
@@ -42,11 +71,24 @@ export declare class SessionBalanceError extends Error {
42
71
  * below a key cell's own 61 CKB.
43
72
  */
44
73
  export declare function keyCellFloor(ownerLock: ccc.ScriptLike): bigint;
74
+ /**
75
+ * A send failed and the node could not then be asked whether it has the
76
+ * transaction (review W6 L4): it may have been taken and committed. Never
77
+ * retried; `txHash` is the transaction to look for, and `cause` the send's own
78
+ * error. Its message never repeats the cause's, so nothing reads it as a
79
+ * refusal for a spent input.
80
+ */
81
+ export declare class SendOutcomeUnknownError extends Error {
82
+ readonly txHash: ccc.Hex;
83
+ constructor(txHash: ccc.Hex, cause: unknown);
84
+ }
45
85
  /**
46
86
  * Signs `tx`, hands its final hash to `beforeSend`, then sends it. The hash is
47
87
  * known before anything leaves, so a caller can record the payment or the key
48
88
  * against it first. If the send throws but the node did take the transaction (a
49
- * timeout on the reply, say), the hash is returned all the same.
89
+ * timeout on the reply, say), the hash is returned all the same. If the send
90
+ * throws and the status lookup fails too, a `SendOutcomeUnknownError` is thrown
91
+ * instead of the send's error, which `rebuildOnDeadInputs` never retries.
50
92
  */
51
93
  export declare function signAndSend(signer: ccc.Signer, tx: ccc.Transaction, beforeSend?: (txHash: ccc.Hex) => Promise<void>): Promise<ccc.Hex>;
52
94
  export declare function keyLockOf(session: Session, client: ccc.Client): Promise<ccc.Script>;
@@ -56,8 +98,110 @@ export declare const tokenData: (amount: bigint, rest?: Uint8Array) => `0x${stri
56
98
  /** A cell's data is exactly a 16-byte amount: what the session lock accepts as token change. */
57
99
  export declare const plainTokenCell: (cell: ccc.Cell, type: ccc.Script) => boolean;
58
100
  export declare function sessionLockOf(session: Session, client: ccc.Client, deployment: SessionLockDeployment, binding: OnChainBinding): Promise<ccc.Script>;
101
+ /**
102
+ * Whether a session-lock cell was created by a transaction its owner or its
103
+ * session key took part in (an input under the lock hash in args bytes 0..32 or
104
+ * 32..64). Nobody else can sign for those, so a planted cell never passes.
105
+ */
106
+ export declare function ownOrigin(client: ccc.Client, cell: ccc.Cell): Promise<boolean>;
107
+ /**
108
+ * `cells` less those whose token amounts would overflow a u128, per token type.
109
+ * Anyone can plant a cell holding 2^128-1 units of a free token under a session
110
+ * lock, and xUDT refuses a transaction whose inputs overflow, so taking every
111
+ * cell would block spends, closes, recovery, charges and cancels (review, 27 and
112
+ * 28 Oct). Which cells stay does not depend on indexer order: cells of the
113
+ * owner's or the key's own making first, then the smallest amounts. Without an
114
+ * overflow nothing is looked up and every cell stays, in order.
115
+ */
116
+ export declare function withinU128(client: ccc.Client, cells: ccc.Cell[]): Promise<ccc.Cell[]>;
117
+ /** One of `cells` at random: payers or chargers at once then rarely compete for the same cell. */
118
+ export declare const randomPick: <T>(cells: T[]) => T | undefined;
119
+ /** Whether `lock` is this network's anyone-can-pay lock (RFC 0026). */
120
+ export declare function isAnyoneCanPay(lock: ccc.Script, client: ccc.Client): Promise<boolean>;
121
+ /** The most cells of a recipient read when picking one to top up: any of them will do. */
122
+ export declare const MAX_PAYOUT_CELLS = 64;
123
+ /** The most payout cells a pick asks the node about (`get_live_cell` with the pool), all at once. */
124
+ export declare const MAX_POOL_CHECKS = 4;
125
+ /**
126
+ * Which of `cells` (the recipient's anyone-can-pay cells) to top up, so payers at
127
+ * the same moment rarely take the same one (mentor review, 2 Nov):
128
+ *
129
+ * - confirmed cells first (listed by the indexer), then this client's own
130
+ * outputs still pending (only CCC's cache lists them), each group shuffled;
131
+ * - `avoid` (out points a node rejection named as spent) left out;
132
+ * - the first `MAX_POOL_CHECKS` asked at once whether a transaction in the
133
+ * node's pool spends them (`get_live_cell` with `include_tx_pool`: a cell a
134
+ * pool transaction consumes is dead); the first free one is taken.
135
+ *
136
+ * If the node cannot be asked, an unchecked cell is taken (a rejected send is
137
+ * retried). If every cell checked is spent in the pool, an unchecked one is taken
138
+ * if any is left; otherwise a `SessionBalanceError` "payout-busy" is thrown and
139
+ * nothing is sent. Undefined when `cells` is empty.
140
+ */
141
+ export declare function pickPayoutCell(client: ccc.Client, lock: ccc.Script, cells: ccc.Cell[], avoid?: ReadonlySet<string>): Promise<ccc.Cell | undefined>;
142
+ /**
143
+ * Tops up one cell under the anyone-can-pay `lock` in `tx`: one input, and one
144
+ * output holding `ckb` shannons more, or with `token`, `units` more of it in a
145
+ * cell of that token. The cell is chosen by `pickPayoutCell`, never one in
146
+ * `avoid`. Returns the cell, or undefined when `lock` has none to add to.
147
+ */
148
+ export declare function addTopUp(tx: ccc.Transaction, client: ccc.Client, lock: ccc.Script, add: {
149
+ ckb?: bigint;
150
+ token?: ccc.Script;
151
+ units?: bigint;
152
+ }, avoid?: ReadonlySet<string>): Promise<ccc.Cell | undefined>;
59
153
  /** Plain cells under `lock`; with `token`, also cells holding exactly that token. */
60
154
  export declare function collect(client: ccc.Client, lock: ccc.Script, token?: ccc.Script): Promise<ccc.Cell[]>;
155
+ /**
156
+ * `cells` (found under `lock`) less those only CCC's client cache vouches for
157
+ * that are gone. The cache lists the outputs of every transaction this client
158
+ * sent as live, ahead of the indexer's cells, and never drops them: once another
159
+ * tab, device or server instance spends them, every transaction built here on
160
+ * them would be rejected until a reload (audit, 31 Oct). A cached cell is kept
161
+ * while the transaction that made it is pending or proposed (a chain of
162
+ * payments faster than the indexer needs it), or once committed while the node
163
+ * says it is still live; it is dropped once the node no longer knows that
164
+ * transaction, or says the cell is spent. When the node cannot be asked, it is kept.
165
+ */
166
+ export declare function withoutDeadCached(client: ccc.Client, lock: ccc.Script, cells: ccc.Cell[]): Promise<ccc.Cell[]>;
167
+ /** Whether a send was rejected because an input is spent or unknown to the node. */
168
+ export declare function deadInputs(e: unknown): boolean;
169
+ /**
170
+ * Whether a send was rejected because an input is already spent by a
171
+ * transaction in the node's pool, on a node with replace-by-fee on: CKB then
172
+ * runs its RBF checks instead of answering `Dead`, and refuses the newcomer
173
+ * (`PoolRejectedRBF`, -1111) unless it pays enough more to replace the other.
174
+ */
175
+ export declare function poolConflict(e: unknown): boolean;
176
+ /** How often, and how patiently, a payment or a charge is built again after a refusal for a spent cell. */
177
+ export interface RetryOptions {
178
+ /** Attempts in all, the first included (default 3; 1 never retries). */
179
+ attempts?: number;
180
+ /** The wait before the second attempt, in ms (default 1000); it doubles each time, with jitter of +-50%. */
181
+ delayMs?: number;
182
+ }
183
+ export declare const DEFAULT_RETRY: Readonly<Required<RetryOptions>>;
184
+ /**
185
+ * Runs `build` (which builds and sends a transaction). If the node definitely
186
+ * refused it because an input was already spent (`Dead`, `Unknown`, or an RBF
187
+ * refusal for a conflict in its pool), or `build` sent nothing because every
188
+ * payout cell was busy ("payout-busy"), clears the client's cache, waits, and
189
+ * runs it again, up to `retry.attempts` in all (audit, 31 Oct; mentor review,
190
+ * 2 Nov). `build` is given the out points rejections named, so a payout cell
191
+ * the node called spent is not picked again. The wait is `retry.delayMs`,
192
+ * doubling each time, times a random 0.5 to 1.5, so payers that collided do
193
+ * not retry in step.
194
+ *
195
+ * Any other error is thrown at once. That includes a send whose outcome is
196
+ * unknown (a timeout, a lost reply): `signAndSend` returns the hash when the
197
+ * node did take the transaction, and otherwise throws the send's own error,
198
+ * which is never retried, so nothing is ever paid twice. When the node cannot
199
+ * be asked either, a refusal for a spent input may only mean the first send
200
+ * committed (CCC retries a send on a fallback node): `signAndSend` then throws
201
+ * `SendOutcomeUnknownError`, never retried (review W6 L4). A `beforeSend` inside
202
+ * runs again with each new transaction's hash: a refused one never commits.
203
+ */
204
+ export declare function rebuildOnDeadInputs<T>(client: ccc.Client, build: (avoid: ReadonlySet<string>) => Promise<T>, retry?: RetryOptions): Promise<T>;
61
205
  export declare function findSessionCells(session: Session, client: ccc.Client, deployment: SessionLockDeployment, binding: OnChainBinding): Promise<SessionCells>;
62
206
  /**
63
207
  * The recipient the session lock will enforce, taken from the session's own scope
@@ -65,24 +209,56 @@ export declare function findSessionCells(session: Session, client: ccc.Client, d
65
209
  * scope listing several cannot be enforced on-chain and is refused.
66
210
  */
67
211
  export declare function onchainRecipient(session: Session): string | undefined;
212
+ /** Options for {@link assertSigningLock}. */
213
+ export type SigningLockOptions = {
214
+ /**
215
+ * Let a lock this kit has not reviewed own a session, with a `console.warn`.
216
+ * Use it only after checking yourself that the lock cannot be unlocked without
217
+ * the owner's signature. A lock known to unlock without it is refused anyway
218
+ * (the deployed anyone-can-pay, PW-Lock and always-success code, under any
219
+ * hash_type; see `assertSigningLock`).
220
+ */
221
+ allowUnreviewedOwnerLock?: boolean;
222
+ };
68
223
  /**
69
- * Throws unless `lock` is known to need a signature over the whole transaction:
70
- * the session lock grants owner mode to any transaction with an input under the
71
- * owner's lock, so that lock is the only thing standing between a stranger and
72
- * every session. An allowlist, not a blocklist (code review, 14 Oct):
224
+ * Throws `SessionPolicyError` unless `lock` is a reviewed configuration of a lock
225
+ * that needs the owner's signature over the whole transaction. The session lock
226
+ * grants owner mode to any transaction with an input under the owner's lock, so
227
+ * that lock is the only thing standing between a stranger and every session.
228
+ * An allowlist, not a blocklist (code review, 14 Oct; mentor review, 2 Nov):
229
+ *
230
+ * Reviewed, accepted:
231
+ * - secp256k1-blake160 (20-byte args); secp256k1 multisig, v1 and v2 (20 bytes,
232
+ * or 28 with a since, which only delays);
233
+ * - Omnilock with a signature auth flag (0x00-0x06: CKB, Ethereum, EOS, Tron,
234
+ * Bitcoin, Dogecoin, multisig; 0x12: Ethereum with the message displayed), and
235
+ * no mode other than time-lock or supply;
236
+ * - Nostr lock in signing mode (args[0] = 0, 21-byte args);
237
+ * - JoyID (closed source: reviewed from its documentation only).
73
238
  *
74
- * - secp256k1-blake160 (single or multisig), JoyID, Nostr, PW lock;
75
- * - Omnilock with a signature auth flag (0x00-0x07: CKB, Ethereum, EOS, Tron,
76
- * Bitcoin, Dogecoin, multisig; 0x12: Ethereum with the message displayed) and neither administrator (0x01) nor
77
- * anyone-can-pay (0x02) mode. Auth flags 0xFC-0xFE hand the decision to
78
- * another lock or to arbitrary code, and are refused.
239
+ * Known to unlock without the owner's signature, refused always (reason
240
+ * `"owner-lock-unsafe"`): anyone-can-pay, PW-Lock, always-success (by their
241
+ * type id, or by the data hash of their deployed binaries under any data
242
+ * hash_type), Omnilock in anyone-can-pay or administrator mode, Nostr lock in
243
+ * PoW mode (args[0] != 0). Another build of those locks (a binary with a
244
+ * different data hash) is not recognised: it is refused as unreviewed.
79
245
  *
80
- * Anyone-can-pay (RFC 0026) unlocks with no signature at all: anyone could spend
81
- * the owner's cell back to itself unchanged and sweep every session.
246
+ * Anything else (Omnilock auth 0xFC-0xFE, which hand the decision to another lock
247
+ * or to other code; the proxy locks; unknown locks; args the lock's code would
248
+ * not accept) is refused with reason `"owner-lock-unreviewed"`, unless
249
+ * `allowUnreviewedOwnerLock` is set: then it passes with a warning.
82
250
  *
83
251
  * The owner must also never sign a partial (open) transaction with this lock.
84
252
  */
85
- export declare function assertSigningLock(lock: ccc.Script, client: ccc.Client): Promise<void>;
253
+ export declare function assertSigningLock(lock: ccc.Script, client: ccc.Client, opts?: SigningLockOptions): Promise<void>;
254
+ /** Whose lock `checkSigningLock` checks, for its messages, and the opt-in that lets an unreviewed one through. */
255
+ export type SigningRole = {
256
+ whose: string;
257
+ can: string;
258
+ optIn: string;
259
+ };
260
+ /** `assertSigningLock` for any role: a subscription's charger lock is checked the same way (review M2), with its own opt-in. */
261
+ export declare function checkSigningLock(lock: ccc.Script, client: ccc.Client, allowUnreviewed: boolean, role: SigningRole): Promise<void>;
86
262
  /**
87
263
  * Owner funds the session: one transaction, one signature from `owner`.
88
264
  * The on-chain recipient comes from `session.policy.scope.recipients`.
@@ -107,7 +283,12 @@ export declare function openSession(owner: ccc.Signer, session: Session, deploym
107
283
  maxPerTx: bigint;
108
284
  };
109
285
  beforeSend?: (binding: OnChainBinding) => Promise<void>;
110
- }): Promise<{
286
+ /**
287
+ * Called before anything is signed, once per `SessionWarning`; throw to stop the
288
+ * opening. Without it, warnings go to `console.warn`.
289
+ */
290
+ onWarning?: (warning: SessionWarning) => void | Promise<void>;
291
+ } & SigningLockOptions): Promise<{
111
292
  txHash: ccc.Hex;
112
293
  binding: OnChainBinding;
113
294
  }>;
@@ -118,6 +299,13 @@ export declare function openSession(owner: ccc.Signer, session: Session, deploym
118
299
  * widens who may mint (owner mode by input or output type, code review 14 Oct).
119
300
  */
120
301
  export declare function assertPlainXudt(type: ccc.Script, client: ccc.Client): Promise<void>;
302
+ /** The network's xUDT code: what `client.getKnownScript(ccc.KnownScript.XUdt)` returns. */
303
+ type XudtCode = {
304
+ codeHash: ccc.Hex;
305
+ hashType: ccc.HashType;
306
+ };
307
+ /** Why `type` is not a plain xUDT under the network's xUDT script `xudt`, or undefined if it is. */
308
+ export declare function plainXudtFault(type: ccc.Script, xudt: XudtCode): string | undefined;
121
309
  /**
122
310
  * The session key pays `to`: `amount` shannons, or in a token session `token`
123
311
  * units of its xUDT. The scope is checked here first, then the network checks it
@@ -135,22 +323,40 @@ export declare function assertPlainXudt(type: ccc.Script, client: ccc.Client): P
135
323
  *
136
324
  * `memo` is recorded in the witness of the session key's own input, which that
137
325
  * key's signature covers: the payment commits to it (see `verifyAccess`).
326
+ *
327
+ * With `topUp`, the recipient's cell is a confirmed one that no transaction in
328
+ * the node's pool spends, where the node can tell (`pickPayoutCell`): readers
329
+ * paying at the same moment compete for those cells.
330
+ *
331
+ * If the node rejects the payment because an input is already spent (another
332
+ * tab, device or payer took it first) or unknown, the client's cache is cleared
333
+ * and the payment is built again from the live cells, with another payout
334
+ * cell, after a short wait: up to `retry.attempts` in all (default 3), waiting
335
+ * `retry.delayMs` (default 1000) doubling each time. `beforeSend` runs again
336
+ * with each new hash (`rebuildOnDeadInputs`). A send whose outcome is unknown
337
+ * is never retried.
138
338
  */
139
- export declare function spendInSession(session: Session, client: ccc.Client, deployment: SessionLockDeployment, binding: OnChainBinding, request: {
339
+ export declare function spendInSession(session: Session, client: ccc.Client, deployment: SessionLockDeployment, binding: OnChainBinding, request: SpendRequest, now?: Date): Promise<ccc.Hex>;
340
+ type SpendCommon = {
140
341
  to: string;
141
- amount: bigint;
142
- token?: undefined;
143
342
  topUp?: boolean;
144
343
  memo?: ccc.HexLike;
145
344
  beforeSend?: (txHash: ccc.Hex) => Promise<void>;
345
+ retry?: RetryOptions;
346
+ };
347
+ type SpendRequest = SpendCommon & ({
348
+ amount: bigint;
349
+ token?: undefined;
146
350
  } | {
147
- to: string;
148
351
  token: bigint;
149
352
  amount?: undefined;
150
- topUp?: boolean;
151
- memo?: ccc.HexLike;
152
- beforeSend?: (txHash: ccc.Hex) => Promise<void>;
153
- }, now?: Date): Promise<ccc.Hex>;
353
+ });
354
+ /**
355
+ * `completeFeeChangeToOutput` into the last output, with CCC's "Insufficient CKB"
356
+ * (the paying cell holds its floor but not the fee too) as a
357
+ * `SessionBalanceError` with reason `no-fee-cell` (audit round 2, 31 Oct).
358
+ */
359
+ export declare function completeFeeOrNoFeeCell(tx: ccc.Transaction, signer: ccc.Signer, message: string): Promise<void>;
154
360
  /** The cells created at least `blocks` blocks before the tip: those a relative `since` of `blocks` can spend. */
155
361
  export declare function matureCells(client: ccc.Client, cells: ccc.Cell[], blocks: bigint): Promise<ccc.Cell[]>;
156
362
  /**
@@ -162,9 +368,16 @@ export declare function matureCells(client: ccc.Client, cells: ccc.Cell[], block
162
368
  * it. A key-cell failure is reported in `keyError`; the key stays usable for a retry.
163
369
  *
164
370
  * `nothingFound` means no cell is left under the session or the key: the key can
165
- * be forgotten. If the funding transaction (`binding.openTx`) is still in flight
166
- * or not yet indexed, this throws instead, so the key is never forgotten before
167
- * the funds it controls arrive.
371
+ * be forgotten. If the funding transaction (`binding.openTx`) is still in flight,
372
+ * or committed with an output under the session or the key the node still holds
373
+ * live (not indexed yet), this throws instead, so the key is never forgotten
374
+ * before the funds it controls arrive. Once the funding is committed and every
375
+ * one of those outputs is spent (a close whose reply was lost, another tab's
376
+ * close), it is `nothingFound` (audit, 31 Oct).
377
+ *
378
+ * `owner` must be the wallet that opened the session (its lock is
379
+ * `binding.ownerLock`): any other is refused with a `SessionPolicyError` before
380
+ * it is asked to sign anything.
168
381
  */
169
382
  export declare function closeSession(owner: ccc.Signer, session: Session, deployment: SessionLockDeployment, binding: OnChainBinding): Promise<{
170
383
  keyTx?: ccc.Hex;
@@ -180,7 +393,9 @@ export declare function closeSession(owner: ccc.Signer, session: Session, deploy
180
393
  * for a token session) are kept: a cell with malformed args would fail the lock
181
394
  * even in owner mode, and anyone can create one with this owner's hash as a prefix
182
395
  * to block a sweep. Likewise a typed cell is kept only if it holds the session's
183
- * own token in well-formed data; anything else could make the sweep fail.
396
+ * own token, a plain xUDT, in exactly 16 bytes of data; anything else could make
397
+ * the sweep fail. Cells
398
+ * whose tokens would overflow a u128 together are left out (`withinU128`).
184
399
  *
185
400
  * Pass several deployments (`[TESTNET_DEPLOYMENT, ...TESTNET_LEGACY_DEPLOYMENTS]`)
186
401
  * to find sessions opened under earlier versions of the lock as well.
@@ -192,9 +407,10 @@ export declare function findOwnedSessionCells(owner: ccc.Signer, deployments: Se
192
407
  *
193
408
  * Anyone can create a cell under a session lock carrying this owner's hash and
194
409
  * their own key, and spend it before the sweep lands, which would make a single
195
- * sweep fail. So if the one transaction is refused, each session (cells sharing a
196
- * session key) is swept on its own, one signature each: a cell someone else can
197
- * spend fails alone, and `failed` counts the sessions not recovered.
410
+ * sweep fail. So if the one transaction is refused, each session (cells under one
411
+ * session lock) is swept on its own, one signature each: a cell someone else can
412
+ * spend fails alone, and `failed` counts the sessions not recovered. A fault no
413
+ * retry can fix (the owner holds no plain cell to prove ownership) is thrown.
198
414
  *
199
415
  * The key cell (the fee budget) is under the session key's own lock and cannot be
200
416
  * recovered without that key; keep it small.
@@ -208,5 +424,28 @@ export declare function recoverSessions(owner: ccc.Signer, deployments: SessionL
208
424
  /**
209
425
  * Spends `cells` in owner mode: everything returns to the owner. Tokens go back
210
426
  * in one cell per token type (xUDT would let a sweep burn them otherwise).
427
+ * `memo` goes in the witness (`input_type`) of the owner's own input, which the
428
+ * owner's signature covers: what the sweep is for (`CLEAR_PLANTED_MEMO`).
211
429
  */
212
- export declare function ownerSweep(owner: ccc.Signer, deployments: SessionLockDeployment | SessionLockDeployment[], cells: ccc.Cell[]): Promise<ccc.Hex>;
430
+ export declare function ownerSweep(owner: ccc.Signer, deployments: SessionLockDeployment | SessionLockDeployment[], cells: ccc.Cell[], opts?: {
431
+ memo?: ccc.Hex;
432
+ }): Promise<ccc.Hex>;
433
+ /**
434
+ * Gives an anyone-can-pay address (a creator's `payTo`) `count` more cells to be
435
+ * topped up, paid by `funder`, in one transaction. Each top-up consumes one of
436
+ * these cells, so readers paying at the same moment compete for them; with more
437
+ * cells they rarely pick the same one (mentor review, 2 Nov). Anyone can be the
438
+ * funder: the anyone-can-pay lock needs no signature to receive a new cell.
439
+ *
440
+ * Each cell is as small as a cell under `payTo` can be (61 CKB for a 20-byte
441
+ * args address) unless `capacity` is given. With `token`, each is a cell of that
442
+ * token holding 0 units (about 142 CKB), for token payments. `count` is 1 to
443
+ * `MAX_PAYOUT_CELLS`: a payment reads no more than that many.
444
+ */
445
+ export declare function createPayoutCells(funder: ccc.Signer, payTo: string, opts: {
446
+ count: number;
447
+ capacity?: bigint;
448
+ token?: ccc.ScriptLike;
449
+ beforeSend?: (txHash: ccc.Hex) => Promise<void>;
450
+ }): Promise<ccc.Hex>;
451
+ export {};