@totemsdk/wots-lease 1.0.0 → 1.0.2
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/LICENSE +202 -21
- package/NOTICE +60 -0
- package/dist/axia.d.ts +1 -1
- package/dist/axia.js +41 -28
- package/dist/certificate.d.ts +37 -0
- package/dist/certificate.js +69 -0
- package/dist/cjs/axia.d.ts +33 -0
- package/dist/cjs/axia.js +145 -0
- package/dist/cjs/certificate.d.ts +37 -0
- package/dist/cjs/certificate.js +74 -0
- package/dist/cjs/device.d.ts +18 -0
- package/dist/cjs/device.js +34 -0
- package/dist/cjs/errors.d.ts +71 -0
- package/dist/cjs/errors.js +135 -0
- package/dist/cjs/hybrid.d.ts +37 -0
- package/dist/cjs/hybrid.js +145 -0
- package/dist/cjs/index.d.ts +17 -0
- package/dist/cjs/index.js +45 -0
- package/dist/cjs/journal.d.ts +30 -0
- package/dist/cjs/journal.js +369 -0
- package/dist/cjs/local.d.ts +83 -0
- package/dist/cjs/local.js +440 -0
- package/dist/cjs/onchain.d.ts +54 -0
- package/dist/cjs/onchain.js +314 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/quorum.d.ts +75 -0
- package/dist/cjs/quorum.js +307 -0
- package/dist/cjs/replicated-backup.d.ts +30 -0
- package/dist/cjs/replicated-backup.js +52 -0
- package/dist/cjs/seed.d.ts +41 -0
- package/dist/cjs/seed.js +84 -0
- package/dist/cjs/stubs.d.ts +50 -0
- package/dist/cjs/stubs.js +125 -0
- package/dist/cjs/types.d.ts +272 -0
- package/dist/cjs/types.js +5 -0
- package/dist/cjs/watermark.d.ts +51 -0
- package/dist/cjs/watermark.js +328 -0
- package/dist/device.js +2 -6
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +49 -25
- package/dist/hybrid.d.ts +4 -1
- package/dist/hybrid.js +72 -26
- package/dist/index.d.ts +6 -2
- package/dist/index.js +12 -38
- package/dist/journal.d.ts +14 -4
- package/dist/journal.js +327 -49
- package/dist/local.d.ts +31 -1
- package/dist/local.js +267 -68
- package/dist/onchain.d.ts +3 -1
- package/dist/onchain.js +63 -28
- package/dist/quorum.d.ts +15 -0
- package/dist/quorum.js +54 -15
- package/dist/replicated-backup.d.ts +30 -0
- package/dist/replicated-backup.js +48 -0
- package/dist/seed.d.ts +41 -0
- package/dist/seed.js +80 -0
- package/dist/stubs.d.ts +2 -0
- package/dist/stubs.js +20 -10
- package/dist/types.d.ts +74 -1
- package/dist/types.js +1 -2
- package/dist/watermark.d.ts +22 -1
- package/dist/watermark.js +133 -16
- package/package.json +25 -15
- package/rust/pkg/package.json +3 -3
- package/rust/pkg/wots_lease_wasm_bg.wasm +0 -0
- package/rust/pkg-node/package.json +3 -3
- package/rust/pkg-node/wots_lease_wasm_bg.wasm +0 -0
package/dist/local.js
CHANGED
|
@@ -1,42 +1,147 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* LocalLeaseProvider — Layer 1: local durable ledger.
|
|
4
3
|
*
|
|
5
4
|
* Single-device, no network coordination.
|
|
6
5
|
* All WOTS safety guaranteed by the local WotsWatermarkStore + LeaseJournal.
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const journal_js_1 = require("./journal.js");
|
|
14
|
-
const errors_js_1 = require("./errors.js");
|
|
7
|
+
import { NoopLogger } from '@totemsdk/core';
|
|
8
|
+
import { LeaseStore } from '@totemsdk/core';
|
|
9
|
+
import { WotsWatermarkStore, flatIndex } from './watermark.js';
|
|
10
|
+
import { LeaseJournal } from './journal.js';
|
|
11
|
+
import { LeaseNotFoundError, InvalidLeaseTransitionError, DeviceRangeViolationError, IndicesUnavailableError, } from './errors.js';
|
|
15
12
|
function randomId() {
|
|
16
13
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
17
14
|
return crypto.randomUUID();
|
|
18
15
|
}
|
|
19
16
|
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
20
17
|
}
|
|
21
|
-
class LocalLeaseProvider {
|
|
22
|
-
constructor(storage, logger = new
|
|
18
|
+
export class LocalLeaseProvider {
|
|
19
|
+
constructor(storage, logger = new NoopLogger(), deviceId = 'local') {
|
|
23
20
|
this.logger = logger;
|
|
24
21
|
this._initialized = false;
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
22
|
+
this.initializePromise = null;
|
|
23
|
+
this.mutationTail = Promise.resolve();
|
|
24
|
+
this.watermark = new WotsWatermarkStore(storage, logger);
|
|
25
|
+
this.leaseStore = new LeaseStore(storage, logger);
|
|
26
|
+
this.journal = new LeaseJournal(storage, logger);
|
|
28
27
|
this.deviceId = deviceId;
|
|
28
|
+
this.cas = LocalLeaseProvider.detectCas(storage);
|
|
29
|
+
}
|
|
30
|
+
static detectCas(storage) {
|
|
31
|
+
const candidate = storage;
|
|
32
|
+
return typeof candidate.conditionalUpdate === 'function' ? candidate : undefined;
|
|
29
33
|
}
|
|
30
34
|
async initialize() {
|
|
31
35
|
if (this._initialized)
|
|
32
36
|
return;
|
|
37
|
+
if (!this.initializePromise) {
|
|
38
|
+
this.initializePromise = this.initializeOnce().catch((error) => {
|
|
39
|
+
this.initializePromise = null;
|
|
40
|
+
throw error;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
await this.initializePromise;
|
|
44
|
+
}
|
|
45
|
+
async initializeOnce() {
|
|
46
|
+
await this.journal.initialize();
|
|
33
47
|
await this.watermark.initialize();
|
|
34
48
|
await this.leaseStore.initialize();
|
|
35
|
-
await this.
|
|
49
|
+
await this.reconcileDurableSafetyState();
|
|
36
50
|
await this.recoverExpiredReservations();
|
|
37
51
|
await this.burnUnresolvedReservations();
|
|
38
52
|
this._initialized = true;
|
|
39
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Rebuild the no-reuse watermark from immutable journal history and leases.
|
|
56
|
+
* The journal is audit authority; the watermark remains allocation authority.
|
|
57
|
+
*/
|
|
58
|
+
async reconcileDurableSafetyState() {
|
|
59
|
+
const entries = this.journal.getAll();
|
|
60
|
+
const latestBySlot = new Map();
|
|
61
|
+
const latestByReservation = new Map();
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
latestBySlot.set(`${entry.treeId}:${entry.wotsIndex}`, entry);
|
|
64
|
+
if (entry.reservationId)
|
|
65
|
+
latestByReservation.set(entry.reservationId, entry);
|
|
66
|
+
}
|
|
67
|
+
for (const entry of latestBySlot.values()) {
|
|
68
|
+
await this.watermark.markUnavailable(entry.treeId, entry.indices, entry.status);
|
|
69
|
+
}
|
|
70
|
+
const leases = this.leaseStore.getAll();
|
|
71
|
+
const leaseIds = new Set(leases.map((lease) => lease.leaseId));
|
|
72
|
+
for (const entry of latestByReservation.values()) {
|
|
73
|
+
if (entry.status !== 'reserved' || leaseIds.has(entry.reservationId))
|
|
74
|
+
continue;
|
|
75
|
+
await this.journal.append({
|
|
76
|
+
...entry,
|
|
77
|
+
status: 'burned',
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
});
|
|
80
|
+
await this.watermark.markUnavailable(entry.treeId, entry.indices, 'burned');
|
|
81
|
+
}
|
|
82
|
+
for (const lease of leases) {
|
|
83
|
+
const treeId = lease.treeId ?? 'default';
|
|
84
|
+
const latest = this.journal.getByReservation(lease.leaseId);
|
|
85
|
+
if (!latest) {
|
|
86
|
+
const status = lease.status === 'finalized'
|
|
87
|
+
? 'committed'
|
|
88
|
+
: lease.status === 'cancelled'
|
|
89
|
+
? 'burned'
|
|
90
|
+
: lease.status === 'expired'
|
|
91
|
+
? 'reserved-expired'
|
|
92
|
+
: 'reserved';
|
|
93
|
+
await this.journal.append({
|
|
94
|
+
treeId,
|
|
95
|
+
branchId: 'default',
|
|
96
|
+
wotsIndex: flatIndex(lease.indices),
|
|
97
|
+
indices: lease.indices,
|
|
98
|
+
status,
|
|
99
|
+
reservationId: lease.leaseId,
|
|
100
|
+
...(lease.txId !== undefined ? { txId: lease.txId } : {}),
|
|
101
|
+
timestamp: lease.createdAt,
|
|
102
|
+
deviceId: this.deviceId,
|
|
103
|
+
});
|
|
104
|
+
await this.watermark.markUnavailable(treeId, lease.indices, status);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (latest.status === 'committed' && lease.status !== 'finalized') {
|
|
108
|
+
await this.leaseStore.updateStatus(lease.leaseId, 'finalized');
|
|
109
|
+
}
|
|
110
|
+
else if (latest.status === 'burned' && lease.status !== 'cancelled') {
|
|
111
|
+
await this.leaseStore.updateStatus(lease.leaseId, 'cancelled');
|
|
112
|
+
}
|
|
113
|
+
else if (latest.status === 'reserved-expired' && lease.status !== 'expired') {
|
|
114
|
+
await this.leaseStore.updateStatus(lease.leaseId, 'expired');
|
|
115
|
+
}
|
|
116
|
+
else if (latest.status === 'reserved' && lease.status === 'finalized') {
|
|
117
|
+
await this.journal.append({
|
|
118
|
+
treeId,
|
|
119
|
+
branchId: 'default',
|
|
120
|
+
wotsIndex: flatIndex(lease.indices),
|
|
121
|
+
indices: lease.indices,
|
|
122
|
+
status: 'committed',
|
|
123
|
+
reservationId: lease.leaseId,
|
|
124
|
+
...(lease.txId !== undefined ? { txId: lease.txId } : {}),
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
deviceId: this.deviceId,
|
|
127
|
+
});
|
|
128
|
+
await this.watermark.markUnavailable(treeId, lease.indices, 'committed');
|
|
129
|
+
}
|
|
130
|
+
else if (latest.status === 'reserved' && lease.status === 'cancelled') {
|
|
131
|
+
await this.journal.append({
|
|
132
|
+
treeId,
|
|
133
|
+
branchId: 'default',
|
|
134
|
+
wotsIndex: flatIndex(lease.indices),
|
|
135
|
+
indices: lease.indices,
|
|
136
|
+
status: 'burned',
|
|
137
|
+
reservationId: lease.leaseId,
|
|
138
|
+
timestamp: Date.now(),
|
|
139
|
+
deviceId: this.deviceId,
|
|
140
|
+
});
|
|
141
|
+
await this.watermark.markUnavailable(treeId, lease.indices, 'burned');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
40
145
|
/** Burn all active (non-expired) leases that have no matching treeId.
|
|
41
146
|
* On restart, leases with missing treeId (from before the treeId field was added)
|
|
42
147
|
* must be burned to prevent index reuse. */
|
|
@@ -45,46 +150,56 @@ class LocalLeaseProvider {
|
|
|
45
150
|
for (const lease of this.leaseStore.getAll()) {
|
|
46
151
|
if (lease.status === 'active' && lease.expiresAt >= now && !lease.treeId) {
|
|
47
152
|
this.logger.warn(`[LocalLeaseProvider] Burning unresolved lease ${lease.leaseId} (no treeId)`);
|
|
153
|
+
if (this.journal.getByReservation(lease.leaseId)?.status !== 'burned') {
|
|
154
|
+
await this.journal.append({
|
|
155
|
+
treeId: 'default',
|
|
156
|
+
branchId: 'default',
|
|
157
|
+
wotsIndex: flatIndex(lease.indices),
|
|
158
|
+
indices: lease.indices,
|
|
159
|
+
status: 'burned',
|
|
160
|
+
reservationId: lease.leaseId,
|
|
161
|
+
timestamp: now,
|
|
162
|
+
deviceId: this.deviceId,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
48
165
|
await this.watermark.markUnavailable('default', lease.indices, 'burned');
|
|
49
166
|
await this.leaseStore.updateStatus(lease.leaseId, 'cancelled');
|
|
50
|
-
await this.journal.append({
|
|
51
|
-
treeId: 'default',
|
|
52
|
-
branchId: 'default',
|
|
53
|
-
wotsIndex: (0, watermark_js_1.flatIndex)(lease.indices),
|
|
54
|
-
indices: lease.indices,
|
|
55
|
-
status: 'burned',
|
|
56
|
-
reservationId: lease.leaseId,
|
|
57
|
-
timestamp: now,
|
|
58
|
-
deviceId: this.deviceId,
|
|
59
|
-
});
|
|
60
167
|
}
|
|
61
168
|
}
|
|
62
169
|
}
|
|
63
170
|
async recoverExpiredReservations() {
|
|
64
171
|
const now = Date.now();
|
|
65
172
|
for (const lease of this.leaseStore.getAll()) {
|
|
66
|
-
if (lease.status === 'active' && lease.expiresAt < now) {
|
|
173
|
+
if (lease.status === 'expired' || (lease.status === 'active' && lease.expiresAt < now)) {
|
|
67
174
|
const treeId = lease.treeId ?? 'default';
|
|
175
|
+
if (this.journal.getByReservation(lease.leaseId)?.status !== 'reserved-expired') {
|
|
176
|
+
await this.journal.append({
|
|
177
|
+
treeId,
|
|
178
|
+
branchId: 'default',
|
|
179
|
+
wotsIndex: flatIndex(lease.indices),
|
|
180
|
+
indices: lease.indices,
|
|
181
|
+
status: 'reserved-expired',
|
|
182
|
+
reservationId: lease.leaseId,
|
|
183
|
+
timestamp: now,
|
|
184
|
+
deviceId: this.deviceId,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
68
187
|
await this.watermark.markUnavailable(treeId, lease.indices, 'reserved-expired');
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
treeId,
|
|
72
|
-
branchId: 'default',
|
|
73
|
-
wotsIndex: (0, watermark_js_1.flatIndex)(lease.indices),
|
|
74
|
-
indices: lease.indices,
|
|
75
|
-
status: 'reserved-expired',
|
|
76
|
-
reservationId: lease.leaseId,
|
|
77
|
-
timestamp: now,
|
|
78
|
-
deviceId: this.deviceId,
|
|
79
|
-
});
|
|
188
|
+
if (lease.status !== 'expired')
|
|
189
|
+
await this.leaseStore.updateStatus(lease.leaseId, 'expired');
|
|
80
190
|
}
|
|
81
191
|
}
|
|
82
192
|
}
|
|
83
193
|
async reserveKeyUse(params) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
194
|
+
return this.withMutationLock(async () => {
|
|
195
|
+
if (!this._initialized)
|
|
196
|
+
await this.initialize();
|
|
197
|
+
// Pick up any allocation made by another provider over the same storage
|
|
198
|
+
// before choosing a slot (AUD-004).
|
|
199
|
+
await this.watermark.refresh();
|
|
200
|
+
const indices = this.watermark.getNextIndices(params.treeId);
|
|
201
|
+
return this.reserveSpecificKeyUseUnlocked(params, indices);
|
|
202
|
+
});
|
|
88
203
|
}
|
|
89
204
|
/**
|
|
90
205
|
* Reserve a specific set of indices (used by quorum coordination so every
|
|
@@ -92,23 +207,63 @@ class LocalLeaseProvider {
|
|
|
92
207
|
* slot is already taken.
|
|
93
208
|
*/
|
|
94
209
|
async reserveSpecificKeyUse(params, indices) {
|
|
210
|
+
return this.withMutationLock(async () => {
|
|
211
|
+
if (!this._initialized)
|
|
212
|
+
await this.initialize();
|
|
213
|
+
await this.watermark.refresh();
|
|
214
|
+
return this.reserveSpecificKeyUseUnlocked(params, indices);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async reserveSpecificKeyUseUnlocked(params, indices) {
|
|
95
218
|
if (!this._initialized)
|
|
96
219
|
await this.initialize();
|
|
97
220
|
const treeId = params.treeId;
|
|
98
221
|
const ttlMs = params.ttlMs ?? 120000;
|
|
99
222
|
if (this.watermark.isUnavailable(treeId, indices)) {
|
|
100
|
-
throw new
|
|
223
|
+
throw new IndicesUnavailableError(treeId, indices);
|
|
101
224
|
}
|
|
102
225
|
// Validate device range BEFORE marking unavailable — prevents permanent index loss
|
|
103
226
|
if (params.deviceId) {
|
|
104
227
|
const range = this.getDeviceRange(params.deviceId);
|
|
105
228
|
if (range && (indices.addressIndex < range.start || indices.addressIndex > range.end)) {
|
|
106
|
-
throw new
|
|
229
|
+
throw new DeviceRangeViolationError(indices.addressIndex, range.start, range.end);
|
|
107
230
|
}
|
|
108
231
|
}
|
|
109
|
-
await this.watermark.markUnavailable(treeId, indices, 'reserved');
|
|
110
232
|
const reservationId = randomId();
|
|
111
233
|
const expiresAt = Date.now() + ttlMs;
|
|
234
|
+
// Cross-instance CAS claim (AUD-004): on a durable medium shared by two
|
|
235
|
+
// provider instances over distinct adapter objects, refresh() above can
|
|
236
|
+
// race and both pick the same slot. Claim it atomically first; a lost race
|
|
237
|
+
// means another instance owns it, so the slot is not re-allocated.
|
|
238
|
+
if (this.cas) {
|
|
239
|
+
const claimKey = `totem_wots_claim:${treeId}:${flatIndex(indices)}`;
|
|
240
|
+
const claim = await this.cas.conditionalUpdate(claimKey, (current) => current
|
|
241
|
+
? { abort: 'slot already claimed' }
|
|
242
|
+
: {
|
|
243
|
+
next: {
|
|
244
|
+
reservationId,
|
|
245
|
+
...(params.deviceId !== undefined ? { deviceId: params.deviceId } : {}),
|
|
246
|
+
claimedAt: Date.now(),
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
if (!claim.applied) {
|
|
250
|
+
throw new IndicesUnavailableError(treeId, indices);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Write-ahead safety record: once this succeeds, recovery will never
|
|
254
|
+
// re-expose the index even if a later snapshot write fails.
|
|
255
|
+
await this.journal.append({
|
|
256
|
+
treeId,
|
|
257
|
+
branchId: params.branchId ?? 'default',
|
|
258
|
+
wotsIndex: flatIndex(indices),
|
|
259
|
+
indices,
|
|
260
|
+
status: 'reserved',
|
|
261
|
+
reservationId,
|
|
262
|
+
payloadHash: params.payloadHash,
|
|
263
|
+
timestamp: Date.now(),
|
|
264
|
+
deviceId: params.deviceId ?? this.deviceId,
|
|
265
|
+
});
|
|
266
|
+
await this.watermark.markUnavailable(treeId, indices, 'reserved');
|
|
112
267
|
const lease = {
|
|
113
268
|
leaseId: reservationId,
|
|
114
269
|
leaseToken: reservationId,
|
|
@@ -120,35 +275,43 @@ class LocalLeaseProvider {
|
|
|
120
275
|
leaseTTL: ttlMs,
|
|
121
276
|
};
|
|
122
277
|
await this.leaseStore.save(lease);
|
|
123
|
-
await this.journal.append({
|
|
124
|
-
treeId,
|
|
125
|
-
branchId: params.branchId ?? 'default',
|
|
126
|
-
wotsIndex: (0, watermark_js_1.flatIndex)(indices),
|
|
127
|
-
indices,
|
|
128
|
-
status: 'reserved',
|
|
129
|
-
reservationId,
|
|
130
|
-
payloadHash: params.payloadHash,
|
|
131
|
-
timestamp: Date.now(),
|
|
132
|
-
deviceId: params.deviceId ?? this.deviceId,
|
|
133
|
-
});
|
|
134
278
|
return { reservationId, indices, expiresAt };
|
|
135
279
|
}
|
|
280
|
+
withMutationLock(operation) {
|
|
281
|
+
const run = this.mutationTail.then(operation, operation);
|
|
282
|
+
this.mutationTail = run.then(() => undefined, () => undefined);
|
|
283
|
+
return run;
|
|
284
|
+
}
|
|
136
285
|
async commitKeyUse(reservationId, txId) {
|
|
286
|
+
return this.withMutationLock(() => this.commitKeyUseUnlocked(reservationId, txId));
|
|
287
|
+
}
|
|
288
|
+
async commitKeyUseUnlocked(reservationId, txId) {
|
|
137
289
|
if (!this._initialized)
|
|
138
290
|
await this.initialize();
|
|
291
|
+
await this.watermark.refresh();
|
|
139
292
|
const lease = this.leaseStore.get(reservationId);
|
|
140
293
|
if (!lease)
|
|
141
|
-
throw new
|
|
142
|
-
if (lease.status === 'finalized')
|
|
143
|
-
return;
|
|
294
|
+
throw new LeaseNotFoundError(reservationId);
|
|
144
295
|
const treeId = lease.treeId ?? 'default';
|
|
145
296
|
const indices = lease.indices;
|
|
146
|
-
|
|
147
|
-
|
|
297
|
+
const latest = this.journal.getByReservation(reservationId);
|
|
298
|
+
if (latest?.status === 'committed') {
|
|
299
|
+
if (latest.txId !== txId) {
|
|
300
|
+
throw new InvalidLeaseTransitionError(reservationId, 'committed', 'committed');
|
|
301
|
+
}
|
|
302
|
+
await this.watermark.markUnavailable(treeId, indices, 'committed');
|
|
303
|
+
if (lease.status !== 'finalized')
|
|
304
|
+
await this.leaseStore.updateStatus(reservationId, 'finalized');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (latest?.status === 'burned' || latest?.status === 'reserved-expired'
|
|
308
|
+
|| lease.status === 'cancelled' || lease.status === 'expired') {
|
|
309
|
+
throw new InvalidLeaseTransitionError(reservationId, latest?.status ?? lease.status, 'committed');
|
|
310
|
+
}
|
|
148
311
|
await this.journal.append({
|
|
149
312
|
treeId,
|
|
150
313
|
branchId: 'default',
|
|
151
|
-
wotsIndex:
|
|
314
|
+
wotsIndex: flatIndex(indices),
|
|
152
315
|
indices,
|
|
153
316
|
status: 'committed',
|
|
154
317
|
reservationId,
|
|
@@ -156,34 +319,71 @@ class LocalLeaseProvider {
|
|
|
156
319
|
timestamp: Date.now(),
|
|
157
320
|
deviceId: this.deviceId,
|
|
158
321
|
});
|
|
322
|
+
await this.watermark.markUnavailable(treeId, indices, 'committed');
|
|
323
|
+
await this.leaseStore.updateStatus(reservationId, 'finalized');
|
|
159
324
|
}
|
|
160
325
|
async burnReservation(reservationId, reason) {
|
|
326
|
+
return this.withMutationLock(() => this.burnReservationUnlocked(reservationId, reason));
|
|
327
|
+
}
|
|
328
|
+
async burnReservationUnlocked(reservationId, reason) {
|
|
161
329
|
if (!this._initialized)
|
|
162
330
|
await this.initialize();
|
|
331
|
+
await this.watermark.refresh();
|
|
163
332
|
const lease = this.leaseStore.get(reservationId);
|
|
164
333
|
if (!lease)
|
|
165
|
-
throw new
|
|
334
|
+
throw new LeaseNotFoundError(reservationId);
|
|
166
335
|
const treeId = lease.treeId ?? 'default';
|
|
167
336
|
const indices = lease.indices;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
337
|
+
const latest = this.journal.getByReservation(reservationId);
|
|
338
|
+
if (latest?.status === 'burned') {
|
|
339
|
+
await this.watermark.markUnavailable(treeId, indices, 'burned');
|
|
340
|
+
if (lease.status !== 'cancelled')
|
|
341
|
+
await this.leaseStore.updateStatus(reservationId, 'cancelled');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (latest?.status === 'committed' || latest?.status === 'reserved-expired'
|
|
345
|
+
|| lease.status === 'finalized' || lease.status === 'expired') {
|
|
346
|
+
throw new InvalidLeaseTransitionError(reservationId, latest?.status ?? lease.status, 'burned');
|
|
347
|
+
}
|
|
171
348
|
await this.journal.append({
|
|
172
349
|
treeId,
|
|
173
350
|
branchId: 'default',
|
|
174
|
-
wotsIndex:
|
|
351
|
+
wotsIndex: flatIndex(indices),
|
|
175
352
|
indices,
|
|
176
353
|
status: 'burned',
|
|
177
354
|
reservationId,
|
|
178
355
|
timestamp: Date.now(),
|
|
179
356
|
deviceId: this.deviceId,
|
|
180
357
|
});
|
|
358
|
+
await this.watermark.markUnavailable(treeId, indices, 'burned');
|
|
359
|
+
await this.leaseStore.updateStatus(reservationId, 'cancelled');
|
|
360
|
+
this.logger.warn(`[LocalLeaseProvider] Burning reservation ${reservationId}: ${reason}`);
|
|
181
361
|
}
|
|
182
362
|
async getLocalWatermark(treeId) {
|
|
183
363
|
if (!this._initialized)
|
|
184
364
|
await this.initialize();
|
|
185
365
|
return this.watermark.getLocalWatermark(treeId);
|
|
186
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Export a portable watermark seed for restore (RFC-013 §9).
|
|
369
|
+
*/
|
|
370
|
+
async exportWatermark(treeId) {
|
|
371
|
+
if (!this._initialized)
|
|
372
|
+
await this.initialize();
|
|
373
|
+
return this.watermark.exportSeed(treeId);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Seed the local watermark from an exported/operator/on-chain snapshot.
|
|
377
|
+
* Forward-only by default; refuses to start below the true high-water mark.
|
|
378
|
+
*/
|
|
379
|
+
async seedWatermark(seed, options) {
|
|
380
|
+
return this.withMutationLock(async () => {
|
|
381
|
+
if (!this._initialized)
|
|
382
|
+
await this.initialize();
|
|
383
|
+
await this.watermark.refresh();
|
|
384
|
+
await this.watermark.seed(seed, options);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
187
387
|
/** List all tree IDs known to the local watermark store. */
|
|
188
388
|
listTrees() {
|
|
189
389
|
return Object.keys(this.watermark.getRawState().trees);
|
|
@@ -202,8 +402,8 @@ class LocalLeaseProvider {
|
|
|
202
402
|
if (!this._initialized)
|
|
203
403
|
await this.initialize();
|
|
204
404
|
const local = this.watermark.getLocalWatermark(treeId);
|
|
205
|
-
const localFlat =
|
|
206
|
-
const remoteFlat =
|
|
405
|
+
const localFlat = flatIndex({ addressIndex: local.addressCursor, l1: local.l1Cursor, l2: local.l2Cursor });
|
|
406
|
+
const remoteFlat = flatIndex({ addressIndex: remote.addressCursor, l1: remote.l1Cursor, l2: remote.l2Cursor });
|
|
207
407
|
if (remoteFlat <= localFlat)
|
|
208
408
|
return false;
|
|
209
409
|
await this.watermark.save(treeId, {
|
|
@@ -234,4 +434,3 @@ class LocalLeaseProvider {
|
|
|
234
434
|
return { start: slot * 8, end: slot * 8 + 7 };
|
|
235
435
|
}
|
|
236
436
|
}
|
|
237
|
-
exports.LocalLeaseProvider = LocalLeaseProvider;
|
package/dist/onchain.d.ts
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
import type { WotsLeaseProvider, ReserveParams, LeaseReservation, LeaseCertificate, LocalWatermark, SyncResult, OnchainWatermarkProviderConfig } from './types.js';
|
|
24
24
|
export declare class OnchainWatermarkProvider implements WotsLeaseProvider {
|
|
25
25
|
private readonly chain;
|
|
26
|
-
private readonly
|
|
26
|
+
private readonly initialWatermarkCoinId;
|
|
27
|
+
private currentCoinId;
|
|
27
28
|
private readonly watermarkAddress;
|
|
28
29
|
private readonly tokenId;
|
|
29
30
|
private readonly amount;
|
|
@@ -31,6 +32,7 @@ export declare class OnchainWatermarkProvider implements WotsLeaseProvider {
|
|
|
31
32
|
private readonly signer;
|
|
32
33
|
private readonly statePort;
|
|
33
34
|
private readonly minBlocksBetweenPublishes;
|
|
35
|
+
private readonly storage?;
|
|
34
36
|
private _initialized;
|
|
35
37
|
private _lastPublishBlock;
|
|
36
38
|
constructor(config: OnchainWatermarkProviderConfig);
|