ckb-session-kit 0.0.0-stage → 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 +337 -0
- package/LICENSE +21 -0
- package/README.md +718 -3
- package/dist/deployment.d.ts +21 -0
- package/dist/deployment.js +92 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +9 -0
- package/dist/lock.d.ts +70 -0
- package/dist/lock.js +106 -0
- package/dist/onchain.d.ts +451 -0
- package/dist/onchain.js +1150 -0
- package/dist/paywall.d.ts +60 -0
- package/dist/paywall.js +175 -0
- package/dist/place.d.ts +113 -0
- package/dist/place.js +155 -0
- package/dist/session.d.ts +71 -0
- package/dist/session.js +57 -0
- package/dist/store.d.ts +27 -0
- package/dist/store.js +192 -0
- package/dist/subscription.d.ts +317 -0
- package/dist/subscription.js +959 -0
- package/dist/transfer.d.ts +19 -0
- package/dist/transfer.js +33 -0
- package/package.json +80 -6
package/dist/store.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
export class SessionStoreConflict extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("another session is already stored here (opened in another tab?); reload the page");
|
|
4
|
+
this.name = "SessionStoreConflict";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
const UNREADABLE = "the stored session is unreadable; it may still hold a funded key, so it is never overwritten";
|
|
8
|
+
/** The stored value's public key, without parsing the rest. */
|
|
9
|
+
function storedKey(raw) {
|
|
10
|
+
if (raw === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
let key;
|
|
13
|
+
try {
|
|
14
|
+
key = JSON.parse(raw).publicKey;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error(UNREADABLE);
|
|
18
|
+
}
|
|
19
|
+
if (typeof key !== "string")
|
|
20
|
+
throw new Error(UNREADABLE);
|
|
21
|
+
return key;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Throws SessionStoreConflict unless `session` may replace the stored value: a
|
|
25
|
+
* different key may hold funds, and a save of the same key without the on-chain
|
|
26
|
+
* binding (a stale tab that never saw the funding) would lose what finds them
|
|
27
|
+
* (review, 28 Oct).
|
|
28
|
+
*/
|
|
29
|
+
function assertReplaceable(raw, session) {
|
|
30
|
+
const held = storedKey(raw);
|
|
31
|
+
if (held === undefined)
|
|
32
|
+
return;
|
|
33
|
+
if (held !== session.publicKey)
|
|
34
|
+
throw new SessionStoreConflict();
|
|
35
|
+
if (!session.onchain && JSON.parse(raw).onchain)
|
|
36
|
+
throw new SessionStoreConflict();
|
|
37
|
+
}
|
|
38
|
+
export function serializeSession(s) {
|
|
39
|
+
const stored = {
|
|
40
|
+
v: 1,
|
|
41
|
+
privateKey: s.privateKey,
|
|
42
|
+
publicKey: s.publicKey,
|
|
43
|
+
expiresAt: s.policy.expiresAt.toISOString(),
|
|
44
|
+
maxPerTx: s.policy.scope.maxPerTx.toString(),
|
|
45
|
+
...(s.policy.scope.recipients && { recipients: s.policy.scope.recipients }),
|
|
46
|
+
...(s.onchain && {
|
|
47
|
+
onchain: {
|
|
48
|
+
ownerLock: s.onchain.ownerLock,
|
|
49
|
+
minInterval: s.onchain.minInterval.toString(),
|
|
50
|
+
...(s.onchain.recipientLockHash && { recipientLockHash: s.onchain.recipientLockHash }),
|
|
51
|
+
...(s.onchain.token && {
|
|
52
|
+
token: { type: s.onchain.token.type, maxPerTx: s.onchain.token.maxPerTx.toString() },
|
|
53
|
+
}),
|
|
54
|
+
...(s.onchain.openTx && { openTx: s.onchain.openTx }),
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
return JSON.stringify(stored);
|
|
59
|
+
}
|
|
60
|
+
export function deserializeSession(json) {
|
|
61
|
+
let d;
|
|
62
|
+
try {
|
|
63
|
+
d = JSON.parse(json);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error(UNREADABLE);
|
|
67
|
+
}
|
|
68
|
+
if (d.v !== 1)
|
|
69
|
+
throw new Error(`unsupported stored session version: ${String(d.v)}`);
|
|
70
|
+
return {
|
|
71
|
+
privateKey: d.privateKey,
|
|
72
|
+
publicKey: d.publicKey,
|
|
73
|
+
policy: {
|
|
74
|
+
expiresAt: new Date(d.expiresAt),
|
|
75
|
+
scope: {
|
|
76
|
+
maxPerTx: BigInt(d.maxPerTx),
|
|
77
|
+
...(d.recipients && { recipients: d.recipients }),
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
...(d.onchain && {
|
|
81
|
+
onchain: {
|
|
82
|
+
ownerLock: {
|
|
83
|
+
codeHash: d.onchain.ownerLock.codeHash,
|
|
84
|
+
hashType: d.onchain.ownerLock.hashType,
|
|
85
|
+
args: d.onchain.ownerLock.args,
|
|
86
|
+
},
|
|
87
|
+
minInterval: BigInt(d.onchain.minInterval),
|
|
88
|
+
...(d.onchain.recipientLockHash && { recipientLockHash: d.onchain.recipientLockHash }),
|
|
89
|
+
...(d.onchain.token && {
|
|
90
|
+
token: {
|
|
91
|
+
type: {
|
|
92
|
+
codeHash: d.onchain.token.type.codeHash,
|
|
93
|
+
hashType: d.onchain.token.type.hashType,
|
|
94
|
+
args: d.onchain.token.type.args,
|
|
95
|
+
},
|
|
96
|
+
maxPerTx: BigInt(d.onchain.token.maxPerTx),
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
...(d.onchain.openTx && { openTx: d.onchain.openTx }),
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function memoryStore() {
|
|
105
|
+
let value;
|
|
106
|
+
return {
|
|
107
|
+
load: async () => (value === undefined ? undefined : deserializeSession(value)),
|
|
108
|
+
save: async (s) => {
|
|
109
|
+
assertReplaceable(value, s);
|
|
110
|
+
value = serializeSession(s);
|
|
111
|
+
},
|
|
112
|
+
clear: async (s) => {
|
|
113
|
+
if (storedKey(value) === s.publicKey)
|
|
114
|
+
value = undefined;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const DB = "ckb-session-kit";
|
|
119
|
+
const STORE = "sessions";
|
|
120
|
+
const KEY = "current";
|
|
121
|
+
function request(r) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
r.onsuccess = () => resolve(r.result);
|
|
124
|
+
r.onerror = () => reject(r.error);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async function open(idb) {
|
|
128
|
+
const r = idb.open(DB, 1);
|
|
129
|
+
r.onupgradeneeded = () => r.result.createObjectStore(STORE);
|
|
130
|
+
return request(r);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* One session per `key`. Pages on the same origin that each run their own
|
|
134
|
+
* session (a wallet page and a reader page, say) pass different keys.
|
|
135
|
+
*/
|
|
136
|
+
export function indexedDbStore(idb = globalThis.indexedDB, key = KEY) {
|
|
137
|
+
// Read and write in one readwrite transaction, so two tabs cannot interleave.
|
|
138
|
+
// It resolves only once the transaction has committed, durably: a save is what
|
|
139
|
+
// makes it safe to send funds to the key.
|
|
140
|
+
const update = async (decide, value) => {
|
|
141
|
+
const db = await open(idb);
|
|
142
|
+
try {
|
|
143
|
+
const tx = db.transaction(STORE, "readwrite", { durability: "strict" });
|
|
144
|
+
const done = new Promise((resolve, reject) => {
|
|
145
|
+
tx.oncomplete = () => resolve();
|
|
146
|
+
tx.onabort = () => reject(tx.error ?? new Error("session store transaction aborted"));
|
|
147
|
+
tx.onerror = () => reject(tx.error ?? new Error("session store transaction failed"));
|
|
148
|
+
});
|
|
149
|
+
done.catch(() => { }); // awaited below; a decide() error aborts it first
|
|
150
|
+
const store = tx.objectStore(STORE);
|
|
151
|
+
const raw = await request(store.get(key));
|
|
152
|
+
let action;
|
|
153
|
+
try {
|
|
154
|
+
action = decide(raw);
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
tx.abort();
|
|
158
|
+
throw e;
|
|
159
|
+
}
|
|
160
|
+
if (action === "put")
|
|
161
|
+
await request(store.put(value, key));
|
|
162
|
+
if (action === "delete")
|
|
163
|
+
await request(store.delete(key));
|
|
164
|
+
await done;
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
db.close();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
load: async () => {
|
|
172
|
+
const db = await open(idb);
|
|
173
|
+
try {
|
|
174
|
+
const raw = await request(db.transaction(STORE, "readonly").objectStore(STORE).get(key));
|
|
175
|
+
return raw === undefined ? undefined : deserializeSession(raw);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
db.close();
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
save: async (session) => {
|
|
182
|
+
const value = serializeSession(session);
|
|
183
|
+
await update((raw) => {
|
|
184
|
+
assertReplaceable(raw, session);
|
|
185
|
+
return "put";
|
|
186
|
+
}, value);
|
|
187
|
+
},
|
|
188
|
+
clear: async (session) => {
|
|
189
|
+
await update((raw) => (storedKey(raw) === session.publicKey ? "delete" : "keep"));
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { ccc } from "@ckb-ccc/core";
|
|
2
|
+
import { type SessionLockDeployment } from "./lock.js";
|
|
3
|
+
import { type SigningLockOptions, type RetryOptions } from "./onchain.js";
|
|
4
|
+
/**
|
|
5
|
+
* Subscriptions (docs/design/subscriptions.md): the same session lock, with the
|
|
6
|
+
* roles moved. The subscriber is the owner; the creator's charger key is the
|
|
7
|
+
* session key; `max_per_tx` is the price, `min_interval` the period, and the
|
|
8
|
+
* recipient the creator's payout address. So the network lets the charger take
|
|
9
|
+
* at most the price, at most once per period, only to the payout address, and
|
|
10
|
+
* the subscriber can sweep everything back alone at any time.
|
|
11
|
+
*
|
|
12
|
+
* - `openSubscription` — the subscriber funds one cell. One signature.
|
|
13
|
+
* - `chargeSubscription` — the creator's charger key takes the price, paying the
|
|
14
|
+
* fee from its own cell. No subscriber involved.
|
|
15
|
+
* - `cancelSubscription` — the subscriber sweeps the cell back. One signature.
|
|
16
|
+
* - `verifySubscription` — the creator checks a subscription it was told about.
|
|
17
|
+
*
|
|
18
|
+
* A `Subscription` holds no private key: both sides can store it as it is.
|
|
19
|
+
*
|
|
20
|
+
* A token subscription (`token` set) is paid in an xUDT token instead: the
|
|
21
|
+
* 160-byte token layout with `max_per_tx` 0, so no CKB ever leaves the cell, and
|
|
22
|
+
* the price as `max_per_tx_token`. Charges top up the payout address's
|
|
23
|
+
* anyone-can-pay cell of that token (docs/design/subscriptions.md, "Token subscriptions").
|
|
24
|
+
*/
|
|
25
|
+
export interface Subscription {
|
|
26
|
+
subscriberLockHash: ccc.Hex;
|
|
27
|
+
chargerLockHash: ccc.Hex;
|
|
28
|
+
payToLockHash: ccc.Hex;
|
|
29
|
+
/** Per charge, in the subscription's asset: shannons, or token units in a token subscription. */
|
|
30
|
+
price: bigint;
|
|
31
|
+
/** Blocks between two charges of the same cell. */
|
|
32
|
+
periodBlocks: bigint;
|
|
33
|
+
/** A token subscription: the plain xUDT type script it is paid in. */
|
|
34
|
+
token?: {
|
|
35
|
+
type: {
|
|
36
|
+
codeHash: ccc.Hex;
|
|
37
|
+
hashType: ccc.HashType;
|
|
38
|
+
args: ccc.Hex;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/** The funding transaction, when this side opened it. */
|
|
42
|
+
openTx?: ccc.Hex;
|
|
43
|
+
}
|
|
44
|
+
export declare function subscriptionLock(deployment: SessionLockDeployment, sub: Subscription): ccc.Script;
|
|
45
|
+
/**
|
|
46
|
+
* The memo an opening commits to, binding the subscription to one account at the
|
|
47
|
+
* creator: a hash, so the account id never appears on-chain and can double as a
|
|
48
|
+
* secret the subscriber's browser presents to the creator.
|
|
49
|
+
*/
|
|
50
|
+
export declare function subscriptionMemo(account: string): ccc.Hex;
|
|
51
|
+
/**
|
|
52
|
+
* The most cells under one subscription's lock that a status or a charge reads
|
|
53
|
+
* (security audit, 30 Oct). Anyone can create cells under any lock, and each
|
|
54
|
+
* cell read costs RPC calls (its creating transaction, and that transaction's
|
|
55
|
+
* inputs), so without a bound one request could be made to cost thousands. A
|
|
56
|
+
* subscription in use holds one cell, or a few after top-ups; 16 is far above that.
|
|
57
|
+
*
|
|
58
|
+
* Beyond it, a status or a charge is refused with a `SessionPolicyError` of
|
|
59
|
+
* reason "too-many-cells", never answered from part of the cells: the cells
|
|
60
|
+
* left unread could hold the newest charge (so access would be denied as
|
|
61
|
+
* unpaid, with a wrong balance) or a mature cell (so a charge would leave it free
|
|
62
|
+
* to pay a second price in the same period). Refusing grants nothing and charges
|
|
63
|
+
* nothing. Only cells of the subscription's own kind count (plain CKB, or its
|
|
64
|
+
* token). The subscriber clears cells planted among them with
|
|
65
|
+
* `clearPlantedCells`, which leaves the subscription's own cells as they are,
|
|
66
|
+
* or with `cancelSubscription`.
|
|
67
|
+
*/
|
|
68
|
+
export declare const MAX_SUBSCRIPTION_CELLS = 16;
|
|
69
|
+
/**
|
|
70
|
+
* The most transactions that only created cells under the lock that `lastCharge`
|
|
71
|
+
* pages past in the recent history (review W6 M2). Anyone can make those, but
|
|
72
|
+
* they are skipped without a fetch, so only the indexer's pages cost anything
|
|
73
|
+
* (`HISTORY_PAGE` entries each). Past it, a status or a charge is refused
|
|
74
|
+
* rather than stopping short of the last charge: blocking one period this way
|
|
75
|
+
* takes over 1024 transactions within it, each locking up at least a cell's
|
|
76
|
+
* size (153 CKB) under the subscription's lock.
|
|
77
|
+
*/
|
|
78
|
+
export declare const MAX_CREATED_HISTORY: number;
|
|
79
|
+
/**
|
|
80
|
+
* The memo `clearPlantedCells` puts in the witness (`input_type`) of the
|
|
81
|
+
* subscriber's own input, under the subscriber's signature: a status tells its
|
|
82
|
+
* cleanup from a cancel by it, so a cleanup does not end a paid period (review
|
|
83
|
+
* W6 L2). Only the subscriber can sign it; a subscriber who marks a cancel
|
|
84
|
+
* with it keeps access only through a period already paid for.
|
|
85
|
+
*/
|
|
86
|
+
export declare const CLEAR_PLANTED_MEMO: `0x${string}`;
|
|
87
|
+
/**
|
|
88
|
+
* The subscriber opens a subscription, in one transaction and one signature: the
|
|
89
|
+
* first period's price goes to `payTo` at once (a new cell cannot be charged for a
|
|
90
|
+
* whole period, so otherwise the first period would be free; review H1), and one
|
|
91
|
+
* cell holding `budget` shannons goes under the session lock. No key cell: the
|
|
92
|
+
* creator already owns cells under its charger key. `budget` of the cell's own
|
|
93
|
+
* size (153 CKB) plus n prices pays n more periods; what is left comes back on
|
|
94
|
+
* cancel. With a price of 153 CKB or more, the last 153 can be charged too.
|
|
95
|
+
*
|
|
96
|
+
* `account` (the subscriber's id at the creator) is committed in the witness of
|
|
97
|
+
* the subscriber's own input, which the subscriber's signature covers; the
|
|
98
|
+
* creator checks it with `verifySubscription`, so nobody else can present this
|
|
99
|
+
* subscription as theirs (review H3).
|
|
100
|
+
*
|
|
101
|
+
* `payTo` should be an anyone-can-pay address with a live cell, so a charge of
|
|
102
|
+
* any size tops it up; otherwise each charge creates a new cell and the price must
|
|
103
|
+
* be at least that cell's size (61 CKB for a plain address). Keep a plain cell in
|
|
104
|
+
* the wallet: cancelling needs one to prove ownership.
|
|
105
|
+
*
|
|
106
|
+
* With `token`, the subscription is paid in that plain xUDT and `price` is in
|
|
107
|
+
* token units: the cell holds `token.amount` units plus its own size in CKB
|
|
108
|
+
* (282 CKB; `budget` may add more, which stays until cancel), and the first price
|
|
109
|
+
* tops up `payTo`'s cell of the token. `payTo` must then be anyone-can-pay and
|
|
110
|
+
* already hold a cell of the token: no CKB may leave a token subscription, so a
|
|
111
|
+
* charge cannot pay for a new token cell.
|
|
112
|
+
*/
|
|
113
|
+
export declare function openSubscription(subscriber: ccc.Signer, deployment: SessionLockDeployment, opts: {
|
|
114
|
+
charger: string;
|
|
115
|
+
payTo: string;
|
|
116
|
+
price: bigint;
|
|
117
|
+
periodBlocks: bigint;
|
|
118
|
+
/** Shannons in the subscription cell. Required without `token`; with one, defaults to the cell's own size. */
|
|
119
|
+
budget?: bigint;
|
|
120
|
+
/** A token subscription: the plain xUDT it is paid in, and the units put in the cell (beyond the first price). */
|
|
121
|
+
token?: {
|
|
122
|
+
type: ccc.ScriptLike;
|
|
123
|
+
amount: bigint;
|
|
124
|
+
};
|
|
125
|
+
account?: string;
|
|
126
|
+
beforeSend?: (sub: Subscription) => Promise<void>;
|
|
127
|
+
/** Let an unreviewed charger lock through, with a warning (`allowUnreviewedOwnerLock` covers the subscriber's own lock only). */
|
|
128
|
+
allowUnreviewedChargerLock?: boolean;
|
|
129
|
+
} & SigningLockOptions): Promise<{
|
|
130
|
+
txHash: ccc.Hex;
|
|
131
|
+
subscription: Subscription;
|
|
132
|
+
}>;
|
|
133
|
+
/**
|
|
134
|
+
* The live cells of a subscription, their balance (shannons) and token balance
|
|
135
|
+
* (units; 0 without a token), and how many blocks until one can be charged: once
|
|
136
|
+
* a cell is a period old and the last charge (the newest transaction that spent
|
|
137
|
+
* a cell under the lock) is too. Throws a `SessionPolicyError` ("too-many-cells")
|
|
138
|
+
* with over `MAX_SUBSCRIPTION_CELLS` cells under the lock.
|
|
139
|
+
*/
|
|
140
|
+
export declare function findSubscriptionCells(client: ccc.Client, deployment: SessionLockDeployment, sub: Subscription): Promise<{
|
|
141
|
+
lock: ccc.Script;
|
|
142
|
+
cells: ccc.Cell[];
|
|
143
|
+
balance: bigint;
|
|
144
|
+
tokenBalance: bigint;
|
|
145
|
+
nextChargeIn: bigint | undefined;
|
|
146
|
+
}>;
|
|
147
|
+
/**
|
|
148
|
+
* The creator's charger key takes one price from the subscription and pays it to
|
|
149
|
+
* `payTo` (topping up its anyone-can-pay cell with `topUp`). The fee comes from
|
|
150
|
+
* one of the charger's own plain cells, which goes back to it smaller. Throws a
|
|
151
|
+
* `SessionBalanceError` while no cell is a full period old or the last charge is
|
|
152
|
+
* under one, or once what is left cannot cover a price and still leave a cell behind.
|
|
153
|
+
*
|
|
154
|
+
* In a token subscription the price is in token units and always tops up
|
|
155
|
+
* `payTo`'s cell of the token (`topUp` is implied). The change keeps every
|
|
156
|
+
* shannon that came in (no CKB may leave) and the tokens less one price, in
|
|
157
|
+
* exactly 16 data bytes.
|
|
158
|
+
*
|
|
159
|
+
* With `topUp` (or a token), `payTo`'s cell is a confirmed one that no
|
|
160
|
+
* transaction in the node's pool spends, where the node can tell
|
|
161
|
+
* (`pickPayoutCell`). A charge the node rejects for a spent or unknown input is
|
|
162
|
+
* built again after clearing the client's cache, with another payout cell,
|
|
163
|
+
* after a short wait: up to `retry.attempts` in all (default 3), waiting
|
|
164
|
+
* `retry.delayMs` (default 1000) doubling each time (`rebuildOnDeadInputs`).
|
|
165
|
+
* `beforeSend` runs again with each new hash. A send whose outcome is unknown
|
|
166
|
+
* is never retried.
|
|
167
|
+
*/
|
|
168
|
+
export declare function chargeSubscription(charger: ccc.Signer, deployment: SessionLockDeployment, sub: Subscription, opts: ChargeOptions): Promise<ccc.Hex>;
|
|
169
|
+
type ChargeOptions = {
|
|
170
|
+
payTo: string;
|
|
171
|
+
topUp?: boolean;
|
|
172
|
+
beforeSend?: (txHash: ccc.Hex) => Promise<void>;
|
|
173
|
+
retry?: RetryOptions;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* The subscriber takes everything left back, in owner mode: no creator involved
|
|
177
|
+
* and no wait. `nothingFound` means no cell is left (already cancelled, or run dry).
|
|
178
|
+
* In a token subscription the tokens come back as one token cell (`refundedTokens`).
|
|
179
|
+
* Subscriptions are also found and swept by `recoverSessions`, since their args
|
|
180
|
+
* begin with the subscriber's lock hash like any session's.
|
|
181
|
+
*/
|
|
182
|
+
export declare function cancelSubscription(subscriber: ccc.Signer, deployment: SessionLockDeployment, sub: Subscription): Promise<{
|
|
183
|
+
txHash?: ccc.Hex;
|
|
184
|
+
refunded: bigint;
|
|
185
|
+
refundedTokens?: bigint;
|
|
186
|
+
nothingFound?: true;
|
|
187
|
+
}>;
|
|
188
|
+
/**
|
|
189
|
+
* The subscriber clears cells a third party created under the subscription's
|
|
190
|
+
* lock, without cancelling (mentor review, 2 Nov). Anyone can create cells under
|
|
191
|
+
* any lock; past `MAX_SUBSCRIPTION_CELLS` of the subscription's kind, a status or
|
|
192
|
+
* a charge is refused ("too-many-cells").
|
|
193
|
+
*
|
|
194
|
+
* A cell is the subscription's own when its creating transaction is the
|
|
195
|
+
* opening (`sub.openTx`), or had an input under the subscriber's or the
|
|
196
|
+
* charger's lock: an opening, a charge, a top-up by the subscriber. Nobody else
|
|
197
|
+
* can sign for those. Every other cell of the subscription's kind is planted:
|
|
198
|
+
* one owner-mode transaction (one signature) sweeps them to the subscriber,
|
|
199
|
+
* who keeps their CKB and tokens. The subscription's own cells are not touched,
|
|
200
|
+
* so their age and the payments that made them still count: access and the
|
|
201
|
+
* charge schedule go on as before.
|
|
202
|
+
*
|
|
203
|
+
* In a token subscription, look-alikes of the token (cells typed with the
|
|
204
|
+
* token's code and longer args that start with the token's, which xUDT accepts
|
|
205
|
+
* and anyone can create with 0 units) are planted too, and swept the same way
|
|
206
|
+
* when an owner sweep can spend them (plain xUDT, no flags; review W6 M1).
|
|
207
|
+
*
|
|
208
|
+
* Bounded: one call keeps at most `4 * MAX_SUBSCRIPTION_CELLS` cells of the
|
|
209
|
+
* kind, reads at most twice as many listed cells for each of the two scans,
|
|
210
|
+
* and looks each creating transaction up once. `more` says cells were left
|
|
211
|
+
* unread: call again once this sweep commits. Refused when none of the cells read is the
|
|
212
|
+
* subscription's own: cancel instead. `nothingFound` when nothing is planted.
|
|
213
|
+
*/
|
|
214
|
+
export declare function clearPlantedCells(subscriber: ccc.Signer, deployment: SessionLockDeployment, sub: Subscription): Promise<{
|
|
215
|
+
txHash?: ccc.Hex;
|
|
216
|
+
swept: number;
|
|
217
|
+
refunded: bigint;
|
|
218
|
+
refundedTokens?: bigint;
|
|
219
|
+
more: boolean;
|
|
220
|
+
nothingFound?: true;
|
|
221
|
+
}>;
|
|
222
|
+
export type SubscriptionCheck = {
|
|
223
|
+
ok: true;
|
|
224
|
+
subscription: Subscription;
|
|
225
|
+
/** Shannons in the cell. */
|
|
226
|
+
balance: bigint;
|
|
227
|
+
/** Token units in the cell (token subscriptions only). */
|
|
228
|
+
tokenBalance?: bigint;
|
|
229
|
+
/** Charges the cell can still pay. */
|
|
230
|
+
chargesLeft: bigint;
|
|
231
|
+
/** The block until which the subscriber has paid: the block that created this cell, plus one period. */
|
|
232
|
+
paidThroughBlock: bigint;
|
|
233
|
+
} | {
|
|
234
|
+
ok: false;
|
|
235
|
+
reason: string;
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* The creator checks a subscription from the chain alone. Nothing in the request
|
|
239
|
+
* is trusted but the out point. The cell at `outPoint` must be live, under this
|
|
240
|
+
* deployment of the session lock (never an older one), untyped and without data,
|
|
241
|
+
* with 112-byte args holding exactly these terms, and at least `minBalance`.
|
|
242
|
+
*
|
|
243
|
+
* And the transaction that created the cell must have paid `payTo` at least one
|
|
244
|
+
* price: the opening pays the first period, every charge pays one. A cell the
|
|
245
|
+
* subscriber re-created in owner mode to reset its age paid nothing, and is
|
|
246
|
+
* refused (review H2). So access is granted up to `paidThroughBlock`; check the
|
|
247
|
+
* current cell again (`findSubscriptionCells`) once that block has passed.
|
|
248
|
+
*
|
|
249
|
+
* With `account`, the creating transaction must also carry `subscriptionMemo(account)`
|
|
250
|
+
* in the witness of an input under the subscriber's lock, which only the
|
|
251
|
+
* subscriber's signature covers: someone else's subscription cannot be claimed
|
|
252
|
+
* (review H3). The memo is in the opening only, so check `account` once, when a
|
|
253
|
+
* subscription is registered, then keep the subscription by its lock.
|
|
254
|
+
*
|
|
255
|
+
* With `token` (a token subscription), the cell must instead be typed with exactly
|
|
256
|
+
* that token and hold its amount, with 160-byte args (`max_per_tx` 0, the token's
|
|
257
|
+
* type hash, `price` in token units as `max_per_tx_token`); `minBalance` is in token
|
|
258
|
+
* units, and the payment counted is token units into `payTo`'s cells of the token.
|
|
259
|
+
* Without `token`, a 160-byte cell is refused, and the reverse.
|
|
260
|
+
*/
|
|
261
|
+
export declare function verifySubscription(client: ccc.Client, deployment: SessionLockDeployment, outPoint: ccc.OutPointLike, expected: {
|
|
262
|
+
charger: string;
|
|
263
|
+
payTo: string;
|
|
264
|
+
price: bigint;
|
|
265
|
+
periodBlocks: bigint;
|
|
266
|
+
minBalance?: bigint;
|
|
267
|
+
account?: string;
|
|
268
|
+
token?: ccc.ScriptLike;
|
|
269
|
+
}): Promise<SubscriptionCheck>;
|
|
270
|
+
export interface SubscriptionStatus {
|
|
271
|
+
/**
|
|
272
|
+
* The block until which the subscriber has paid, from the newest paying cell or
|
|
273
|
+
* a charge that took the whole balance; undefined when neither is left.
|
|
274
|
+
*/
|
|
275
|
+
paidThroughBlock: bigint | undefined;
|
|
276
|
+
tip: bigint;
|
|
277
|
+
/** Whether the tip is within the paid period. */
|
|
278
|
+
active: boolean;
|
|
279
|
+
/** Shannons in the subscription's cells. */
|
|
280
|
+
balance: bigint;
|
|
281
|
+
/** Token units in the subscription's cells (token subscriptions only). */
|
|
282
|
+
tokenBalance?: bigint;
|
|
283
|
+
chargesLeft: bigint;
|
|
284
|
+
/** Blocks until the creator may charge again (0 = now). */
|
|
285
|
+
nextChargeIn: bigint | undefined;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Where a subscription stands, for a creator's server that keeps no database:
|
|
289
|
+
* the subscriber sends the terms' subscriber lock hash, the opening's hash and
|
|
290
|
+
* its account id on every request. The opening must have paid the first price
|
|
291
|
+
* into a cell under exactly this subscription's lock and committed to `account`
|
|
292
|
+
* under the subscriber's signature; then the live cells give the period paid for.
|
|
293
|
+
* Each must have been created by a payment, so a cell re-created by the
|
|
294
|
+
* subscriber to reset its age counts for nothing. Cancelling ends access at once.
|
|
295
|
+
* A token subscription (`sub.token`) counts payments in its token, as `verifySubscription` does.
|
|
296
|
+
*
|
|
297
|
+
* Throws a `SessionPolicyError` of reason "too-many-cells" when over
|
|
298
|
+
* `MAX_SUBSCRIPTION_CELLS` cells (or recent transactions) sit under the lock:
|
|
299
|
+
* not a verdict on the subscription, so not an `ok: false`. A server should
|
|
300
|
+
* answer it as a conflict the subscriber can clear by cancelling and opening again.
|
|
301
|
+
*/
|
|
302
|
+
export declare function subscriptionStatus(client: ccc.Client, deployment: SessionLockDeployment, sub: Subscription, proof: {
|
|
303
|
+
openTx: ccc.HexLike;
|
|
304
|
+
account: string;
|
|
305
|
+
}): Promise<{
|
|
306
|
+
ok: true;
|
|
307
|
+
status: SubscriptionStatus;
|
|
308
|
+
} | {
|
|
309
|
+
ok: false;
|
|
310
|
+
reason: string;
|
|
311
|
+
}>;
|
|
312
|
+
/**
|
|
313
|
+
* About how many blocks `seconds` is, from the average block time over the last
|
|
314
|
+
* `sample` blocks. Block time varies; round up when a period means "no more often than".
|
|
315
|
+
*/
|
|
316
|
+
export declare function blocksForSeconds(client: ccc.Client, seconds: number, sample?: bigint): Promise<bigint>;
|
|
317
|
+
export {};
|