@totemsdk/wots-lease 0.1.1 → 0.1.3
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 +21 -0
- package/README.md +97 -0
- package/dist/axia.js +21 -17
- package/dist/device.js +6 -2
- package/dist/errors.js +17 -7
- package/dist/hybrid.js +5 -1
- package/dist/index.js +30 -8
- package/dist/journal.js +7 -3
- package/dist/local.js +21 -17
- package/dist/stubs.js +30 -24
- package/dist/types.js +2 -1
- package/dist/watermark.js +15 -9
- package/package.json +27 -6
- package/src/__tests__/watermark.test.ts +0 -200
- package/src/axia.ts +0 -169
- package/src/device.ts +0 -40
- package/src/errors.ts +0 -57
- package/src/hybrid.ts +0 -98
- package/src/index.ts +0 -51
- package/src/journal.ts +0 -52
- package/src/local.ts +0 -196
- package/src/stubs.ts +0 -106
- package/src/types.ts +0 -119
- package/src/watermark.ts +0 -234
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
20
|
+
function flatIndex(idx) {
|
|
16
21
|
return idx.addressIndex * MAX_L * MAX_L + idx.l1 * MAX_L + idx.l2;
|
|
17
22
|
}
|
|
18
|
-
|
|
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
|
-
|
|
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,26 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totemsdk/wots-lease",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
|
-
"
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
17
18
|
],
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"@totemsdk/
|
|
20
|
-
"@totemsdk/
|
|
20
|
+
"@totemsdk/lookup-protocol": "0.1.3",
|
|
21
|
+
"@totemsdk/core": "1.0.10"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
|
-
"@noble/hashes": "^
|
|
24
|
+
"@noble/hashes": "^2.2.0",
|
|
24
25
|
"@types/jest": "^29.0.0",
|
|
25
26
|
"@types/node": "^20.0.0",
|
|
26
27
|
"jest": "^29.0.0",
|
|
@@ -34,6 +35,26 @@
|
|
|
34
35
|
"access": "public"
|
|
35
36
|
},
|
|
36
37
|
"license": "MIT",
|
|
38
|
+
"author": "Totem SDK",
|
|
39
|
+
"homepage": "https://totemsdk.com",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/MrGheek/axia-totem/issues"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/MrGheek/axia-totem.git",
|
|
46
|
+
"directory": "packages/totem-sdk/packages/wots-lease"
|
|
47
|
+
},
|
|
48
|
+
"keywords": [
|
|
49
|
+
"totem",
|
|
50
|
+
"totemsdk",
|
|
51
|
+
"minima",
|
|
52
|
+
"blockchain",
|
|
53
|
+
"quantum-resistant",
|
|
54
|
+
"wots",
|
|
55
|
+
"kissvm",
|
|
56
|
+
"utxo"
|
|
57
|
+
],
|
|
37
58
|
"scripts": {
|
|
38
59
|
"build": "tsc",
|
|
39
60
|
"clean": "rm -rf dist",
|
|
@@ -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
|
-
});
|
package/src/axia.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AxiaLeaseProvider — wraps the Axia API's /v1/wots-hardened endpoints.
|
|
3
|
-
*
|
|
4
|
-
* Callers do NOT need to call /finalize separately — commitKeyUse() does it.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { StorageAdapter, LoggerAdapter } from '@totemsdk/core';
|
|
8
|
-
import { NoopLogger, LeaseStore, type StoredLease } from '@totemsdk/core';
|
|
9
|
-
import { prepareLease, finalizeLease } from '@totemsdk/core';
|
|
10
|
-
import type {
|
|
11
|
-
WotsLeaseProvider,
|
|
12
|
-
ReserveParams,
|
|
13
|
-
LeaseReservation,
|
|
14
|
-
LeaseCertificate,
|
|
15
|
-
LocalWatermark,
|
|
16
|
-
SyncResult,
|
|
17
|
-
SigningIndices,
|
|
18
|
-
} from './types.js';
|
|
19
|
-
import { WotsWatermarkStore, flatIndex } from './watermark.js';
|
|
20
|
-
import { LeaseJournal } from './journal.js';
|
|
21
|
-
import { LeaseNotFoundError } from './errors.js';
|
|
22
|
-
|
|
23
|
-
export interface AxiaLeaseProviderConfig {
|
|
24
|
-
apiUrl: string;
|
|
25
|
-
apiKey: string;
|
|
26
|
-
rootPublicKey: string;
|
|
27
|
-
storage: StorageAdapter;
|
|
28
|
-
logger?: LoggerAdapter;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export class AxiaLeaseProvider implements WotsLeaseProvider {
|
|
32
|
-
private readonly watermark: WotsWatermarkStore;
|
|
33
|
-
private readonly leaseStore: LeaseStore;
|
|
34
|
-
private readonly journal: LeaseJournal;
|
|
35
|
-
private readonly apiUrl: string;
|
|
36
|
-
private readonly apiKey: string;
|
|
37
|
-
private readonly rootPublicKey: string;
|
|
38
|
-
private readonly logger: LoggerAdapter;
|
|
39
|
-
private _initialized = false;
|
|
40
|
-
|
|
41
|
-
constructor(config: AxiaLeaseProviderConfig) {
|
|
42
|
-
this.apiUrl = config.apiUrl;
|
|
43
|
-
this.apiKey = config.apiKey;
|
|
44
|
-
this.rootPublicKey = config.rootPublicKey;
|
|
45
|
-
this.logger = config.logger ?? new NoopLogger();
|
|
46
|
-
this.watermark = new WotsWatermarkStore(config.storage, this.logger);
|
|
47
|
-
this.leaseStore = new LeaseStore(config.storage, this.logger);
|
|
48
|
-
this.journal = new LeaseJournal(config.storage, this.logger);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async initialize(): Promise<void> {
|
|
52
|
-
if (this._initialized) return;
|
|
53
|
-
await this.watermark.initialize();
|
|
54
|
-
await this.leaseStore.initialize();
|
|
55
|
-
await this.journal.initialize();
|
|
56
|
-
this._initialized = true;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async reserveKeyUse(params: ReserveParams): Promise<LeaseReservation> {
|
|
60
|
-
if (!this._initialized) await this.initialize();
|
|
61
|
-
|
|
62
|
-
const txId = params.payloadHash ?? `tx-${Date.now()}`;
|
|
63
|
-
const resp = await prepareLease(this.apiUrl, this.apiKey, {
|
|
64
|
-
txId,
|
|
65
|
-
rootPublicKey: this.rootPublicKey,
|
|
66
|
-
to: params.treeId,
|
|
67
|
-
amount: params.valueHint ?? '0',
|
|
68
|
-
ttlMs: params.ttlMs ?? 120_000,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const indices: SigningIndices = resp.lease;
|
|
72
|
-
const expiresAt = Date.now() + (params.ttlMs ?? 120_000);
|
|
73
|
-
const reservationId = resp.txId;
|
|
74
|
-
|
|
75
|
-
const lease: StoredLease = {
|
|
76
|
-
leaseId: reservationId,
|
|
77
|
-
leaseToken: resp.leaseToken,
|
|
78
|
-
indices,
|
|
79
|
-
expiresAt,
|
|
80
|
-
status: 'active',
|
|
81
|
-
createdAt: Date.now(),
|
|
82
|
-
leaseTTL: params.ttlMs ?? 120_000,
|
|
83
|
-
};
|
|
84
|
-
await this.leaseStore.save(lease);
|
|
85
|
-
|
|
86
|
-
await this.journal.append({
|
|
87
|
-
treeId: params.treeId,
|
|
88
|
-
branchId: params.branchId ?? 'default',
|
|
89
|
-
wotsIndex: flatIndex(indices),
|
|
90
|
-
indices,
|
|
91
|
-
status: 'reserved',
|
|
92
|
-
payloadHash: params.payloadHash,
|
|
93
|
-
timestamp: Date.now(),
|
|
94
|
-
deviceId: params.deviceId ?? 'axia',
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
return { reservationId, indices, expiresAt, leaseToken: resp.leaseToken };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async commitKeyUse(reservationId: string, txId: string): Promise<void> {
|
|
101
|
-
if (!this._initialized) await this.initialize();
|
|
102
|
-
const lease = this.leaseStore.get(reservationId);
|
|
103
|
-
if (!lease) throw new LeaseNotFoundError(reservationId);
|
|
104
|
-
|
|
105
|
-
await finalizeLease(this.apiUrl, this.apiKey, lease.leaseToken, txId);
|
|
106
|
-
|
|
107
|
-
const indices: SigningIndices = lease.indices;
|
|
108
|
-
await this.watermark.markUnavailable(
|
|
109
|
-
lease.indices.addressIndex.toString(),
|
|
110
|
-
indices,
|
|
111
|
-
'committed',
|
|
112
|
-
);
|
|
113
|
-
await this.leaseStore.updateStatus(reservationId, 'finalized');
|
|
114
|
-
|
|
115
|
-
await this.journal.append({
|
|
116
|
-
treeId: reservationId,
|
|
117
|
-
branchId: 'default',
|
|
118
|
-
wotsIndex: flatIndex(indices),
|
|
119
|
-
indices,
|
|
120
|
-
status: 'committed',
|
|
121
|
-
txId,
|
|
122
|
-
timestamp: Date.now(),
|
|
123
|
-
deviceId: 'axia',
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async burnReservation(reservationId: string, reason: string): Promise<void> {
|
|
128
|
-
if (!this._initialized) await this.initialize();
|
|
129
|
-
const lease = this.leaseStore.get(reservationId);
|
|
130
|
-
if (!lease) throw new LeaseNotFoundError(reservationId);
|
|
131
|
-
|
|
132
|
-
const indices: SigningIndices = lease.indices;
|
|
133
|
-
await this.watermark.markUnavailable(
|
|
134
|
-
lease.indices.addressIndex.toString(),
|
|
135
|
-
indices,
|
|
136
|
-
'burned',
|
|
137
|
-
);
|
|
138
|
-
await this.leaseStore.updateStatus(reservationId, 'cancelled');
|
|
139
|
-
|
|
140
|
-
this.logger.warn(`[AxiaLeaseProvider] Burning reservation ${reservationId}: ${reason}`);
|
|
141
|
-
await this.journal.append({
|
|
142
|
-
treeId: reservationId,
|
|
143
|
-
branchId: 'default',
|
|
144
|
-
wotsIndex: flatIndex(indices),
|
|
145
|
-
indices,
|
|
146
|
-
status: 'burned',
|
|
147
|
-
timestamp: Date.now(),
|
|
148
|
-
deviceId: 'axia',
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async getLocalWatermark(treeId: string): Promise<LocalWatermark> {
|
|
153
|
-
if (!this._initialized) await this.initialize();
|
|
154
|
-
return this.watermark.getLocalWatermark(treeId);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async publishWatermark(_treeId: string): Promise<void> {
|
|
158
|
-
// Server maintains its own watermark via the lease API
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async syncLeaseJournal(): Promise<SyncResult> {
|
|
162
|
-
return { synced: true, conflicts: [] };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean> {
|
|
166
|
-
if (cert === undefined) return true;
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
}
|
package/src/device.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Device keyspace partitioning — Layer 2.
|
|
3
|
-
*
|
|
4
|
-
* Deterministic address-index range assignment per device slot.
|
|
5
|
-
* Does NOT accept a raw seed; the caller derives the TreeKey in @totemsdk/core.
|
|
6
|
-
*
|
|
7
|
-
* Layout (8 device slots × 8 addresses each = 64 total):
|
|
8
|
-
* device 0 → addressIndex 0–7
|
|
9
|
-
* device 1 → addressIndex 8–15
|
|
10
|
-
* device 2 → addressIndex 16–23
|
|
11
|
-
* ...
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { DeviceKeyRange } from './types.js';
|
|
15
|
-
|
|
16
|
-
const ADDRESSES_PER_DEVICE = 8;
|
|
17
|
-
const MAX_DEVICE_SLOTS = 8;
|
|
18
|
-
|
|
19
|
-
export function allocateDeviceRange(params: {
|
|
20
|
-
deviceSlot: number;
|
|
21
|
-
deviceId: string;
|
|
22
|
-
}): DeviceKeyRange {
|
|
23
|
-
if (params.deviceSlot < 0 || params.deviceSlot >= MAX_DEVICE_SLOTS) {
|
|
24
|
-
throw new RangeError(
|
|
25
|
-
`Device slot must be 0–${MAX_DEVICE_SLOTS - 1}, got ${params.deviceSlot}`,
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
const startAddressIndex = params.deviceSlot * ADDRESSES_PER_DEVICE;
|
|
29
|
-
const endAddressIndex = startAddressIndex + ADDRESSES_PER_DEVICE - 1;
|
|
30
|
-
return {
|
|
31
|
-
deviceId: params.deviceId,
|
|
32
|
-
startAddressIndex,
|
|
33
|
-
endAddressIndex,
|
|
34
|
-
addressCount: ADDRESSES_PER_DEVICE,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function deviceSlotForAddressIndex(addressIndex: number): number {
|
|
39
|
-
return Math.floor(addressIndex / ADDRESSES_PER_DEVICE);
|
|
40
|
-
}
|
package/src/errors.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
export class WatermarkMonotonicityError extends Error {
|
|
2
|
-
constructor(message: string) {
|
|
3
|
-
super(message);
|
|
4
|
-
this.name = 'WatermarkMonotonicityError';
|
|
5
|
-
}
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export class WatermarkExhaustedError extends Error {
|
|
9
|
-
constructor(public readonly treeId: string) {
|
|
10
|
-
super(`WOTS keyspace exhausted for tree: ${treeId}`);
|
|
11
|
-
this.name = 'WatermarkExhaustedError';
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class LeaseNotFoundError extends Error {
|
|
16
|
-
constructor(public readonly reservationId: string) {
|
|
17
|
-
super(`Lease reservation not found: ${reservationId}`);
|
|
18
|
-
this.name = 'LeaseNotFoundError';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class PersonalLeaseNodeNotConfiguredError extends Error {
|
|
23
|
-
constructor() {
|
|
24
|
-
super(
|
|
25
|
-
'PersonalLeaseNodeProvider requires a running lookup node. ' +
|
|
26
|
-
'Deploy @totemsdk/lookup-node and configure { nodeUrl, nodePubkey }.',
|
|
27
|
-
);
|
|
28
|
-
this.name = 'PersonalLeaseNodeNotConfiguredError';
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class P2PQuorumNotImplementedError extends Error {
|
|
33
|
-
constructor() {
|
|
34
|
-
super('P2PQuorumLeaseProvider is not yet implemented.');
|
|
35
|
-
this.name = 'P2PQuorumNotImplementedError';
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export class OnchainWatermarkNotImplementedError extends Error {
|
|
40
|
-
constructor() {
|
|
41
|
-
super('OnchainWatermarkProvider is not yet implemented.');
|
|
42
|
-
this.name = 'OnchainWatermarkNotImplementedError';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export class DeviceRangeViolationError extends Error {
|
|
47
|
-
constructor(
|
|
48
|
-
public readonly addressIndex: number,
|
|
49
|
-
public readonly allowedStart: number,
|
|
50
|
-
public readonly allowedEnd: number,
|
|
51
|
-
) {
|
|
52
|
-
super(
|
|
53
|
-
`Address index ${addressIndex} is outside device range [${allowedStart}, ${allowedEnd}]`,
|
|
54
|
-
);
|
|
55
|
-
this.name = 'DeviceRangeViolationError';
|
|
56
|
-
}
|
|
57
|
-
}
|
package/src/hybrid.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HybridLeaseProvider — recommended production default.
|
|
3
|
-
*
|
|
4
|
-
* Always uses local first, then gates on node cert and/or quorum attestation
|
|
5
|
-
* for transactions above a configurable value threshold.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
WotsLeaseProvider,
|
|
10
|
-
ReserveParams,
|
|
11
|
-
LeaseReservation,
|
|
12
|
-
LeaseCertificate,
|
|
13
|
-
LocalWatermark,
|
|
14
|
-
SyncResult,
|
|
15
|
-
} from './types.js';
|
|
16
|
-
import type { LocalLeaseProvider } from './local.js';
|
|
17
|
-
import type { PersonalLeaseNodeProvider } from './stubs.js';
|
|
18
|
-
import type { P2PQuorumLeaseProvider } from './stubs.js';
|
|
19
|
-
import type { OnchainWatermarkProvider } from './stubs.js';
|
|
20
|
-
|
|
21
|
-
export interface HybridLeaseProviderConfig {
|
|
22
|
-
local: LocalLeaseProvider;
|
|
23
|
-
node?: PersonalLeaseNodeProvider;
|
|
24
|
-
quorum?: P2PQuorumLeaseProvider;
|
|
25
|
-
onchain?: OnchainWatermarkProvider;
|
|
26
|
-
threshold?: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export class HybridLeaseProvider implements WotsLeaseProvider {
|
|
30
|
-
private readonly local: LocalLeaseProvider;
|
|
31
|
-
private readonly node?: PersonalLeaseNodeProvider;
|
|
32
|
-
private readonly quorum?: P2PQuorumLeaseProvider;
|
|
33
|
-
private readonly onchain?: OnchainWatermarkProvider;
|
|
34
|
-
private readonly threshold: number;
|
|
35
|
-
|
|
36
|
-
constructor(config: HybridLeaseProviderConfig) {
|
|
37
|
-
this.local = config.local;
|
|
38
|
-
this.node = config.node;
|
|
39
|
-
this.quorum = config.quorum;
|
|
40
|
-
this.onchain = config.onchain;
|
|
41
|
-
this.threshold = config.threshold ?? Infinity;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private isHighValue(params: ReserveParams): boolean {
|
|
45
|
-
if (!params.valueHint) return false;
|
|
46
|
-
const val = parseFloat(params.valueHint);
|
|
47
|
-
return !isNaN(val) && val >= this.threshold;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async reserveKeyUse(params: ReserveParams): Promise<LeaseReservation> {
|
|
51
|
-
const reservation = await this.local.reserveKeyUse(params);
|
|
52
|
-
|
|
53
|
-
if (this.isHighValue(params)) {
|
|
54
|
-
if (this.node) {
|
|
55
|
-
try {
|
|
56
|
-
const nodeReservation = await this.node.reserveKeyUse(params);
|
|
57
|
-
return { ...reservation, certificate: nodeReservation.certificate };
|
|
58
|
-
} catch {
|
|
59
|
-
// Node unavailable — continue with local only
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return reservation;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
commitKeyUse(reservationId: string, txId: string): Promise<void> {
|
|
68
|
-
return this.local.commitKeyUse(reservationId, txId);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
burnReservation(reservationId: string, reason: string): Promise<void> {
|
|
72
|
-
return this.local.burnReservation(reservationId, reason);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
getLocalWatermark(treeId: string): Promise<LocalWatermark> {
|
|
76
|
-
return this.local.getLocalWatermark(treeId);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
publishWatermark(treeId: string): Promise<void> {
|
|
80
|
-
if (this.node) {
|
|
81
|
-
return this.node.publishWatermark(treeId).catch(() => this.local.publishWatermark(treeId));
|
|
82
|
-
}
|
|
83
|
-
return this.local.publishWatermark(treeId);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
syncLeaseJournal(): Promise<SyncResult> {
|
|
87
|
-
if (this.node) {
|
|
88
|
-
return this.node.syncLeaseJournal().catch(() => this.local.syncLeaseJournal());
|
|
89
|
-
}
|
|
90
|
-
return this.local.syncLeaseJournal();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async verifyLeaseCertificate(cert?: LeaseCertificate): Promise<boolean> {
|
|
94
|
-
if (!cert) return this.local.verifyLeaseCertificate(cert);
|
|
95
|
-
if (this.node) return this.node.verifyLeaseCertificate(cert);
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
}
|