@arkade-os/sdk 0.5.0-rc.9 → 0.5.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.
Files changed (94) hide show
  1. package/README.md +231 -50
  2. package/dist/adapters/expo.cjs +5 -5
  3. package/dist/adapters/expo.d.cts +5 -5
  4. package/dist/adapters/expo.d.ts +5 -5
  5. package/dist/adapters/expo.js +3 -3
  6. package/dist/{asyncStorageTaskQueue-DZYrAWN8.d.ts → asyncStorageTaskQueue-BFv2O1y1.d.ts} +2 -2
  7. package/dist/{asyncStorageTaskQueue-ChKX-D51.d.cts → asyncStorageTaskQueue-qRRQUOE8.d.cts} +2 -2
  8. package/dist/{chunk-PKD5VPYX.cjs → chunk-AU77RYOC.cjs} +6086 -4522
  9. package/dist/chunk-AU77RYOC.cjs.map +1 -0
  10. package/dist/{chunk-VMVHRKXM.cjs → chunk-GN2K4WBP.cjs} +494 -1122
  11. package/dist/chunk-GN2K4WBP.cjs.map +1 -0
  12. package/dist/{chunk-PO2ZDZZC.cjs → chunk-GOM26K6H.cjs} +12 -10
  13. package/dist/chunk-GOM26K6H.cjs.map +1 -0
  14. package/dist/{chunk-3TCWQPAC.js → chunk-GWJWXVUB.js} +5793 -4249
  15. package/dist/chunk-GWJWXVUB.js.map +1 -0
  16. package/dist/{chunk-J2K7WWMJ.js → chunk-HABGVRLF.js} +112 -243
  17. package/dist/chunk-HABGVRLF.js.map +1 -0
  18. package/dist/{chunk-V4JZOFYT.cjs → chunk-IQWLFYT2.cjs} +124 -254
  19. package/dist/chunk-IQWLFYT2.cjs.map +1 -0
  20. package/dist/{chunk-7OL2P5TV.cjs → chunk-JT5YVMCJ.cjs} +39 -46
  21. package/dist/chunk-JT5YVMCJ.cjs.map +1 -0
  22. package/dist/{chunk-FQCMEAWN.js → chunk-K2E6X6JT.js} +32 -39
  23. package/dist/chunk-K2E6X6JT.js.map +1 -0
  24. package/dist/{chunk-R4KRWX7R.js → chunk-K352GMVL.js} +427 -1078
  25. package/dist/chunk-K352GMVL.js.map +1 -0
  26. package/dist/{chunk-Z3QM5XFY.js → chunk-TZSKLERU.js} +7 -5
  27. package/dist/chunk-TZSKLERU.js.map +1 -0
  28. package/dist/{chunk-VTXMC4N7.cjs → chunk-UUSKJOSX.cjs} +548 -399
  29. package/dist/chunk-UUSKJOSX.cjs.map +1 -0
  30. package/dist/{chunk-JFZBKPSV.js → chunk-ZOEB53NQ.js} +541 -400
  31. package/dist/chunk-ZOEB53NQ.js.map +1 -0
  32. package/dist/contracts/handlers/index.cjs +9 -9
  33. package/dist/contracts/handlers/index.d.cts +5 -5
  34. package/dist/contracts/handlers/index.d.ts +5 -5
  35. package/dist/contracts/handlers/index.js +2 -2
  36. package/dist/{delegate-Cyv2suFd.d.cts → delegate-B5BmYkEY.d.cts} +1 -1
  37. package/dist/{delegate-CyBnNY9v.d.ts → delegate-DNbiUgy8.d.ts} +1 -1
  38. package/dist/index-B4QuO9sH.d.cts +645 -0
  39. package/dist/index-CYeyT3kn.d.ts +645 -0
  40. package/dist/{index-BIL9boGL.d.ts → index-CZw-MMfY.d.cts} +1644 -2291
  41. package/dist/{index-BIL9boGL.d.cts → index-CZw-MMfY.d.ts} +1644 -2291
  42. package/dist/index.cjs +399 -311
  43. package/dist/index.d.cts +950 -1124
  44. package/dist/index.d.ts +950 -1124
  45. package/dist/index.js +4 -4
  46. package/dist/repositories/realm/index.cjs +165 -103
  47. package/dist/repositories/realm/index.cjs.map +1 -1
  48. package/dist/repositories/realm/index.d.cts +18 -14
  49. package/dist/repositories/realm/index.d.ts +18 -14
  50. package/dist/repositories/realm/index.js +153 -91
  51. package/dist/repositories/realm/index.js.map +1 -1
  52. package/dist/repositories/sqlite/index.cjs +226 -184
  53. package/dist/repositories/sqlite/index.cjs.map +1 -1
  54. package/dist/repositories/sqlite/index.d.cts +22 -21
  55. package/dist/repositories/sqlite/index.d.ts +22 -21
  56. package/dist/repositories/sqlite/index.js +218 -176
  57. package/dist/repositories/sqlite/index.js.map +1 -1
  58. package/dist/{taskRunner-DxpSuKmh.d.cts → taskRunner-B3SLvUfL.d.cts} +2 -2
  59. package/dist/{taskRunner-Cu4uO1to.d.ts → taskRunner-wm1bhmtP.d.ts} +2 -2
  60. package/dist/wallet/expo/background.cjs +14 -14
  61. package/dist/wallet/expo/background.cjs.map +1 -1
  62. package/dist/wallet/expo/background.d.cts +10 -16
  63. package/dist/wallet/expo/background.d.ts +10 -16
  64. package/dist/wallet/expo/background.js +6 -6
  65. package/dist/wallet/expo/background.js.map +1 -1
  66. package/dist/wallet/expo/index.cjs +19 -14
  67. package/dist/wallet/expo/index.cjs.map +1 -1
  68. package/dist/wallet/expo/index.d.cts +8 -6
  69. package/dist/wallet/expo/index.d.ts +8 -6
  70. package/dist/wallet/expo/index.js +10 -5
  71. package/dist/wallet/expo/index.js.map +1 -1
  72. package/dist/wallet-DCZ2-Bea.d.cts +1874 -0
  73. package/dist/wallet-DGLXuwXT.d.ts +1874 -0
  74. package/dist/worker/expo/index.cjs +9 -9
  75. package/dist/worker/expo/index.d.cts +6 -6
  76. package/dist/worker/expo/index.d.ts +6 -6
  77. package/dist/worker/expo/index.js +5 -5
  78. package/package.json +10 -11
  79. package/dist/chunk-3TCWQPAC.js.map +0 -1
  80. package/dist/chunk-7OL2P5TV.cjs.map +0 -1
  81. package/dist/chunk-FQCMEAWN.js.map +0 -1
  82. package/dist/chunk-J2K7WWMJ.js.map +0 -1
  83. package/dist/chunk-JFZBKPSV.js.map +0 -1
  84. package/dist/chunk-PKD5VPYX.cjs.map +0 -1
  85. package/dist/chunk-PO2ZDZZC.cjs.map +0 -1
  86. package/dist/chunk-R4KRWX7R.js.map +0 -1
  87. package/dist/chunk-V4JZOFYT.cjs.map +0 -1
  88. package/dist/chunk-VMVHRKXM.cjs.map +0 -1
  89. package/dist/chunk-VTXMC4N7.cjs.map +0 -1
  90. package/dist/chunk-Z3QM5XFY.js.map +0 -1
  91. package/dist/hdWalletCapable-D1ImQqlu.d.cts +0 -2214
  92. package/dist/hdWalletCapable-DvumVrKW.d.ts +0 -2214
  93. package/dist/index-DleNQcz5.d.cts +0 -850
  94. package/dist/index-QVhQgYvU.d.ts +0 -850
@@ -1,8 +1,8 @@
1
- import { scriptFromArkAddress, legacyVtxoFacts, serializeVtxo, isVtxoForScript, serializeUtxo, serializeAssets, deserializeVtxo, deserializeUtxo, deserializeAssets, intentMatchesFilter, intentPageBounds, isTerminalIntentState, mergeChainedTxType } from '../../chunk-3TCWQPAC.js';
2
- export { ChainedTxType } from '../../chunk-3TCWQPAC.js';
3
- import '../../chunk-J2K7WWMJ.js';
4
- import '../../chunk-R4KRWX7R.js';
5
- import '../../chunk-JFZBKPSV.js';
1
+ import { scriptFromArkAddress, legacyVtxoFacts, serializeVtxo, createdAtToIso, assertPageRequest, pageResult, checkSaveVtxosForScript, utxoEntries, serializeUtxo, assertHistoryPageFilter, serializeAssets, deserializeVtxo, deserializeUtxo, deserializeAssets, contractFilterMatchesNothing, intentMatchesFilter, isTerminalIntentState, mergeChainedTxType } from '../../chunk-GWJWXVUB.js';
2
+ export { ChainedTxType } from '../../chunk-GWJWXVUB.js';
3
+ import '../../chunk-HABGVRLF.js';
4
+ import '../../chunk-K352GMVL.js';
5
+ import { isVtxoSpent } from '../../chunk-ZOEB53NQ.js';
6
6
 
7
7
  // src/repositories/sqlite/transaction.ts
8
8
  var writeChains = /* @__PURE__ */ new WeakMap();
@@ -29,11 +29,19 @@ function runInTransaction(db, fn) {
29
29
  return run;
30
30
  }
31
31
 
32
+ // src/repositories/sqlite/prefix.ts
33
+ var SAFE_PREFIX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
34
+ function sanitizeTablePrefix(p) {
35
+ if (!SAFE_PREFIX.test(p)) throw new Error(`Invalid table prefix "${p}"`);
36
+ return p;
37
+ }
38
+
32
39
  // src/repositories/sqlite/walletRepository.ts
40
+ var MAX_BOUND_PARAMETERS = 999;
33
41
  var SQLiteWalletRepository = class {
34
42
  constructor(db, options) {
35
43
  this.db = db;
36
- this.prefix = sanitizePrefix(options?.prefix ?? "ark_");
44
+ this.prefix = sanitizeTablePrefix(options?.prefix ?? "ark_");
37
45
  this.tables = {
38
46
  vtxos: `${this.prefix}vtxos`,
39
47
  utxos: `${this.prefix}utxos`,
@@ -42,7 +50,7 @@ var SQLiteWalletRepository = class {
42
50
  };
43
51
  }
44
52
  db;
45
- version = 1;
53
+ version = 2;
46
54
  initPromise = null;
47
55
  prefix;
48
56
  tables;
@@ -98,23 +106,42 @@ var SQLiteWalletRepository = class {
98
106
  await this.db.run(
99
107
  `CREATE INDEX IF NOT EXISTS idx_${this.prefix}vtxos_script ON ${this.tables.vtxos} (script)`
100
108
  );
109
+ await this.db.run(
110
+ `CREATE INDEX IF NOT EXISTS idx_${this.prefix}vtxos_script_page ON ${this.tables.vtxos} (script, address, txid, vout)`
111
+ );
112
+ await this.db.run(
113
+ `CREATE INDEX IF NOT EXISTS idx_${this.prefix}vtxos_address_page ON ${this.tables.vtxos} (address, txid, vout)`
114
+ );
115
+ await this.db.run(`DROP INDEX IF EXISTS idx_${this.prefix}vtxos_live_script`);
116
+ await this.db.run(
117
+ `CREATE INDEX IF NOT EXISTS idx_${this.prefix}vtxos_live_script_page ON ${this.tables.vtxos} (script, address, txid, vout)
118
+ WHERE ${UNSPENT_VTXO_PREDICATE}`
119
+ );
101
120
  await this.db.run(
102
121
  `CREATE INDEX IF NOT EXISTS idx_${this.prefix}utxos_address ON ${this.tables.utxos} (address)`
103
122
  );
123
+ await this.db.run(
124
+ `CREATE INDEX IF NOT EXISTS idx_${this.prefix}utxos_address_page ON ${this.tables.utxos} (address, txid, vout)`
125
+ );
104
126
  await this.db.run(
105
127
  `CREATE INDEX IF NOT EXISTS idx_${this.prefix}transactions_address ON ${this.tables.transactions} (address)`
106
128
  );
129
+ await this.db.run(
130
+ `CREATE INDEX IF NOT EXISTS idx_${this.prefix}transactions_history ON ${this.tables.transactions} (address, created_at, boarding_txid, commitment_txid, ark_txid)`
131
+ );
107
132
  }
108
133
  /**
109
134
  * Bring the `vtxos` table to the current schema (v1 = `script` NOT NULL).
110
135
  *
111
- * Three cases:
136
+ * Four cases:
112
137
  * - Fresh install: create the v1 schema directly.
113
138
  * - Legacy install without a `script` column: add it, backfill from
114
139
  * `address`, then rebuild the table with NOT NULL (SQLite cannot add
115
140
  * the NOT NULL constraint in place).
116
141
  * - Legacy install with a nullable `script` column: backfill the NULLs
117
142
  * and rebuild.
143
+ * - 0.4.x install: `script` is already NOT NULL but `virtual_status_json
144
+ * NOT NULL` is still there, and `saveVtxos` no longer writes it.
118
145
  *
119
146
  * The backfill derives `script` from the Ark address, matching what the
120
147
  * indexer would have returned — new rows from the indexer always carry a
@@ -158,10 +185,11 @@ var SQLiteWalletRepository = class {
158
185
  addedCanonicalColumns = true;
159
186
  }
160
187
  }
161
- if (addedCanonicalColumns && cols.some((c) => c.name === "virtual_status_json")) {
188
+ const hasLegacyBlob = cols.some((c) => c.name === "virtual_status_json");
189
+ if (addedCanonicalColumns && hasLegacyBlob) {
162
190
  await this.backfillCanonicalVtxoColumns();
163
191
  }
164
- if (scriptCol && scriptCol.notnull === 1) {
192
+ if (scriptCol && scriptCol.notnull === 1 && !hasLegacyBlob) {
165
193
  return;
166
194
  }
167
195
  if (!scriptCol) {
@@ -262,6 +290,23 @@ var SQLiteWalletRepository = class {
262
290
  PRIMARY KEY (txid, vout)
263
291
  )`;
264
292
  }
293
+ /**
294
+ * `INSERT OR REPLACE` rows, as many to a statement as the parameter budget allows.
295
+ * No enclosing transaction: on the shared connection it would take in other repos' writes.
296
+ */
297
+ async insertOrReplace(table, columns, rows) {
298
+ const width = columns.split(",").length;
299
+ const tuple = `(${Array(width).fill("?").join(", ")})`;
300
+ const rowsPerStatement = Math.floor(MAX_BOUND_PARAMETERS / width);
301
+ for (let i = 0; i < rows.length; i += rowsPerStatement) {
302
+ const chunk = rows.slice(i, i + rowsPerStatement);
303
+ await this.db.run(
304
+ `INSERT OR REPLACE INTO ${table} (${columns})
305
+ VALUES ${chunk.map(() => tuple).join(", ")}`,
306
+ chunk.flat()
307
+ );
308
+ }
309
+ }
265
310
  async [Symbol.asyncDispose]() {
266
311
  }
267
312
  // ── Clear ──────────────────────────────────────────────────────────
@@ -273,33 +318,22 @@ var SQLiteWalletRepository = class {
273
318
  await this.db.run(`DELETE FROM ${this.tables.walletState}`);
274
319
  }
275
320
  // ── VTXO management ────────────────────────────────────────────────
276
- async getVtxos(address) {
277
- await this.ensureInit();
278
- const rows = await this.db.all(
279
- `SELECT * FROM ${this.tables.vtxos} WHERE address = ?`,
280
- [address]
281
- );
282
- return rows.map(vtxoRowToDomain);
321
+ async getVtxosPage(address, page) {
322
+ return this.pageByAddress(this.tables.vtxos, address, page, vtxoRowToDomain);
283
323
  }
284
324
  async saveVtxos(address, vtxos) {
285
325
  await this.ensureInit();
286
- for (const vtxo of vtxos) {
287
- const s = serializeVtxo(vtxo);
288
- await this.db.run(
289
- `INSERT OR REPLACE INTO ${this.tables.vtxos}
290
- (txid, vout, value, address,
291
- tap_tree, forfeit_cb, forfeit_s, intent_cb, intent_s,
292
- status_json, created_at, is_unrolled, is_spent,
293
- is_swept, is_preconfirmed, commitment_txids_json,
294
- expires_at, expires_at_height,
295
- spent_by, settled_by, ark_tx_id, extra_witness_json, assets_json, script)
296
- VALUES (?, ?, ?, ?,
297
- ?, ?, ?, ?, ?,
298
- ?, ?, ?, ?,
299
- ?, ?, ?,
300
- ?, ?,
301
- ?, ?, ?, ?, ?, ?)`,
302
- [
326
+ await this.insertOrReplace(
327
+ this.tables.vtxos,
328
+ `txid, vout, value, address,
329
+ tap_tree, forfeit_cb, forfeit_s, intent_cb, intent_s,
330
+ status_json, created_at, is_unrolled, is_spent,
331
+ is_swept, is_preconfirmed, commitment_txids_json,
332
+ expires_at, expires_at_height,
333
+ spent_by, settled_by, ark_tx_id, extra_witness_json, assets_json, script`,
334
+ vtxos.map((vtxo) => {
335
+ const s = serializeVtxo(vtxo);
336
+ return [
303
337
  s.txid,
304
338
  s.vout,
305
339
  s.value,
@@ -310,13 +344,13 @@ var SQLiteWalletRepository = class {
310
344
  s.intentTapLeafScript.cb,
311
345
  s.intentTapLeafScript.s,
312
346
  JSON.stringify(s.status),
313
- typeof s.createdAt === "string" ? s.createdAt : s.createdAt instanceof Date ? s.createdAt.toISOString() : new Date(s.createdAt).toISOString(),
347
+ createdAtToIso(s.createdAt),
314
348
  s.isUnrolled ? 1 : 0,
315
349
  s.isSpent === void 0 ? null : s.isSpent ? 1 : 0,
316
350
  s.isSwept === void 0 ? null : s.isSwept ? 1 : 0,
317
351
  s.isPreconfirmed === void 0 ? null : s.isPreconfirmed ? 1 : 0,
318
352
  s.commitmentTxIds ? JSON.stringify(s.commitmentTxIds) : null,
319
- s.expiresAt === void 0 ? null : s.expiresAt instanceof Date ? s.expiresAt.toISOString() : new Date(s.expiresAt).toISOString(),
353
+ s.expiresAt === void 0 ? null : new Date(s.expiresAt).toISOString(),
320
354
  s.expiresAtHeight ?? null,
321
355
  s.spentBy ?? null,
322
356
  s.settledBy ?? null,
@@ -324,61 +358,68 @@ var SQLiteWalletRepository = class {
324
358
  s.extraWitness ? JSON.stringify(s.extraWitness) : null,
325
359
  s.assets ? JSON.stringify(s.assets) : null,
326
360
  s.script ?? null
327
- ]
328
- );
329
- }
361
+ ];
362
+ })
363
+ );
330
364
  }
331
365
  async deleteVtxos(address) {
332
366
  await this.ensureInit();
333
367
  await this.db.run(`DELETE FROM ${this.tables.vtxos} WHERE address = ?`, [address]);
334
368
  }
335
- async getVtxosForScript(script) {
369
+ async getVtxosForScriptPage(script, page, options) {
370
+ assertPageRequest(page);
336
371
  await this.ensureInit();
372
+ const after = page.after;
337
373
  const rows = await this.db.all(
338
- `SELECT * FROM ${this.tables.vtxos} WHERE script = ?`,
339
- [script]
374
+ `SELECT * FROM ${this.tables.vtxos} WHERE script = ?
375
+ ${options?.unspentOnly ? `AND ${UNSPENT_VTXO_PREDICATE}` : ""}
376
+ ${after ? "AND (address > ? OR (address = ? AND (txid > ? OR (txid = ? AND vout > ?))))" : ""}
377
+ ORDER BY address, txid, vout LIMIT ?`,
378
+ after ? [
379
+ script,
380
+ after.address,
381
+ after.address,
382
+ after.txid,
383
+ after.txid,
384
+ after.vout,
385
+ page.limit + 1
386
+ ] : [script, page.limit + 1]
387
+ );
388
+ const result = pageResult(
389
+ rows.map((row) => ({ address: row.address, vtxo: vtxoRowToDomain(row) })),
390
+ page.limit,
391
+ (row) => ({ address: row.address, txid: row.vtxo.txid, vout: row.vtxo.vout })
340
392
  );
341
- return rows.map(vtxoRowToDomain);
393
+ return options?.unspentOnly ? { ...result, items: result.items.filter((row) => !isVtxoSpent(row.vtxo)) } : result;
342
394
  }
343
395
  async saveVtxosForScript(key, vtxos) {
344
- if (!key.address) {
345
- throw new Error("SQLiteWalletRepository requires an address");
346
- }
347
- for (const vtxo of vtxos) {
348
- if (!isVtxoForScript(vtxo, key.script)) {
349
- throw new Error(
350
- `VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`
351
- );
352
- }
353
- }
354
- return this.saveVtxos(key.address, vtxos);
396
+ return this.saveVtxos(checkSaveVtxosForScript("SQLiteWalletRepository", key, vtxos), vtxos);
355
397
  }
356
398
  async deleteVtxosForScript(script) {
357
399
  await this.ensureInit();
358
400
  await this.db.run(`DELETE FROM ${this.tables.vtxos} WHERE script = ?`, [script]);
359
401
  }
360
402
  // ── UTXO management ────────────────────────────────────────────────
361
- async getUtxos(address) {
403
+ async getUtxosPage(address, page) {
404
+ return this.pageByAddress(this.tables.utxos, address, page, utxoRowToDomain);
405
+ }
406
+ async pageByAddress(table, address, page, deserialize) {
407
+ assertPageRequest(page);
362
408
  await this.ensureInit();
363
409
  const rows = await this.db.all(
364
- `SELECT * FROM ${this.tables.utxos} WHERE address = ?`,
365
- [address]
410
+ `SELECT * FROM ${table} WHERE address = ? AND (txid, vout) > (?, ?)
411
+ ORDER BY txid, vout LIMIT ?`,
412
+ [address, page.after?.txid ?? "", page.after?.vout ?? -1, page.limit + 1]
366
413
  );
367
- return rows.map(utxoRowToDomain);
414
+ const result = pageResult(rows, page.limit, ({ txid, vout }) => ({ txid, vout }));
415
+ return { items: result.items.map(deserialize), nextCursor: result.nextCursor };
368
416
  }
369
- async saveUtxos(address, utxos) {
417
+ async saveUtxos(addressOrBatch, utxos) {
370
418
  await this.ensureInit();
371
- for (const utxo of utxos) {
372
- const s = serializeUtxo(utxo);
373
- await this.db.run(
374
- `INSERT OR REPLACE INTO ${this.tables.utxos}
375
- (txid, vout, value, address,
376
- tap_tree, forfeit_cb, forfeit_s, intent_cb, intent_s,
377
- status_json, extra_witness_json)
378
- VALUES (?, ?, ?, ?,
379
- ?, ?, ?, ?, ?,
380
- ?, ?)`,
381
- [
419
+ const rows = utxoEntries(addressOrBatch, utxos).flatMap(
420
+ ([address, list]) => list.map((utxo) => {
421
+ const s = serializeUtxo(utxo);
422
+ return [
382
423
  s.txid,
383
424
  s.vout,
384
425
  s.value,
@@ -390,45 +431,71 @@ var SQLiteWalletRepository = class {
390
431
  s.intentTapLeafScript.s,
391
432
  JSON.stringify(s.status),
392
433
  s.extraWitness ? JSON.stringify(s.extraWitness) : null
393
- ]
394
- );
395
- }
434
+ ];
435
+ })
436
+ );
437
+ await this.insertOrReplace(
438
+ this.tables.utxos,
439
+ `txid, vout, value, address,
440
+ tap_tree, forfeit_cb, forfeit_s, intent_cb, intent_s,
441
+ status_json, extra_witness_json`,
442
+ rows
443
+ );
396
444
  }
397
445
  async deleteUtxos(address) {
398
446
  await this.ensureInit();
399
447
  await this.db.run(`DELETE FROM ${this.tables.utxos} WHERE address = ?`, [address]);
400
448
  }
401
449
  // ── Transaction history ────────────────────────────────────────────
402
- async getTransactionHistory(address) {
450
+ async getTransactionHistoryPage(filter, page) {
451
+ assertPageRequest(page);
452
+ assertHistoryPageFilter(filter);
403
453
  await this.ensureInit();
454
+ const conditions = ["address = ?"];
455
+ const params = [filter.address];
456
+ if (filter.since !== void 0) {
457
+ conditions.push("created_at >= ?");
458
+ params.push(filter.since);
459
+ }
460
+ if (page.after !== void 0) {
461
+ conditions.push(
462
+ "(created_at, boarding_txid, commitment_txid, ark_txid) > (?, ?, ?, ?)"
463
+ );
464
+ params.push(
465
+ page.after.createdAt,
466
+ page.after.key.boardingTxid,
467
+ page.after.key.commitmentTxid,
468
+ page.after.key.arkTxid
469
+ );
470
+ }
404
471
  const rows = await this.db.all(
405
- `SELECT * FROM ${this.tables.transactions} WHERE address = ? ORDER BY created_at ASC`,
406
- [address]
472
+ `SELECT * FROM ${this.tables.transactions} WHERE ${conditions.join(" AND ")}
473
+ ORDER BY created_at, boarding_txid, commitment_txid, ark_txid LIMIT ?`,
474
+ [...params, page.limit + 1]
407
475
  );
408
- return rows.map(txRowToDomain);
476
+ return pageResult(rows.map(txRowToDomain), page.limit, (tx) => ({
477
+ createdAt: tx.createdAt,
478
+ key: tx.key
479
+ }));
409
480
  }
410
481
  async saveTransactions(address, txs) {
411
482
  await this.ensureInit();
412
- for (const tx of txs) {
413
- await this.db.run(
414
- `INSERT OR REPLACE INTO ${this.tables.transactions}
415
- (address, boarding_txid, commitment_txid, ark_txid,
416
- type, amount, settled, created_at, assets_json)
417
- VALUES (?, ?, ?, ?,
418
- ?, ?, ?, ?, ?)`,
419
- [
420
- address,
421
- tx.key.boardingTxid,
422
- tx.key.commitmentTxid,
423
- tx.key.arkTxid,
424
- tx.type,
425
- tx.amount,
426
- tx.settled ? 1 : 0,
427
- tx.createdAt,
428
- tx.assets ? JSON.stringify(serializeAssets(tx.assets)) : null
429
- ]
430
- );
431
- }
483
+ await this.insertOrReplace(
484
+ this.tables.transactions,
485
+ `address, boarding_txid, commitment_txid, ark_txid,
486
+ type, amount, settled, created_at, assets_json`,
487
+ txs.map((tx) => [
488
+ address,
489
+ tx.key.boardingTxid,
490
+ tx.key.commitmentTxid,
491
+ tx.key.arkTxid,
492
+ tx.type,
493
+ tx.amount,
494
+ tx.settled ? 1 : 0,
495
+ tx.createdAt,
496
+ tx.assets ? JSON.stringify(serializeAssets(tx.assets)) : null
497
+ ])
498
+ );
432
499
  }
433
500
  async deleteTransactions(address) {
434
501
  await this.ensureInit();
@@ -463,15 +530,7 @@ var SQLiteWalletRepository = class {
463
530
  );
464
531
  }
465
532
  };
466
- var SAFE_PREFIX = /^[a-zA-Z0-9_]+$/;
467
- function sanitizePrefix(prefix) {
468
- if (!SAFE_PREFIX.test(prefix)) {
469
- throw new Error(
470
- `Invalid table prefix "${prefix}": only letters, digits, and underscores are allowed`
471
- );
472
- }
473
- return prefix;
474
- }
533
+ var UNSPENT_VTXO_PREDICATE = "(is_spent IS NULL OR is_spent = 0) AND (spent_by IS NULL OR spent_by = '') AND (settled_by IS NULL OR settled_by = '')";
475
534
  function vtxoRowToDomain(row) {
476
535
  const serialized = {
477
536
  txid: row.txid,
@@ -545,14 +604,15 @@ function txRowToDomain(row) {
545
604
  }
546
605
 
547
606
  // src/repositories/sqlite/contractRepository.ts
607
+ var WATCH_STATE = "IFNULL(watch, 'watched')";
548
608
  var SQLiteContractRepository = class {
549
609
  constructor(db, options) {
550
610
  this.db = db;
551
- this.prefix = sanitizePrefix2(options?.prefix ?? "ark_");
611
+ this.prefix = sanitizeTablePrefix(options?.prefix ?? "ark_");
552
612
  this.table = `${this.prefix}contracts`;
553
613
  }
554
614
  db;
555
- version = 2;
615
+ version = 3;
556
616
  initPromise = null;
557
617
  prefix;
558
618
  table;
@@ -585,6 +645,12 @@ var SQLiteContractRepository = class {
585
645
  await this.db.run(
586
646
  `CREATE INDEX IF NOT EXISTS idx_${this.prefix}contracts_state ON ${this.table} (state)`
587
647
  );
648
+ await this.db.run(
649
+ `CREATE INDEX IF NOT EXISTS idx_${this.prefix}contracts_watch_state ON ${this.table} (${WATCH_STATE}, script)`
650
+ );
651
+ await this.db.run(
652
+ `CREATE INDEX IF NOT EXISTS idx_${this.prefix}contracts_type_watch_state ON ${this.table} (type, ${WATCH_STATE}, script)`
653
+ );
588
654
  }
589
655
  async addColumnIfMissing(column, type) {
590
656
  const columns = await this.db.all(`PRAGMA table_info(${this.table})`);
@@ -599,22 +665,28 @@ var SQLiteContractRepository = class {
599
665
  await this.db.run(`DELETE FROM ${this.table}`);
600
666
  }
601
667
  // ── Contract management ────────────────────────────────────────────
602
- async getContracts(filter) {
668
+ async getContractsPage(filter, page) {
669
+ assertPageRequest(page);
603
670
  await this.ensureInit();
671
+ if (contractFilterMatchesNothing(filter)) return { items: [] };
604
672
  const conditions = [];
605
673
  const params = [];
606
674
  if (filter) {
607
675
  this.addFilterCondition(conditions, params, "script", filter.script);
608
676
  this.addFilterCondition(conditions, params, "state", filter.state);
609
677
  this.addFilterCondition(conditions, params, "type", filter.type);
610
- this.addWatchCondition(conditions, params, filter.watch);
678
+ this.addFilterCondition(conditions, params, WATCH_STATE, filter.watch);
611
679
  }
612
- let sql = `SELECT * FROM ${this.table}`;
613
- if (conditions.length > 0) {
614
- sql += ` WHERE ${conditions.join(" AND ")}`;
680
+ if (page.after !== void 0) {
681
+ conditions.push("script > ?");
682
+ params.push(page.after);
615
683
  }
616
- const rows = await this.db.all(sql, params);
617
- return rows.map(contractRowToDomain);
684
+ const where = conditions.length ? ` WHERE ${conditions.join(" AND ")}` : "";
685
+ const rows = await this.db.all(
686
+ `SELECT * FROM ${this.table}${where} ORDER BY script LIMIT ?`,
687
+ [...params, page.limit + 1]
688
+ );
689
+ return pageResult(rows.map(contractRowToDomain), page.limit, (contract) => contract.script);
618
690
  }
619
691
  async saveContract(contract) {
620
692
  await this.ensureInit();
@@ -654,29 +726,7 @@ var SQLiteContractRepository = class {
654
726
  params.push(value);
655
727
  }
656
728
  }
657
- /**
658
- * Same as {@link addFilterCondition}, except a row predating the
659
- * column stores NULL and must match `"watched"`.
660
- */
661
- addWatchCondition(conditions, params, value) {
662
- if (value === void 0) return;
663
- const wanted = Array.isArray(value) ? value : [value];
664
- if (wanted.length === 0) return;
665
- const placeholders = wanted.map(() => "?").join(", ");
666
- const clause = `watch IN (${placeholders})`;
667
- conditions.push(wanted.includes("watched") ? `(${clause} OR watch IS NULL)` : clause);
668
- params.push(...wanted);
669
- }
670
729
  };
671
- var SAFE_PREFIX2 = /^[a-zA-Z0-9_]+$/;
672
- function sanitizePrefix2(prefix) {
673
- if (!SAFE_PREFIX2.test(prefix)) {
674
- throw new Error(
675
- `Invalid table prefix "${prefix}": only letters, digits, and underscores are allowed`
676
- );
677
- }
678
- return prefix;
679
- }
680
730
  function contractRowToDomain(row) {
681
731
  const contract = {
682
732
  script: row.script,
@@ -699,15 +749,10 @@ function contractRowToDomain(row) {
699
749
  }
700
750
 
701
751
  // src/repositories/sqlite/intentRepository.ts
702
- var SAFE_PREFIX3 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
703
- function sanitizePrefix3(p) {
704
- if (!SAFE_PREFIX3.test(p)) throw new Error(`Invalid table prefix "${p}"`);
705
- return p;
706
- }
707
752
  var SQLiteIntentRepository = class {
708
753
  constructor(db, options) {
709
754
  this.db = db;
710
- this.prefix = sanitizePrefix3(options?.prefix ?? "ark_");
755
+ this.prefix = sanitizeTablePrefix(options?.prefix ?? "ark_");
711
756
  this.t = `${this.prefix}intents`;
712
757
  }
713
758
  db;
@@ -746,19 +791,16 @@ var SQLiteIntentRepository = class {
746
791
  `CREATE INDEX IF NOT EXISTS idx_${this.prefix}intent_state ON ${this.t} (state)`
747
792
  );
748
793
  }
749
- withTx(fn) {
750
- return runInTransaction(this.db, fn);
751
- }
752
794
  async clear() {
753
795
  await this.ensureInit();
754
- await this.withTx(async () => {
796
+ await runInTransaction(this.db, async () => {
755
797
  await this.db.run(`DELETE FROM ${this.t}`);
756
798
  });
757
799
  }
758
800
  async saveIntent(intent) {
759
801
  await this.ensureInit();
760
802
  const now = Date.now();
761
- await this.withTx(async () => {
803
+ await runInTransaction(this.db, async () => {
762
804
  await this.db.run(
763
805
  `INSERT INTO ${this.t}
764
806
  (intent_tx_id, intent_id, state, valid_from, valid_until,
@@ -806,15 +848,30 @@ var SQLiteIntentRepository = class {
806
848
  );
807
849
  });
808
850
  }
809
- async getIntents(filter) {
851
+ async getIntentsPage(filter, page) {
852
+ assertPageRequest(page);
810
853
  await this.ensureInit();
811
- const rows = await this.db.all(
812
- `SELECT * FROM ${this.t} ORDER BY created_at ASC, intent_tx_id ASC`
813
- );
814
- let out = rows.map(rowToIntent);
815
- if (filter) out = out.filter((i) => intentMatchesFilter(i, filter));
816
- const { skip, end } = intentPageBounds(filter, out.length);
817
- return out.slice(skip, end);
854
+ if (filter?.states?.length === 0) return { items: [] };
855
+ const stateSql = filter?.states ? ` AND state IN (${filter.states.map(() => "?").join(", ")})` : "";
856
+ const stateParams = filter?.states ?? [];
857
+ const batchSize = Math.max(64, page.limit + 1);
858
+ const rows = [];
859
+ let after = page.after;
860
+ while (rows.length <= page.limit) {
861
+ const batch = await this.db.all(
862
+ `SELECT * FROM ${this.t} WHERE intent_tx_id > ?${stateSql} ORDER BY intent_tx_id LIMIT ?`,
863
+ [after ?? "", ...stateParams, batchSize]
864
+ );
865
+ if (batch.length === 0) break;
866
+ for (const row of batch) {
867
+ after = row.intent_tx_id;
868
+ const intent = rowToIntent(row);
869
+ if (!filter || intentMatchesFilter(intent, filter)) rows.push(intent);
870
+ if (rows.length > page.limit) break;
871
+ }
872
+ if (batch.length < batchSize) break;
873
+ }
874
+ return pageResult(rows, page.limit, (intent) => intent.intentTxId);
818
875
  }
819
876
  async getLockedVtxoOutpoints() {
820
877
  await this.ensureInit();
@@ -852,15 +909,10 @@ function rowToIntent(r) {
852
909
  }
853
910
 
854
911
  // src/repositories/sqlite/virtualTxRepository.ts
855
- var SAFE_PREFIX4 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
856
- function sanitizePrefix4(p) {
857
- if (!SAFE_PREFIX4.test(p)) throw new Error(`Invalid table prefix "${p}"`);
858
- return p;
859
- }
860
912
  var SQLiteVirtualTxRepository = class {
861
913
  constructor(db, options) {
862
914
  this.db = db;
863
- this.prefix = sanitizePrefix4(options?.prefix ?? "ark_");
915
+ this.prefix = sanitizeTablePrefix(options?.prefix ?? "ark_");
864
916
  this.tTx = `${this.prefix}virtual_txs`;
865
917
  this.tBranch = `${this.prefix}vtxo_branches`;
866
918
  }
@@ -892,12 +944,9 @@ var SQLiteVirtualTxRepository = class {
892
944
  `CREATE INDEX IF NOT EXISTS idx_${this.prefix}branch_vtxid ON ${this.tBranch} (virtual_txid)`
893
945
  );
894
946
  }
895
- tx(fn) {
896
- return runInTransaction(this.db, fn);
897
- }
898
947
  async clear() {
899
948
  await this.ensureInit();
900
- await this.tx(async () => {
949
+ await runInTransaction(this.db, async () => {
901
950
  await this.db.run(`DELETE FROM ${this.tTx}`);
902
951
  await this.db.run(`DELETE FROM ${this.tBranch}`);
903
952
  });
@@ -905,7 +954,7 @@ var SQLiteVirtualTxRepository = class {
905
954
  async upsertVirtualTxs(txs) {
906
955
  if (txs.length === 0) return;
907
956
  await this.ensureInit();
908
- await this.tx(async () => {
957
+ await runInTransaction(this.db, async () => {
909
958
  const placeholders = txs.map(() => "?").join(", ");
910
959
  const existing = await this.db.all(
911
960
  `SELECT * FROM ${this.tTx} WHERE txid IN (${placeholders})`,
@@ -932,7 +981,7 @@ var SQLiteVirtualTxRepository = class {
932
981
  }
933
982
  async setBranch(vtxo, branch) {
934
983
  await this.ensureInit();
935
- await this.tx(async () => {
984
+ await runInTransaction(this.db, async () => {
936
985
  await this.db.run(`DELETE FROM ${this.tBranch} WHERE vtxo_txid = ? AND vtxo_vout = ?`, [
937
986
  vtxo.txid,
938
987
  vtxo.vout
@@ -967,7 +1016,7 @@ var SQLiteVirtualTxRepository = class {
967
1016
  }
968
1017
  async pruneForSpentVtxo(vtxo) {
969
1018
  await this.ensureInit();
970
- await this.tx(async () => {
1019
+ await runInTransaction(this.db, async () => {
971
1020
  const removed = await this.db.all(
972
1021
  `SELECT DISTINCT virtual_txid FROM ${this.tBranch} WHERE vtxo_txid = ? AND vtxo_vout = ?`,
973
1022
  [vtxo.txid, vtxo.vout]
@@ -1000,13 +1049,6 @@ function rowToTx(r) {
1000
1049
  };
1001
1050
  }
1002
1051
 
1003
- // src/repositories/sqlite/prefix.ts
1004
- var SAFE_PREFIX5 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
1005
- function sanitizeTablePrefix(p) {
1006
- if (!SAFE_PREFIX5.test(p)) throw new Error(`Invalid table prefix "${p}"`);
1007
- return p;
1008
- }
1009
-
1010
1052
  export { SQLiteContractRepository, SQLiteIntentRepository, SQLiteVirtualTxRepository, SQLiteWalletRepository, runInTransaction, sanitizeTablePrefix };
1011
1053
  //# sourceMappingURL=index.js.map
1012
1054
  //# sourceMappingURL=index.js.map