@1sat/wallet 0.0.3 → 0.0.5

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 (121) hide show
  1. package/dist/OneSatWallet.d.ts +222 -0
  2. package/dist/OneSatWallet.d.ts.map +1 -0
  3. package/dist/OneSatWallet.js +754 -0
  4. package/dist/OneSatWallet.js.map +1 -0
  5. package/dist/address-sync/AddressManager.d.ts +86 -0
  6. package/dist/address-sync/AddressManager.d.ts.map +1 -0
  7. package/dist/address-sync/AddressManager.js +108 -0
  8. package/dist/address-sync/AddressManager.js.map +1 -0
  9. package/dist/address-sync/AddressSyncManager.d.ts +204 -0
  10. package/dist/address-sync/AddressSyncManager.d.ts.map +1 -0
  11. package/dist/address-sync/AddressSyncManager.js +525 -0
  12. package/dist/address-sync/AddressSyncManager.js.map +1 -0
  13. package/dist/address-sync/AddressSyncQueueIdb.d.ts +31 -0
  14. package/dist/address-sync/AddressSyncQueueIdb.d.ts.map +1 -0
  15. package/dist/address-sync/AddressSyncQueueIdb.js +356 -0
  16. package/dist/address-sync/AddressSyncQueueIdb.js.map +1 -0
  17. package/dist/address-sync/AddressSyncQueueSqlite.d.ts +55 -0
  18. package/dist/address-sync/AddressSyncQueueSqlite.d.ts.map +1 -0
  19. package/dist/address-sync/AddressSyncQueueSqlite.js +198 -0
  20. package/dist/address-sync/AddressSyncQueueSqlite.js.map +1 -0
  21. package/dist/address-sync/index.d.ts +8 -0
  22. package/dist/address-sync/index.d.ts.map +1 -0
  23. package/dist/address-sync/index.js +5 -0
  24. package/dist/address-sync/index.js.map +1 -0
  25. package/dist/backup/FileBackupProvider.d.ts +97 -0
  26. package/dist/backup/FileBackupProvider.d.ts.map +1 -0
  27. package/dist/backup/FileBackupProvider.js +185 -0
  28. package/dist/backup/FileBackupProvider.js.map +1 -0
  29. package/dist/backup/FileRestoreReader.d.ts +59 -0
  30. package/dist/backup/FileRestoreReader.d.ts.map +1 -0
  31. package/dist/backup/FileRestoreReader.js +89 -0
  32. package/dist/backup/FileRestoreReader.js.map +1 -0
  33. package/dist/backup/index.d.ts +6 -0
  34. package/dist/backup/index.d.ts.map +1 -0
  35. package/dist/backup/index.js +5 -0
  36. package/dist/backup/index.js.map +1 -0
  37. package/dist/backup/types.d.ts +32 -0
  38. package/dist/backup/types.d.ts.map +1 -0
  39. package/dist/backup/types.js +2 -0
  40. package/dist/backup/types.js.map +1 -0
  41. package/dist/cwi/chrome.d.ts +12 -0
  42. package/dist/cwi/chrome.d.ts.map +1 -0
  43. package/dist/cwi/chrome.js +44 -0
  44. package/dist/cwi/chrome.js.map +1 -0
  45. package/dist/cwi/event.d.ts +12 -0
  46. package/dist/cwi/event.d.ts.map +1 -0
  47. package/dist/cwi/event.js +39 -0
  48. package/dist/cwi/event.js.map +1 -0
  49. package/dist/cwi/factory.d.ts +15 -0
  50. package/dist/cwi/factory.d.ts.map +1 -0
  51. package/dist/cwi/factory.js +45 -0
  52. package/dist/cwi/factory.js.map +1 -0
  53. package/dist/cwi/index.d.ts +12 -0
  54. package/dist/cwi/index.d.ts.map +1 -0
  55. package/dist/cwi/index.js +12 -0
  56. package/dist/cwi/index.js.map +1 -0
  57. package/dist/cwi/types.d.ts +40 -0
  58. package/dist/cwi/types.d.ts.map +1 -0
  59. package/dist/cwi/types.js +40 -0
  60. package/dist/cwi/types.js.map +1 -0
  61. package/dist/index.d.ts +17 -20
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +22 -21
  64. package/dist/index.js.map +1 -1
  65. package/dist/indexers/Bsv21Indexer.d.ts +36 -0
  66. package/dist/indexers/Bsv21Indexer.d.ts.map +1 -0
  67. package/dist/indexers/Bsv21Indexer.js +232 -0
  68. package/dist/indexers/Bsv21Indexer.js.map +1 -0
  69. package/dist/indexers/CosignIndexer.d.ts +14 -0
  70. package/dist/indexers/CosignIndexer.d.ts.map +1 -0
  71. package/dist/indexers/CosignIndexer.js +27 -0
  72. package/dist/indexers/CosignIndexer.js.map +1 -0
  73. package/dist/indexers/FundIndexer.d.ts +20 -0
  74. package/dist/indexers/FundIndexer.d.ts.map +1 -0
  75. package/dist/indexers/FundIndexer.js +65 -0
  76. package/dist/indexers/FundIndexer.js.map +1 -0
  77. package/dist/indexers/InscriptionIndexer.d.ts +33 -0
  78. package/dist/indexers/InscriptionIndexer.d.ts.map +1 -0
  79. package/dist/indexers/InscriptionIndexer.js +116 -0
  80. package/dist/indexers/InscriptionIndexer.js.map +1 -0
  81. package/dist/indexers/LockIndexer.d.ts +11 -0
  82. package/dist/indexers/LockIndexer.d.ts.map +1 -0
  83. package/dist/indexers/LockIndexer.js +44 -0
  84. package/dist/indexers/LockIndexer.js.map +1 -0
  85. package/dist/indexers/MapIndexer.d.ts +14 -0
  86. package/dist/indexers/MapIndexer.d.ts.map +1 -0
  87. package/dist/indexers/MapIndexer.js +65 -0
  88. package/dist/indexers/MapIndexer.js.map +1 -0
  89. package/dist/indexers/OpNSIndexer.d.ts +10 -0
  90. package/dist/indexers/OpNSIndexer.d.ts.map +1 -0
  91. package/dist/indexers/OpNSIndexer.js +39 -0
  92. package/dist/indexers/OpNSIndexer.js.map +1 -0
  93. package/dist/indexers/OrdLockIndexer.d.ts +18 -0
  94. package/dist/indexers/OrdLockIndexer.d.ts.map +1 -0
  95. package/dist/indexers/OrdLockIndexer.js +65 -0
  96. package/dist/indexers/OrdLockIndexer.js.map +1 -0
  97. package/dist/indexers/OriginIndexer.d.ts +38 -0
  98. package/dist/indexers/OriginIndexer.d.ts.map +1 -0
  99. package/dist/indexers/OriginIndexer.js +241 -0
  100. package/dist/indexers/OriginIndexer.js.map +1 -0
  101. package/dist/indexers/Outpoint.d.ts +11 -0
  102. package/dist/indexers/Outpoint.d.ts.map +1 -0
  103. package/dist/indexers/Outpoint.js +62 -0
  104. package/dist/indexers/Outpoint.js.map +1 -0
  105. package/dist/indexers/SigmaIndexer.d.ts +29 -0
  106. package/dist/indexers/SigmaIndexer.d.ts.map +1 -0
  107. package/dist/indexers/SigmaIndexer.js +134 -0
  108. package/dist/indexers/SigmaIndexer.js.map +1 -0
  109. package/dist/indexers/index.d.ts +14 -0
  110. package/dist/indexers/index.d.ts.map +1 -0
  111. package/dist/indexers/index.js +14 -0
  112. package/dist/indexers/index.js.map +1 -0
  113. package/dist/indexers/parseAddress.d.ts +10 -0
  114. package/dist/indexers/parseAddress.d.ts.map +1 -0
  115. package/dist/indexers/parseAddress.js +25 -0
  116. package/dist/indexers/parseAddress.js.map +1 -0
  117. package/dist/signers/ReadOnlySigner.d.ts +16 -0
  118. package/dist/signers/ReadOnlySigner.d.ts.map +1 -0
  119. package/dist/signers/ReadOnlySigner.js +48 -0
  120. package/dist/signers/ReadOnlySigner.js.map +1 -0
  121. package/package.json +8 -6
@@ -0,0 +1,754 @@
1
+ import { OneSatServices } from '@1sat/client';
2
+ import { Beef, KeyDeriver, Random, Transaction, Utils, } from '@bsv/sdk';
3
+ import { Wallet, } from '@bsv/wallet-toolbox-mobile';
4
+ import { Bsv21Indexer, CosignIndexer, FundIndexer, InscriptionIndexer, LockIndexer, MapIndexer, OpNSIndexer, OrdLockIndexer, OriginIndexer, SigmaIndexer, } from './indexers';
5
+ import { Outpoint } from './indexers/Outpoint';
6
+ import { ReadOnlySigner } from './signers/ReadOnlySigner';
7
+ /**
8
+ * OneSatWallet extends the BRC-100 Wallet with 1Sat-specific indexing and services.
9
+ *
10
+ * Can be instantiated with either:
11
+ * - A public key (read-only mode for queries)
12
+ * - A private key (full signing capability)
13
+ */
14
+ export class OneSatWallet extends Wallet {
15
+ isReadOnly;
16
+ indexers;
17
+ services;
18
+ owners;
19
+ listeners = {};
20
+ activeSyncs = new Map();
21
+ activeMultiSync = null;
22
+ constructor(args) {
23
+ const rootKey = args.rootKey;
24
+ const isReadOnly = typeof rootKey === 'string';
25
+ let keyDeriver;
26
+ if (typeof rootKey === 'string') {
27
+ keyDeriver = new ReadOnlySigner(rootKey);
28
+ }
29
+ else {
30
+ keyDeriver = new KeyDeriver(rootKey);
31
+ }
32
+ const services = new OneSatServices(args.chain, args.onesatUrl);
33
+ const network = args.chain === 'main' ? 'mainnet' : 'testnet';
34
+ const owners = args.owners ?? new Set();
35
+ super({
36
+ chain: args.chain,
37
+ keyDeriver,
38
+ storage: args.storage,
39
+ services,
40
+ });
41
+ this.isReadOnly = isReadOnly;
42
+ this.services = services;
43
+ this.owners = owners;
44
+ this.indexers =
45
+ args.indexers ??
46
+ [
47
+ new FundIndexer(owners, network),
48
+ new LockIndexer(owners, network),
49
+ new InscriptionIndexer(owners, network),
50
+ new SigmaIndexer(owners, network),
51
+ new MapIndexer(owners, network),
52
+ new OriginIndexer(owners, network, services),
53
+ new Bsv21Indexer(owners, network, services),
54
+ new OrdLockIndexer(owners, network),
55
+ new OpNSIndexer(owners, network),
56
+ new CosignIndexer(owners, network),
57
+ ];
58
+ if (args.autoSync) {
59
+ this.syncAll();
60
+ }
61
+ }
62
+ /**
63
+ * Returns true if this wallet was created with only a public key.
64
+ * Read-only wallets can query but not sign transactions.
65
+ */
66
+ get readOnly() {
67
+ return this.isReadOnly;
68
+ }
69
+ /**
70
+ * Subscribe to wallet events
71
+ */
72
+ on(event, callback) {
73
+ if (!this.listeners[event]) {
74
+ this.listeners[event] = new Set();
75
+ }
76
+ ;
77
+ this.listeners[event].add(callback);
78
+ }
79
+ /**
80
+ * Unsubscribe from wallet events
81
+ */
82
+ off(event, callback) {
83
+ ;
84
+ this.listeners[event]?.delete(callback);
85
+ }
86
+ /**
87
+ * Emit a wallet event
88
+ */
89
+ emit(event, data) {
90
+ const callbacks = this.listeners[event];
91
+ if (callbacks) {
92
+ for (const cb of callbacks) {
93
+ cb(data);
94
+ }
95
+ }
96
+ }
97
+ /**
98
+ * Add an address to the set of owned addresses.
99
+ * Outputs to these addresses will be indexed.
100
+ */
101
+ addOwner(address) {
102
+ this.owners.add(address);
103
+ }
104
+ /**
105
+ * Parse a transaction through indexers without internalizing.
106
+ *
107
+ * This is useful for debugging/testing to see what the indexers produce
108
+ * without actually storing the transaction in the wallet.
109
+ */
110
+ async parseTransaction(txOrTxid, isBroadcasted = true) {
111
+ const tx = typeof txOrTxid === 'string'
112
+ ? await this.loadTransaction(txOrTxid)
113
+ : txOrTxid;
114
+ await this.hydrateSourceTransactions(tx);
115
+ const ctx = this.buildParseContext(tx);
116
+ await this.parseInputs(ctx);
117
+ for (const txo of ctx.txos) {
118
+ await this.runIndexersOnTxo(txo);
119
+ }
120
+ for (const indexer of this.indexers) {
121
+ const summary = await indexer.summarize(ctx, isBroadcasted);
122
+ if (summary) {
123
+ ctx.summary[indexer.tag] = summary;
124
+ }
125
+ }
126
+ return ctx;
127
+ }
128
+ /**
129
+ * Parse a single output without full transaction context.
130
+ * Runs all indexers' parse() methods but NOT summarize().
131
+ */
132
+ async parseOutput(output, outpoint) {
133
+ const txo = { output, outpoint, data: {} };
134
+ await this.runIndexersOnTxo(txo);
135
+ return txo;
136
+ }
137
+ /**
138
+ * Load and parse a single output by outpoint.
139
+ * Loads the transaction, extracts the output, and runs indexers on it.
140
+ */
141
+ async loadTxo(outpoint) {
142
+ const op = new Outpoint(outpoint);
143
+ const tx = await this.loadTransaction(op.txid);
144
+ const output = tx.outputs[op.vout];
145
+ if (!output) {
146
+ throw new Error(`Output ${op.vout} not found in transaction ${op.txid}`);
147
+ }
148
+ return this.parseOutput(output, op);
149
+ }
150
+ /**
151
+ * Run all indexers on a single Txo and populate its data/owner/basket
152
+ */
153
+ async runIndexersOnTxo(txo) {
154
+ for (const indexer of this.indexers) {
155
+ const result = await indexer.parse(txo);
156
+ if (result) {
157
+ txo.data[indexer.tag] = {
158
+ data: result.data,
159
+ tags: result.tags,
160
+ };
161
+ if (result.owner) {
162
+ txo.owner = result.owner;
163
+ }
164
+ if (result.basket) {
165
+ txo.basket = result.basket;
166
+ }
167
+ }
168
+ }
169
+ }
170
+ /**
171
+ * Parse all inputs - run indexers on source outputs to populate ctx.spends
172
+ */
173
+ async parseInputs(ctx) {
174
+ for (const input of ctx.tx.inputs) {
175
+ if (!input.sourceTransaction)
176
+ continue;
177
+ const sourceOutput = input.sourceTransaction.outputs[input.sourceOutputIndex];
178
+ if (!sourceOutput)
179
+ continue;
180
+ const sourceTxid = input.sourceTransaction.id('hex');
181
+ const sourceVout = input.sourceOutputIndex;
182
+ const spendTxo = {
183
+ output: sourceOutput,
184
+ outpoint: new Outpoint(sourceTxid, sourceVout),
185
+ data: {},
186
+ };
187
+ await this.runIndexersOnTxo(spendTxo);
188
+ ctx.spends.push(spendTxo);
189
+ }
190
+ }
191
+ /**
192
+ * Load a transaction by txid.
193
+ * Checks storage first, falls back to beef service.
194
+ */
195
+ async loadTransaction(txid) {
196
+ const userId = await this.storage.getUserId();
197
+ const existingTx = await this.storage.runAsStorageProvider(async (sp) => {
198
+ const txs = await sp.findTransactions({ partial: { userId, txid } });
199
+ return txs.length > 0 ? txs[0] : null;
200
+ });
201
+ if (existingTx?.rawTx) {
202
+ return Transaction.fromBinary(existingTx.rawTx);
203
+ }
204
+ const beefBytes = await this.services.beef.getBeef(txid);
205
+ return Transaction.fromBEEF(Array.from(beefBytes));
206
+ }
207
+ /**
208
+ * Load and attach source transactions for all inputs (1 level deep).
209
+ * Modifies the transaction in place.
210
+ */
211
+ async hydrateSourceTransactions(tx) {
212
+ const loaded = new Map();
213
+ for (const input of tx.inputs) {
214
+ if (!input.sourceTransaction && input.sourceTXID) {
215
+ if (!loaded.has(input.sourceTXID)) {
216
+ loaded.set(input.sourceTXID, await this.loadTransaction(input.sourceTXID));
217
+ }
218
+ input.sourceTransaction = loaded.get(input.sourceTXID);
219
+ }
220
+ }
221
+ }
222
+ /**
223
+ * Build minimal parse context from transaction
224
+ */
225
+ buildParseContext(tx) {
226
+ const txid = tx.id('hex');
227
+ return {
228
+ tx,
229
+ txid,
230
+ txos: tx.outputs.map((output, vout) => ({
231
+ output,
232
+ outpoint: new Outpoint(txid, vout),
233
+ data: {},
234
+ })),
235
+ spends: [],
236
+ summary: {},
237
+ indexers: this.indexers,
238
+ };
239
+ }
240
+ /**
241
+ * Ingest a transaction by running it through indexers and writing directly to storage.
242
+ */
243
+ async ingestTransaction(tx, description, labels, isBroadcasted = true) {
244
+ const ctx = await this.parseTransaction(tx, isBroadcasted);
245
+ const txid = tx.id('hex');
246
+ const ownedTxos = ctx.txos.filter((txo) => txo.owner && this.owners.has(txo.owner));
247
+ const userId = await this.storage.getUserId();
248
+ const internalizedCount = await this.storage.runAsStorageProvider(async (sp) => {
249
+ return sp.transaction(async (trx) => {
250
+ const existingTxs = await sp.findTransactions({
251
+ partial: { userId, txid },
252
+ trx,
253
+ });
254
+ let transactionId;
255
+ let isNewTransaction = false;
256
+ if (existingTxs.length > 0) {
257
+ transactionId = existingTxs[0].transactionId;
258
+ }
259
+ else {
260
+ let isOutgoing = false;
261
+ let satoshisSpent = 0;
262
+ for (const input of tx.inputs) {
263
+ if (input.sourceTXID) {
264
+ const spentOutputs = await sp.findOutputs({
265
+ partial: {
266
+ userId,
267
+ txid: input.sourceTXID,
268
+ vout: input.sourceOutputIndex,
269
+ },
270
+ trx,
271
+ });
272
+ if (spentOutputs.length > 0) {
273
+ isOutgoing = true;
274
+ satoshisSpent += spentOutputs[0].satoshis;
275
+ }
276
+ }
277
+ }
278
+ const satoshisReceived = ownedTxos.reduce((sum, txo) => sum + (txo.output.satoshis || 0), 0);
279
+ const satoshis = satoshisReceived - satoshisSpent;
280
+ const now = new Date();
281
+ const reference = Utils.toBase64(Random(12));
282
+ const status = isBroadcasted
283
+ ? 'completed'
284
+ : 'unproven';
285
+ const newTx = {
286
+ created_at: now,
287
+ updated_at: now,
288
+ transactionId: 0,
289
+ userId,
290
+ status,
291
+ reference,
292
+ isOutgoing,
293
+ satoshis,
294
+ description,
295
+ version: tx.version,
296
+ lockTime: tx.lockTime,
297
+ txid,
298
+ rawTx: Array.from(tx.toBinary()),
299
+ };
300
+ transactionId = await sp.insertTransaction(newTx, trx);
301
+ isNewTransaction = true;
302
+ const txQueue = [...tx.inputs];
303
+ for (const input of txQueue) {
304
+ if (!input.sourceTransaction)
305
+ continue;
306
+ const sourceTxid = input.sourceTransaction.id('hex');
307
+ const existing = await sp.findTransactions({
308
+ partial: { userId, txid: sourceTxid },
309
+ trx,
310
+ });
311
+ if (existing.length > 0)
312
+ continue;
313
+ const sourceNow = new Date();
314
+ const sourceRef = Utils.toBase64(Random(12));
315
+ await sp.insertTransaction({
316
+ created_at: sourceNow,
317
+ updated_at: sourceNow,
318
+ transactionId: 0,
319
+ userId,
320
+ status: 'completed',
321
+ reference: sourceRef,
322
+ isOutgoing: false,
323
+ satoshis: 0,
324
+ description: 'source transaction',
325
+ version: input.sourceTransaction.version,
326
+ lockTime: input.sourceTransaction.lockTime,
327
+ txid: sourceTxid,
328
+ rawTx: Array.from(input.sourceTransaction.toBinary()),
329
+ }, trx);
330
+ txQueue.push(...input.sourceTransaction.inputs);
331
+ }
332
+ for (const label of labels || []) {
333
+ const txLabel = await sp.findOrInsertTxLabel(userId, label, trx);
334
+ if (txLabel.txLabelId) {
335
+ await sp.findOrInsertTxLabelMap(transactionId, txLabel.txLabelId, trx);
336
+ }
337
+ }
338
+ }
339
+ if (isNewTransaction) {
340
+ for (const input of tx.inputs) {
341
+ if (input.sourceTXID) {
342
+ const spentOutputs = await sp.findOutputs({
343
+ partial: {
344
+ userId,
345
+ txid: input.sourceTXID,
346
+ vout: input.sourceOutputIndex,
347
+ },
348
+ trx,
349
+ });
350
+ if (spentOutputs.length > 0) {
351
+ const output = spentOutputs[0];
352
+ if (output.outputId) {
353
+ await sp.updateOutput(output.outputId, {
354
+ spendable: false,
355
+ spentBy: transactionId,
356
+ }, trx);
357
+ }
358
+ }
359
+ }
360
+ }
361
+ }
362
+ let outputsCreated = 0;
363
+ for (const txo of ownedTxos) {
364
+ const existingOutputs = await sp.findOutputs({
365
+ partial: { userId, txid, vout: txo.outpoint.vout },
366
+ trx,
367
+ });
368
+ if (existingOutputs.length > 0) {
369
+ continue;
370
+ }
371
+ const tags = [];
372
+ if (txo.owner) {
373
+ tags.push(`own:${txo.owner}`);
374
+ }
375
+ for (const indexData of Object.values(txo.data)) {
376
+ if (indexData.tags) {
377
+ tags.push(...indexData.tags);
378
+ }
379
+ }
380
+ const basketName = txo.basket || 'default';
381
+ const basket = await sp.findOrInsertOutputBasket(userId, basketName, trx);
382
+ const now = new Date();
383
+ const providedBy = 'you';
384
+ const newOutput = {
385
+ created_at: now,
386
+ updated_at: now,
387
+ outputId: 0,
388
+ userId,
389
+ transactionId,
390
+ basketId: basket.basketId,
391
+ spendable: true,
392
+ change: basketName === 'default',
393
+ outputDescription: '',
394
+ vout: txo.outpoint.vout,
395
+ satoshis: txo.output.satoshis || 0,
396
+ providedBy,
397
+ purpose: basketName === 'default' ? 'change' : '',
398
+ type: 'custom',
399
+ txid,
400
+ lockingScript: Array.from(txo.output.lockingScript.toBinary()),
401
+ spentBy: undefined,
402
+ };
403
+ const outputId = await sp.insertOutput(newOutput, trx);
404
+ for (const tag of tags) {
405
+ const outputTag = await sp.findOrInsertOutputTag(userId, tag, trx);
406
+ if (outputTag.outputTagId) {
407
+ await sp.findOrInsertOutputTagMap(outputId, outputTag.outputTagId, trx);
408
+ }
409
+ }
410
+ outputsCreated++;
411
+ }
412
+ return outputsCreated;
413
+ });
414
+ });
415
+ return { parseContext: ctx, internalizedCount };
416
+ }
417
+ /**
418
+ * Broadcast a transaction and ingest it into the wallet if successful.
419
+ */
420
+ async broadcast(tx, description, labels) {
421
+ const txid = tx.id('hex');
422
+ const beef = new Beef();
423
+ beef.mergeTransaction(tx);
424
+ const results = await this.services.postBeef(beef, [txid]);
425
+ const result = results[0];
426
+ if (result.status !== 'success') {
427
+ const errorMsg = result.error?.message || 'Broadcast failed';
428
+ throw new Error(`Broadcast failed for ${txid}: ${errorMsg}`);
429
+ }
430
+ return this.ingestTransaction(tx, description, labels, true);
431
+ }
432
+ /**
433
+ * Sync a single address from the 1Sat indexer using Server-Sent Events.
434
+ * Runs in the background - use stopSync() or close() to stop.
435
+ */
436
+ syncAddress(address, fromScore = 0) {
437
+ this.stopSync(address);
438
+ this.emit('sync:start', { address, fromScore });
439
+ const seenTxids = new Set();
440
+ const processOutput = async (output) => {
441
+ this.emit('sync:output', { address, output });
442
+ const txid = output.outpoint.substring(0, 64);
443
+ if (seenTxids.has(txid)) {
444
+ this.emit('sync:skipped', {
445
+ address,
446
+ outpoint: output.outpoint,
447
+ reason: 'already processed in this session',
448
+ });
449
+ if (output.spendTxid && !seenTxids.has(output.spendTxid)) {
450
+ await this.processSpendTx(address, output, seenTxids);
451
+ }
452
+ return;
453
+ }
454
+ const vout = Number.parseInt(output.outpoint.substring(65), 10);
455
+ const hasOutput = await this.storage.runAsStorageProvider(async (sp) => {
456
+ const outputs = await sp.findOutputs({ partial: { txid, vout } });
457
+ return outputs.length > 0;
458
+ });
459
+ if (!hasOutput) {
460
+ if (output.spendTxid) {
461
+ seenTxids.add(txid);
462
+ this.emit('sync:skipped', {
463
+ address,
464
+ outpoint: output.outpoint,
465
+ reason: 'already spent, skipping historical output',
466
+ });
467
+ return;
468
+ }
469
+ const tx = await this.loadTransaction(txid);
470
+ const result = await this.ingestTransaction(tx, '1sat-sync');
471
+ seenTxids.add(txid);
472
+ this.emit('sync:parsed', {
473
+ address,
474
+ txid,
475
+ parseContext: result.parseContext,
476
+ internalizedCount: result.internalizedCount,
477
+ });
478
+ }
479
+ else {
480
+ seenTxids.add(txid);
481
+ if (output.spendTxid) {
482
+ await this.processSpendTx(address, output, seenTxids);
483
+ }
484
+ else {
485
+ this.emit('sync:skipped', {
486
+ address,
487
+ outpoint: output.outpoint,
488
+ reason: 'already have output in storage',
489
+ });
490
+ }
491
+ }
492
+ };
493
+ const unsubscribe = this.services.owner.sync([address], (output) => {
494
+ processOutput(output);
495
+ }, fromScore, () => {
496
+ this.activeSyncs.delete(address);
497
+ this.emit('sync:complete', { address });
498
+ }, (error) => {
499
+ this.activeSyncs.delete(address);
500
+ this.emit('sync:error', { address, error });
501
+ });
502
+ this.activeSyncs.set(address, unsubscribe);
503
+ }
504
+ /**
505
+ * Process a spend transaction during sync.
506
+ * Only marks our output as spent - doesn't ingest the spend tx.
507
+ */
508
+ async processSpendTx(address, output, seenTxids) {
509
+ if (!output.spendTxid || seenTxids.has(output.spendTxid)) {
510
+ return;
511
+ }
512
+ const spentTxid = output.outpoint.substring(0, 64);
513
+ const spentVout = Number.parseInt(output.outpoint.substring(65), 10);
514
+ const userId = await this.storage.getUserId();
515
+ const outputRecord = await this.storage.runAsStorageProvider(async (sp) => {
516
+ const outputs = await sp.findOutputs({
517
+ partial: { userId, txid: spentTxid, vout: spentVout },
518
+ });
519
+ return outputs.length > 0 ? outputs[0] : null;
520
+ });
521
+ if (!outputRecord || !outputRecord.spendable) {
522
+ seenTxids.add(output.spendTxid);
523
+ this.emit('sync:skipped', {
524
+ address,
525
+ outpoint: output.outpoint,
526
+ reason: outputRecord ? 'already marked spent' : 'output not in wallet',
527
+ });
528
+ return;
529
+ }
530
+ let spendTx;
531
+ const existingTx = await this.storage.runAsStorageProvider(async (sp) => {
532
+ const txs = await sp.findTransactions({
533
+ partial: { userId, txid: output.spendTxid },
534
+ });
535
+ return txs.length > 0 ? txs[0] : null;
536
+ });
537
+ if (existingTx?.rawTx) {
538
+ spendTx = Transaction.fromBinary(existingTx.rawTx);
539
+ }
540
+ else {
541
+ const beefBytes = await this.services.beef.getBeef(output.spendTxid);
542
+ spendTx = Transaction.fromBEEF(Array.from(beefBytes));
543
+ const chainTracker = await this.services.getChainTracker();
544
+ const isValid = await spendTx.verify(chainTracker);
545
+ if (!isValid) {
546
+ this.emit('sync:error', {
547
+ address,
548
+ error: new Error(`Spend tx ${output.spendTxid} failed SPV verification`),
549
+ });
550
+ return;
551
+ }
552
+ }
553
+ const inputFound = spendTx.inputs.some((input) => input.sourceTXID === spentTxid && input.sourceOutputIndex === spentVout);
554
+ if (!inputFound) {
555
+ this.emit('sync:error', {
556
+ address,
557
+ error: new Error(`Outpoint ${output.outpoint} not found in spend tx ${output.spendTxid} inputs`),
558
+ });
559
+ return;
560
+ }
561
+ seenTxids.add(output.spendTxid);
562
+ const outputId = outputRecord.outputId;
563
+ if (outputId) {
564
+ await this.storage.runAsStorageProvider(async (sp) => {
565
+ await sp.updateOutput(outputId, { spendable: false });
566
+ });
567
+ }
568
+ this.emit('sync:skipped', {
569
+ address,
570
+ outpoint: output.outpoint,
571
+ reason: 'marked as spent',
572
+ });
573
+ }
574
+ /**
575
+ * Stop syncing a specific address.
576
+ */
577
+ stopSync(address) {
578
+ const unsubscribe = this.activeSyncs.get(address);
579
+ if (unsubscribe) {
580
+ unsubscribe();
581
+ this.activeSyncs.delete(address);
582
+ }
583
+ }
584
+ /**
585
+ * Close the wallet and cleanup all active sync connections.
586
+ */
587
+ close() {
588
+ this.stopSyncAll();
589
+ for (const unsubscribe of this.activeSyncs.values()) {
590
+ unsubscribe();
591
+ }
592
+ this.activeSyncs.clear();
593
+ this.services.close();
594
+ }
595
+ /**
596
+ * Start syncing all owner addresses using a single multi-owner SSE connection.
597
+ * This is more efficient than syncing each address individually.
598
+ */
599
+ syncAll(fromScore = 0) {
600
+ this.stopSyncAll();
601
+ const addresses = Array.from(this.owners);
602
+ if (addresses.length === 0)
603
+ return;
604
+ if (addresses.length === 1) {
605
+ this.syncAddress(addresses[0], fromScore);
606
+ return;
607
+ }
608
+ this.emit('syncAll:start', { addresses, fromScore });
609
+ const seenTxids = new Set();
610
+ const processOutput = async (output) => {
611
+ this.emit('syncAll:output', { output });
612
+ const txid = output.outpoint.substring(0, 64);
613
+ if (seenTxids.has(txid)) {
614
+ this.emit('syncAll:skipped', {
615
+ outpoint: output.outpoint,
616
+ reason: 'already processed in this session',
617
+ });
618
+ if (output.spendTxid && !seenTxids.has(output.spendTxid)) {
619
+ await this.processSpendTxMulti(output, seenTxids);
620
+ }
621
+ return;
622
+ }
623
+ const vout = Number.parseInt(output.outpoint.substring(65), 10);
624
+ const hasOutput = await this.storage.runAsStorageProvider(async (sp) => {
625
+ const outputs = await sp.findOutputs({ partial: { txid, vout } });
626
+ return outputs.length > 0;
627
+ });
628
+ if (!hasOutput) {
629
+ if (output.spendTxid) {
630
+ seenTxids.add(txid);
631
+ this.emit('syncAll:skipped', {
632
+ outpoint: output.outpoint,
633
+ reason: 'already spent, skipping historical output',
634
+ });
635
+ return;
636
+ }
637
+ const tx = await this.loadTransaction(txid);
638
+ const result = await this.ingestTransaction(tx, '1sat-sync');
639
+ seenTxids.add(txid);
640
+ this.emit('syncAll:parsed', {
641
+ txid,
642
+ parseContext: result.parseContext,
643
+ internalizedCount: result.internalizedCount,
644
+ });
645
+ }
646
+ else {
647
+ seenTxids.add(txid);
648
+ if (output.spendTxid) {
649
+ await this.processSpendTxMulti(output, seenTxids);
650
+ }
651
+ else {
652
+ this.emit('syncAll:skipped', {
653
+ outpoint: output.outpoint,
654
+ reason: 'already have output in storage',
655
+ });
656
+ }
657
+ }
658
+ };
659
+ const unsubscribe = this.services.owner.sync(addresses, (output) => {
660
+ processOutput(output);
661
+ }, fromScore, () => {
662
+ this.activeMultiSync = null;
663
+ this.emit('syncAll:complete', { addresses });
664
+ }, (error) => {
665
+ this.activeMultiSync = null;
666
+ this.emit('syncAll:error', { error });
667
+ });
668
+ this.activeMultiSync = unsubscribe;
669
+ }
670
+ /**
671
+ * Process a spend transaction during multi-owner sync.
672
+ */
673
+ async processSpendTxMulti(output, seenTxids) {
674
+ if (!output.spendTxid || seenTxids.has(output.spendTxid)) {
675
+ return;
676
+ }
677
+ const spentTxid = output.outpoint.substring(0, 64);
678
+ const spentVout = Number.parseInt(output.outpoint.substring(65), 10);
679
+ const userId = await this.storage.getUserId();
680
+ const outputRecord = await this.storage.runAsStorageProvider(async (sp) => {
681
+ const outputs = await sp.findOutputs({
682
+ partial: { userId, txid: spentTxid, vout: spentVout },
683
+ });
684
+ return outputs.length > 0 ? outputs[0] : null;
685
+ });
686
+ if (!outputRecord || !outputRecord.spendable) {
687
+ seenTxids.add(output.spendTxid);
688
+ this.emit('syncAll:skipped', {
689
+ outpoint: output.outpoint,
690
+ reason: outputRecord ? 'already marked spent' : 'output not in wallet',
691
+ });
692
+ return;
693
+ }
694
+ let spendTx;
695
+ const existingTx = await this.storage.runAsStorageProvider(async (sp) => {
696
+ const txs = await sp.findTransactions({
697
+ partial: { userId, txid: output.spendTxid },
698
+ });
699
+ return txs.length > 0 ? txs[0] : null;
700
+ });
701
+ if (existingTx?.rawTx) {
702
+ spendTx = Transaction.fromBinary(existingTx.rawTx);
703
+ }
704
+ else {
705
+ const beefBytes = await this.services.beef.getBeef(output.spendTxid);
706
+ spendTx = Transaction.fromBEEF(Array.from(beefBytes));
707
+ const chainTracker = await this.services.getChainTracker();
708
+ const isValid = await spendTx.verify(chainTracker);
709
+ if (!isValid) {
710
+ this.emit('syncAll:error', {
711
+ error: new Error(`Spend tx ${output.spendTxid} failed SPV verification`),
712
+ });
713
+ return;
714
+ }
715
+ }
716
+ const inputFound = spendTx.inputs.some((input) => input.sourceTXID === spentTxid && input.sourceOutputIndex === spentVout);
717
+ if (!inputFound) {
718
+ this.emit('syncAll:error', {
719
+ error: new Error(`Outpoint ${output.outpoint} not found in spend tx ${output.spendTxid} inputs`),
720
+ });
721
+ return;
722
+ }
723
+ seenTxids.add(output.spendTxid);
724
+ const outputId = outputRecord.outputId;
725
+ if (outputId) {
726
+ await this.storage.runAsStorageProvider(async (sp) => {
727
+ await sp.updateOutput(outputId, { spendable: false });
728
+ });
729
+ }
730
+ this.emit('syncAll:skipped', {
731
+ outpoint: output.outpoint,
732
+ reason: 'marked as spent',
733
+ });
734
+ }
735
+ /**
736
+ * Stop the multi-owner sync.
737
+ */
738
+ stopSyncAll() {
739
+ if (this.activeMultiSync) {
740
+ this.activeMultiSync();
741
+ this.activeMultiSync = null;
742
+ }
743
+ }
744
+ /**
745
+ * Start syncing each owner address individually.
746
+ * For most cases, use syncAll() instead which uses a single connection.
747
+ */
748
+ syncEach() {
749
+ for (const addr of this.owners) {
750
+ this.syncAddress(addr);
751
+ }
752
+ }
753
+ }
754
+ //# sourceMappingURL=OneSatWallet.js.map