@totemsdk/wots-lease 0.1.2 → 0.1.4

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/dist/stubs.js CHANGED
@@ -1,92 +1,156 @@
1
+ "use strict";
1
2
  /**
2
- * Stub implementations for Layers 3–5.
3
- * These throw descriptive errors until the full implementations land.
3
+ * Implementations for Layers 3–5 of the WotsLeaseProvider hierarchy.
4
+ *
5
+ * Layer 3 (PersonalLeaseNodeProvider) — HTTP client for a personal @totemsdk/lookup-node
6
+ * that has lease coordination enabled ({ lease: { enabled: true } }).
7
+ * The node exposes REST endpoints under /v1/lease/*.
8
+ *
9
+ * Layers 4–5 (P2PQuorumLeaseProvider, OnchainWatermarkProvider) — require
10
+ * distributed infrastructure not yet available; they throw descriptive errors.
4
11
  */
5
- import { PersonalLeaseNodeNotConfiguredError, P2PQuorumNotImplementedError, OnchainWatermarkNotImplementedError, } from './errors.js';
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.OnchainWatermarkProvider = exports.P2PQuorumLeaseProvider = exports.PersonalLeaseNodeProvider = void 0;
14
+ const errors_js_1 = require("./errors.js");
6
15
  /**
7
- * Layer 3 — personal p2p lease node.
16
+ * Layer 3 — personal lookup-node lease coordinator.
17
+ *
18
+ * Calls the HTTP REST API exposed by a running @totemsdk/lookup-node that has
19
+ * lease coordination enabled. All write operations are forwarded to the node's
20
+ * LeaseCoordinator so the node acts as the source-of-truth watermark journal
21
+ * for high-value transactions.
22
+ *
23
+ * Wrap with HybridLeaseProvider so local reservations always succeed even when
24
+ * the personal node is temporarily unreachable:
8
25
  *
9
- * Verifies ed25519 certs issued by a configured node public key.
10
- * Reserve/commit throws until the node is reachable — callers should
11
- * wrap with HybridLeaseProvider using a LocalLeaseProvider as the local layer.
26
+ * ```ts
27
+ * const provider = new HybridLeaseProvider({
28
+ * local: new LocalLeaseProvider(storage),
29
+ * node: new PersonalLeaseNodeProvider({ nodeUrl, nodePubkey }),
30
+ * threshold: 10, // escalate to node for txns >= 10 MIN
31
+ * });
32
+ * ```
12
33
  */
13
- export class PersonalLeaseNodeProvider {
34
+ class PersonalLeaseNodeProvider {
14
35
  constructor(config) {
15
- this.config = config;
16
- }
17
- reserveKeyUse(_params) {
18
- throw new PersonalLeaseNodeNotConfiguredError();
19
- }
20
- commitKeyUse(_reservationId, _txId) {
21
- throw new PersonalLeaseNodeNotConfiguredError();
22
- }
23
- burnReservation(_reservationId, _reason) {
24
- throw new PersonalLeaseNodeNotConfiguredError();
25
- }
26
- getLocalWatermark(_treeId) {
27
- throw new PersonalLeaseNodeNotConfiguredError();
28
- }
29
- publishWatermark(_treeId) {
30
- throw new PersonalLeaseNodeNotConfiguredError();
31
- }
32
- syncLeaseJournal() {
33
- throw new PersonalLeaseNodeNotConfiguredError();
36
+ this.baseUrl = config.nodeUrl.replace(/\/$/, '');
37
+ this.nodePubkey = config.nodePubkey;
38
+ this.authToken = config.authToken;
39
+ }
40
+ headers() {
41
+ const h = { 'Content-Type': 'application/json' };
42
+ if (this.authToken)
43
+ h['Authorization'] = `Bearer ${this.authToken}`;
44
+ return h;
45
+ }
46
+ async post(path, body) {
47
+ const res = await fetch(`${this.baseUrl}${path}`, {
48
+ method: 'POST',
49
+ headers: this.headers(),
50
+ body: JSON.stringify(body),
51
+ });
52
+ if (!res.ok) {
53
+ const text = await res.text().catch(() => '');
54
+ throw new Error(`PersonalLeaseNodeProvider: ${path} HTTP ${res.status} — ${text.slice(0, 200)}`);
55
+ }
56
+ return res.json();
57
+ }
58
+ async get(path) {
59
+ const res = await fetch(`${this.baseUrl}${path}`, { headers: this.headers() });
60
+ if (!res.ok) {
61
+ const text = await res.text().catch(() => '');
62
+ throw new Error(`PersonalLeaseNodeProvider: GET ${path} HTTP ${res.status} — ${text.slice(0, 200)}`);
63
+ }
64
+ return res.json();
65
+ }
66
+ async reserveKeyUse(params) {
67
+ const data = await this.post('/v1/lease/reserve', {
68
+ treeId: params.treeId,
69
+ branchId: params.branchId,
70
+ deviceId: params.deviceId,
71
+ ttlMs: params.ttlMs,
72
+ payloadHash: params.payloadHash,
73
+ purpose: params.purpose,
74
+ });
75
+ return { ...data.reservation, certificate: data.certificate };
76
+ }
77
+ async commitKeyUse(reservationId, txId) {
78
+ const data = await this.post('/v1/lease/commit', { reservationId, txId });
79
+ if (!data.ok)
80
+ throw new Error(`PersonalLeaseNodeProvider: commit failed — ${data.error ?? 'unknown'}`);
81
+ }
82
+ async burnReservation(reservationId, reason) {
83
+ const data = await this.post('/v1/lease/burn', { reservationId, reason });
84
+ if (!data.ok)
85
+ throw new Error(`PersonalLeaseNodeProvider: burn failed — ${data.error ?? 'unknown'}`);
86
+ }
87
+ async getLocalWatermark(treeId) {
88
+ return this.get(`/v1/lease/watermark/${encodeURIComponent(treeId)}`);
89
+ }
90
+ async publishWatermark(treeId) {
91
+ await this.post(`/v1/lease/watermark/${encodeURIComponent(treeId)}/publish`, {});
92
+ }
93
+ async syncLeaseJournal() {
94
+ return this.get('/v1/lease/journal/sync');
34
95
  }
35
96
  async verifyLeaseCertificate(cert) {
36
97
  if (!cert)
37
98
  return false;
38
- if (cert.issuedBy !== this.config.nodePubkey)
99
+ if (cert.issuedBy !== this.nodePubkey)
39
100
  return false;
40
101
  if (cert.expiresAt <= Date.now())
41
102
  return false;
42
103
  return true;
43
104
  }
44
105
  }
106
+ exports.PersonalLeaseNodeProvider = PersonalLeaseNodeProvider;
45
107
  /** Layer 4 — p2p replicated lease witnesses. */
46
- export class P2PQuorumLeaseProvider {
108
+ class P2PQuorumLeaseProvider {
47
109
  reserveKeyUse(_params) {
48
- throw new P2PQuorumNotImplementedError();
110
+ throw new errors_js_1.P2PQuorumNotImplementedError();
49
111
  }
50
112
  commitKeyUse(_reservationId, _txId) {
51
- throw new P2PQuorumNotImplementedError();
113
+ throw new errors_js_1.P2PQuorumNotImplementedError();
52
114
  }
53
115
  burnReservation(_reservationId, _reason) {
54
- throw new P2PQuorumNotImplementedError();
116
+ throw new errors_js_1.P2PQuorumNotImplementedError();
55
117
  }
56
118
  getLocalWatermark(_treeId) {
57
- throw new P2PQuorumNotImplementedError();
119
+ throw new errors_js_1.P2PQuorumNotImplementedError();
58
120
  }
59
121
  publishWatermark(_treeId) {
60
- throw new P2PQuorumNotImplementedError();
122
+ throw new errors_js_1.P2PQuorumNotImplementedError();
61
123
  }
62
124
  syncLeaseJournal() {
63
- throw new P2PQuorumNotImplementedError();
125
+ throw new errors_js_1.P2PQuorumNotImplementedError();
64
126
  }
65
127
  verifyLeaseCertificate(_cert) {
66
- throw new P2PQuorumNotImplementedError();
128
+ throw new errors_js_1.P2PQuorumNotImplementedError();
67
129
  }
68
130
  }
131
+ exports.P2PQuorumLeaseProvider = P2PQuorumLeaseProvider;
69
132
  /** Layer 5 — on-chain watermark coin. */
70
- export class OnchainWatermarkProvider {
133
+ class OnchainWatermarkProvider {
71
134
  reserveKeyUse(_params) {
72
- throw new OnchainWatermarkNotImplementedError();
135
+ throw new errors_js_1.OnchainWatermarkNotImplementedError();
73
136
  }
74
137
  commitKeyUse(_reservationId, _txId) {
75
- throw new OnchainWatermarkNotImplementedError();
138
+ throw new errors_js_1.OnchainWatermarkNotImplementedError();
76
139
  }
77
140
  burnReservation(_reservationId, _reason) {
78
- throw new OnchainWatermarkNotImplementedError();
141
+ throw new errors_js_1.OnchainWatermarkNotImplementedError();
79
142
  }
80
143
  getLocalWatermark(_treeId) {
81
- throw new OnchainWatermarkNotImplementedError();
144
+ throw new errors_js_1.OnchainWatermarkNotImplementedError();
82
145
  }
83
146
  publishWatermark(_treeId) {
84
- throw new OnchainWatermarkNotImplementedError();
147
+ throw new errors_js_1.OnchainWatermarkNotImplementedError();
85
148
  }
86
149
  syncLeaseJournal() {
87
- throw new OnchainWatermarkNotImplementedError();
150
+ throw new errors_js_1.OnchainWatermarkNotImplementedError();
88
151
  }
89
152
  verifyLeaseCertificate(_cert) {
90
- throw new OnchainWatermarkNotImplementedError();
153
+ throw new errors_js_1.OnchainWatermarkNotImplementedError();
91
154
  }
92
155
  }
156
+ exports.OnchainWatermarkProvider = OnchainWatermarkProvider;
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @totemsdk/wots-lease — canonical v3 watermark types and WotsLeaseProvider interface
3
3
  */
4
- export type UnavailableReason = 'committed' | 'burned' | 'reserved-expired';
4
+ export type UnavailableReason = 'reserved' | 'committed' | 'burned' | 'reserved-expired';
5
5
  export interface SigningIndices {
6
6
  addressIndex: number;
7
7
  l1: number;
@@ -86,10 +86,13 @@ export interface JournalEntry {
86
86
  wotsIndex: number;
87
87
  indices: SigningIndices;
88
88
  status: 'reserved' | 'committed' | 'burned' | 'reserved-expired';
89
+ reservationId?: string;
89
90
  payloadHash?: string;
90
91
  txId?: string;
91
92
  timestamp: number;
92
93
  deviceId: string;
94
+ previousHash?: string;
95
+ hash?: string;
93
96
  }
94
97
  export interface DeviceKeyRange {
95
98
  deviceId: string;
package/dist/types.js CHANGED
@@ -1,4 +1,5 @@
1
+ "use strict";
1
2
  /**
2
3
  * @totemsdk/wots-lease — canonical v3 watermark types and WotsLeaseProvider interface
3
4
  */
4
- export {};
5
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/watermark.js CHANGED
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  /**
2
3
  * WotsWatermarkStore — canonical v3 implementation
3
4
  *
@@ -6,16 +7,20 @@
6
7
  * - Enforces monotonicity: unavailable map can only grow, cursors can only advance.
7
8
  * - Storage key unchanged ('totem_wots_watermark') to avoid storage churn.
8
9
  */
9
- import { NoopLogger } from '@totemsdk/core';
10
- import { WatermarkMonotonicityError, WatermarkExhaustedError } from './errors.js';
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.WotsWatermarkStore = void 0;
12
+ exports.flatIndex = flatIndex;
13
+ exports.fromFlatIndex = fromFlatIndex;
14
+ const core_1 = require("@totemsdk/core");
15
+ const errors_js_1 = require("./errors.js");
11
16
  const STORAGE_KEY = 'totem_wots_watermark';
12
17
  const MAX_L = 64;
13
18
  const CAPACITY_PER_TREE = MAX_L * MAX_L * MAX_L;
14
19
  const DEFAULT_TREE = 'default';
15
- export function flatIndex(idx) {
20
+ function flatIndex(idx) {
16
21
  return idx.addressIndex * MAX_L * MAX_L + idx.l1 * MAX_L + idx.l2;
17
22
  }
18
- export function fromFlatIndex(flat) {
23
+ function fromFlatIndex(flat) {
19
24
  const addressIndex = Math.floor(flat / (MAX_L * MAX_L));
20
25
  const rem = flat % (MAX_L * MAX_L);
21
26
  const l1 = Math.floor(rem / MAX_L);
@@ -82,8 +87,8 @@ function migrateToV3(raw) {
82
87
  tree.l2Cursor = v1.next_l2 ?? 0;
83
88
  return { version: 3, trees: { [DEFAULT_TREE]: tree } };
84
89
  }
85
- export class WotsWatermarkStore {
86
- constructor(storage, logger = new NoopLogger()) {
90
+ class WotsWatermarkStore {
91
+ constructor(storage, logger = new core_1.NoopLogger()) {
87
92
  this.storage = storage;
88
93
  this.state = null;
89
94
  this._initialized = false;
@@ -132,7 +137,7 @@ export class WotsWatermarkStore {
132
137
  cur = nxt;
133
138
  attempts++;
134
139
  }
135
- throw new WatermarkExhaustedError(treeId);
140
+ throw new errors_js_1.WatermarkExhaustedError(treeId);
136
141
  }
137
142
  async markUnavailable(treeId, indices, reason) {
138
143
  const tree = this.getOrCreateTree(treeId);
@@ -164,12 +169,12 @@ export class WotsWatermarkStore {
164
169
  const existingFlat = flatIndex({ addressIndex: existing.addressCursor, l1: existing.l1Cursor, l2: existing.l2Cursor });
165
170
  const newFlat = flatIndex({ addressIndex: merged.addressCursor, l1: merged.l1Cursor, l2: merged.l2Cursor });
166
171
  if (newFlat < existingFlat) {
167
- throw new WatermarkMonotonicityError(`Cannot decrease watermark cursor for ${treeId}: ${existingFlat} → ${newFlat}`);
172
+ throw new errors_js_1.WatermarkMonotonicityError(`Cannot decrease watermark cursor for ${treeId}: ${existingFlat} → ${newFlat}`);
168
173
  }
169
174
  const existingUnavailCount = Object.keys(existing.unavailable).length;
170
175
  const newUnavailCount = Object.keys(merged.unavailable).length;
171
176
  if (newUnavailCount < existingUnavailCount) {
172
- throw new WatermarkMonotonicityError(`Cannot shrink unavailable map for ${treeId}: ${existingUnavailCount} → ${newUnavailCount}`);
177
+ throw new errors_js_1.WatermarkMonotonicityError(`Cannot shrink unavailable map for ${treeId}: ${existingUnavailCount} → ${newUnavailCount}`);
173
178
  }
174
179
  s.trees[treeId] = merged;
175
180
  await this.persist();
@@ -196,3 +201,4 @@ export class WotsWatermarkStore {
196
201
  await this.storage.remove(STORAGE_KEY);
197
202
  }
198
203
  }
204
+ exports.WotsWatermarkStore = WotsWatermarkStore;
package/package.json CHANGED
@@ -1,31 +1,37 @@
1
1
  {
2
2
  "name": "@totemsdk/wots-lease",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "WOTS key-use coordination — canonical v3 watermark, provider-based lease safety layers",
5
- "type": "module",
6
5
  "main": "dist/index.js",
7
6
  "types": "dist/index.d.ts",
8
7
  "exports": {
9
8
  ".": {
10
9
  "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js",
11
11
  "import": "./dist/index.js"
12
12
  }
13
13
  },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "prepare": "tsc",
17
+ "clean": "rm -rf dist",
18
+ "test": "jest --passWithNoTests"
19
+ },
14
20
  "files": [
15
21
  "dist",
16
- "src"
22
+ "README.md",
23
+ "LICENSE"
17
24
  ],
18
25
  "dependencies": {
19
- "@totemsdk/core": "1.0.9",
20
- "@totemsdk/lookup-protocol": "0.1.2"
26
+ "@totemsdk/core": "^1.1.0",
27
+ "@totemsdk/lookup-protocol": "^0.1.3"
21
28
  },
22
29
  "devDependencies": {
23
- "@noble/hashes": "^1.3.0",
24
- "@types/jest": "^29.0.0",
30
+ "@types/jest": "^30.0.0",
25
31
  "@types/node": "^20.0.0",
26
- "jest": "^29.0.0",
32
+ "jest": "^30.4.2",
27
33
  "ts-jest": "^29.0.0",
28
- "typescript": "^5.0.0"
34
+ "typescript": "^7.0.2"
29
35
  },
30
36
  "engines": {
31
37
  "node": ">=18.0.0"
@@ -34,9 +40,24 @@
34
40
  "access": "public"
35
41
  },
36
42
  "license": "MIT",
37
- "scripts": {
38
- "build": "tsc",
39
- "clean": "rm -rf dist",
40
- "test": "jest --passWithNoTests"
41
- }
42
- }
43
+ "author": "Totem SDK",
44
+ "homepage": "https://totem.ing",
45
+ "bugs": {
46
+ "url": "https://github.com/totem-sdk/totem-sdk/issues"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/totem-sdk/totem-sdk.git",
51
+ "directory": "packages/totem-sdk/packages/wots-lease"
52
+ },
53
+ "keywords": [
54
+ "totem",
55
+ "totemsdk",
56
+ "minima",
57
+ "blockchain",
58
+ "quantum-resistant",
59
+ "wots",
60
+ "kissvm",
61
+ "utxo"
62
+ ]
63
+ }
@@ -1,200 +0,0 @@
1
- import { WotsWatermarkStore, flatIndex, fromFlatIndex } from '../watermark';
2
- import { WatermarkMonotonicityError, WatermarkExhaustedError } from '../errors';
3
- import { LocalLeaseProvider } from '../local';
4
- import { allocateDeviceRange } from '../device';
5
- import type { StorageAdapter } from '@totemsdk/core';
6
-
7
- function makeMemStorage(): StorageAdapter {
8
- const store = new Map<string, unknown>();
9
- return {
10
- get: async <T>(key: string) => (store.has(key) ? (store.get(key) as T) : null),
11
- set: async <T>(key: string, value: T) => { store.set(key, value); },
12
- remove: async (key: string) => { const had = store.has(key); store.delete(key); return had; },
13
- clear: async () => { store.clear(); },
14
- keys: async () => Array.from(store.keys()),
15
- has: async (key: string) => store.has(key),
16
- };
17
- }
18
-
19
- describe('flatIndex / fromFlatIndex', () => {
20
- it('round-trips', () => {
21
- const cases = [
22
- { addressIndex: 0, l1: 0, l2: 0 },
23
- { addressIndex: 0, l1: 0, l2: 63 },
24
- { addressIndex: 0, l1: 63, l2: 63 },
25
- { addressIndex: 63, l1: 63, l2: 63 },
26
- { addressIndex: 5, l1: 12, l2: 37 },
27
- ];
28
- for (const idx of cases) {
29
- expect(fromFlatIndex(flatIndex(idx))).toEqual(idx);
30
- }
31
- });
32
-
33
- it('total capacity is 64^3 = 262144', () => {
34
- expect(flatIndex({ addressIndex: 63, l1: 63, l2: 63 })).toBe(262143);
35
- });
36
- });
37
-
38
- describe('WotsWatermarkStore', () => {
39
- it('initializes empty on first use', async () => {
40
- const store = new WotsWatermarkStore(makeMemStorage());
41
- await store.initialize();
42
- const wm = store.getLocalWatermark('tree1');
43
- expect(wm.addressCursor).toBe(0);
44
- expect(wm.l1Cursor).toBe(0);
45
- expect(wm.l2Cursor).toBe(0);
46
- expect(wm.unavailableCount).toBe(0);
47
- expect(wm.capacity).toBe(262144);
48
- });
49
-
50
- it('getNextIndices returns cursor position', async () => {
51
- const store = new WotsWatermarkStore(makeMemStorage());
52
- await store.initialize();
53
- const idx = store.getNextIndices('tree1');
54
- expect(idx).toEqual({ addressIndex: 0, l1: 0, l2: 0 });
55
- });
56
-
57
- it('markUnavailable advances cursor and records reason', async () => {
58
- const store = new WotsWatermarkStore(makeMemStorage());
59
- await store.initialize();
60
- await store.markUnavailable('tree1', { addressIndex: 0, l1: 0, l2: 0 }, 'committed');
61
- const next = store.getNextIndices('tree1');
62
- expect(next).toEqual({ addressIndex: 0, l1: 0, l2: 1 });
63
- expect(store.isUnavailable('tree1', { addressIndex: 0, l1: 0, l2: 0 })).toBe(true);
64
- expect(store.isUnavailable('tree1', { addressIndex: 0, l1: 0, l2: 1 })).toBe(false);
65
- });
66
-
67
- it('migrates v1 flat state automatically', async () => {
68
- const storage = makeMemStorage();
69
- await storage.set('totem_wots_watermark', {
70
- next_addressIndex: 0,
71
- next_l1: 0,
72
- next_l2: 3,
73
- usedIndices: [[0, 0, 0], [0, 0, 1], [0, 0, 2]],
74
- });
75
- const store = new WotsWatermarkStore(storage);
76
- await store.initialize();
77
- const wm = store.getLocalWatermark('default');
78
- expect(wm.unavailableCount).toBe(3);
79
- });
80
-
81
- it('migrates v2 per-address state automatically', async () => {
82
- const storage = makeMemStorage();
83
- await storage.set('totem_wots_watermark', {
84
- version: 2,
85
- addresses: {
86
- 0: { next_l1: 0, next_l2: 2, usedIndices: [[0, 0], [0, 1]] },
87
- },
88
- });
89
- const store = new WotsWatermarkStore(storage);
90
- await store.initialize();
91
- const wm = store.getLocalWatermark('default');
92
- expect(wm.unavailableCount).toBe(2);
93
- });
94
-
95
- it('throws WatermarkMonotonicityError if cursor decreases', async () => {
96
- const store = new WotsWatermarkStore(makeMemStorage());
97
- await store.initialize();
98
- await store.markUnavailable('t', { addressIndex: 0, l1: 0, l2: 5 }, 'committed');
99
- await expect(
100
- store.save('t', { addressCursor: 0, l1Cursor: 0, l2Cursor: 0, unavailable: {} }),
101
- ).rejects.toThrow(WatermarkMonotonicityError);
102
- });
103
-
104
- it('throws WatermarkMonotonicityError if unavailable map shrinks', async () => {
105
- const store = new WotsWatermarkStore(makeMemStorage());
106
- await store.initialize();
107
- await store.markUnavailable('t', { addressIndex: 0, l1: 0, l2: 0 }, 'committed');
108
- await expect(
109
- store.save('t', { addressCursor: 0, l1Cursor: 0, l2Cursor: 1, unavailable: {} }),
110
- ).rejects.toThrow(WatermarkMonotonicityError);
111
- });
112
- });
113
-
114
- describe('LocalLeaseProvider', () => {
115
- it('reserves and commits successfully', async () => {
116
- const provider = new LocalLeaseProvider(makeMemStorage());
117
- await provider.initialize();
118
-
119
- const reservation = await provider.reserveKeyUse({ treeId: 'wallet' });
120
- expect(reservation.reservationId).toBeTruthy();
121
- expect(reservation.indices).toEqual({ addressIndex: 0, l1: 0, l2: 0 });
122
-
123
- await provider.commitKeyUse(reservation.reservationId, '0xTXID');
124
-
125
- const wm = await provider.getLocalWatermark('wallet');
126
- expect(wm.unavailableCount).toBe(1);
127
- });
128
-
129
- it('burns reservation — marks index permanently unavailable', async () => {
130
- const storage = makeMemStorage();
131
- const provider = new LocalLeaseProvider(storage);
132
- await provider.initialize();
133
-
134
- const reservation = await provider.reserveKeyUse({ treeId: 'wallet' });
135
- await provider.burnReservation(reservation.reservationId, 'test burn');
136
-
137
- const store = new WotsWatermarkStore(storage);
138
- await store.initialize();
139
- expect(store.isUnavailable('wallet', reservation.indices)).toBe(true);
140
- });
141
-
142
- it('two consecutive reserves yield different indices', async () => {
143
- const provider = new LocalLeaseProvider(makeMemStorage());
144
- await provider.initialize();
145
-
146
- const r1 = await provider.reserveKeyUse({ treeId: 'wallet' });
147
- await provider.commitKeyUse(r1.reservationId, '0xA');
148
- const r2 = await provider.reserveKeyUse({ treeId: 'wallet' });
149
-
150
- expect(flatIndex(r2.indices)).toBeGreaterThan(flatIndex(r1.indices));
151
- });
152
-
153
- it('syncLeaseJournal returns synced:true (local no-op)', async () => {
154
- const provider = new LocalLeaseProvider(makeMemStorage());
155
- await provider.initialize();
156
- const result = await provider.syncLeaseJournal();
157
- expect(result.synced).toBe(true);
158
- });
159
-
160
- it('verifyLeaseCertificate returns true when cert is undefined', async () => {
161
- const provider = new LocalLeaseProvider(makeMemStorage());
162
- await provider.initialize();
163
- expect(await provider.verifyLeaseCertificate(undefined)).toBe(true);
164
- });
165
-
166
- it('verifyLeaseCertificate returns false for external cert', async () => {
167
- const provider = new LocalLeaseProvider(makeMemStorage());
168
- await provider.initialize();
169
- const cert = {
170
- reservationId: 'r1',
171
- treeId: 'wallet',
172
- indices: { addressIndex: 0, l1: 0, l2: 0 },
173
- issuedBy: 'external-key',
174
- issuedAt: Date.now(),
175
- expiresAt: Date.now() + 120000,
176
- signature: 'sig',
177
- };
178
- expect(await provider.verifyLeaseCertificate(cert)).toBe(false);
179
- });
180
- });
181
-
182
- describe('allocateDeviceRange', () => {
183
- it('slot 0 gets addressIndex 0–7', () => {
184
- const range = allocateDeviceRange({ deviceSlot: 0, deviceId: 'phone' });
185
- expect(range.startAddressIndex).toBe(0);
186
- expect(range.endAddressIndex).toBe(7);
187
- expect(range.addressCount).toBe(8);
188
- });
189
-
190
- it('slot 3 gets addressIndex 24–31', () => {
191
- const range = allocateDeviceRange({ deviceSlot: 3, deviceId: 'pear' });
192
- expect(range.startAddressIndex).toBe(24);
193
- expect(range.endAddressIndex).toBe(31);
194
- });
195
-
196
- it('throws on out-of-range slot', () => {
197
- expect(() => allocateDeviceRange({ deviceSlot: 8, deviceId: 'x' })).toThrow(RangeError);
198
- expect(() => allocateDeviceRange({ deviceSlot: -1, deviceId: 'x' })).toThrow(RangeError);
199
- });
200
- });