@ghostlytools/cli 1.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/README.md +405 -0
- package/SKILL.md +282 -0
- package/dist/assets/argon2id-088uQfnH.js +645 -0
- package/dist/assets/argon2id-088uQfnH.js.map +1 -0
- package/dist/assets/bark_ffi_wasm_bg-Bxru_BVW.wasm +0 -0
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js +6 -0
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -0
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js +6 -0
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -0
- package/dist/assets/bitcoindevkit_bg-CRkZTSbO.wasm +0 -0
- package/dist/assets/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/dist/assets/endpoint-h3cqRyRz.js +185 -0
- package/dist/assets/endpoint-h3cqRyRz.js.map +1 -0
- package/dist/assets/fedimintWorker.mjs +95 -0
- package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js +6 -0
- package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -0
- package/dist/assets/fedimint_client_wasm_bg-DeFtkIv6.wasm +0 -0
- package/dist/assets/fileBytes-ByS16quP.js +154 -0
- package/dist/assets/fileBytes-ByS16quP.js.map +1 -0
- package/dist/assets/fileBytesIdb-COzoilo9.js +230 -0
- package/dist/assets/fileBytesIdb-COzoilo9.js.map +1 -0
- package/dist/assets/fileBytesOpfs-B27wAMUh.js +145 -0
- package/dist/assets/fileBytesOpfs-B27wAMUh.js.map +1 -0
- package/dist/assets/ghostly_iroh_web-CPa_YEfb.js +1154 -0
- package/dist/assets/ghostly_iroh_web-CPa_YEfb.js.map +1 -0
- package/dist/assets/ghostly_iroh_web_bg.wasm +0 -0
- package/dist/assets/hyperdhtRelay-BKsFpPjr.js +342 -0
- package/dist/assets/hyperdhtRelay-BKsFpPjr.js.map +1 -0
- package/dist/assets/legacy_ciphers-JJOu0JXL.js +5185 -0
- package/dist/assets/legacy_ciphers-JJOu0JXL.js.map +1 -0
- package/dist/assets/mainline-DOncVbJN.js +176 -0
- package/dist/assets/mainline-DOncVbJN.js.map +1 -0
- package/dist/assets/nacl-fast-CDhNCYHs.js +1512 -0
- package/dist/assets/nacl-fast-CDhNCYHs.js.map +1 -0
- package/dist/assets/noble_curves-DDWn5AH0.js +2978 -0
- package/dist/assets/noble_curves-DDWn5AH0.js.map +1 -0
- package/dist/assets/noble_hashes-B7AZyFyS.js +427 -0
- package/dist/assets/noble_hashes-B7AZyFyS.js.map +1 -0
- package/dist/assets/openpgp-BBFr-zHQ.js +16573 -0
- package/dist/assets/openpgp-BBFr-zHQ.js.map +1 -0
- package/dist/assets/server-5T5l8hfy.js +28293 -0
- package/dist/assets/server-5T5l8hfy.js.map +1 -0
- package/dist/assets/sha512-BYId-G-T.js +1034 -0
- package/dist/assets/sha512-BYId-G-T.js.map +1 -0
- package/dist/assets/src-DptDr2et.js +20971 -0
- package/dist/assets/src-DptDr2et.js.map +1 -0
- package/dist/assets/storage-Bya5b2Xt.js +1648 -0
- package/dist/assets/storage-Bya5b2Xt.js.map +1 -0
- package/dist/assets/tree-store-N362nuzZ.js +1031 -0
- package/dist/assets/tree-store-N362nuzZ.js.map +1 -0
- package/dist/assets/unbzip2-stream-B1C-JX9L.js +681 -0
- package/dist/assets/unbzip2-stream-B1C-JX9L.js.map +1 -0
- package/dist/assets/web-OUQl4Q7L.js +3802 -0
- package/dist/assets/web-OUQl4Q7L.js.map +1 -0
- package/dist/ghostly.mjs +17087 -0
- package/dist/ghostly.mjs.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
//#region ../../node_modules/@breeztech/breez-sdk-spark/web/tree-store/index.js
|
|
2
|
+
/**
|
|
3
|
+
* ES module implementation of the durable tree store for browsers, backed by
|
|
4
|
+
* IndexedDB. Ports the two-table (leaves + ancestors) model and reservation /
|
|
5
|
+
* refresh / spent-guard semantics of the PostgreSQL tree store to a single-user
|
|
6
|
+
* IndexedDB database: no user_id scoping and no `brz_` table prefix, since each
|
|
7
|
+
* browser origin has its own isolated database.
|
|
8
|
+
*
|
|
9
|
+
* IndexedDB transaction lifetime: a transaction auto-commits once its request
|
|
10
|
+
* queue drains and control returns to the event loop, so awaiting any non-IDB
|
|
11
|
+
* promise mid-transaction closes it. Every logical operation therefore runs in
|
|
12
|
+
* ONE transaction: all reads are issued up front, and the compute + writes run
|
|
13
|
+
* synchronously inside the last read's success handler (never awaiting anything
|
|
14
|
+
* that isn't part of the transaction). Because IndexedDB serializes readwrite
|
|
15
|
+
* transactions with overlapping store scope, this also gives the mutating ops
|
|
16
|
+
* the same serialization the Postgres backend gets from its per-tenant lock.
|
|
17
|
+
*/
|
|
18
|
+
/** Reservations older than this are stale and get released on the next refresh. */
|
|
19
|
+
var RESERVATION_TIMEOUT_MS = 3e5;
|
|
20
|
+
/** Spent-leaf markers older than this (relative to a refresh) are pruned. */
|
|
21
|
+
var SPENT_MARKER_CLEANUP_THRESHOLD_MS = 3e5;
|
|
22
|
+
var DB_VERSION = 5;
|
|
23
|
+
var STORE_LEAVES = "leaves";
|
|
24
|
+
var STORE_ANCESTORS = "ancestors";
|
|
25
|
+
var STORE_RESERVATIONS = "reservations";
|
|
26
|
+
var STORE_SPENT = "spent";
|
|
27
|
+
var STORE_SWAP_STATUS = "swapStatus";
|
|
28
|
+
/** Singleton key of the swap-status row. */
|
|
29
|
+
var SWAP_STATUS_ID = 1;
|
|
30
|
+
var TreeStoreError = class TreeStoreError extends Error {
|
|
31
|
+
constructor(message, cause = null) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "TreeStoreError";
|
|
34
|
+
this.cause = cause;
|
|
35
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, TreeStoreError);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function _resolveIndexedDB() {
|
|
39
|
+
if (typeof indexedDB !== "undefined") return indexedDB;
|
|
40
|
+
if (typeof globalThis !== "undefined" && globalThis.indexedDB) return globalThis.indexedDB;
|
|
41
|
+
if (typeof self !== "undefined" && self.indexedDB) return self.indexedDB;
|
|
42
|
+
if (typeof window !== "undefined" && window.indexedDB) return window.indexedDB;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Pair a leaf with its ancestors (nearest first) by walking `parent_node_id`
|
|
47
|
+
* through `nodes`. Returns null if the leaf itself is absent; stops at a gap or
|
|
48
|
+
* cycle, returning a partial chain.
|
|
49
|
+
* @param {Map<string, object>} nodes
|
|
50
|
+
* @param {string} leafId
|
|
51
|
+
* @returns {{leaf: object, ancestors: Array<object>}|null}
|
|
52
|
+
*/
|
|
53
|
+
function assembleExitChain(nodes, leafId) {
|
|
54
|
+
const leaf = nodes.get(leafId);
|
|
55
|
+
if (!leaf) return null;
|
|
56
|
+
const ancestors = [];
|
|
57
|
+
const visited = /* @__PURE__ */ new Set([leafId]);
|
|
58
|
+
let current = leaf.parent_node_id;
|
|
59
|
+
while (current != null && !visited.has(current)) {
|
|
60
|
+
visited.add(current);
|
|
61
|
+
const node = nodes.get(current);
|
|
62
|
+
if (!node) break;
|
|
63
|
+
ancestors.push(node);
|
|
64
|
+
current = node.parent_node_id;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
leaf,
|
|
68
|
+
ancestors
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
var WebTreeStore = class {
|
|
72
|
+
constructor(dbName = "BreezSdkSparkTree", logger = null) {
|
|
73
|
+
this.dbName = dbName;
|
|
74
|
+
this.db = null;
|
|
75
|
+
this.logger = logger;
|
|
76
|
+
}
|
|
77
|
+
async initialize() {
|
|
78
|
+
if (this.db) return this;
|
|
79
|
+
const idbFactory = _resolveIndexedDB();
|
|
80
|
+
if (!idbFactory) throw new TreeStoreError("IndexedDB is not available in this environment");
|
|
81
|
+
this.db = await new Promise((resolve, reject) => {
|
|
82
|
+
const request = idbFactory.open(this.dbName, DB_VERSION);
|
|
83
|
+
request.onupgradeneeded = (event) => {
|
|
84
|
+
const db = event.target.result;
|
|
85
|
+
if (!db.objectStoreNames.contains(STORE_LEAVES)) {
|
|
86
|
+
const leaves = db.createObjectStore(STORE_LEAVES, { keyPath: "id" });
|
|
87
|
+
leaves.createIndex("reservation_id", "reservation_id", { unique: false });
|
|
88
|
+
leaves.createIndex("chain_complete", "chain_complete", { unique: false });
|
|
89
|
+
}
|
|
90
|
+
if (event.oldVersion < 2 && db.objectStoreNames.contains(STORE_ANCESTORS)) db.deleteObjectStore(STORE_ANCESTORS);
|
|
91
|
+
if (!db.objectStoreNames.contains(STORE_ANCESTORS)) db.createObjectStore(STORE_ANCESTORS, { keyPath: ["leaf_id", "id"] }).createIndex("leaf_id", "leaf_id", { unique: false });
|
|
92
|
+
if (event.oldVersion > 0 && event.oldVersion < DB_VERSION) {
|
|
93
|
+
const tx = event.target.transaction;
|
|
94
|
+
const leaves = tx.objectStore(STORE_LEAVES);
|
|
95
|
+
if (!leaves.indexNames.contains("chain_complete")) leaves.createIndex("chain_complete", "chain_complete", { unique: false });
|
|
96
|
+
const ancestors = tx.objectStore(STORE_ANCESTORS);
|
|
97
|
+
const linked = /* @__PURE__ */ new Set();
|
|
98
|
+
ancestors.openCursor().onsuccess = (cursorEvent) => {
|
|
99
|
+
const cursor = cursorEvent.target.result;
|
|
100
|
+
if (cursor) {
|
|
101
|
+
linked.add(`${cursor.value.leaf_id}\u0000${cursor.value.id}`);
|
|
102
|
+
cursor.continue();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
leaves.openCursor().onsuccess = (leafEvent) => {
|
|
106
|
+
const leafCursor = leafEvent.target.result;
|
|
107
|
+
if (!leafCursor) return;
|
|
108
|
+
const row = leafCursor.value;
|
|
109
|
+
row.chain_complete = row.parent_node_id == null || linked.has(`${row.id}\u0000${row.parent_node_id}`) ? 1 : 0;
|
|
110
|
+
leafCursor.update(row);
|
|
111
|
+
leafCursor.continue();
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (!db.objectStoreNames.contains(STORE_RESERVATIONS)) db.createObjectStore(STORE_RESERVATIONS, { keyPath: "id" });
|
|
116
|
+
if (!db.objectStoreNames.contains(STORE_SPENT)) db.createObjectStore(STORE_SPENT, { keyPath: "id" });
|
|
117
|
+
if (!db.objectStoreNames.contains(STORE_SWAP_STATUS)) db.createObjectStore(STORE_SWAP_STATUS, { keyPath: "id" });
|
|
118
|
+
};
|
|
119
|
+
request.onsuccess = () => {
|
|
120
|
+
const db = request.result;
|
|
121
|
+
db.onversionchange = () => {
|
|
122
|
+
db.close();
|
|
123
|
+
this.db = null;
|
|
124
|
+
};
|
|
125
|
+
resolve(db);
|
|
126
|
+
};
|
|
127
|
+
request.onerror = () => reject(new TreeStoreError(`Failed to open IndexedDB: ${request.error?.message || "Unknown error"}`, request.error));
|
|
128
|
+
request.onblocked = () => {};
|
|
129
|
+
});
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
close() {
|
|
133
|
+
if (this.db) {
|
|
134
|
+
this.db.close();
|
|
135
|
+
this.db = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Runs one transaction. `reads` is a list of `{ name, store, key?, index?,
|
|
140
|
+
* all? }`: with a key it is a `get`, without it a `getAll`, and `all` with a
|
|
141
|
+
* key reads every record under that key; `index` reads through that
|
|
142
|
+
* index on `store` instead of the store directly. All reads are issued up
|
|
143
|
+
* front; once the last completes, `compute(results, tx)` runs synchronously
|
|
144
|
+
* in that read's success handler and may issue writes on `tx`. Its return
|
|
145
|
+
* value resolves the promise on `oncomplete`, so writes are durable before
|
|
146
|
+
* resolve. `compute` must never await a non-IDB promise (it would close `tx`).
|
|
147
|
+
*/
|
|
148
|
+
_txRun(storeNames, mode, reads, compute) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
if (!this.db) {
|
|
151
|
+
reject(new TreeStoreError("Database not initialized"));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let tx;
|
|
155
|
+
try {
|
|
156
|
+
tx = this.db.transaction(storeNames, mode);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
reject(e);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
let outcome;
|
|
162
|
+
let settled = false;
|
|
163
|
+
let computeError = null;
|
|
164
|
+
const done = (fn) => {
|
|
165
|
+
if (!settled) {
|
|
166
|
+
settled = true;
|
|
167
|
+
fn();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
tx.oncomplete = () => done(() => resolve(outcome));
|
|
171
|
+
tx.onabort = () => done(() => reject(computeError || new TreeStoreError(`transaction aborted: ${tx.error?.message || "unknown"}`, tx.error)));
|
|
172
|
+
tx.onerror = () => done(() => reject(computeError || new TreeStoreError(`transaction error: ${tx.error?.message || "unknown"}`, tx.error)));
|
|
173
|
+
const runCompute = (results) => {
|
|
174
|
+
try {
|
|
175
|
+
outcome = compute(results, tx);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
computeError = e;
|
|
178
|
+
try {
|
|
179
|
+
tx.abort();
|
|
180
|
+
} catch (_) {
|
|
181
|
+
done(() => reject(e));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
if (reads.length === 0) {
|
|
186
|
+
runCompute({});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const results = {};
|
|
190
|
+
let pending = reads.length;
|
|
191
|
+
for (const r of reads) {
|
|
192
|
+
let req;
|
|
193
|
+
try {
|
|
194
|
+
const store = tx.objectStore(r.store);
|
|
195
|
+
const source = r.index ? store.index(r.index) : store;
|
|
196
|
+
if (r.keysOnly) req = source.getAllKeys(r.key);
|
|
197
|
+
else if (r.all) req = source.getAll(r.key);
|
|
198
|
+
else req = "key" in r ? source.get(r.key) : source.getAll();
|
|
199
|
+
} catch (e) {
|
|
200
|
+
done(() => reject(e));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
req.onsuccess = () => {
|
|
204
|
+
results[r.name] = req.result;
|
|
205
|
+
pending -= 1;
|
|
206
|
+
if (pending === 0) runCompute(results);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Reconstruct the exit chains for many leaves in one transaction, each as
|
|
213
|
+
* { leaf, ancestors } with ancestors nearest first. A leaf absent from the store
|
|
214
|
+
* is skipped; a chain that hits a gap comes back partial.
|
|
215
|
+
* @param {Array<string>} leafIds
|
|
216
|
+
* @returns {Promise<Array<{leaf: object, ancestors: Array<object>}>>}
|
|
217
|
+
*/
|
|
218
|
+
async getExitChains(leafIds) {
|
|
219
|
+
try {
|
|
220
|
+
if (!leafIds || leafIds.length === 0) return [];
|
|
221
|
+
return await new Promise((resolve, reject) => {
|
|
222
|
+
if (!this.db) {
|
|
223
|
+
reject(new TreeStoreError("Database not initialized"));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const tx = this.db.transaction([STORE_LEAVES, STORE_ANCESTORS], "readonly");
|
|
227
|
+
const leavesStore = tx.objectStore(STORE_LEAVES);
|
|
228
|
+
const ancestorsIndex = tx.objectStore(STORE_ANCESTORS).index("leaf_id");
|
|
229
|
+
const leafById = /* @__PURE__ */ new Map();
|
|
230
|
+
const ancestorsByLeaf = /* @__PURE__ */ new Map();
|
|
231
|
+
let settled = false;
|
|
232
|
+
const fail = (msg) => {
|
|
233
|
+
if (!settled) {
|
|
234
|
+
settled = true;
|
|
235
|
+
reject(new TreeStoreError(msg));
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
tx.onabort = () => fail(`Failed to get exit chains: ${tx.error?.message || "aborted"}`);
|
|
239
|
+
tx.onerror = () => fail(`Failed to get exit chains: ${tx.error?.message || "error"}`);
|
|
240
|
+
tx.oncomplete = () => {
|
|
241
|
+
if (settled) return;
|
|
242
|
+
settled = true;
|
|
243
|
+
const result = [];
|
|
244
|
+
for (const id of leafIds) {
|
|
245
|
+
const leafRow = leafById.get(id);
|
|
246
|
+
if (!leafRow) continue;
|
|
247
|
+
const nodes = ancestorsByLeaf.get(id) || /* @__PURE__ */ new Map();
|
|
248
|
+
nodes.set(leafRow.id, leafRow.data);
|
|
249
|
+
const pedigree = assembleExitChain(nodes, id);
|
|
250
|
+
if (pedigree) result.push(pedigree);
|
|
251
|
+
}
|
|
252
|
+
resolve(result);
|
|
253
|
+
};
|
|
254
|
+
for (const id of leafIds) {
|
|
255
|
+
const leafReq = leavesStore.get(id);
|
|
256
|
+
leafReq.onsuccess = () => {
|
|
257
|
+
if (leafReq.result) leafById.set(id, leafReq.result);
|
|
258
|
+
};
|
|
259
|
+
const ancestorsReq = ancestorsIndex.getAll(id);
|
|
260
|
+
ancestorsReq.onsuccess = () => {
|
|
261
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
262
|
+
for (const row of ancestorsReq.result || []) nodes.set(row.id, row.data);
|
|
263
|
+
ancestorsByLeaf.set(id, nodes);
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error instanceof TreeStoreError) throw error;
|
|
269
|
+
throw new TreeStoreError(`Failed to get exit chains: ${error.message}`, error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Ids of the stored leaves whose chain cannot back an exit: the leaf has a
|
|
274
|
+
* parent, and no ancestor row of its own holds that parent.
|
|
275
|
+
* @returns {Promise<Array<string>>}
|
|
276
|
+
*/
|
|
277
|
+
async leavesMissingExitChains() {
|
|
278
|
+
try {
|
|
279
|
+
return await this._txRun([STORE_LEAVES], "readonly", [{
|
|
280
|
+
name: "ids",
|
|
281
|
+
store: STORE_LEAVES,
|
|
282
|
+
index: "chain_complete",
|
|
283
|
+
key: 0,
|
|
284
|
+
keysOnly: true
|
|
285
|
+
}], (res) => res.ids);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (error instanceof TreeStoreError) throw error;
|
|
288
|
+
throw new TreeStoreError(`Failed to get leaves missing exit chains: ${error.message}`, error);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async getLeaves() {
|
|
292
|
+
try {
|
|
293
|
+
return await this._txRun([STORE_LEAVES, STORE_RESERVATIONS], "readonly", [{
|
|
294
|
+
name: "leaves",
|
|
295
|
+
store: STORE_LEAVES
|
|
296
|
+
}, {
|
|
297
|
+
name: "reservations",
|
|
298
|
+
store: STORE_RESERVATIONS
|
|
299
|
+
}], (res) => {
|
|
300
|
+
const resMap = new Map(res.reservations.map((r) => [r.id, r]));
|
|
301
|
+
const available = [];
|
|
302
|
+
const notAvailable = [];
|
|
303
|
+
const availableMissingFromOperators = [];
|
|
304
|
+
const reservedForPayment = [];
|
|
305
|
+
const reservedForSwap = [];
|
|
306
|
+
for (const row of res.leaves) {
|
|
307
|
+
const node = row.data;
|
|
308
|
+
const purpose = row.reservation_id != null ? resMap.get(row.reservation_id)?.purpose : void 0;
|
|
309
|
+
const spendable = node.status === "Available";
|
|
310
|
+
if (purpose) {
|
|
311
|
+
if (purpose === "Payment") reservedForPayment.push(node);
|
|
312
|
+
else if (purpose === "Swap") reservedForSwap.push(node);
|
|
313
|
+
} else if (!spendable) notAvailable.push(node);
|
|
314
|
+
else if (row.is_missing_from_operators) availableMissingFromOperators.push(node);
|
|
315
|
+
else available.push(node);
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
available,
|
|
319
|
+
notAvailable,
|
|
320
|
+
availableMissingFromOperators,
|
|
321
|
+
reservedForPayment,
|
|
322
|
+
reservedForSwap
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (error instanceof TreeStoreError) throw error;
|
|
327
|
+
throw new TreeStoreError(`Failed to get leaves: ${error.message}`, error);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async getAvailableBalance() {
|
|
331
|
+
try {
|
|
332
|
+
return await this._txRun([STORE_LEAVES, STORE_RESERVATIONS], "readonly", [{
|
|
333
|
+
name: "leaves",
|
|
334
|
+
store: STORE_LEAVES
|
|
335
|
+
}, {
|
|
336
|
+
name: "reservations",
|
|
337
|
+
store: STORE_RESERVATIONS
|
|
338
|
+
}], (res) => {
|
|
339
|
+
const resMap = new Map(res.reservations.map((r) => [r.id, r]));
|
|
340
|
+
let balance = 0n;
|
|
341
|
+
for (const row of res.leaves) {
|
|
342
|
+
const purpose = row.reservation_id != null ? resMap.get(row.reservation_id)?.purpose : void 0;
|
|
343
|
+
if (row.reservation_id == null && row.status === "Available" || purpose === "Swap") balance += BigInt(row.value);
|
|
344
|
+
}
|
|
345
|
+
return balance;
|
|
346
|
+
});
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (error instanceof TreeStoreError) throw error;
|
|
349
|
+
throw new TreeStoreError(`Failed to get available balance: ${error.message}`, error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async getVerifiedLeafKeys() {
|
|
353
|
+
try {
|
|
354
|
+
return await this._txRun([STORE_LEAVES], "readonly", [{
|
|
355
|
+
name: "leaves",
|
|
356
|
+
store: STORE_LEAVES
|
|
357
|
+
}], (res) => res.leaves.map((row) => [
|
|
358
|
+
row.id,
|
|
359
|
+
row.verifying_public_key,
|
|
360
|
+
row.signing_public_key
|
|
361
|
+
]));
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (error instanceof TreeStoreError) throw error;
|
|
364
|
+
throw new TreeStoreError(`Failed to get verified leaf keys: ${error.message}`, error);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async now() {
|
|
368
|
+
return this._nowMs();
|
|
369
|
+
}
|
|
370
|
+
async addLeaves(leaves) {
|
|
371
|
+
try {
|
|
372
|
+
if (!leaves || leaves.length === 0) return;
|
|
373
|
+
const leafIds = leaves.map((l) => l.id);
|
|
374
|
+
await this._txRun([STORE_LEAVES, STORE_SPENT], "readwrite", [{
|
|
375
|
+
name: "leaves",
|
|
376
|
+
store: STORE_LEAVES
|
|
377
|
+
}], (res, tx) => {
|
|
378
|
+
const leavesStore = tx.objectStore(STORE_LEAVES);
|
|
379
|
+
const spentStore = tx.objectStore(STORE_SPENT);
|
|
380
|
+
const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
|
|
381
|
+
for (const id of leafIds) spentStore.delete(id);
|
|
382
|
+
for (const leaf of leaves) leavesStore.put(this._leafRow(leaf, false, leafMap.get(leaf.id)));
|
|
383
|
+
});
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (error instanceof TreeStoreError) throw error;
|
|
386
|
+
throw new TreeStoreError(`Failed to add leaves: ${error.message}`, error);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async storeAncestors(pedigrees) {
|
|
390
|
+
try {
|
|
391
|
+
if (!pedigrees || pedigrees.length === 0) return;
|
|
392
|
+
const reads = [];
|
|
393
|
+
for (const p of pedigrees) {
|
|
394
|
+
reads.push({
|
|
395
|
+
name: `leaf:${p.leaf.id}`,
|
|
396
|
+
store: STORE_LEAVES,
|
|
397
|
+
key: p.leaf.id
|
|
398
|
+
});
|
|
399
|
+
reads.push({
|
|
400
|
+
name: `ancestors:${p.leaf.id}`,
|
|
401
|
+
store: STORE_ANCESTORS,
|
|
402
|
+
index: "leaf_id",
|
|
403
|
+
key: p.leaf.id,
|
|
404
|
+
all: true
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
await this._txRun([STORE_LEAVES, STORE_ANCESTORS], "readwrite", reads, (res, tx) => {
|
|
408
|
+
const ancestorsStore = tx.objectStore(STORE_ANCESTORS);
|
|
409
|
+
const leavesStore = tx.objectStore(STORE_LEAVES);
|
|
410
|
+
const ancestorRowsByLeaf = this._ancestorRowsByLeaf(pedigrees.flatMap((p) => res[`ancestors:${p.leaf.id}`] || []));
|
|
411
|
+
const leafRowsById = /* @__PURE__ */ new Map();
|
|
412
|
+
for (const p of pedigrees) {
|
|
413
|
+
const row = res[`leaf:${p.leaf.id}`];
|
|
414
|
+
if (row) leafRowsById.set(row.id, row);
|
|
415
|
+
}
|
|
416
|
+
for (const p of pedigrees) {
|
|
417
|
+
const leafRow = leafRowsById.get(p.leaf.id);
|
|
418
|
+
if (!leafRow) continue;
|
|
419
|
+
this._replaceAncestors(ancestorsStore, ancestorRowsByLeaf, p.leaf.id, p.ancestors);
|
|
420
|
+
const chainComplete = this._chainComplete(leafRow.data, p.ancestors, leafRow);
|
|
421
|
+
if (chainComplete !== !!leafRow.chain_complete) leavesStore.put({
|
|
422
|
+
...leafRow,
|
|
423
|
+
chain_complete: chainComplete ? 1 : 0
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (error instanceof TreeStoreError) throw error;
|
|
429
|
+
throw new TreeStoreError(`Failed to store ancestors: ${error.message}`, error);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async setLeaves(leaves, missingLeaves, refreshStartedAtMs) {
|
|
433
|
+
try {
|
|
434
|
+
const reported = leaves || [];
|
|
435
|
+
const missing = missingLeaves || [];
|
|
436
|
+
const refreshMs = refreshStartedAtMs;
|
|
437
|
+
await this._txRun([
|
|
438
|
+
STORE_LEAVES,
|
|
439
|
+
STORE_ANCESTORS,
|
|
440
|
+
STORE_RESERVATIONS,
|
|
441
|
+
STORE_SPENT,
|
|
442
|
+
STORE_SWAP_STATUS
|
|
443
|
+
], "readwrite", [
|
|
444
|
+
{
|
|
445
|
+
name: "leaves",
|
|
446
|
+
store: STORE_LEAVES
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: "ancestors",
|
|
450
|
+
store: STORE_ANCESTORS
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
name: "reservations",
|
|
454
|
+
store: STORE_RESERVATIONS
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
name: "spent",
|
|
458
|
+
store: STORE_SPENT
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: "swap",
|
|
462
|
+
store: STORE_SWAP_STATUS,
|
|
463
|
+
key: SWAP_STATUS_ID
|
|
464
|
+
}
|
|
465
|
+
], (res, tx) => {
|
|
466
|
+
const leavesStore = tx.objectStore(STORE_LEAVES);
|
|
467
|
+
const ancestorsStore = tx.objectStore(STORE_ANCESTORS);
|
|
468
|
+
const reservationsStore = tx.objectStore(STORE_RESERVATIONS);
|
|
469
|
+
const spentStore = tx.objectStore(STORE_SPENT);
|
|
470
|
+
const nowMs = this._nowMs();
|
|
471
|
+
const leafMap = new Map(res.leaves.map((r) => [r.id, { ...r }]));
|
|
472
|
+
const ancestorRowsByLeaf = this._ancestorRowsByLeaf(res.ancestors);
|
|
473
|
+
const staleCutoff = nowMs - RESERVATION_TIMEOUT_MS;
|
|
474
|
+
const staleIds = new Set(res.reservations.filter((r) => r.created_at < staleCutoff).map((r) => r.id));
|
|
475
|
+
for (const id of staleIds) reservationsStore.delete(id);
|
|
476
|
+
for (const row of leafMap.values()) if (row.reservation_id != null && staleIds.has(row.reservation_id)) {
|
|
477
|
+
row.reservation_id = null;
|
|
478
|
+
leavesStore.put(row);
|
|
479
|
+
}
|
|
480
|
+
const hasActiveSwap = res.reservations.filter((r) => !staleIds.has(r.id)).some((r) => r.purpose === "Swap");
|
|
481
|
+
const swapCompletedDuringRefresh = !!res.swap && res.swap.last_completed_at >= refreshMs;
|
|
482
|
+
if (hasActiveSwap || swapCompletedDuringRefresh) return;
|
|
483
|
+
const spentCleanupCutoff = refreshMs - SPENT_MARKER_CLEANUP_THRESHOLD_MS;
|
|
484
|
+
for (const s of res.spent) if (s.spent_at < spentCleanupCutoff) spentStore.delete(s.id);
|
|
485
|
+
const spentIds = new Set(res.spent.filter((s) => s.spent_at >= refreshMs).map((s) => s.id));
|
|
486
|
+
const priorRows = new Map(leafMap);
|
|
487
|
+
const deletedIds = [];
|
|
488
|
+
for (const row of Array.from(leafMap.values())) if (row.reservation_id == null && row.added_at < refreshMs) {
|
|
489
|
+
leavesStore.delete(row.id);
|
|
490
|
+
leafMap.delete(row.id);
|
|
491
|
+
deletedIds.push(row.id);
|
|
492
|
+
}
|
|
493
|
+
const liveLeaves = reported.filter((p) => !spentIds.has(p.id));
|
|
494
|
+
const liveMissing = missing.filter((p) => !spentIds.has(p.id));
|
|
495
|
+
for (const p of liveLeaves) {
|
|
496
|
+
const row = this._leafRow(p, false, priorRows.get(p.id));
|
|
497
|
+
leavesStore.put(row);
|
|
498
|
+
leafMap.set(row.id, row);
|
|
499
|
+
}
|
|
500
|
+
for (const p of liveMissing) {
|
|
501
|
+
const row = this._leafRow(p, true, priorRows.get(p.id));
|
|
502
|
+
leavesStore.put(row);
|
|
503
|
+
leafMap.set(row.id, row);
|
|
504
|
+
}
|
|
505
|
+
const survivingIds = /* @__PURE__ */ new Set();
|
|
506
|
+
for (const p of reported.concat(missing)) if (!spentIds.has(p.id)) survivingIds.add(p.id);
|
|
507
|
+
for (const id of deletedIds) {
|
|
508
|
+
if (survivingIds.has(id)) continue;
|
|
509
|
+
for (const existingId of (ancestorRowsByLeaf.get(id) || /* @__PURE__ */ new Map()).keys()) ancestorsStore.delete([id, existingId]);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (error instanceof TreeStoreError) throw error;
|
|
514
|
+
throw new TreeStoreError(`Failed to set leaves: ${error.message}`, error);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async cancelReservation(id, leavesToKeep) {
|
|
518
|
+
try {
|
|
519
|
+
const keep = leavesToKeep || [];
|
|
520
|
+
await this._txRun([
|
|
521
|
+
STORE_LEAVES,
|
|
522
|
+
STORE_ANCESTORS,
|
|
523
|
+
STORE_RESERVATIONS
|
|
524
|
+
], "readwrite", [
|
|
525
|
+
{
|
|
526
|
+
name: "leaves",
|
|
527
|
+
store: STORE_LEAVES
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
name: "ancestors",
|
|
531
|
+
store: STORE_ANCESTORS
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: "res",
|
|
535
|
+
store: STORE_RESERVATIONS,
|
|
536
|
+
key: id
|
|
537
|
+
}
|
|
538
|
+
], (res, tx) => {
|
|
539
|
+
const leavesStore = tx.objectStore(STORE_LEAVES);
|
|
540
|
+
const ancestorsStore = tx.objectStore(STORE_ANCESTORS);
|
|
541
|
+
const reservationsStore = tx.objectStore(STORE_RESERVATIONS);
|
|
542
|
+
const keepIds = new Set(keep.map((l) => l.id));
|
|
543
|
+
const ancestorRowsByLeaf = this._ancestorRowsByLeaf(res.ancestors);
|
|
544
|
+
const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
|
|
545
|
+
const priorRows = new Map(leafMap);
|
|
546
|
+
for (const l of res.leaves) {
|
|
547
|
+
if (l.reservation_id !== id) continue;
|
|
548
|
+
leavesStore.delete(l.id);
|
|
549
|
+
leafMap.delete(l.id);
|
|
550
|
+
if (!keepIds.has(l.id)) for (const existingId of (ancestorRowsByLeaf.get(l.id) || /* @__PURE__ */ new Map()).keys()) ancestorsStore.delete([l.id, existingId]);
|
|
551
|
+
}
|
|
552
|
+
reservationsStore.delete(id);
|
|
553
|
+
if (keep.length > 0) for (const leaf of keep) {
|
|
554
|
+
const prior = priorRows.get(leaf.id);
|
|
555
|
+
const held = prior?.reservation_id === id ? void 0 : prior;
|
|
556
|
+
const chainComplete = this._chainComplete(leaf, void 0, prior);
|
|
557
|
+
leavesStore.put(this._leafRow(leaf, false, held, chainComplete));
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if (error instanceof TreeStoreError) throw error;
|
|
562
|
+
throw new TreeStoreError(`Failed to cancel reservation '${id}': ${error.message}`, error);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async finalizeReservation(id, newLeaves) {
|
|
566
|
+
try {
|
|
567
|
+
const added = newLeaves || null;
|
|
568
|
+
await this._txRun([
|
|
569
|
+
STORE_LEAVES,
|
|
570
|
+
STORE_ANCESTORS,
|
|
571
|
+
STORE_RESERVATIONS,
|
|
572
|
+
STORE_SPENT,
|
|
573
|
+
STORE_SWAP_STATUS
|
|
574
|
+
], "readwrite", [
|
|
575
|
+
{
|
|
576
|
+
name: "leaves",
|
|
577
|
+
store: STORE_LEAVES
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
name: "ancestors",
|
|
581
|
+
store: STORE_ANCESTORS
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
name: "res",
|
|
585
|
+
store: STORE_RESERVATIONS,
|
|
586
|
+
key: id
|
|
587
|
+
}
|
|
588
|
+
], (res, tx) => {
|
|
589
|
+
const leavesStore = tx.objectStore(STORE_LEAVES);
|
|
590
|
+
const ancestorsStore = tx.objectStore(STORE_ANCESTORS);
|
|
591
|
+
const reservationsStore = tx.objectStore(STORE_RESERVATIONS);
|
|
592
|
+
const spentStore = tx.objectStore(STORE_SPENT);
|
|
593
|
+
const swapStore = tx.objectStore(STORE_SWAP_STATUS);
|
|
594
|
+
const nowMs = this._nowMs();
|
|
595
|
+
const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
|
|
596
|
+
const ancestorRowsByLeaf = this._ancestorRowsByLeaf(res.ancestors);
|
|
597
|
+
let isSwap = false;
|
|
598
|
+
if (res.res) {
|
|
599
|
+
isSwap = res.res.purpose === "Swap";
|
|
600
|
+
for (const l of res.leaves) if (l.reservation_id === id) {
|
|
601
|
+
spentStore.put({
|
|
602
|
+
id: l.id,
|
|
603
|
+
spent_at: nowMs
|
|
604
|
+
});
|
|
605
|
+
leavesStore.delete(l.id);
|
|
606
|
+
leafMap.delete(l.id);
|
|
607
|
+
for (const existingId of (ancestorRowsByLeaf.get(l.id) || /* @__PURE__ */ new Map()).keys()) ancestorsStore.delete([l.id, existingId]);
|
|
608
|
+
}
|
|
609
|
+
reservationsStore.delete(id);
|
|
610
|
+
}
|
|
611
|
+
if (added && added.length > 0) for (const p of added) {
|
|
612
|
+
const row = this._leafRow(p, false, leafMap.get(p.id));
|
|
613
|
+
leavesStore.put(row);
|
|
614
|
+
leafMap.set(row.id, row);
|
|
615
|
+
}
|
|
616
|
+
if (isSwap && added && added.length > 0) swapStore.put({
|
|
617
|
+
id: SWAP_STATUS_ID,
|
|
618
|
+
last_completed_at: nowMs
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (error instanceof TreeStoreError) throw error;
|
|
623
|
+
throw new TreeStoreError(`Failed to finalize reservation '${id}': ${error.message}`, error);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
async updateReservation(reservationId, reservedLeaves, changeLeaves) {
|
|
627
|
+
try {
|
|
628
|
+
const reserved = reservedLeaves || [];
|
|
629
|
+
const change = changeLeaves || [];
|
|
630
|
+
return await this._txRun([
|
|
631
|
+
STORE_LEAVES,
|
|
632
|
+
STORE_ANCESTORS,
|
|
633
|
+
STORE_RESERVATIONS,
|
|
634
|
+
STORE_SPENT
|
|
635
|
+
], "readwrite", [
|
|
636
|
+
{
|
|
637
|
+
name: "leaves",
|
|
638
|
+
store: STORE_LEAVES
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: "ancestors",
|
|
642
|
+
store: STORE_ANCESTORS
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: "res",
|
|
646
|
+
store: STORE_RESERVATIONS,
|
|
647
|
+
key: reservationId
|
|
648
|
+
}
|
|
649
|
+
], (res, tx) => {
|
|
650
|
+
if (!res.res) throw new TreeStoreError(`Reservation ${reservationId} not found`);
|
|
651
|
+
const leavesStore = tx.objectStore(STORE_LEAVES);
|
|
652
|
+
const ancestorsStore = tx.objectStore(STORE_ANCESTORS);
|
|
653
|
+
const reservationsStore = tx.objectStore(STORE_RESERVATIONS);
|
|
654
|
+
const spentStore = tx.objectStore(STORE_SPENT);
|
|
655
|
+
const nowMs = this._nowMs();
|
|
656
|
+
const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
|
|
657
|
+
const ancestorRowsByLeaf = this._ancestorRowsByLeaf(res.ancestors);
|
|
658
|
+
for (const l of res.leaves) if (l.reservation_id === reservationId) {
|
|
659
|
+
spentStore.put({
|
|
660
|
+
id: l.id,
|
|
661
|
+
spent_at: nowMs
|
|
662
|
+
});
|
|
663
|
+
leavesStore.delete(l.id);
|
|
664
|
+
leafMap.delete(l.id);
|
|
665
|
+
for (const existingId of (ancestorRowsByLeaf.get(l.id) || /* @__PURE__ */ new Map()).keys()) ancestorsStore.delete([l.id, existingId]);
|
|
666
|
+
}
|
|
667
|
+
change.concat(reserved);
|
|
668
|
+
for (const p of change) {
|
|
669
|
+
const row = this._leafRow(p, false, leafMap.get(p.id));
|
|
670
|
+
leavesStore.put(row);
|
|
671
|
+
leafMap.set(row.id, row);
|
|
672
|
+
}
|
|
673
|
+
for (const p of reserved) {
|
|
674
|
+
const row = this._leafRow(p, false, leafMap.get(p.id));
|
|
675
|
+
row.reservation_id = reservationId;
|
|
676
|
+
leavesStore.put(row);
|
|
677
|
+
leafMap.set(row.id, row);
|
|
678
|
+
}
|
|
679
|
+
reservationsStore.put({
|
|
680
|
+
...res.res,
|
|
681
|
+
pending_change_amount: 0
|
|
682
|
+
});
|
|
683
|
+
return {
|
|
684
|
+
id: reservationId,
|
|
685
|
+
leaves: reserved
|
|
686
|
+
};
|
|
687
|
+
});
|
|
688
|
+
} catch (error) {
|
|
689
|
+
if (error instanceof TreeStoreError) throw error;
|
|
690
|
+
throw new TreeStoreError(`Failed to update reservation '${reservationId}': ${error.message}`, error);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async tryReserveLeaves(targetAmounts, exactOnly, purpose) {
|
|
694
|
+
try {
|
|
695
|
+
return await this._txRun([STORE_LEAVES, STORE_RESERVATIONS], "readwrite", [{
|
|
696
|
+
name: "leaves",
|
|
697
|
+
store: STORE_LEAVES
|
|
698
|
+
}, {
|
|
699
|
+
name: "reservations",
|
|
700
|
+
store: STORE_RESERVATIONS
|
|
701
|
+
}], (res, tx) => {
|
|
702
|
+
const leavesStore = tx.objectStore(STORE_LEAVES);
|
|
703
|
+
const reservationsStore = tx.objectStore(STORE_RESERVATIONS);
|
|
704
|
+
const targetAmount = targetAmounts ? this._totalSats(targetAmounts) : 0;
|
|
705
|
+
const maxTarget = this._maxTargetForPrefilter(targetAmounts);
|
|
706
|
+
const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
|
|
707
|
+
const eligible = this._eligibleSlim(res.leaves);
|
|
708
|
+
const available = eligible.reduce((s, l) => s + l.value, 0);
|
|
709
|
+
const slim = this._slimCandidates(eligible, maxTarget);
|
|
710
|
+
const pending = res.reservations.reduce((s, r) => s + (r.pending_change_amount || 0), 0);
|
|
711
|
+
const selected = this._selectLeavesByTargetAmounts(slim, targetAmounts);
|
|
712
|
+
if (selected !== null) {
|
|
713
|
+
if (selected.length === 0) throw new TreeStoreError("NonReservableLeaves");
|
|
714
|
+
const ids = selected.map((s) => s.id);
|
|
715
|
+
const fullLeaves = ids.map((leafId) => leafMap.get(leafId).data);
|
|
716
|
+
const reservationId = this._generateId();
|
|
717
|
+
this._createReservation(reservationsStore, leavesStore, leafMap, reservationId, ids, purpose, 0);
|
|
718
|
+
return {
|
|
719
|
+
type: "success",
|
|
720
|
+
reservation: {
|
|
721
|
+
id: reservationId,
|
|
722
|
+
leaves: fullLeaves
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
if (!exactOnly) {
|
|
727
|
+
const minSelected = this._selectLeavesByMinimumAmount(slim, targetAmount);
|
|
728
|
+
if (minSelected !== null) {
|
|
729
|
+
const ids = minSelected.map((s) => s.id);
|
|
730
|
+
const fullLeaves = ids.map((leafId) => leafMap.get(leafId).data);
|
|
731
|
+
const reservedAmount = fullLeaves.reduce((s, l) => s + l.value, 0);
|
|
732
|
+
const pendingChange = reservedAmount > targetAmount && targetAmount > 0 ? reservedAmount - targetAmount : 0;
|
|
733
|
+
const reservationId = this._generateId();
|
|
734
|
+
this._createReservation(reservationsStore, leavesStore, leafMap, reservationId, ids, purpose, pendingChange);
|
|
735
|
+
return {
|
|
736
|
+
type: "success",
|
|
737
|
+
reservation: {
|
|
738
|
+
id: reservationId,
|
|
739
|
+
leaves: fullLeaves
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (available + pending >= targetAmount) return {
|
|
745
|
+
type: "waitForPending",
|
|
746
|
+
needed: targetAmount,
|
|
747
|
+
available,
|
|
748
|
+
pending
|
|
749
|
+
};
|
|
750
|
+
return { type: "insufficientFunds" };
|
|
751
|
+
});
|
|
752
|
+
} catch (error) {
|
|
753
|
+
if (error instanceof TreeStoreError) throw error;
|
|
754
|
+
throw new TreeStoreError(`Failed to try reserve leaves: ${error.message}`, error);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
async tryReserveLeavesByIds(leafIds, purpose) {
|
|
758
|
+
try {
|
|
759
|
+
if (!leafIds || leafIds.length === 0) throw new TreeStoreError("NonReservableLeaves");
|
|
760
|
+
return await this._txRun([STORE_LEAVES, STORE_RESERVATIONS], "readwrite", [{
|
|
761
|
+
name: "leaves",
|
|
762
|
+
store: STORE_LEAVES
|
|
763
|
+
}], (res, tx) => {
|
|
764
|
+
const leavesStore = tx.objectStore(STORE_LEAVES);
|
|
765
|
+
const reservationsStore = tx.objectStore(STORE_RESERVATIONS);
|
|
766
|
+
const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
|
|
767
|
+
const eligibleIds = new Set(res.leaves.filter((r) => r.status === "Available" && !r.is_missing_from_operators && r.reservation_id == null).map((r) => r.id));
|
|
768
|
+
if (new Set(leafIds.filter((id) => eligibleIds.has(id))).size !== leafIds.length) throw new TreeStoreError("NonReservableLeaves");
|
|
769
|
+
const fullLeaves = leafIds.map((id) => leafMap.get(id).data);
|
|
770
|
+
const reservationId = this._generateId();
|
|
771
|
+
this._createReservation(reservationsStore, leavesStore, leafMap, reservationId, leafIds, purpose, 0);
|
|
772
|
+
return {
|
|
773
|
+
id: reservationId,
|
|
774
|
+
leaves: fullLeaves
|
|
775
|
+
};
|
|
776
|
+
});
|
|
777
|
+
} catch (error) {
|
|
778
|
+
if (error instanceof TreeStoreError) throw error;
|
|
779
|
+
throw new TreeStoreError(`Failed to try reserve leaves by ids: ${error.message}`, error);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async trySelectLeaves(targetAmounts) {
|
|
783
|
+
try {
|
|
784
|
+
const targetAmount = targetAmounts ? this._totalSats(targetAmounts) : 0;
|
|
785
|
+
const maxTarget = this._maxTargetForPrefilter(targetAmounts);
|
|
786
|
+
return await this._txRun([STORE_LEAVES], "readonly", [{
|
|
787
|
+
name: "leaves",
|
|
788
|
+
store: STORE_LEAVES
|
|
789
|
+
}], (res) => {
|
|
790
|
+
const leafMap = new Map(res.leaves.map((r) => [r.id, r]));
|
|
791
|
+
const slim = this._slimCandidates(this._eligibleSlim(res.leaves), maxTarget);
|
|
792
|
+
const selected = this._selectLeavesByTargetAmounts(slim, targetAmounts);
|
|
793
|
+
if (selected !== null && selected.length > 0) return {
|
|
794
|
+
type: "exact",
|
|
795
|
+
leaves: selected.map((s) => leafMap.get(s.id).data)
|
|
796
|
+
};
|
|
797
|
+
const minSelected = this._selectLeavesByMinimumAmount(slim, targetAmount);
|
|
798
|
+
if (minSelected !== null) return {
|
|
799
|
+
type: "swapNeeded",
|
|
800
|
+
leaves: minSelected.map((s) => leafMap.get(s.id).data)
|
|
801
|
+
};
|
|
802
|
+
return { type: "insufficientFunds" };
|
|
803
|
+
});
|
|
804
|
+
} catch (error) {
|
|
805
|
+
if (error instanceof TreeStoreError) throw error;
|
|
806
|
+
throw new TreeStoreError(`Failed to try select leaves: ${error.message}`, error);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
_nowMs() {
|
|
810
|
+
return Date.now();
|
|
811
|
+
}
|
|
812
|
+
_generateId() {
|
|
813
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
|
|
814
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
815
|
+
const r = Math.random() * 16 | 0;
|
|
816
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
/** Row shape for the leaf pool. Preserves `reservation_id` from an existing
|
|
820
|
+
* row; every other field comes from the operators' latest copy. */
|
|
821
|
+
_leafRow(node, isMissing, existingRow, chainComplete) {
|
|
822
|
+
return {
|
|
823
|
+
id: node.id,
|
|
824
|
+
parent_node_id: node.parent_node_id ?? null,
|
|
825
|
+
status: node.status,
|
|
826
|
+
value: node.value,
|
|
827
|
+
verifying_public_key: node.verifying_public_key,
|
|
828
|
+
signing_public_key: node.signing_keyshare.public_key,
|
|
829
|
+
is_missing_from_operators: !!isMissing,
|
|
830
|
+
reservation_id: existingRow ? existingRow.reservation_id ?? null : null,
|
|
831
|
+
added_at: this._nowMs(),
|
|
832
|
+
chain_complete: (chainComplete !== void 0 ? chainComplete : this._chainComplete(node, void 0, existingRow)) ? 1 : 0,
|
|
833
|
+
data: node
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Whether `node` has a stored chain reaching its current parent, which is what
|
|
838
|
+
* makes it exitable without the operators. A stored chain runs from some parent
|
|
839
|
+
* to a root, so holding the parent the leaf has now means it spans the whole
|
|
840
|
+
* path. A leaf that is itself a root needs no ancestors. An empty incoming
|
|
841
|
+
* chain means "unknown", not "none", so it keeps whatever the row already
|
|
842
|
+
* claimed, unless the leaf has been reparented since: the chain then describes
|
|
843
|
+
* the parent it had before.
|
|
844
|
+
*/
|
|
845
|
+
_chainComplete(node, ancestors, existingRow) {
|
|
846
|
+
if (node.parent_node_id == null) return true;
|
|
847
|
+
if (ancestors && ancestors.length > 0) return ancestors.some((a) => a.id === node.parent_node_id);
|
|
848
|
+
if (!existingRow) return false;
|
|
849
|
+
return existingRow.parent_node_id === node.parent_node_id && !!existingRow.chain_complete;
|
|
850
|
+
}
|
|
851
|
+
/** Row shape for an ancestor: no pool metadata (reservation / missing / added_at).
|
|
852
|
+
* `leaf_id` plus `id` is the key, so the same node stores one row per leaf
|
|
853
|
+
* that descends from it rather than one row shared by all of them. */
|
|
854
|
+
_ancestorRow(leafId, node) {
|
|
855
|
+
return {
|
|
856
|
+
leaf_id: leafId,
|
|
857
|
+
id: node.id,
|
|
858
|
+
parent_node_id: node.parent_node_id ?? null,
|
|
859
|
+
status: node.status,
|
|
860
|
+
value: node.value,
|
|
861
|
+
verifying_public_key: node.verifying_public_key,
|
|
862
|
+
signing_public_key: node.signing_keyshare.public_key,
|
|
863
|
+
data: node
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/** Groups ancestor rows (from a full-store read) by the leaf that owns them,
|
|
867
|
+
* as `Map<leaf_id, Map<id, row>>`, so a leaf's existing rows can be found
|
|
868
|
+
* and deleted by key without a range scan on the compound `[leaf_id, id]`
|
|
869
|
+
* keyPath. */
|
|
870
|
+
_ancestorRowsByLeaf(ancestorRows) {
|
|
871
|
+
const map = /* @__PURE__ */ new Map();
|
|
872
|
+
for (const row of ancestorRows || []) {
|
|
873
|
+
let byId = map.get(row.leaf_id);
|
|
874
|
+
if (!byId) {
|
|
875
|
+
byId = /* @__PURE__ */ new Map();
|
|
876
|
+
map.set(row.leaf_id, byId);
|
|
877
|
+
}
|
|
878
|
+
byId.set(row.id, row);
|
|
879
|
+
}
|
|
880
|
+
return map;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Replaces `leafId`'s ancestor rows wholesale (delete then insert), keeping
|
|
884
|
+
* `ancestorRowsByLeaf` current so a later step in the same transaction sees
|
|
885
|
+
* this leaf's live rows rather than a stale snapshot. An empty `nodes` list
|
|
886
|
+
* is a no-op: it means the chain is unknown, not that the leaf has none.
|
|
887
|
+
*/
|
|
888
|
+
_replaceAncestors(ancestorsStore, ancestorRowsByLeaf, leafId, nodes) {
|
|
889
|
+
if (!nodes || nodes.length === 0) return;
|
|
890
|
+
const ownRows = ancestorRowsByLeaf.get(leafId);
|
|
891
|
+
if (ownRows) for (const existingId of ownRows.keys()) ancestorsStore.delete([leafId, existingId]);
|
|
892
|
+
const newRows = /* @__PURE__ */ new Map();
|
|
893
|
+
for (const node of nodes) {
|
|
894
|
+
const row = this._ancestorRow(leafId, node);
|
|
895
|
+
ancestorsStore.put(row);
|
|
896
|
+
newRows.set(node.id, row);
|
|
897
|
+
}
|
|
898
|
+
ancestorRowsByLeaf.set(leafId, newRows);
|
|
899
|
+
}
|
|
900
|
+
_createReservation(reservationsStore, leavesStore, leafMap, reservationId, leafIds, purpose, pendingChange) {
|
|
901
|
+
reservationsStore.put({
|
|
902
|
+
id: reservationId,
|
|
903
|
+
purpose,
|
|
904
|
+
pending_change_amount: pendingChange,
|
|
905
|
+
created_at: this._nowMs()
|
|
906
|
+
});
|
|
907
|
+
for (const id of leafIds) {
|
|
908
|
+
const row = leafMap.get(id);
|
|
909
|
+
if (row) {
|
|
910
|
+
row.reservation_id = reservationId;
|
|
911
|
+
leavesStore.put(row);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
/** Slim `{id, value}` projection of the leaves eligible for selection. */
|
|
916
|
+
_eligibleSlim(leafRows) {
|
|
917
|
+
return leafRows.filter((r) => r.status === "Available" && !r.is_missing_from_operators && r.reservation_id == null).map((r) => ({
|
|
918
|
+
id: r.id,
|
|
919
|
+
value: r.value
|
|
920
|
+
}));
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Prefilter mirroring the SQL slim candidate set: every eligible leaf with
|
|
924
|
+
* value <= maxTarget, plus the single smallest leaf with value > maxTarget
|
|
925
|
+
* (the minimum-amount fallback where one larger leaf alone suffices).
|
|
926
|
+
*/
|
|
927
|
+
_slimCandidates(eligible, maxTarget) {
|
|
928
|
+
const small = eligible.filter((l) => l.value <= maxTarget);
|
|
929
|
+
let smallestBig = null;
|
|
930
|
+
for (const l of eligible) if (l.value > maxTarget && (smallestBig === null || l.value < smallestBig.value)) smallestBig = l;
|
|
931
|
+
return smallestBig ? [...small, smallestBig] : small;
|
|
932
|
+
}
|
|
933
|
+
_maxTargetForPrefilter(targetAmounts) {
|
|
934
|
+
if (!targetAmounts) return Number.MAX_SAFE_INTEGER;
|
|
935
|
+
if (targetAmounts.type === "amountAndFee") return targetAmounts.amountSats + (targetAmounts.feeSats || 0);
|
|
936
|
+
if (targetAmounts.type === "exactDenominations") return targetAmounts.denominations.reduce((m, v) => m + v, 0);
|
|
937
|
+
return Number.MAX_SAFE_INTEGER;
|
|
938
|
+
}
|
|
939
|
+
_totalSats(targetAmounts) {
|
|
940
|
+
if (targetAmounts.type === "amountAndFee") return targetAmounts.amountSats + (targetAmounts.feeSats || 0);
|
|
941
|
+
if (targetAmounts.type === "exactDenominations") return targetAmounts.denominations.reduce((sum, d) => sum + d, 0);
|
|
942
|
+
return 0;
|
|
943
|
+
}
|
|
944
|
+
_selectLeavesByTargetAmounts(leaves, targetAmounts) {
|
|
945
|
+
if (!targetAmounts) return [...leaves];
|
|
946
|
+
if (targetAmounts.type === "amountAndFee") {
|
|
947
|
+
const amountLeaves = this._selectLeavesByExactAmount(leaves, targetAmounts.amountSats);
|
|
948
|
+
if (amountLeaves === null) return null;
|
|
949
|
+
if (targetAmounts.feeSats != null && targetAmounts.feeSats > 0) {
|
|
950
|
+
const amountIds = new Set(amountLeaves.map((l) => l.id));
|
|
951
|
+
const remaining = leaves.filter((l) => !amountIds.has(l.id));
|
|
952
|
+
const feeLeaves = this._selectLeavesByExactAmount(remaining, targetAmounts.feeSats);
|
|
953
|
+
if (feeLeaves === null) return null;
|
|
954
|
+
return [...amountLeaves, ...feeLeaves];
|
|
955
|
+
}
|
|
956
|
+
return amountLeaves;
|
|
957
|
+
}
|
|
958
|
+
if (targetAmounts.type === "exactDenominations") return this._selectLeavesByExactDenominations(leaves, targetAmounts.denominations);
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
_selectLeavesByExactAmount(leaves, targetAmount) {
|
|
962
|
+
if (targetAmount === 0) return null;
|
|
963
|
+
if (leaves.reduce((sum, l) => sum + l.value, 0) < targetAmount) return null;
|
|
964
|
+
const single = leaves.find((l) => l.value === targetAmount);
|
|
965
|
+
if (single) return [single];
|
|
966
|
+
return this._findExactMultipleMatch(leaves, targetAmount);
|
|
967
|
+
}
|
|
968
|
+
_selectLeavesByExactDenominations(leaves, denominations) {
|
|
969
|
+
const remaining = [...leaves];
|
|
970
|
+
const selected = [];
|
|
971
|
+
for (const denomination of denominations) {
|
|
972
|
+
const idx = remaining.findIndex((l) => l.value === denomination);
|
|
973
|
+
if (idx === -1) return null;
|
|
974
|
+
selected.push(remaining[idx]);
|
|
975
|
+
remaining.splice(idx, 1);
|
|
976
|
+
}
|
|
977
|
+
return selected;
|
|
978
|
+
}
|
|
979
|
+
_selectLeavesByMinimumAmount(leaves, targetAmount) {
|
|
980
|
+
if (targetAmount === 0) return null;
|
|
981
|
+
if (leaves.reduce((sum, l) => sum + l.value, 0) < targetAmount) return null;
|
|
982
|
+
const result = [];
|
|
983
|
+
let sum = 0;
|
|
984
|
+
for (const leaf of leaves) {
|
|
985
|
+
sum += leaf.value;
|
|
986
|
+
result.push(leaf);
|
|
987
|
+
if (sum >= targetAmount) break;
|
|
988
|
+
}
|
|
989
|
+
return sum >= targetAmount ? result : null;
|
|
990
|
+
}
|
|
991
|
+
_findExactMultipleMatch(leaves, targetAmount) {
|
|
992
|
+
if (targetAmount === 0) return [];
|
|
993
|
+
if (leaves.length === 0) return null;
|
|
994
|
+
const result = this._greedyExactMatch(leaves, targetAmount);
|
|
995
|
+
if (result) return result;
|
|
996
|
+
const powerOfTwoLeaves = leaves.filter((l) => this._isPowerOfTwo(l.value));
|
|
997
|
+
if (powerOfTwoLeaves.length === leaves.length) return null;
|
|
998
|
+
return this._greedyExactMatch(powerOfTwoLeaves, targetAmount);
|
|
999
|
+
}
|
|
1000
|
+
_greedyExactMatch(leaves, targetAmount) {
|
|
1001
|
+
const sorted = [...leaves].sort((a, b) => b.value - a.value);
|
|
1002
|
+
const result = [];
|
|
1003
|
+
let remaining = targetAmount;
|
|
1004
|
+
for (const leaf of sorted) {
|
|
1005
|
+
if (leaf.value > remaining) continue;
|
|
1006
|
+
remaining -= leaf.value;
|
|
1007
|
+
result.push(leaf);
|
|
1008
|
+
if (remaining === 0) return result;
|
|
1009
|
+
}
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
_isPowerOfTwo(value) {
|
|
1013
|
+
return value > 0 && (value & value - 1) === 0;
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
/**
|
|
1017
|
+
* Opens (or creates) the IndexedDB database and returns a ready tree store.
|
|
1018
|
+
*
|
|
1019
|
+
* @param {string} dbName - Database name (one database per SDK instance).
|
|
1020
|
+
* @param {object} [logger] - Optional logger.
|
|
1021
|
+
* @returns {Promise<WebTreeStore>}
|
|
1022
|
+
*/
|
|
1023
|
+
async function createWebTreeStore(dbName, logger = null) {
|
|
1024
|
+
const store = new WebTreeStore(dbName, logger);
|
|
1025
|
+
await store.initialize();
|
|
1026
|
+
return store;
|
|
1027
|
+
}
|
|
1028
|
+
//#endregion
|
|
1029
|
+
export { createWebTreeStore };
|
|
1030
|
+
|
|
1031
|
+
//# sourceMappingURL=tree-store-N362nuzZ.js.map
|