@arkade-os/sdk 0.5.0-rc.2 → 0.5.0-rc.20

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 (126) hide show
  1. package/README.md +90 -83
  2. package/dist/adapters/expo.cjs +5 -6
  3. package/dist/adapters/expo.d.cts +4 -4
  4. package/dist/adapters/expo.d.ts +4 -4
  5. package/dist/adapters/expo.js +3 -4
  6. package/dist/{asyncStorageTaskQueue-C5rBYlru.d.ts → asyncStorageTaskQueue-aL7K14e4.d.ts} +2 -2
  7. package/dist/{asyncStorageTaskQueue-BuDvgWh6.d.cts → asyncStorageTaskQueue-x8ok3tJy.d.cts} +2 -2
  8. package/dist/{chunk-43FKNLX4.cjs → chunk-5SLUXXWQ.cjs} +357 -1114
  9. package/dist/chunk-5SLUXXWQ.cjs.map +1 -0
  10. package/dist/{chunk-HLCGYOIH.cjs → chunk-6YVGWOLG.cjs} +37 -36
  11. package/dist/chunk-6YVGWOLG.cjs.map +1 -0
  12. package/dist/{chunk-4PKLULWT.js → chunk-AARSVV3S.js} +109 -246
  13. package/dist/chunk-AARSVV3S.js.map +1 -0
  14. package/dist/{chunk-D25O4EGY.cjs → chunk-AFJSJACF.cjs} +12 -10
  15. package/dist/chunk-AFJSJACF.cjs.map +1 -0
  16. package/dist/{chunk-6DNR46C7.js → chunk-IM3J3T5P.js} +7 -5
  17. package/dist/chunk-IM3J3T5P.js.map +1 -0
  18. package/dist/{chunk-ISO4TI5J.js → chunk-NR4XYTN3.js} +566 -459
  19. package/dist/chunk-NR4XYTN3.js.map +1 -0
  20. package/dist/{chunk-ZRNQV6HV.js → chunk-O2ZZD5Z6.js} +4578 -4396
  21. package/dist/chunk-O2ZZD5Z6.js.map +1 -0
  22. package/dist/{chunk-ZOTXDOS5.js → chunk-P267BXL7.js} +30 -29
  23. package/dist/chunk-P267BXL7.js.map +1 -0
  24. package/dist/{chunk-NVZJSKZS.cjs → chunk-P42MGA4K.cjs} +121 -257
  25. package/dist/chunk-P42MGA4K.cjs.map +1 -0
  26. package/dist/{chunk-GMZZQXV5.cjs → chunk-QARA4IYT.cjs} +4961 -4767
  27. package/dist/chunk-QARA4IYT.cjs.map +1 -0
  28. package/dist/{chunk-JZWLXNHV.cjs → chunk-T6N6B7HK.cjs} +574 -464
  29. package/dist/chunk-T6N6B7HK.cjs.map +1 -0
  30. package/dist/{chunk-3R476TDV.js → chunk-TIVYJIIX.js} +293 -1071
  31. package/dist/chunk-TIVYJIIX.js.map +1 -0
  32. package/dist/contracts/handlers/index.cjs +9 -10
  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 -3
  36. package/dist/{delegate-BAX5QftB.d.ts → delegate-BT5iON1T.d.ts} +1 -1
  37. package/dist/{delegate-1yp1HyP7.d.cts → delegate-u6kXTtWY.d.cts} +1 -1
  38. package/dist/index-BCnhItOQ.d.cts +639 -0
  39. package/dist/{index-DUxufw76.d.cts → index-CK5QbTmZ.d.cts} +1387 -2404
  40. package/dist/{index-DUxufw76.d.ts → index-CK5QbTmZ.d.ts} +1387 -2404
  41. package/dist/index-D1D4cR-G.d.ts +639 -0
  42. package/dist/index.cjs +375 -340
  43. package/dist/index.d.cts +784 -1197
  44. package/dist/index.d.ts +784 -1197
  45. package/dist/index.js +4 -5
  46. package/dist/repositories/realm/index.cjs +201 -106
  47. package/dist/repositories/realm/index.cjs.map +1 -1
  48. package/dist/repositories/realm/index.d.cts +28 -16
  49. package/dist/repositories/realm/index.d.ts +28 -16
  50. package/dist/repositories/realm/index.js +190 -95
  51. package/dist/repositories/realm/index.js.map +1 -1
  52. package/dist/repositories/sqlite/index.cjs +301 -189
  53. package/dist/repositories/sqlite/index.cjs.map +1 -1
  54. package/dist/repositories/sqlite/index.d.cts +32 -21
  55. package/dist/repositories/sqlite/index.d.ts +32 -21
  56. package/dist/repositories/sqlite/index.js +294 -182
  57. package/dist/repositories/sqlite/index.js.map +1 -1
  58. package/dist/{taskRunner-D_iPiZIi.d.ts → taskRunner-BeYR6NpQ.d.ts} +2 -2
  59. package/dist/{taskRunner-Dr3NStc0.d.cts → taskRunner-Csey3EYJ.d.cts} +2 -2
  60. package/dist/wallet/expo/background.cjs +14 -15
  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 -7
  65. package/dist/wallet/expo/background.js.map +1 -1
  66. package/dist/wallet/expo/index.cjs +16 -23
  67. package/dist/wallet/expo/index.cjs.map +1 -1
  68. package/dist/wallet/expo/index.d.cts +6 -9
  69. package/dist/wallet/expo/index.d.ts +6 -9
  70. package/dist/wallet/expo/index.js +8 -15
  71. package/dist/wallet/expo/index.js.map +1 -1
  72. package/dist/wallet-DG3Qmbmt.d.cts +1848 -0
  73. package/dist/wallet-DzSLMxBZ.d.ts +1848 -0
  74. package/dist/worker/expo/index.cjs +9 -10
  75. package/dist/worker/expo/index.cjs.map +1 -1
  76. package/dist/worker/expo/index.d.cts +6 -6
  77. package/dist/worker/expo/index.d.ts +6 -6
  78. package/dist/worker/expo/index.js +5 -6
  79. package/dist/worker/expo/index.js.map +1 -1
  80. package/package.json +12 -51
  81. package/dist/adapters/asyncStorage.cjs +0 -48
  82. package/dist/adapters/asyncStorage.cjs.map +0 -1
  83. package/dist/adapters/asyncStorage.d.cts +0 -16
  84. package/dist/adapters/asyncStorage.d.ts +0 -16
  85. package/dist/adapters/asyncStorage.js +0 -46
  86. package/dist/adapters/asyncStorage.js.map +0 -1
  87. package/dist/adapters/fileSystem.cjs +0 -116
  88. package/dist/adapters/fileSystem.cjs.map +0 -1
  89. package/dist/adapters/fileSystem.d.cts +0 -17
  90. package/dist/adapters/fileSystem.d.ts +0 -17
  91. package/dist/adapters/fileSystem.js +0 -93
  92. package/dist/adapters/fileSystem.js.map +0 -1
  93. package/dist/adapters/indexedDB.cjs +0 -103
  94. package/dist/adapters/indexedDB.cjs.map +0 -1
  95. package/dist/adapters/indexedDB.d.cts +0 -18
  96. package/dist/adapters/indexedDB.d.ts +0 -18
  97. package/dist/adapters/indexedDB.js +0 -101
  98. package/dist/adapters/indexedDB.js.map +0 -1
  99. package/dist/adapters/localStorage.cjs +0 -50
  100. package/dist/adapters/localStorage.cjs.map +0 -1
  101. package/dist/adapters/localStorage.d.cts +0 -14
  102. package/dist/adapters/localStorage.d.ts +0 -14
  103. package/dist/adapters/localStorage.js +0 -48
  104. package/dist/adapters/localStorage.js.map +0 -1
  105. package/dist/chunk-3R476TDV.js.map +0 -1
  106. package/dist/chunk-43FKNLX4.cjs.map +0 -1
  107. package/dist/chunk-4PKLULWT.js.map +0 -1
  108. package/dist/chunk-5BLDMQED.cjs +0 -18
  109. package/dist/chunk-5BLDMQED.cjs.map +0 -1
  110. package/dist/chunk-6DNR46C7.js.map +0 -1
  111. package/dist/chunk-D25O4EGY.cjs.map +0 -1
  112. package/dist/chunk-GMZZQXV5.cjs.map +0 -1
  113. package/dist/chunk-HLCGYOIH.cjs.map +0 -1
  114. package/dist/chunk-ISO4TI5J.js.map +0 -1
  115. package/dist/chunk-JZWLXNHV.cjs.map +0 -1
  116. package/dist/chunk-NSBPE2FW.js +0 -15
  117. package/dist/chunk-NSBPE2FW.js.map +0 -1
  118. package/dist/chunk-NVZJSKZS.cjs.map +0 -1
  119. package/dist/chunk-ZOTXDOS5.js.map +0 -1
  120. package/dist/chunk-ZRNQV6HV.js.map +0 -1
  121. package/dist/hdWalletCapable-CmEU0NP1.d.ts +0 -2232
  122. package/dist/hdWalletCapable-eJgaT-ec.d.cts +0 -2232
  123. package/dist/index-Bem5vKzX.d.ts +0 -850
  124. package/dist/index-C0IanN1m.d.cts +0 -11
  125. package/dist/index-C0IanN1m.d.ts +0 -11
  126. package/dist/index-W_oMcOmG.d.cts +0 -850
@@ -1,9 +1,8 @@
1
- import { scriptFromArkAddress, serializeVtxo, isVtxoForScript, serializeUtxo, serializeAssets, deserializeVtxo, deserializeUtxo, deserializeAssets, intentMatchesFilter, intentPageBounds, isTerminalIntentState, mergeChainedTxType } from '../../chunk-ZRNQV6HV.js';
2
- export { ChainedTxType } from '../../chunk-ZRNQV6HV.js';
3
- import '../../chunk-4PKLULWT.js';
4
- import '../../chunk-3R476TDV.js';
5
- import '../../chunk-ISO4TI5J.js';
6
- import '../../chunk-NSBPE2FW.js';
1
+ import { scriptFromArkAddress, legacyVtxoFacts, serializeVtxo, createdAtToIso, assertPageRequest, pageResult, checkSaveVtxosForScript, utxoEntries, serializeUtxo, assertHistoryPageFilter, serializeAssets, deserializeVtxo, deserializeUtxo, deserializeAssets, contractFilterMatchesNothing, intentMatchesFilter, isTerminalIntentState, mergeChainedTxType } from '../../chunk-O2ZZD5Z6.js';
2
+ export { ChainedTxType } from '../../chunk-O2ZZD5Z6.js';
3
+ import '../../chunk-AARSVV3S.js';
4
+ import '../../chunk-TIVYJIIX.js';
5
+ import { isVtxoSpent } from '../../chunk-NR4XYTN3.js';
7
6
 
8
7
  // src/repositories/sqlite/transaction.ts
9
8
  var writeChains = /* @__PURE__ */ new WeakMap();
@@ -30,11 +29,19 @@ function runInTransaction(db, fn) {
30
29
  return run;
31
30
  }
32
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
+
33
39
  // src/repositories/sqlite/walletRepository.ts
40
+ var MAX_BOUND_PARAMETERS = 999;
34
41
  var SQLiteWalletRepository = class {
35
42
  constructor(db, options) {
36
43
  this.db = db;
37
- this.prefix = sanitizePrefix(options?.prefix ?? "ark_");
44
+ this.prefix = sanitizeTablePrefix(options?.prefix ?? "ark_");
38
45
  this.tables = {
39
46
  vtxos: `${this.prefix}vtxos`,
40
47
  utxos: `${this.prefix}utxos`,
@@ -43,7 +50,7 @@ var SQLiteWalletRepository = class {
43
50
  };
44
51
  }
45
52
  db;
46
- version = 1;
53
+ version = 2;
47
54
  initPromise = null;
48
55
  prefix;
49
56
  tables;
@@ -99,23 +106,42 @@ var SQLiteWalletRepository = class {
99
106
  await this.db.run(
100
107
  `CREATE INDEX IF NOT EXISTS idx_${this.prefix}vtxos_script ON ${this.tables.vtxos} (script)`
101
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
+ );
102
120
  await this.db.run(
103
121
  `CREATE INDEX IF NOT EXISTS idx_${this.prefix}utxos_address ON ${this.tables.utxos} (address)`
104
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
+ );
105
126
  await this.db.run(
106
127
  `CREATE INDEX IF NOT EXISTS idx_${this.prefix}transactions_address ON ${this.tables.transactions} (address)`
107
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
+ );
108
132
  }
109
133
  /**
110
134
  * Bring the `vtxos` table to the current schema (v1 = `script` NOT NULL).
111
135
  *
112
- * Three cases:
136
+ * Four cases:
113
137
  * - Fresh install: create the v1 schema directly.
114
138
  * - Legacy install without a `script` column: add it, backfill from
115
139
  * `address`, then rebuild the table with NOT NULL (SQLite cannot add
116
140
  * the NOT NULL constraint in place).
117
141
  * - Legacy install with a nullable `script` column: backfill the NULLs
118
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.
119
145
  *
120
146
  * The backfill derives `script` from the Ark address, matching what the
121
147
  * indexer would have returned — new rows from the indexer always carry a
@@ -143,7 +169,27 @@ var SQLiteWalletRepository = class {
143
169
  `PRAGMA table_info(${this.tables.vtxos})`
144
170
  );
145
171
  const scriptCol = cols.find((c) => c.name === "script");
146
- if (scriptCol && scriptCol.notnull === 1) {
172
+ const nullableCanonicalColumns = [
173
+ ["is_swept", "INTEGER"],
174
+ ["is_preconfirmed", "INTEGER"],
175
+ ["commitment_txids_json", "TEXT"],
176
+ ["expires_at", "TEXT"],
177
+ ["expires_at_height", "INTEGER"]
178
+ ];
179
+ let addedCanonicalColumns = false;
180
+ for (const [name, type] of nullableCanonicalColumns) {
181
+ if (!cols.some((c) => c.name === name)) {
182
+ await this.db.run(
183
+ `ALTER TABLE ${this.tables.vtxos} ADD COLUMN ${name} ${type}`
184
+ );
185
+ addedCanonicalColumns = true;
186
+ }
187
+ }
188
+ const hasLegacyBlob = cols.some((c) => c.name === "virtual_status_json");
189
+ if (addedCanonicalColumns && hasLegacyBlob) {
190
+ await this.backfillCanonicalVtxoColumns();
191
+ }
192
+ if (scriptCol && scriptCol.notnull === 1 && !hasLegacyBlob) {
147
193
  return;
148
194
  }
149
195
  if (!scriptCol) {
@@ -163,20 +209,58 @@ var SQLiteWalletRepository = class {
163
209
  INSERT INTO ${tempName}
164
210
  (txid, vout, value, address, tap_tree,
165
211
  forfeit_cb, forfeit_s, intent_cb, intent_s,
166
- status_json, virtual_status_json, created_at,
167
- is_unrolled, is_spent, spent_by, settled_by, ark_tx_id,
168
- extra_witness_json, assets_json, script)
212
+ status_json, created_at, is_unrolled, is_spent,
213
+ is_swept, is_preconfirmed, commitment_txids_json,
214
+ expires_at, expires_at_height,
215
+ spent_by, settled_by, ark_tx_id, extra_witness_json, assets_json, script)
169
216
  SELECT txid, vout, value, address, tap_tree,
170
217
  forfeit_cb, forfeit_s, intent_cb, intent_s,
171
- status_json, virtual_status_json, created_at,
172
- is_unrolled, is_spent, spent_by, settled_by, ark_tx_id,
173
- extra_witness_json, assets_json, script
218
+ status_json, created_at, is_unrolled, is_spent,
219
+ is_swept, is_preconfirmed, commitment_txids_json,
220
+ expires_at, expires_at_height,
221
+ spent_by, settled_by, ark_tx_id, extra_witness_json, assets_json, script
174
222
  FROM ${this.tables.vtxos}
175
223
  `);
176
224
  await this.db.run(`DROP TABLE ${this.tables.vtxos}`);
177
225
  await this.db.run(`ALTER TABLE ${tempName} RENAME TO ${this.tables.vtxos}`);
178
226
  });
179
227
  }
228
+ /**
229
+ * Copy the canonical VTXO facts out of the legacy `virtual_status_json` blob.
230
+ *
231
+ * Scoped to rows that still have `is_swept` NULL, which is what "never backfilled" looks
232
+ * like: the copy writes 0 or 1 to every row it touches, so a second pass — a later column
233
+ * addition, say — cannot overwrite what an indexer sync has since corrected. Rows whose blob
234
+ * is missing or corrupt are left alone: a migration must not be stopped by one bad row, and
235
+ * the next sync repairs it.
236
+ */
237
+ async backfillCanonicalVtxoColumns() {
238
+ const rows = await this.db.all(
239
+ `SELECT txid, vout, virtual_status_json FROM ${this.tables.vtxos}
240
+ WHERE virtual_status_json IS NOT NULL AND is_swept IS NULL`
241
+ );
242
+ for (const row of rows) {
243
+ const facts = legacyVtxoFacts(row.virtual_status_json);
244
+ if (!facts) continue;
245
+ await this.db.run(
246
+ `UPDATE ${this.tables.vtxos}
247
+ SET is_swept = ?, is_preconfirmed = ?, commitment_txids_json = ?,
248
+ expires_at = ?, expires_at_height = ?,
249
+ is_spent = COALESCE(is_spent, ?)
250
+ WHERE txid = ? AND vout = ?`,
251
+ [
252
+ facts.isSwept ? 1 : 0,
253
+ facts.isPreconfirmed ? 1 : 0,
254
+ facts.commitmentTxIds ? JSON.stringify(facts.commitmentTxIds) : null,
255
+ facts.expiresAt ? facts.expiresAt.toISOString() : null,
256
+ facts.expiresAtHeight ?? null,
257
+ facts.isSpent ? 1 : 0,
258
+ row.txid,
259
+ row.vout
260
+ ]
261
+ );
262
+ }
263
+ }
180
264
  vtxosCreateSql(tableName) {
181
265
  return `CREATE TABLE ${tableName} (
182
266
  txid TEXT NOT NULL,
@@ -189,10 +273,14 @@ var SQLiteWalletRepository = class {
189
273
  intent_cb TEXT NOT NULL,
190
274
  intent_s TEXT NOT NULL,
191
275
  status_json TEXT NOT NULL,
192
- virtual_status_json TEXT NOT NULL,
193
276
  created_at TEXT NOT NULL,
194
277
  is_unrolled INTEGER NOT NULL DEFAULT 0,
195
278
  is_spent INTEGER,
279
+ is_swept INTEGER,
280
+ is_preconfirmed INTEGER,
281
+ commitment_txids_json TEXT,
282
+ expires_at TEXT,
283
+ expires_at_height INTEGER,
196
284
  spent_by TEXT,
197
285
  settled_by TEXT,
198
286
  ark_tx_id TEXT,
@@ -202,6 +290,23 @@ var SQLiteWalletRepository = class {
202
290
  PRIMARY KEY (txid, vout)
203
291
  )`;
204
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
+ }
205
310
  async [Symbol.asyncDispose]() {
206
311
  }
207
312
  // ── Clear ──────────────────────────────────────────────────────────
@@ -213,31 +318,22 @@ var SQLiteWalletRepository = class {
213
318
  await this.db.run(`DELETE FROM ${this.tables.walletState}`);
214
319
  }
215
320
  // ── VTXO management ────────────────────────────────────────────────
216
- async getVtxos(address) {
217
- await this.ensureInit();
218
- const rows = await this.db.all(
219
- `SELECT * FROM ${this.tables.vtxos} WHERE address = ?`,
220
- [address]
221
- );
222
- return rows.map(vtxoRowToDomain);
321
+ async getVtxosPage(address, page) {
322
+ return this.pageByAddress(this.tables.vtxos, address, page, vtxoRowToDomain);
223
323
  }
224
324
  async saveVtxos(address, vtxos) {
225
325
  await this.ensureInit();
226
- for (const vtxo of vtxos) {
227
- const s = serializeVtxo(vtxo);
228
- await this.db.run(
229
- `INSERT OR REPLACE INTO ${this.tables.vtxos}
230
- (txid, vout, value, address,
231
- tap_tree, forfeit_cb, forfeit_s, intent_cb, intent_s,
232
- status_json, virtual_status_json, created_at,
233
- is_unrolled, is_spent, spent_by, settled_by, ark_tx_id,
234
- extra_witness_json, assets_json, script)
235
- VALUES (?, ?, ?, ?,
236
- ?, ?, ?, ?, ?,
237
- ?, ?, ?,
238
- ?, ?, ?, ?, ?,
239
- ?, ?, ?)`,
240
- [
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 [
241
337
  s.txid,
242
338
  s.vout,
243
339
  s.value,
@@ -248,71 +344,82 @@ var SQLiteWalletRepository = class {
248
344
  s.intentTapLeafScript.cb,
249
345
  s.intentTapLeafScript.s,
250
346
  JSON.stringify(s.status),
251
- JSON.stringify(s.virtualStatus),
252
- typeof s.createdAt === "string" ? s.createdAt : s.createdAt instanceof Date ? s.createdAt.toISOString() : new Date(s.createdAt).toISOString(),
347
+ createdAtToIso(s.createdAt),
253
348
  s.isUnrolled ? 1 : 0,
254
349
  s.isSpent === void 0 ? null : s.isSpent ? 1 : 0,
350
+ s.isSwept === void 0 ? null : s.isSwept ? 1 : 0,
351
+ s.isPreconfirmed === void 0 ? null : s.isPreconfirmed ? 1 : 0,
352
+ s.commitmentTxIds ? JSON.stringify(s.commitmentTxIds) : null,
353
+ s.expiresAt === void 0 ? null : new Date(s.expiresAt).toISOString(),
354
+ s.expiresAtHeight ?? null,
255
355
  s.spentBy ?? null,
256
356
  s.settledBy ?? null,
257
357
  s.arkTxId ?? null,
258
358
  s.extraWitness ? JSON.stringify(s.extraWitness) : null,
259
359
  s.assets ? JSON.stringify(s.assets) : null,
260
360
  s.script ?? null
261
- ]
262
- );
263
- }
361
+ ];
362
+ })
363
+ );
264
364
  }
265
365
  async deleteVtxos(address) {
266
366
  await this.ensureInit();
267
367
  await this.db.run(`DELETE FROM ${this.tables.vtxos} WHERE address = ?`, [address]);
268
368
  }
269
- async getVtxosForScript(script) {
369
+ async getVtxosForScriptPage(script, page, options) {
370
+ assertPageRequest(page);
270
371
  await this.ensureInit();
372
+ const after = page.after;
271
373
  const rows = await this.db.all(
272
- `SELECT * FROM ${this.tables.vtxos} WHERE script = ?`,
273
- [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]
274
387
  );
275
- return rows.map(vtxoRowToDomain);
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 })
392
+ );
393
+ return options?.unspentOnly ? { ...result, items: result.items.filter((row) => !isVtxoSpent(row.vtxo)) } : result;
276
394
  }
277
395
  async saveVtxosForScript(key, vtxos) {
278
- if (!key.address) {
279
- throw new Error("SQLiteWalletRepository requires an address");
280
- }
281
- for (const vtxo of vtxos) {
282
- if (!isVtxoForScript(vtxo, key.script)) {
283
- throw new Error(
284
- `VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`
285
- );
286
- }
287
- }
288
- return this.saveVtxos(key.address, vtxos);
396
+ return this.saveVtxos(checkSaveVtxosForScript("SQLiteWalletRepository", key, vtxos), vtxos);
289
397
  }
290
398
  async deleteVtxosForScript(script) {
291
399
  await this.ensureInit();
292
400
  await this.db.run(`DELETE FROM ${this.tables.vtxos} WHERE script = ?`, [script]);
293
401
  }
294
402
  // ── UTXO management ────────────────────────────────────────────────
295
- 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);
296
408
  await this.ensureInit();
297
409
  const rows = await this.db.all(
298
- `SELECT * FROM ${this.tables.utxos} WHERE address = ?`,
299
- [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]
300
413
  );
301
- 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 };
302
416
  }
303
- async saveUtxos(address, utxos) {
417
+ async saveUtxos(addressOrBatch, utxos) {
304
418
  await this.ensureInit();
305
- for (const utxo of utxos) {
306
- const s = serializeUtxo(utxo);
307
- await this.db.run(
308
- `INSERT OR REPLACE INTO ${this.tables.utxos}
309
- (txid, vout, value, address,
310
- tap_tree, forfeit_cb, forfeit_s, intent_cb, intent_s,
311
- status_json, extra_witness_json)
312
- VALUES (?, ?, ?, ?,
313
- ?, ?, ?, ?, ?,
314
- ?, ?)`,
315
- [
419
+ const rows = utxoEntries(addressOrBatch, utxos).flatMap(
420
+ ([address, list]) => list.map((utxo) => {
421
+ const s = serializeUtxo(utxo);
422
+ return [
316
423
  s.txid,
317
424
  s.vout,
318
425
  s.value,
@@ -324,45 +431,71 @@ var SQLiteWalletRepository = class {
324
431
  s.intentTapLeafScript.s,
325
432
  JSON.stringify(s.status),
326
433
  s.extraWitness ? JSON.stringify(s.extraWitness) : null
327
- ]
328
- );
329
- }
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
+ );
330
444
  }
331
445
  async deleteUtxos(address) {
332
446
  await this.ensureInit();
333
447
  await this.db.run(`DELETE FROM ${this.tables.utxos} WHERE address = ?`, [address]);
334
448
  }
335
449
  // ── Transaction history ────────────────────────────────────────────
336
- async getTransactionHistory(address) {
450
+ async getTransactionHistoryPage(filter, page) {
451
+ assertPageRequest(page);
452
+ assertHistoryPageFilter(filter);
337
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
+ }
338
471
  const rows = await this.db.all(
339
- `SELECT * FROM ${this.tables.transactions} WHERE address = ? ORDER BY created_at ASC`,
340
- [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]
341
475
  );
342
- return rows.map(txRowToDomain);
476
+ return pageResult(rows.map(txRowToDomain), page.limit, (tx) => ({
477
+ createdAt: tx.createdAt,
478
+ key: tx.key
479
+ }));
343
480
  }
344
481
  async saveTransactions(address, txs) {
345
482
  await this.ensureInit();
346
- for (const tx of txs) {
347
- await this.db.run(
348
- `INSERT OR REPLACE INTO ${this.tables.transactions}
349
- (address, boarding_txid, commitment_txid, ark_txid,
350
- type, amount, settled, created_at, assets_json)
351
- VALUES (?, ?, ?, ?,
352
- ?, ?, ?, ?, ?)`,
353
- [
354
- address,
355
- tx.key.boardingTxid,
356
- tx.key.commitmentTxid,
357
- tx.key.arkTxid,
358
- tx.type,
359
- tx.amount,
360
- tx.settled ? 1 : 0,
361
- tx.createdAt,
362
- tx.assets ? JSON.stringify(serializeAssets(tx.assets)) : null
363
- ]
364
- );
365
- }
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
+ );
366
499
  }
367
500
  async deleteTransactions(address) {
368
501
  await this.ensureInit();
@@ -397,15 +530,7 @@ var SQLiteWalletRepository = class {
397
530
  );
398
531
  }
399
532
  };
400
- var SAFE_PREFIX = /^[a-zA-Z0-9_]+$/;
401
- function sanitizePrefix(prefix) {
402
- if (!SAFE_PREFIX.test(prefix)) {
403
- throw new Error(
404
- `Invalid table prefix "${prefix}": only letters, digits, and underscores are allowed`
405
- );
406
- }
407
- return prefix;
408
- }
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 = '')";
409
534
  function vtxoRowToDomain(row) {
410
535
  const serialized = {
411
536
  txid: row.txid,
@@ -421,10 +546,14 @@ function vtxoRowToDomain(row) {
421
546
  s: row.intent_s
422
547
  },
423
548
  status: JSON.parse(row.status_json),
424
- virtualStatus: JSON.parse(row.virtual_status_json),
425
549
  createdAt: new Date(row.created_at),
426
550
  isUnrolled: row.is_unrolled === 1,
427
551
  isSpent: row.is_spent === null ? void 0 : row.is_spent === 1,
552
+ isSwept: row.is_swept == null ? void 0 : row.is_swept === 1,
553
+ isPreconfirmed: row.is_preconfirmed == null ? void 0 : row.is_preconfirmed === 1,
554
+ commitmentTxIds: row.commitment_txids_json ? JSON.parse(row.commitment_txids_json) : void 0,
555
+ expiresAt: row.expires_at ? new Date(row.expires_at) : void 0,
556
+ expiresAtHeight: row.expires_at_height ?? void 0,
428
557
  spentBy: row.spent_by ?? void 0,
429
558
  settledBy: row.settled_by ?? void 0,
430
559
  arkTxId: row.ark_tx_id ?? void 0,
@@ -475,14 +604,15 @@ function txRowToDomain(row) {
475
604
  }
476
605
 
477
606
  // src/repositories/sqlite/contractRepository.ts
607
+ var WATCH_STATE = "IFNULL(watch, 'watched')";
478
608
  var SQLiteContractRepository = class {
479
609
  constructor(db, options) {
480
610
  this.db = db;
481
- this.prefix = sanitizePrefix2(options?.prefix ?? "ark_");
611
+ this.prefix = sanitizeTablePrefix(options?.prefix ?? "ark_");
482
612
  this.table = `${this.prefix}contracts`;
483
613
  }
484
614
  db;
485
- version = 2;
615
+ version = 3;
486
616
  initPromise = null;
487
617
  prefix;
488
618
  table;
@@ -515,6 +645,12 @@ var SQLiteContractRepository = class {
515
645
  await this.db.run(
516
646
  `CREATE INDEX IF NOT EXISTS idx_${this.prefix}contracts_state ON ${this.table} (state)`
517
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
+ );
518
654
  }
519
655
  async addColumnIfMissing(column, type) {
520
656
  const columns = await this.db.all(`PRAGMA table_info(${this.table})`);
@@ -529,22 +665,28 @@ var SQLiteContractRepository = class {
529
665
  await this.db.run(`DELETE FROM ${this.table}`);
530
666
  }
531
667
  // ── Contract management ────────────────────────────────────────────
532
- async getContracts(filter) {
668
+ async getContractsPage(filter, page) {
669
+ assertPageRequest(page);
533
670
  await this.ensureInit();
671
+ if (contractFilterMatchesNothing(filter)) return { items: [] };
534
672
  const conditions = [];
535
673
  const params = [];
536
674
  if (filter) {
537
675
  this.addFilterCondition(conditions, params, "script", filter.script);
538
676
  this.addFilterCondition(conditions, params, "state", filter.state);
539
677
  this.addFilterCondition(conditions, params, "type", filter.type);
540
- this.addWatchCondition(conditions, params, filter.watch);
678
+ this.addFilterCondition(conditions, params, WATCH_STATE, filter.watch);
541
679
  }
542
- let sql = `SELECT * FROM ${this.table}`;
543
- if (conditions.length > 0) {
544
- sql += ` WHERE ${conditions.join(" AND ")}`;
680
+ if (page.after !== void 0) {
681
+ conditions.push("script > ?");
682
+ params.push(page.after);
545
683
  }
546
- const rows = await this.db.all(sql, params);
547
- 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);
548
690
  }
549
691
  async saveContract(contract) {
550
692
  await this.ensureInit();
@@ -584,29 +726,7 @@ var SQLiteContractRepository = class {
584
726
  params.push(value);
585
727
  }
586
728
  }
587
- /**
588
- * Same as {@link addFilterCondition}, except a row predating the
589
- * column stores NULL and must match `"watched"`.
590
- */
591
- addWatchCondition(conditions, params, value) {
592
- if (value === void 0) return;
593
- const wanted = Array.isArray(value) ? value : [value];
594
- if (wanted.length === 0) return;
595
- const placeholders = wanted.map(() => "?").join(", ");
596
- const clause = `watch IN (${placeholders})`;
597
- conditions.push(wanted.includes("watched") ? `(${clause} OR watch IS NULL)` : clause);
598
- params.push(...wanted);
599
- }
600
729
  };
601
- var SAFE_PREFIX2 = /^[a-zA-Z0-9_]+$/;
602
- function sanitizePrefix2(prefix) {
603
- if (!SAFE_PREFIX2.test(prefix)) {
604
- throw new Error(
605
- `Invalid table prefix "${prefix}": only letters, digits, and underscores are allowed`
606
- );
607
- }
608
- return prefix;
609
- }
610
730
  function contractRowToDomain(row) {
611
731
  const contract = {
612
732
  script: row.script,
@@ -629,15 +749,10 @@ function contractRowToDomain(row) {
629
749
  }
630
750
 
631
751
  // src/repositories/sqlite/intentRepository.ts
632
- var SAFE_PREFIX3 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
633
- function sanitizePrefix3(p) {
634
- if (!SAFE_PREFIX3.test(p)) throw new Error(`Invalid table prefix "${p}"`);
635
- return p;
636
- }
637
752
  var SQLiteIntentRepository = class {
638
753
  constructor(db, options) {
639
754
  this.db = db;
640
- this.prefix = sanitizePrefix3(options?.prefix ?? "ark_");
755
+ this.prefix = sanitizeTablePrefix(options?.prefix ?? "ark_");
641
756
  this.t = `${this.prefix}intents`;
642
757
  }
643
758
  db;
@@ -676,19 +791,16 @@ var SQLiteIntentRepository = class {
676
791
  `CREATE INDEX IF NOT EXISTS idx_${this.prefix}intent_state ON ${this.t} (state)`
677
792
  );
678
793
  }
679
- withTx(fn) {
680
- return runInTransaction(this.db, fn);
681
- }
682
794
  async clear() {
683
795
  await this.ensureInit();
684
- await this.withTx(async () => {
796
+ await runInTransaction(this.db, async () => {
685
797
  await this.db.run(`DELETE FROM ${this.t}`);
686
798
  });
687
799
  }
688
800
  async saveIntent(intent) {
689
801
  await this.ensureInit();
690
802
  const now = Date.now();
691
- await this.withTx(async () => {
803
+ await runInTransaction(this.db, async () => {
692
804
  await this.db.run(
693
805
  `INSERT INTO ${this.t}
694
806
  (intent_tx_id, intent_id, state, valid_from, valid_until,
@@ -736,15 +848,30 @@ var SQLiteIntentRepository = class {
736
848
  );
737
849
  });
738
850
  }
739
- async getIntents(filter) {
851
+ async getIntentsPage(filter, page) {
852
+ assertPageRequest(page);
740
853
  await this.ensureInit();
741
- const rows = await this.db.all(
742
- `SELECT * FROM ${this.t} ORDER BY created_at ASC, intent_tx_id ASC`
743
- );
744
- let out = rows.map(rowToIntent);
745
- if (filter) out = out.filter((i) => intentMatchesFilter(i, filter));
746
- const { skip, end } = intentPageBounds(filter, out.length);
747
- 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);
748
875
  }
749
876
  async getLockedVtxoOutpoints() {
750
877
  await this.ensureInit();
@@ -782,15 +909,10 @@ function rowToIntent(r) {
782
909
  }
783
910
 
784
911
  // src/repositories/sqlite/virtualTxRepository.ts
785
- var SAFE_PREFIX4 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
786
- function sanitizePrefix4(p) {
787
- if (!SAFE_PREFIX4.test(p)) throw new Error(`Invalid table prefix "${p}"`);
788
- return p;
789
- }
790
912
  var SQLiteVirtualTxRepository = class {
791
913
  constructor(db, options) {
792
914
  this.db = db;
793
- this.prefix = sanitizePrefix4(options?.prefix ?? "ark_");
915
+ this.prefix = sanitizeTablePrefix(options?.prefix ?? "ark_");
794
916
  this.tTx = `${this.prefix}virtual_txs`;
795
917
  this.tBranch = `${this.prefix}vtxo_branches`;
796
918
  }
@@ -822,12 +944,9 @@ var SQLiteVirtualTxRepository = class {
822
944
  `CREATE INDEX IF NOT EXISTS idx_${this.prefix}branch_vtxid ON ${this.tBranch} (virtual_txid)`
823
945
  );
824
946
  }
825
- tx(fn) {
826
- return runInTransaction(this.db, fn);
827
- }
828
947
  async clear() {
829
948
  await this.ensureInit();
830
- await this.tx(async () => {
949
+ await runInTransaction(this.db, async () => {
831
950
  await this.db.run(`DELETE FROM ${this.tTx}`);
832
951
  await this.db.run(`DELETE FROM ${this.tBranch}`);
833
952
  });
@@ -835,7 +954,7 @@ var SQLiteVirtualTxRepository = class {
835
954
  async upsertVirtualTxs(txs) {
836
955
  if (txs.length === 0) return;
837
956
  await this.ensureInit();
838
- await this.tx(async () => {
957
+ await runInTransaction(this.db, async () => {
839
958
  const placeholders = txs.map(() => "?").join(", ");
840
959
  const existing = await this.db.all(
841
960
  `SELECT * FROM ${this.tTx} WHERE txid IN (${placeholders})`,
@@ -862,7 +981,7 @@ var SQLiteVirtualTxRepository = class {
862
981
  }
863
982
  async setBranch(vtxo, branch) {
864
983
  await this.ensureInit();
865
- await this.tx(async () => {
984
+ await runInTransaction(this.db, async () => {
866
985
  await this.db.run(`DELETE FROM ${this.tBranch} WHERE vtxo_txid = ? AND vtxo_vout = ?`, [
867
986
  vtxo.txid,
868
987
  vtxo.vout
@@ -897,7 +1016,7 @@ var SQLiteVirtualTxRepository = class {
897
1016
  }
898
1017
  async pruneForSpentVtxo(vtxo) {
899
1018
  await this.ensureInit();
900
- await this.tx(async () => {
1019
+ await runInTransaction(this.db, async () => {
901
1020
  const removed = await this.db.all(
902
1021
  `SELECT DISTINCT virtual_txid FROM ${this.tBranch} WHERE vtxo_txid = ? AND vtxo_vout = ?`,
903
1022
  [vtxo.txid, vtxo.vout]
@@ -930,13 +1049,6 @@ function rowToTx(r) {
930
1049
  };
931
1050
  }
932
1051
 
933
- // src/repositories/sqlite/prefix.ts
934
- var SAFE_PREFIX5 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
935
- function sanitizeTablePrefix(p) {
936
- if (!SAFE_PREFIX5.test(p)) throw new Error(`Invalid table prefix "${p}"`);
937
- return p;
938
- }
939
-
940
1052
  export { SQLiteContractRepository, SQLiteIntentRepository, SQLiteVirtualTxRepository, SQLiteWalletRepository, runInTransaction, sanitizeTablePrefix };
941
1053
  //# sourceMappingURL=index.js.map
942
1054
  //# sourceMappingURL=index.js.map