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.
- package/CHANGELOG.md +299 -0
- package/README.md +181 -21
- package/dist/deployment.d.ts +21 -0
- package/dist/deployment.js +92 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/onchain.d.ts +270 -31
- package/dist/onchain.js +675 -86
- package/dist/paywall.d.ts +29 -0
- package/dist/paywall.js +84 -25
- package/dist/place.d.ts +18 -9
- package/dist/place.js +32 -28
- package/dist/session.d.ts +9 -1
- package/dist/session.js +4 -1
- package/dist/store.d.ts +2 -1
- package/dist/store.js +17 -6
- package/dist/subscription.d.ts +103 -7
- package/dist/subscription.js +456 -119
- package/package.json +13 -3
|
@@ -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 {
|
|
9
|
-
export
|
|
10
|
-
export {
|
|
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 {
|
|
6
|
-
export {
|
|
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
|
-
|
|
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
|
|
70
|
-
*
|
|
71
|
-
* owner
|
|
72
|
-
*
|
|
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
|
-
*
|
|
75
|
-
* -
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
-
*
|
|
81
|
-
* the
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
167
|
-
* the
|
|
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
|
|
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
|
|
196
|
-
* session
|
|
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[]
|
|
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 {};
|