@arkade-os/sdk 0.4.0-next.0 → 0.4.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +127 -24
  2. package/dist/cjs/bip322/index.js +270 -0
  3. package/dist/cjs/index.js +4 -2
  4. package/dist/cjs/intent/index.js +19 -9
  5. package/dist/cjs/repositories/indexedDB/contractRepository.js +1 -1
  6. package/dist/cjs/repositories/indexedDB/db.js +8 -46
  7. package/dist/cjs/repositories/indexedDB/walletRepository.js +1 -1
  8. package/dist/cjs/repositories/realm/contractRepository.js +120 -0
  9. package/dist/cjs/repositories/realm/index.js +9 -0
  10. package/dist/cjs/repositories/realm/schemas.js +108 -0
  11. package/dist/cjs/repositories/realm/types.js +7 -0
  12. package/dist/cjs/repositories/realm/walletRepository.js +273 -0
  13. package/dist/cjs/repositories/serialization.js +49 -0
  14. package/dist/cjs/repositories/sqlite/contractRepository.js +139 -0
  15. package/dist/cjs/repositories/sqlite/index.js +7 -0
  16. package/dist/cjs/repositories/sqlite/types.js +2 -0
  17. package/dist/cjs/repositories/sqlite/walletRepository.js +328 -0
  18. package/dist/cjs/wallet/serviceWorker/wallet.js +9 -1
  19. package/dist/cjs/wallet/vtxo-manager.js +7 -0
  20. package/dist/cjs/worker/messageBus.js +22 -2
  21. package/dist/esm/bip322/index.js +267 -0
  22. package/dist/esm/index.js +4 -1
  23. package/dist/esm/intent/index.js +16 -7
  24. package/dist/esm/repositories/indexedDB/contractRepository.js +1 -1
  25. package/dist/esm/repositories/indexedDB/db.js +2 -40
  26. package/dist/esm/repositories/indexedDB/walletRepository.js +1 -1
  27. package/dist/esm/repositories/realm/contractRepository.js +116 -0
  28. package/dist/esm/repositories/realm/index.js +3 -0
  29. package/dist/esm/repositories/realm/schemas.js +105 -0
  30. package/dist/esm/repositories/realm/types.js +6 -0
  31. package/dist/esm/repositories/realm/walletRepository.js +269 -0
  32. package/dist/esm/repositories/serialization.js +40 -0
  33. package/dist/esm/repositories/sqlite/contractRepository.js +135 -0
  34. package/dist/esm/repositories/sqlite/index.js +2 -0
  35. package/dist/esm/repositories/sqlite/types.js +1 -0
  36. package/dist/esm/repositories/sqlite/walletRepository.js +324 -0
  37. package/dist/esm/wallet/serviceWorker/wallet.js +9 -1
  38. package/dist/esm/wallet/vtxo-manager.js +7 -0
  39. package/dist/esm/worker/messageBus.js +22 -2
  40. package/dist/types/bip322/index.d.ts +55 -0
  41. package/dist/types/index.d.ts +3 -2
  42. package/dist/types/intent/index.d.ts +13 -0
  43. package/dist/types/repositories/indexedDB/db.d.ts +2 -54
  44. package/dist/types/repositories/realm/contractRepository.d.ts +24 -0
  45. package/dist/types/repositories/realm/index.d.ts +4 -0
  46. package/dist/types/repositories/realm/schemas.d.ts +208 -0
  47. package/dist/types/repositories/realm/types.d.ts +16 -0
  48. package/dist/types/repositories/realm/walletRepository.d.ts +31 -0
  49. package/dist/types/repositories/serialization.d.ts +40 -0
  50. package/dist/types/repositories/sqlite/contractRepository.d.ts +33 -0
  51. package/dist/types/repositories/sqlite/index.d.ts +3 -0
  52. package/dist/types/repositories/sqlite/types.d.ts +18 -0
  53. package/dist/types/repositories/sqlite/walletRepository.d.ts +40 -0
  54. package/package.json +18 -14
  55. package/dist/cjs/adapters/expo-db.js +0 -35
  56. package/dist/esm/adapters/expo-db.js +0 -27
  57. package/dist/types/adapters/expo-db.d.ts +0 -7
  58. /package/dist/cjs/{db → repositories/indexedDB}/manager.js +0 -0
  59. /package/dist/esm/{db → repositories/indexedDB}/manager.js +0 -0
  60. /package/dist/types/{db → repositories/indexedDB}/manager.d.ts +0 -0
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SQLiteContractRepository = void 0;
4
+ /**
5
+ * SQLite-based implementation of ContractRepository.
6
+ *
7
+ * Uses the SQLExecutor interface so consumers can plug in any SQLite driver
8
+ * (expo-sqlite, better-sqlite3, etc.).
9
+ *
10
+ * Tables are created lazily on first operation via `ensureInit()`.
11
+ * The consumer owns the SQLExecutor lifecycle — `[Symbol.asyncDispose]` is a no-op.
12
+ */
13
+ class SQLiteContractRepository {
14
+ constructor(db, options) {
15
+ this.db = db;
16
+ this.version = 1;
17
+ this.initPromise = null;
18
+ this.prefix = sanitizePrefix(options?.prefix ?? "ark_");
19
+ this.table = `${this.prefix}contracts`;
20
+ }
21
+ // ── Lifecycle ──────────────────────────────────────────────────────
22
+ ensureInit() {
23
+ if (!this.initPromise) {
24
+ this.initPromise = this.init();
25
+ }
26
+ return this.initPromise;
27
+ }
28
+ async init() {
29
+ await this.db.run(`
30
+ CREATE TABLE IF NOT EXISTS ${this.table} (
31
+ script TEXT PRIMARY KEY,
32
+ address TEXT NOT NULL,
33
+ type TEXT NOT NULL,
34
+ state TEXT NOT NULL,
35
+ params_json TEXT NOT NULL,
36
+ created_at INTEGER NOT NULL,
37
+ expires_at INTEGER,
38
+ label TEXT,
39
+ metadata_json TEXT
40
+ )
41
+ `);
42
+ await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}contracts_type ON ${this.table} (type)`);
43
+ await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}contracts_state ON ${this.table} (state)`);
44
+ }
45
+ async [Symbol.asyncDispose]() {
46
+ // no-op — consumer owns the SQLExecutor lifecycle
47
+ }
48
+ // ── Clear ──────────────────────────────────────────────────────────
49
+ async clear() {
50
+ await this.ensureInit();
51
+ await this.db.run(`DELETE FROM ${this.table}`);
52
+ }
53
+ // ── Contract management ────────────────────────────────────────────
54
+ async getContracts(filter) {
55
+ await this.ensureInit();
56
+ const conditions = [];
57
+ const params = [];
58
+ if (filter) {
59
+ this.addFilterCondition(conditions, params, "script", filter.script);
60
+ this.addFilterCondition(conditions, params, "state", filter.state);
61
+ this.addFilterCondition(conditions, params, "type", filter.type);
62
+ }
63
+ let sql = `SELECT * FROM ${this.table}`;
64
+ if (conditions.length > 0) {
65
+ sql += ` WHERE ${conditions.join(" AND ")}`;
66
+ }
67
+ const rows = await this.db.all(sql, params);
68
+ return rows.map(contractRowToDomain);
69
+ }
70
+ async saveContract(contract) {
71
+ await this.ensureInit();
72
+ await this.db.run(`INSERT OR REPLACE INTO ${this.table}
73
+ (script, address, type, state, params_json,
74
+ created_at, expires_at, label, metadata_json)
75
+ VALUES (?, ?, ?, ?, ?,
76
+ ?, ?, ?, ?)`, [
77
+ contract.script,
78
+ contract.address,
79
+ contract.type,
80
+ contract.state,
81
+ JSON.stringify(contract.params),
82
+ contract.createdAt,
83
+ contract.expiresAt ?? null,
84
+ contract.label ?? null,
85
+ contract.metadata ? JSON.stringify(contract.metadata) : null,
86
+ ]);
87
+ }
88
+ async deleteContract(script) {
89
+ await this.ensureInit();
90
+ await this.db.run(`DELETE FROM ${this.table} WHERE script = ?`, [
91
+ script,
92
+ ]);
93
+ }
94
+ // ── Helpers ─────────────────────────────────────────────────────────
95
+ addFilterCondition(conditions, params, column, value) {
96
+ if (value === undefined)
97
+ return;
98
+ if (Array.isArray(value)) {
99
+ if (value.length === 0)
100
+ return;
101
+ const placeholders = value.map(() => "?").join(", ");
102
+ conditions.push(`${column} IN (${placeholders})`);
103
+ params.push(...value);
104
+ }
105
+ else {
106
+ conditions.push(`${column} = ?`);
107
+ params.push(value);
108
+ }
109
+ }
110
+ }
111
+ exports.SQLiteContractRepository = SQLiteContractRepository;
112
+ // ── Row → Domain converter ──────────────────────────────────────────────
113
+ const SAFE_PREFIX = /^[a-zA-Z0-9_]+$/;
114
+ function sanitizePrefix(prefix) {
115
+ if (!SAFE_PREFIX.test(prefix)) {
116
+ throw new Error(`Invalid table prefix "${prefix}": only letters, digits, and underscores are allowed`);
117
+ }
118
+ return prefix;
119
+ }
120
+ function contractRowToDomain(row) {
121
+ const contract = {
122
+ script: row.script,
123
+ address: row.address,
124
+ type: row.type,
125
+ state: row.state,
126
+ params: JSON.parse(row.params_json),
127
+ createdAt: row.created_at,
128
+ };
129
+ if (row.expires_at !== null) {
130
+ contract.expiresAt = row.expires_at;
131
+ }
132
+ if (row.label !== null) {
133
+ contract.label = row.label;
134
+ }
135
+ if (row.metadata_json !== null) {
136
+ contract.metadata = JSON.parse(row.metadata_json);
137
+ }
138
+ return contract;
139
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SQLiteContractRepository = exports.SQLiteWalletRepository = void 0;
4
+ var walletRepository_1 = require("./walletRepository");
5
+ Object.defineProperty(exports, "SQLiteWalletRepository", { enumerable: true, get: function () { return walletRepository_1.SQLiteWalletRepository; } });
6
+ var contractRepository_1 = require("./contractRepository");
7
+ Object.defineProperty(exports, "SQLiteContractRepository", { enumerable: true, get: function () { return contractRepository_1.SQLiteContractRepository; } });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,328 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SQLiteWalletRepository = void 0;
4
+ const serialization_1 = require("../serialization");
5
+ /**
6
+ * SQLite-based implementation of WalletRepository.
7
+ *
8
+ * Uses the SQLExecutor interface so consumers can plug in any SQLite driver
9
+ * (expo-sqlite, better-sqlite3, etc.).
10
+ *
11
+ * Tables are created lazily on first operation via `ensureInit()`.
12
+ * The consumer owns the SQLExecutor lifecycle — `[Symbol.asyncDispose]` is a no-op.
13
+ */
14
+ class SQLiteWalletRepository {
15
+ constructor(db, options) {
16
+ this.db = db;
17
+ this.version = 1;
18
+ this.initPromise = null;
19
+ this.prefix = sanitizePrefix(options?.prefix ?? "ark_");
20
+ this.tables = {
21
+ vtxos: `${this.prefix}vtxos`,
22
+ utxos: `${this.prefix}utxos`,
23
+ transactions: `${this.prefix}transactions`,
24
+ walletState: `${this.prefix}wallet_state`,
25
+ };
26
+ }
27
+ // ── Lifecycle ──────────────────────────────────────────────────────
28
+ ensureInit() {
29
+ if (!this.initPromise) {
30
+ this.initPromise = this.init();
31
+ }
32
+ return this.initPromise;
33
+ }
34
+ async init() {
35
+ await this.db.run(`
36
+ CREATE TABLE IF NOT EXISTS ${this.tables.vtxos} (
37
+ txid TEXT NOT NULL,
38
+ vout INTEGER NOT NULL,
39
+ value INTEGER NOT NULL,
40
+ address TEXT NOT NULL,
41
+ tap_tree TEXT NOT NULL,
42
+ forfeit_cb TEXT NOT NULL,
43
+ forfeit_s TEXT NOT NULL,
44
+ intent_cb TEXT NOT NULL,
45
+ intent_s TEXT NOT NULL,
46
+ status_json TEXT NOT NULL,
47
+ virtual_status_json TEXT NOT NULL,
48
+ created_at TEXT NOT NULL,
49
+ is_unrolled INTEGER NOT NULL DEFAULT 0,
50
+ is_spent INTEGER,
51
+ spent_by TEXT,
52
+ settled_by TEXT,
53
+ ark_tx_id TEXT,
54
+ extra_witness_json TEXT,
55
+ assets_json TEXT,
56
+ PRIMARY KEY (txid, vout)
57
+ )
58
+ `);
59
+ await this.db.run(`
60
+ CREATE TABLE IF NOT EXISTS ${this.tables.utxos} (
61
+ txid TEXT NOT NULL,
62
+ vout INTEGER NOT NULL,
63
+ value INTEGER NOT NULL,
64
+ address TEXT NOT NULL,
65
+ tap_tree TEXT NOT NULL,
66
+ forfeit_cb TEXT NOT NULL,
67
+ forfeit_s TEXT NOT NULL,
68
+ intent_cb TEXT NOT NULL,
69
+ intent_s TEXT NOT NULL,
70
+ status_json TEXT NOT NULL,
71
+ extra_witness_json TEXT,
72
+ PRIMARY KEY (txid, vout)
73
+ )
74
+ `);
75
+ await this.db.run(`
76
+ CREATE TABLE IF NOT EXISTS ${this.tables.transactions} (
77
+ address TEXT NOT NULL,
78
+ boarding_txid TEXT NOT NULL,
79
+ commitment_txid TEXT NOT NULL,
80
+ ark_txid TEXT NOT NULL,
81
+ type TEXT NOT NULL,
82
+ amount INTEGER NOT NULL,
83
+ settled INTEGER NOT NULL DEFAULT 0,
84
+ created_at INTEGER NOT NULL,
85
+ assets_json TEXT,
86
+ PRIMARY KEY (address, boarding_txid, commitment_txid, ark_txid)
87
+ )
88
+ `);
89
+ await this.db.run(`
90
+ CREATE TABLE IF NOT EXISTS ${this.tables.walletState} (
91
+ key TEXT PRIMARY KEY,
92
+ last_sync_time INTEGER,
93
+ settings_json TEXT
94
+ )
95
+ `);
96
+ await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}vtxos_address ON ${this.tables.vtxos} (address)`);
97
+ await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}utxos_address ON ${this.tables.utxos} (address)`);
98
+ await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}transactions_address ON ${this.tables.transactions} (address)`);
99
+ }
100
+ async [Symbol.asyncDispose]() {
101
+ // no-op — consumer owns the SQLExecutor lifecycle
102
+ }
103
+ // ── Clear ──────────────────────────────────────────────────────────
104
+ async clear() {
105
+ await this.ensureInit();
106
+ await this.db.run(`DELETE FROM ${this.tables.vtxos}`);
107
+ await this.db.run(`DELETE FROM ${this.tables.utxos}`);
108
+ await this.db.run(`DELETE FROM ${this.tables.transactions}`);
109
+ await this.db.run(`DELETE FROM ${this.tables.walletState}`);
110
+ }
111
+ // ── VTXO management ────────────────────────────────────────────────
112
+ async getVtxos(address) {
113
+ await this.ensureInit();
114
+ const rows = await this.db.all(`SELECT * FROM ${this.tables.vtxos} WHERE address = ?`, [address]);
115
+ return rows.map(vtxoRowToDomain);
116
+ }
117
+ async saveVtxos(address, vtxos) {
118
+ await this.ensureInit();
119
+ for (const vtxo of vtxos) {
120
+ const s = (0, serialization_1.serializeVtxo)(vtxo);
121
+ await this.db.run(`INSERT OR REPLACE INTO ${this.tables.vtxos}
122
+ (txid, vout, value, address,
123
+ tap_tree, forfeit_cb, forfeit_s, intent_cb, intent_s,
124
+ status_json, virtual_status_json, created_at,
125
+ is_unrolled, is_spent, spent_by, settled_by, ark_tx_id,
126
+ extra_witness_json, assets_json)
127
+ VALUES (?, ?, ?, ?,
128
+ ?, ?, ?, ?, ?,
129
+ ?, ?, ?,
130
+ ?, ?, ?, ?, ?,
131
+ ?, ?)`, [
132
+ s.txid,
133
+ s.vout,
134
+ s.value,
135
+ address,
136
+ s.tapTree,
137
+ s.forfeitTapLeafScript.cb,
138
+ s.forfeitTapLeafScript.s,
139
+ s.intentTapLeafScript.cb,
140
+ s.intentTapLeafScript.s,
141
+ JSON.stringify(s.status),
142
+ JSON.stringify(s.virtualStatus),
143
+ typeof s.createdAt === "string"
144
+ ? s.createdAt
145
+ : s.createdAt instanceof Date
146
+ ? s.createdAt.toISOString()
147
+ : new Date(s.createdAt).toISOString(),
148
+ s.isUnrolled ? 1 : 0,
149
+ s.isSpent === undefined ? null : s.isSpent ? 1 : 0,
150
+ s.spentBy ?? null,
151
+ s.settledBy ?? null,
152
+ s.arkTxId ?? null,
153
+ s.extraWitness ? JSON.stringify(s.extraWitness) : null,
154
+ s.assets ? JSON.stringify(s.assets) : null,
155
+ ]);
156
+ }
157
+ }
158
+ async deleteVtxos(address) {
159
+ await this.ensureInit();
160
+ await this.db.run(`DELETE FROM ${this.tables.vtxos} WHERE address = ?`, [address]);
161
+ }
162
+ // ── UTXO management ────────────────────────────────────────────────
163
+ async getUtxos(address) {
164
+ await this.ensureInit();
165
+ const rows = await this.db.all(`SELECT * FROM ${this.tables.utxos} WHERE address = ?`, [address]);
166
+ return rows.map(utxoRowToDomain);
167
+ }
168
+ async saveUtxos(address, utxos) {
169
+ await this.ensureInit();
170
+ for (const utxo of utxos) {
171
+ const s = (0, serialization_1.serializeUtxo)(utxo);
172
+ await this.db.run(`INSERT OR REPLACE INTO ${this.tables.utxos}
173
+ (txid, vout, value, address,
174
+ tap_tree, forfeit_cb, forfeit_s, intent_cb, intent_s,
175
+ status_json, extra_witness_json)
176
+ VALUES (?, ?, ?, ?,
177
+ ?, ?, ?, ?, ?,
178
+ ?, ?)`, [
179
+ s.txid,
180
+ s.vout,
181
+ s.value,
182
+ address,
183
+ s.tapTree,
184
+ s.forfeitTapLeafScript.cb,
185
+ s.forfeitTapLeafScript.s,
186
+ s.intentTapLeafScript.cb,
187
+ s.intentTapLeafScript.s,
188
+ JSON.stringify(s.status),
189
+ s.extraWitness ? JSON.stringify(s.extraWitness) : null,
190
+ ]);
191
+ }
192
+ }
193
+ async deleteUtxos(address) {
194
+ await this.ensureInit();
195
+ await this.db.run(`DELETE FROM ${this.tables.utxos} WHERE address = ?`, [address]);
196
+ }
197
+ // ── Transaction history ────────────────────────────────────────────
198
+ async getTransactionHistory(address) {
199
+ await this.ensureInit();
200
+ const rows = await this.db.all(`SELECT * FROM ${this.tables.transactions} WHERE address = ? ORDER BY created_at ASC`, [address]);
201
+ return rows.map(txRowToDomain);
202
+ }
203
+ async saveTransactions(address, txs) {
204
+ await this.ensureInit();
205
+ for (const tx of txs) {
206
+ await this.db.run(`INSERT OR REPLACE INTO ${this.tables.transactions}
207
+ (address, boarding_txid, commitment_txid, ark_txid,
208
+ type, amount, settled, created_at, assets_json)
209
+ VALUES (?, ?, ?, ?,
210
+ ?, ?, ?, ?, ?)`, [
211
+ address,
212
+ tx.key.boardingTxid,
213
+ tx.key.commitmentTxid,
214
+ tx.key.arkTxid,
215
+ tx.type,
216
+ tx.amount,
217
+ tx.settled ? 1 : 0,
218
+ tx.createdAt,
219
+ tx.assets ? JSON.stringify(tx.assets) : null,
220
+ ]);
221
+ }
222
+ }
223
+ async deleteTransactions(address) {
224
+ await this.ensureInit();
225
+ await this.db.run(`DELETE FROM ${this.tables.transactions} WHERE address = ?`, [address]);
226
+ }
227
+ // ── Wallet state ───────────────────────────────────────────────────
228
+ async getWalletState() {
229
+ await this.ensureInit();
230
+ const row = await this.db.get(`SELECT * FROM ${this.tables.walletState} WHERE key = ?`, ["state"]);
231
+ if (!row)
232
+ return null;
233
+ const state = {};
234
+ if (row.last_sync_time !== null && row.last_sync_time !== undefined) {
235
+ state.lastSyncTime = row.last_sync_time;
236
+ }
237
+ if (row.settings_json) {
238
+ state.settings = JSON.parse(row.settings_json);
239
+ }
240
+ return state;
241
+ }
242
+ async saveWalletState(state) {
243
+ await this.ensureInit();
244
+ await this.db.run(`INSERT OR REPLACE INTO ${this.tables.walletState}
245
+ (key, last_sync_time, settings_json)
246
+ VALUES (?, ?, ?)`, [
247
+ "state",
248
+ state.lastSyncTime ?? null,
249
+ state.settings ? JSON.stringify(state.settings) : null,
250
+ ]);
251
+ }
252
+ }
253
+ exports.SQLiteWalletRepository = SQLiteWalletRepository;
254
+ const SAFE_PREFIX = /^[a-zA-Z0-9_]+$/;
255
+ function sanitizePrefix(prefix) {
256
+ if (!SAFE_PREFIX.test(prefix)) {
257
+ throw new Error(`Invalid table prefix "${prefix}": only letters, digits, and underscores are allowed`);
258
+ }
259
+ return prefix;
260
+ }
261
+ // ── Row → Domain converters ────────────────────────────────────────────
262
+ function vtxoRowToDomain(row) {
263
+ const serialized = {
264
+ txid: row.txid,
265
+ vout: row.vout,
266
+ value: row.value,
267
+ tapTree: row.tap_tree,
268
+ forfeitTapLeafScript: {
269
+ cb: row.forfeit_cb,
270
+ s: row.forfeit_s,
271
+ },
272
+ intentTapLeafScript: {
273
+ cb: row.intent_cb,
274
+ s: row.intent_s,
275
+ },
276
+ status: JSON.parse(row.status_json),
277
+ virtualStatus: JSON.parse(row.virtual_status_json),
278
+ createdAt: new Date(row.created_at),
279
+ isUnrolled: row.is_unrolled === 1,
280
+ isSpent: row.is_spent === null ? undefined : row.is_spent === 1,
281
+ spentBy: row.spent_by ?? undefined,
282
+ settledBy: row.settled_by ?? undefined,
283
+ arkTxId: row.ark_tx_id ?? undefined,
284
+ extraWitness: row.extra_witness_json
285
+ ? JSON.parse(row.extra_witness_json)
286
+ : undefined,
287
+ assets: row.assets_json ? JSON.parse(row.assets_json) : undefined,
288
+ };
289
+ return (0, serialization_1.deserializeVtxo)(serialized);
290
+ }
291
+ function utxoRowToDomain(row) {
292
+ const serialized = {
293
+ txid: row.txid,
294
+ vout: row.vout,
295
+ value: row.value,
296
+ tapTree: row.tap_tree,
297
+ forfeitTapLeafScript: {
298
+ cb: row.forfeit_cb,
299
+ s: row.forfeit_s,
300
+ },
301
+ intentTapLeafScript: {
302
+ cb: row.intent_cb,
303
+ s: row.intent_s,
304
+ },
305
+ status: JSON.parse(row.status_json),
306
+ extraWitness: row.extra_witness_json
307
+ ? JSON.parse(row.extra_witness_json)
308
+ : undefined,
309
+ };
310
+ return (0, serialization_1.deserializeUtxo)(serialized);
311
+ }
312
+ function txRowToDomain(row) {
313
+ const tx = {
314
+ key: {
315
+ boardingTxid: row.boarding_txid,
316
+ commitmentTxid: row.commitment_txid,
317
+ arkTxid: row.ark_txid,
318
+ },
319
+ type: row.type,
320
+ amount: row.amount,
321
+ settled: row.settled === 1,
322
+ createdAt: row.created_at,
323
+ };
324
+ if (row.assets_json) {
325
+ tx.assets = JSON.parse(row.assets_json);
326
+ }
327
+ return tx;
328
+ }
@@ -169,12 +169,20 @@ class ServiceWorkerReadonlyWallet {
169
169
  // send a message and wait for a response
170
170
  async sendMessage(request) {
171
171
  return new Promise((resolve, reject) => {
172
+ const cleanup = () => {
173
+ clearTimeout(timeoutId);
174
+ navigator.serviceWorker.removeEventListener("message", messageHandler);
175
+ };
176
+ const timeoutId = setTimeout(() => {
177
+ cleanup();
178
+ reject(new Error(`Service worker message timed out (${request.type})`));
179
+ }, 30000);
172
180
  const messageHandler = (event) => {
173
181
  const response = event.data;
174
182
  if (request.id !== response.id) {
175
183
  return;
176
184
  }
177
- navigator.serviceWorker.removeEventListener("message", messageHandler);
185
+ cleanup();
178
186
  if (response.error) {
179
187
  reject(response.error);
180
188
  }
@@ -93,6 +93,13 @@ function isVtxoExpiringSoon(vtxo, thresholdMs // in milliseconds
93
93
  const { batchExpiry } = vtxo.virtualStatus;
94
94
  if (!batchExpiry)
95
95
  return false; // it doesn't expire
96
+ // we use this as a workaround to avoid issue on regtest where expiry date is
97
+ // expressed in blockheight instead of timestamp. If expiry, as Date, is before 2025,
98
+ // then we admit it's too small to be a timestamp
99
+ // TODO: API should return the expiry unit
100
+ const expireAt = new Date(batchExpiry);
101
+ if (expireAt.getFullYear() < 2025)
102
+ return false;
96
103
  const now = Date.now();
97
104
  if (batchExpiry <= now)
98
105
  return false; // already expired
@@ -107,8 +107,19 @@ class MessageBus {
107
107
  }
108
108
  }
109
109
  async waitForInit(config) {
110
- if (this.initialized)
111
- return;
110
+ if (this.initialized) {
111
+ // Stop existing handlers before re-initializing.
112
+ // This handles the case where CLEAR was called, which nullifies
113
+ // handler state (readonlyWallet, etc.) without resetting the
114
+ // initialized flag. Without this, handlers never get start()
115
+ // called again and all messages fail with "not initialized".
116
+ //
117
+ // Clear the flag first so onMessage() rejects incoming messages
118
+ // during the stop/start window instead of routing them to
119
+ // half-reset handlers. Restored to true after start() completes.
120
+ this.initialized = false;
121
+ await Promise.all(Array.from(this.handlers.values()).map((h) => h.stop().catch(() => { })));
122
+ }
112
123
  const services = await this.buildServicesFn(config);
113
124
  // Start all handlers
114
125
  for (const updater of this.handlers.values()) {
@@ -168,6 +179,15 @@ class MessageBus {
168
179
  if (!this.initialized) {
169
180
  if (this.debug)
170
181
  console.warn("Event received before initialization, dropping", event.data);
182
+ // Send error response so the caller's promise rejects instead of
183
+ // hanging forever. This happens when the browser kills and restarts
184
+ // the service worker — the new instance has initialized=false and
185
+ // messages arrive before INITIALIZE_MESSAGE_BUS is re-sent.
186
+ event.source?.postMessage({
187
+ id,
188
+ tag: tag ?? "unknown",
189
+ error: new Error("MessageBus not initialized"),
190
+ });
171
191
  return;
172
192
  }
173
193
  if (!id || !tag) {