@bsv/wallet-toolbox 2.10.4 → 2.11.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 (155) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +47 -0
  3. package/docs/managed-change-liquidity.md +7 -0
  4. package/docs/no-send-expiry.md +167 -0
  5. package/docs/prepared-beef.md +197 -0
  6. package/docs/setup.md +11 -0
  7. package/docs/storage.md +11 -0
  8. package/out/src/Setup.d.ts +3 -0
  9. package/out/src/Setup.d.ts.map +1 -1
  10. package/out/src/Setup.js +1 -0
  11. package/out/src/Setup.js.map +1 -1
  12. package/out/src/Wallet.d.ts.map +1 -1
  13. package/out/src/Wallet.js +11 -1
  14. package/out/src/Wallet.js.map +1 -1
  15. package/out/src/WalletPermissionsManager.d.ts +30 -6
  16. package/out/src/WalletPermissionsManager.d.ts.map +1 -1
  17. package/out/src/WalletPermissionsManager.js +84 -9
  18. package/out/src/WalletPermissionsManager.js.map +1 -1
  19. package/out/src/mockchain/MockServices.d.ts +2 -0
  20. package/out/src/mockchain/MockServices.d.ts.map +1 -1
  21. package/out/src/mockchain/MockServices.js +20 -0
  22. package/out/src/mockchain/MockServices.js.map +1 -1
  23. package/out/src/monitor/Monitor.d.ts +1 -0
  24. package/out/src/monitor/Monitor.d.ts.map +1 -1
  25. package/out/src/monitor/Monitor.js +19 -3
  26. package/out/src/monitor/Monitor.js.map +1 -1
  27. package/out/src/monitor/tasks/TaskNoSendExpiry.d.ts +19 -0
  28. package/out/src/monitor/tasks/TaskNoSendExpiry.d.ts.map +1 -0
  29. package/out/src/monitor/tasks/TaskNoSendExpiry.js +47 -0
  30. package/out/src/monitor/tasks/TaskNoSendExpiry.js.map +1 -0
  31. package/out/src/monitor/tasks/index.all.d.ts +1 -0
  32. package/out/src/monitor/tasks/index.all.d.ts.map +1 -1
  33. package/out/src/monitor/tasks/index.all.js +1 -0
  34. package/out/src/monitor/tasks/index.all.js.map +1 -1
  35. package/out/src/sdk/ActionBatch.interfaces.d.ts +4 -0
  36. package/out/src/sdk/ActionBatch.interfaces.d.ts.map +1 -1
  37. package/out/src/sdk/WalletStorage.interfaces.d.ts +35 -0
  38. package/out/src/sdk/WalletStorage.interfaces.d.ts.map +1 -1
  39. package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageKnex.d.ts +0 -1
  40. package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageKnex.d.ts.map +1 -1
  41. package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageKnex.js +23 -12
  42. package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageKnex.js.map +1 -1
  43. package/out/src/signer/methods/createNoSendExpiryAction.d.ts +7 -0
  44. package/out/src/signer/methods/createNoSendExpiryAction.d.ts.map +1 -0
  45. package/out/src/signer/methods/createNoSendExpiryAction.js +249 -0
  46. package/out/src/signer/methods/createNoSendExpiryAction.js.map +1 -0
  47. package/out/src/signer/methods/signAction.d.ts.map +1 -1
  48. package/out/src/signer/methods/signAction.js +9 -0
  49. package/out/src/signer/methods/signAction.js.map +1 -1
  50. package/out/src/storage/StorageIdb.d.ts +3 -0
  51. package/out/src/storage/StorageIdb.d.ts.map +1 -1
  52. package/out/src/storage/StorageIdb.js +37 -1
  53. package/out/src/storage/StorageIdb.js.map +1 -1
  54. package/out/src/storage/StorageKnex.d.ts +48 -3
  55. package/out/src/storage/StorageKnex.d.ts.map +1 -1
  56. package/out/src/storage/StorageKnex.js +234 -14
  57. package/out/src/storage/StorageKnex.js.map +1 -1
  58. package/out/src/storage/StorageProvider.d.ts +6 -1
  59. package/out/src/storage/StorageProvider.d.ts.map +1 -1
  60. package/out/src/storage/StorageProvider.js +147 -5
  61. package/out/src/storage/StorageProvider.js.map +1 -1
  62. package/out/src/storage/StorageReaderWriter.d.ts +2 -0
  63. package/out/src/storage/StorageReaderWriter.d.ts.map +1 -1
  64. package/out/src/storage/StorageReaderWriter.js +4 -0
  65. package/out/src/storage/StorageReaderWriter.js.map +1 -1
  66. package/out/src/storage/WalletStorageManager.d.ts +11 -0
  67. package/out/src/storage/WalletStorageManager.d.ts.map +1 -1
  68. package/out/src/storage/WalletStorageManager.js +77 -12
  69. package/out/src/storage/WalletStorageManager.js.map +1 -1
  70. package/out/src/storage/idbHelpers.d.ts.map +1 -1
  71. package/out/src/storage/idbHelpers.js +14 -2
  72. package/out/src/storage/idbHelpers.js.map +1 -1
  73. package/out/src/storage/index.all.d.ts +2 -0
  74. package/out/src/storage/index.all.d.ts.map +1 -1
  75. package/out/src/storage/index.all.js +2 -0
  76. package/out/src/storage/index.all.js.map +1 -1
  77. package/out/src/storage/methods/createAction.d.ts +57 -1
  78. package/out/src/storage/methods/createAction.d.ts.map +1 -1
  79. package/out/src/storage/methods/createAction.js +195 -49
  80. package/out/src/storage/methods/createAction.js.map +1 -1
  81. package/out/src/storage/methods/noSendExpiry.d.ts +11 -0
  82. package/out/src/storage/methods/noSendExpiry.d.ts.map +1 -0
  83. package/out/src/storage/methods/noSendExpiry.js +357 -0
  84. package/out/src/storage/methods/noSendExpiry.js.map +1 -0
  85. package/out/src/storage/methods/noSendExpiryLifecycle.d.ts +14 -0
  86. package/out/src/storage/methods/noSendExpiryLifecycle.d.ts.map +1 -0
  87. package/out/src/storage/methods/noSendExpiryLifecycle.js +691 -0
  88. package/out/src/storage/methods/noSendExpiryLifecycle.js.map +1 -0
  89. package/out/src/storage/methods/preparedBeef.d.ts +104 -0
  90. package/out/src/storage/methods/preparedBeef.d.ts.map +1 -0
  91. package/out/src/storage/methods/preparedBeef.js +525 -0
  92. package/out/src/storage/methods/preparedBeef.js.map +1 -0
  93. package/out/src/storage/methods/processAction.d.ts.map +1 -1
  94. package/out/src/storage/methods/processAction.js +109 -28
  95. package/out/src/storage/methods/processAction.js.map +1 -1
  96. package/out/src/storage/methods/purgeData.d.ts.map +1 -1
  97. package/out/src/storage/methods/purgeData.js +9 -0
  98. package/out/src/storage/methods/purgeData.js.map +1 -1
  99. package/out/src/storage/methods/validateSyncProof.d.ts +24 -0
  100. package/out/src/storage/methods/validateSyncProof.d.ts.map +1 -0
  101. package/out/src/storage/methods/validateSyncProof.js +116 -0
  102. package/out/src/storage/methods/validateSyncProof.js.map +1 -0
  103. package/out/src/storage/remoting/StorageClientBase.d.ts +4 -1
  104. package/out/src/storage/remoting/StorageClientBase.d.ts.map +1 -1
  105. package/out/src/storage/remoting/StorageClientBase.js +9 -0
  106. package/out/src/storage/remoting/StorageClientBase.js.map +1 -1
  107. package/out/src/storage/remoting/StorageServer.d.ts.map +1 -1
  108. package/out/src/storage/remoting/StorageServer.js +16 -3
  109. package/out/src/storage/remoting/StorageServer.js.map +1 -1
  110. package/out/src/storage/schema/KnexMigrations.d.ts +2 -0
  111. package/out/src/storage/schema/KnexMigrations.d.ts.map +1 -1
  112. package/out/src/storage/schema/KnexMigrations.js +66 -1
  113. package/out/src/storage/schema/KnexMigrations.js.map +1 -1
  114. package/out/src/storage/schema/StorageIdbSchema.d.ts +2 -0
  115. package/out/src/storage/schema/StorageIdbSchema.d.ts.map +1 -1
  116. package/out/src/storage/schema/entities/EntityOutput.d.ts.map +1 -1
  117. package/out/src/storage/schema/entities/EntityOutput.js +13 -0
  118. package/out/src/storage/schema/entities/EntityOutput.js.map +1 -1
  119. package/out/src/storage/schema/entities/EntityProvenTx.d.ts +2 -0
  120. package/out/src/storage/schema/entities/EntityProvenTx.d.ts.map +1 -1
  121. package/out/src/storage/schema/entities/EntityProvenTx.js +33 -3
  122. package/out/src/storage/schema/entities/EntityProvenTx.js.map +1 -1
  123. package/out/src/storage/schema/entities/EntityTransaction.d.ts +26 -0
  124. package/out/src/storage/schema/entities/EntityTransaction.d.ts.map +1 -1
  125. package/out/src/storage/schema/entities/EntityTransaction.js +221 -21
  126. package/out/src/storage/schema/entities/EntityTransaction.js.map +1 -1
  127. package/out/src/storage/schema/tables/TablePreparedBeef.interfaces.d.ts +23 -0
  128. package/out/src/storage/schema/tables/TablePreparedBeef.interfaces.d.ts.map +1 -0
  129. package/out/src/storage/schema/tables/TablePreparedBeef.interfaces.js +3 -0
  130. package/out/src/storage/schema/tables/TablePreparedBeef.interfaces.js.map +1 -0
  131. package/out/src/storage/schema/tables/TableTransaction.d.ts +16 -0
  132. package/out/src/storage/schema/tables/TableTransaction.d.ts.map +1 -1
  133. package/out/src/storage/schema/tables/TableTransaction.js +14 -1
  134. package/out/src/storage/schema/tables/TableTransaction.js.map +1 -1
  135. package/out/src/utility/Argon2idBackend.d.ts +36 -0
  136. package/out/src/utility/Argon2idBackend.d.ts.map +1 -0
  137. package/out/src/utility/Argon2idBackend.js +60 -0
  138. package/out/src/utility/Argon2idBackend.js.map +1 -0
  139. package/out/src/utility/brc177NoSendExpiry.d.ts +34 -0
  140. package/out/src/utility/brc177NoSendExpiry.d.ts.map +1 -0
  141. package/out/src/utility/brc177NoSendExpiry.js +81 -0
  142. package/out/src/utility/brc177NoSendExpiry.js.map +1 -0
  143. package/out/src/utility/hashWasm.d.ts +2 -1
  144. package/out/src/utility/hashWasm.d.ts.map +1 -1
  145. package/out/src/utility/hashWasm.js +83 -1
  146. package/out/src/utility/hashWasm.js.map +1 -1
  147. package/out/src/utility/index.all.d.ts +2 -0
  148. package/out/src/utility/index.all.d.ts.map +1 -1
  149. package/out/src/utility/index.all.js +2 -0
  150. package/out/src/utility/index.all.js.map +1 -1
  151. package/out/src/utility/index.client.d.ts +2 -0
  152. package/out/src/utility/index.client.d.ts.map +1 -1
  153. package/out/src/utility/index.client.js +2 -0
  154. package/out/src/utility/index.client.js.map +1 -1
  155. package/package.json +2 -1
@@ -0,0 +1,691 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.processNoSendExpiryLifecycle = processNoSendExpiryLifecycle;
4
+ const sdk_1 = require("@bsv/sdk");
5
+ const WERR_errors_1 = require("../../sdk/WERR_errors");
6
+ const parseTxScriptOffsets_1 = require("../../utility/parseTxScriptOffsets");
7
+ const utilityHelpers_1 = require("../../utility/utilityHelpers");
8
+ const utilityHelpers_noBuffer_1 = require("../../utility/utilityHelpers.noBuffer");
9
+ const EntityProvenTxReq_1 = require("../schema/entities/EntityProvenTxReq");
10
+ // Descending lifecycle order ensures a row advanced by this pass moves only
11
+ // into a state whose page has already run, so it is never counted or acted on
12
+ // twice without retaining an unbounded set of transaction IDs.
13
+ const ACTIVE_STATES = [
14
+ 'reclaiming',
15
+ 'broadcast',
16
+ 'conflicted',
17
+ 'revocation-requested',
18
+ 'signed',
19
+ 'unsigned',
20
+ 'preparing'
21
+ ];
22
+ const QUERY_PAGE_SIZE = 100;
23
+ const STATUS_BATCH_SIZE = 100;
24
+ const RECLAIM_RETRY_BASE_MSECS = 30_000;
25
+ const RECLAIM_RETRY_MAX_MSECS = 60 * 60_000;
26
+ function emptyResult() {
27
+ return {
28
+ inspected: 0,
29
+ cancelled: 0,
30
+ observed: 0,
31
+ reclaimActivated: 0,
32
+ reclaimRetried: 0,
33
+ reclaimed: 0,
34
+ targetWon: 0,
35
+ deferred: 0,
36
+ errors: 0
37
+ };
38
+ }
39
+ async function activeTransactionsOnPage(storage, page) {
40
+ // A remote database may be retained as a synchronized backup while another
41
+ // provider is authoritative for a user. Check only the bounded set of users
42
+ // represented by this page rather than materializing every active user or
43
+ // expiry in a multi-user service.
44
+ const activeStorage = storage.getSettings().storageIdentityKey;
45
+ const activeUsers = new Set();
46
+ for (const userId of new Set(page.map(transaction => transaction.userId))) {
47
+ const user = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findUsers({
48
+ partial: { userId, activeStorage },
49
+ paged: { limit: 1 }
50
+ }));
51
+ if (user != null)
52
+ activeUsers.add(userId);
53
+ }
54
+ return page.filter(transaction => activeUsers.has(transaction.userId));
55
+ }
56
+ function isDue(transaction, nowSeconds, height) {
57
+ if (transaction.noSendExpiryState === 'revocation-requested')
58
+ return true;
59
+ if (transaction.noSendExpiryDeadline == null)
60
+ return false;
61
+ return transaction.noSendExpiryMode === 'blockheight'
62
+ ? height != null && height >= transaction.noSendExpiryDeadline
63
+ : nowSeconds >= transaction.noSendExpiryDeadline;
64
+ }
65
+ async function getStatuses(storage, transactions) {
66
+ const txids = [
67
+ ...new Set(transactions.flatMap(transaction => {
68
+ const txids = [];
69
+ if (transaction.txid)
70
+ txids.push(transaction.txid);
71
+ if (transaction.noSendExpiryState === 'reclaiming' && transaction.noSendExpiryReclaimTxid) {
72
+ txids.push(transaction.noSendExpiryReclaimTxid);
73
+ }
74
+ return txids;
75
+ }))
76
+ ];
77
+ if (txids.length === 0)
78
+ return new Map();
79
+ const statuses = new Map();
80
+ for (let offset = 0; offset < txids.length; offset += STATUS_BATCH_SIZE) {
81
+ try {
82
+ const result = await storage.getServices().getStatusForTxids(txids.slice(offset, offset + STATUS_BATCH_SIZE));
83
+ if (result.status !== 'success')
84
+ continue;
85
+ for (const item of result.results)
86
+ statuses.set(item.txid, item.status);
87
+ }
88
+ catch { }
89
+ }
90
+ return statuses;
91
+ }
92
+ async function updateReqStatus(storage, txid, status, note, trx) {
93
+ if (!txid)
94
+ return;
95
+ const req = await EntityProvenTxReq_1.EntityProvenTxReq.fromStorageTxid(storage, txid, trx);
96
+ if (req == null || req.status === 'completed')
97
+ return;
98
+ req.status = status;
99
+ req.addHistoryNote({ what: note });
100
+ await req.updateStorageDynamicProperties(storage, trx);
101
+ }
102
+ async function quarantineTransactionOutputs(storage, transactionId, trx) {
103
+ const outputs = await storage.findOutputs({ partial: { transactionId }, trx });
104
+ for (const output of outputs) {
105
+ if (output.spendable)
106
+ await storage.updateOutput(output.outputId, { spendable: false }, trx);
107
+ }
108
+ }
109
+ async function markObservedTarget(storage, transaction, final) {
110
+ return await storage.transaction(async (trx) => {
111
+ const current = (0, utilityHelpers_1.verifyOne)(await storage.findTransactions({
112
+ partial: { transactionId: (0, utilityHelpers_1.verifyId)(transaction.transactionId) },
113
+ trx
114
+ }));
115
+ if (current.noSendExpiryState === 'reclaimed' || current.noSendExpiryState === 'cancelled')
116
+ return false;
117
+ if (final) {
118
+ return await finishTargetWinner(storage, current, trx);
119
+ }
120
+ if (current.noSendExpiryState !== 'signed' &&
121
+ current.noSendExpiryState !== 'revocation-requested' &&
122
+ current.noSendExpiryState !== 'broadcast' &&
123
+ current.noSendExpiryState !== 'conflicted') {
124
+ return false;
125
+ }
126
+ // Claim even an existing broadcast state. Besides making conflicted ->
127
+ // broadcast monotonic, the conditional write holds this row for the rest
128
+ // of the transaction so a concurrent reclaim/proof winner cannot be
129
+ // overwritten by the observation metadata update below.
130
+ if (!(await storage.compareAndSetNoSendExpiryState(current.transactionId, current.noSendExpiryState, 'broadcast', trx))) {
131
+ return false;
132
+ }
133
+ await storage.updateTransaction(current.transactionId, {
134
+ noSendExpiryObservedAt: Date.now(),
135
+ ...(current.status === 'nosend' ? { status: 'unproven' } : {})
136
+ }, trx);
137
+ await updateReqStatus(storage, current.txid, 'unmined', 'brc177-target-observed', trx);
138
+ return true;
139
+ });
140
+ }
141
+ async function noteTargetObservedDuringRace(storage, transaction) {
142
+ return await storage.transaction(async (trx) => {
143
+ const current = (0, utilityHelpers_1.verifyOne)(await storage.findTransactions({
144
+ partial: { transactionId: (0, utilityHelpers_1.verifyId)(transaction.transactionId) },
145
+ trx
146
+ }));
147
+ if (current.noSendExpiryState !== 'reclaiming')
148
+ return false;
149
+ // Lock the lifecycle row before changing transaction/request metadata. If
150
+ // a proof winner committed after our read, this self-transition fails and
151
+ // prevents stale observation data from reviving the losing transaction.
152
+ if (!(await storage.compareAndSetNoSendExpiryState(current.transactionId, 'reclaiming', 'reclaiming', trx))) {
153
+ return false;
154
+ }
155
+ let changed = false;
156
+ if (current.status === 'nosend') {
157
+ await storage.updateTransaction(current.transactionId, {
158
+ status: 'unproven',
159
+ noSendExpiryObservedAt: current.noSendExpiryObservedAt ?? Date.now()
160
+ }, trx);
161
+ changed = true;
162
+ }
163
+ if (current.txid != null) {
164
+ const req = await EntityProvenTxReq_1.EntityProvenTxReq.fromStorageTxid(storage, current.txid, trx);
165
+ if (req?.status === 'nosend') {
166
+ req.status = 'unmined';
167
+ req.addHistoryNote({ what: 'brc177-target-observed-during-reclaim-race' });
168
+ await req.updateStorageDynamicProperties(storage, trx);
169
+ changed = true;
170
+ }
171
+ }
172
+ if (current.noSendExpiryReclaimTxid != null) {
173
+ const reclaim = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findTransactions({
174
+ partial: { userId: current.userId, txid: current.noSendExpiryReclaimTxid },
175
+ trx
176
+ }));
177
+ const reclaimReq = await EntityProvenTxReq_1.EntityProvenTxReq.fromStorageTxid(storage, current.noSendExpiryReclaimTxid, trx);
178
+ // Once the target is positively observed, BRC-177 stops future reclaim
179
+ // submissions. Keep the request in proof tracking because a prior send
180
+ // may still have reached a processor and either side can win the race.
181
+ if (reclaimReq != null && (reclaimReq.status === 'unsent' || reclaimReq.status === 'sending')) {
182
+ reclaimReq.status = 'unmined';
183
+ reclaimReq.addHistoryNote({ what: 'brc177-reclaim-retry-suppressed-target-observed' });
184
+ await reclaimReq.updateStorageDynamicProperties(storage, trx);
185
+ if (reclaim != null && (reclaim.status === 'unprocessed' || reclaim.status === 'sending')) {
186
+ await storage.updateTransaction(reclaim.transactionId, { status: 'unproven' }, trx);
187
+ }
188
+ changed = true;
189
+ }
190
+ }
191
+ return changed;
192
+ });
193
+ }
194
+ async function cancelUnsigned(storage, transaction) {
195
+ return await storage.transaction(async (trx) => {
196
+ const expected = transaction.noSendExpiryState;
197
+ if (expected !== 'preparing' && expected !== 'unsigned')
198
+ return false;
199
+ if (!(await storage.compareAndSetNoSendExpiryState(transaction.transactionId, expected, 'cancelled', trx))) {
200
+ return false;
201
+ }
202
+ await storage.updateTransactionStatus('failed', transaction.transactionId, undefined, undefined, trx);
203
+ return true;
204
+ });
205
+ }
206
+ function validateReclaim(transaction) {
207
+ if (transaction.noSendExpiryReclaimRawTx == null ||
208
+ !transaction.noSendExpiryReclaimTxid ||
209
+ !transaction.noSendExpiryReclaimDerivationPrefix ||
210
+ !transaction.noSendExpiryReclaimDerivationSuffix ||
211
+ transaction.noSendExpiryReclaimSatoshis == null ||
212
+ !transaction.noSendExpiryAnchorTxid ||
213
+ transaction.noSendExpiryAnchorVout == null) {
214
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('BRC-177 reclaim metadata is incomplete');
215
+ }
216
+ const reclaim = sdk_1.Transaction.fromBinary((0, utilityHelpers_noBuffer_1.asArray)(transaction.noSendExpiryReclaimRawTx));
217
+ if (reclaim.id('hex') !== transaction.noSendExpiryReclaimTxid ||
218
+ reclaim.inputs.length !== 1 ||
219
+ reclaim.outputs.length !== 1 ||
220
+ reclaim.inputs[0].sourceTXID !== transaction.noSendExpiryAnchorTxid ||
221
+ reclaim.inputs[0].sourceOutputIndex !== transaction.noSendExpiryAnchorVout ||
222
+ reclaim.outputs[0].satoshis !== transaction.noSendExpiryReclaimSatoshis) {
223
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('BRC-177 reclaim metadata does not describe the signed reclaim transaction');
224
+ }
225
+ return reclaim;
226
+ }
227
+ async function insertReclaim(storage, target, anchor, trx) {
228
+ const reclaim = validateReclaim(target);
229
+ const rawTx = (0, utilityHelpers_noBuffer_1.asArray)(target.noSendExpiryReclaimRawTx);
230
+ const reclaimTxid = target.noSendExpiryReclaimTxid;
231
+ const existingTransaction = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findTransactions({
232
+ partial: { userId: target.userId, txid: reclaimTxid },
233
+ trx
234
+ }));
235
+ let reclaimTransaction = existingTransaction;
236
+ if (reclaimTransaction == null) {
237
+ const now = new Date();
238
+ reclaimTransaction = {
239
+ created_at: now,
240
+ updated_at: now,
241
+ transactionId: 0,
242
+ userId: target.userId,
243
+ status: 'unprocessed',
244
+ reference: (0, utilityHelpers_1.randomBytesBase64)(12),
245
+ isOutgoing: true,
246
+ satoshis: target.noSendExpiryReclaimSatoshis - anchor.satoshis,
247
+ description: 'BRC-177 expiry reclaim',
248
+ version: reclaim.version,
249
+ lockTime: reclaim.lockTime,
250
+ txid: reclaimTxid
251
+ };
252
+ reclaimTransaction.transactionId = await storage.insertTransaction(reclaimTransaction, trx);
253
+ const basket = (0, utilityHelpers_1.verifyOne)(await storage.findOutputBaskets({
254
+ partial: { userId: target.userId, name: 'default' },
255
+ trx
256
+ }));
257
+ const offsets = (0, parseTxScriptOffsets_1.parseTxScriptOffsets)(rawTx);
258
+ const scriptOffset = offsets.outputs[0];
259
+ const lockingScript = rawTx.slice(scriptOffset.offset, scriptOffset.offset + scriptOffset.length);
260
+ await storage.insertOutput({
261
+ created_at: now,
262
+ updated_at: now,
263
+ outputId: 0,
264
+ userId: target.userId,
265
+ transactionId: reclaimTransaction.transactionId,
266
+ basketId: basket.basketId,
267
+ // The reclaim is deliberately racing the released target. Do not expose
268
+ // its output as wallet liquidity until a locally validated proof wins.
269
+ spendable: false,
270
+ change: true,
271
+ outputDescription: '',
272
+ vout: 0,
273
+ satoshis: target.noSendExpiryReclaimSatoshis,
274
+ providedBy: 'storage',
275
+ purpose: 'change',
276
+ type: 'P2PKH',
277
+ txid: reclaimTxid,
278
+ derivationPrefix: target.noSendExpiryReclaimDerivationPrefix,
279
+ derivationSuffix: target.noSendExpiryReclaimDerivationSuffix,
280
+ scriptLength: scriptOffset.length,
281
+ scriptOffset: scriptOffset.offset,
282
+ lockingScript: scriptOffset.length > storage.getSettings().maxOutputScript ? undefined : lockingScript
283
+ }, trx);
284
+ }
285
+ await storage.updateOutput(anchor.outputId, {
286
+ spendable: false,
287
+ spentBy: reclaimTransaction.transactionId
288
+ }, trx);
289
+ const targetReq = target.txid == null ? undefined : await EntityProvenTxReq_1.EntityProvenTxReq.fromStorageTxid(storage, target.txid, trx);
290
+ if (targetReq == null)
291
+ throw new WERR_errors_1.WERR_INTERNAL('BRC-177 protected transaction request is missing');
292
+ const req = EntityProvenTxReq_1.EntityProvenTxReq.fromTxid(reclaimTxid, rawTx, targetReq.api.inputBEEF);
293
+ req.status = 'unsent';
294
+ req.addNotifyTransactionId(reclaimTransaction.transactionId);
295
+ req.addHistoryNote({ what: 'brc177-reclaim-activated', targetTxid: target.txid });
296
+ return await req.insertOrMerge(storage, trx);
297
+ }
298
+ async function activateReclaim(storage, transaction, anchor, expected) {
299
+ return await storage.transaction(async (trx) => {
300
+ if (!(await storage.compareAndSetNoSendExpiryState(transaction.transactionId, expected, 'reclaiming', trx)))
301
+ return undefined;
302
+ return await insertReclaim(storage, transaction, anchor, trx);
303
+ });
304
+ }
305
+ async function finishReclaimWinner(storage, target, reclaimTransaction, trx) {
306
+ const currentTarget = (0, utilityHelpers_1.verifyOne)(await storage.findTransactions({
307
+ partial: { transactionId: target.transactionId, userId: target.userId },
308
+ trx
309
+ }));
310
+ const currentReclaim = (0, utilityHelpers_1.verifyOne)(await storage.findTransactions({
311
+ partial: { transactionId: reclaimTransaction.transactionId, userId: target.userId },
312
+ trx
313
+ }));
314
+ if (currentTarget.noSendExpiryState !== 'reclaiming' ||
315
+ currentTarget.status === 'completed' ||
316
+ currentTarget.provenTxId != null ||
317
+ currentReclaim.provenTxId == null) {
318
+ return false;
319
+ }
320
+ if (!(await storage.compareAndSetNoSendExpiryState(currentTarget.transactionId, 'reclaiming', 'reclaimed', trx))) {
321
+ return false;
322
+ }
323
+ if (currentTarget.status !== 'failed') {
324
+ await storage.updateTransactionStatus('failed', currentTarget.transactionId, undefined, undefined, trx);
325
+ }
326
+ await updateReqStatus(storage, currentTarget.txid, 'invalid', 'brc177-reclaim-won', trx);
327
+ const anchor = (0, utilityHelpers_1.verifyOne)(await storage.findOutputs({
328
+ partial: {
329
+ userId: currentTarget.userId,
330
+ txid: currentTarget.noSendExpiryAnchorTxid,
331
+ vout: currentTarget.noSendExpiryAnchorVout
332
+ },
333
+ trx
334
+ }));
335
+ await storage.updateOutput(anchor.outputId, {
336
+ spendable: false,
337
+ spentBy: currentReclaim.transactionId
338
+ }, trx);
339
+ const reclaimOutput = (0, utilityHelpers_1.verifyOne)(await storage.findOutputs({
340
+ partial: { transactionId: currentReclaim.transactionId, vout: 0 },
341
+ trx
342
+ }));
343
+ await storage.updateOutput(reclaimOutput.outputId, { spendable: true }, trx);
344
+ return true;
345
+ }
346
+ async function finishTargetWinner(storage, target, trx) {
347
+ const current = (0, utilityHelpers_1.verifyOne)(await storage.findTransactions({
348
+ partial: { transactionId: target.transactionId, userId: target.userId },
349
+ trx
350
+ }));
351
+ if (current.noSendExpiryState == null ||
352
+ current.noSendExpiryState === 'reclaimed' ||
353
+ current.noSendExpiryState === 'cancelled' ||
354
+ current.noSendExpiryState === 'target-won') {
355
+ return false;
356
+ }
357
+ const reclaimTxid = current.noSendExpiryReclaimTxid;
358
+ const reclaimTransaction = reclaimTxid == null
359
+ ? undefined
360
+ : (0, utilityHelpers_1.verifyOneOrNone)(await storage.findTransactions({
361
+ partial: { userId: current.userId, txid: reclaimTxid },
362
+ trx
363
+ }));
364
+ // Contradictory completion/proof signals for competing spends cannot both
365
+ // describe the same best chain. Keep every output quarantined until the
366
+ // ordinary proof reconciliation machinery resolves that inconsistency.
367
+ if (reclaimTransaction?.status === 'completed' || reclaimTransaction?.provenTxId != null) {
368
+ await quarantineTransactionOutputs(storage, current.transactionId, trx);
369
+ await quarantineTransactionOutputs(storage, reclaimTransaction.transactionId, trx);
370
+ return false;
371
+ }
372
+ // A status flag alone suppresses reclaim but never releases value or
373
+ // finalizes a winner; only the linked, locally validated proof may do that.
374
+ if (current.provenTxId == null)
375
+ return false;
376
+ if (!(await storage.compareAndSetNoSendExpiryState(current.transactionId, current.noSendExpiryState, 'target-won', trx))) {
377
+ return false;
378
+ }
379
+ if (reclaimTransaction != null && reclaimTransaction.status !== 'failed') {
380
+ await storage.updateTransactionStatus('failed', reclaimTransaction.transactionId, undefined, undefined, trx);
381
+ await updateReqStatus(storage, reclaimTxid, 'doubleSpend', 'brc177-target-won', trx);
382
+ }
383
+ const anchor = (0, utilityHelpers_1.verifyOne)(await storage.findOutputs({
384
+ partial: {
385
+ userId: current.userId,
386
+ txid: current.noSendExpiryAnchorTxid,
387
+ vout: current.noSendExpiryAnchorVout
388
+ },
389
+ trx
390
+ }));
391
+ await storage.updateOutput(anchor.outputId, {
392
+ spendable: false,
393
+ spentBy: current.transactionId
394
+ }, trx);
395
+ await storage.updateTransaction(current.transactionId, {
396
+ noSendExpiryObservedAt: current.noSendExpiryObservedAt ?? Date.now(),
397
+ ...(current.status === 'nosend' ? { status: 'unproven' } : {})
398
+ }, trx);
399
+ return true;
400
+ }
401
+ async function reconcileRace(storage, target) {
402
+ const reclaim = target.noSendExpiryReclaimTxid == null
403
+ ? undefined
404
+ : (0, utilityHelpers_1.verifyOneOrNone)(await storage.findTransactions({
405
+ partial: { userId: target.userId, txid: target.noSendExpiryReclaimTxid }
406
+ }));
407
+ if (target.status === 'completed' || target.provenTxId != null) {
408
+ const won = await storage.transaction(async (trx) => await finishTargetWinner(storage, target, trx));
409
+ return won ? 'target' : 'deferred';
410
+ }
411
+ if (reclaim?.status === 'completed' || reclaim?.provenTxId != null) {
412
+ const won = await storage.transaction(async (trx) => await finishReclaimWinner(storage, target, reclaim, trx));
413
+ return won ? 'reclaimed' : 'deferred';
414
+ }
415
+ return 'deferred';
416
+ }
417
+ function isKnownOrMined(status) {
418
+ return status === 'known' || status === 'mined';
419
+ }
420
+ function recordRaceResult(result, race) {
421
+ if (race === 'reclaimed')
422
+ result.reclaimed++;
423
+ else if (race === 'target')
424
+ result.targetWon++;
425
+ else
426
+ result.deferred++;
427
+ }
428
+ function reclaimRetryDelay(rebroadcastAttempts) {
429
+ return Math.min(RECLAIM_RETRY_BASE_MSECS * 2 ** Math.min(rebroadcastAttempts, 30), RECLAIM_RETRY_MAX_MSECS);
430
+ }
431
+ /**
432
+ * A processor may reject a reclaim even though the same anchor later becomes a
433
+ * conclusive UTXO again (for example, after a conflicting mempool spend is
434
+ * evicted). Re-submit only the already validated, pre-signed reclaim and never
435
+ * release its anchor or output based on a rejection response.
436
+ */
437
+ async function retryRejectedReclaim(storage, target, targetStatus) {
438
+ if (targetStatus !== 'unknown' ||
439
+ target.noSendExpiryObservedAt != null ||
440
+ target.noSendExpiryReclaimTxid == null) {
441
+ return 'not-applicable';
442
+ }
443
+ const reclaim = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findTransactions({
444
+ partial: { userId: target.userId, txid: target.noSendExpiryReclaimTxid }
445
+ }));
446
+ const req = await EntityProvenTxReq_1.EntityProvenTxReq.fromStorageTxid(storage, target.noSendExpiryReclaimTxid);
447
+ if (reclaim?.status !== 'failed' ||
448
+ reclaim.provenTxId != null ||
449
+ req == null ||
450
+ (req.status !== 'invalid' && req.status !== 'doubleSpend')) {
451
+ return 'not-applicable';
452
+ }
453
+ const retryAfter = (0, utilityHelpers_1.verifyTruthy)(req.updated_at).getTime() + reclaimRetryDelay(req.rebroadcastAttempts);
454
+ if (Date.now() < retryAfter)
455
+ return 'deferred';
456
+ const anchor = (0, utilityHelpers_1.verifyOne)(await storage.findOutputs({
457
+ partial: {
458
+ userId: target.userId,
459
+ txid: target.noSendExpiryAnchorTxid,
460
+ vout: target.noSendExpiryAnchorVout
461
+ }
462
+ }));
463
+ let anchorIsUtxo;
464
+ try {
465
+ anchorIsUtxo = await storage.getServices().isUtxo(anchor);
466
+ }
467
+ catch {
468
+ return 'deferred';
469
+ }
470
+ if (!anchorIsUtxo)
471
+ return 'deferred';
472
+ const sendable = await storage.transaction(async (trx) => {
473
+ if (!(await storage.compareAndSetNoSendExpiryState(target.transactionId, 'reclaiming', 'reclaiming', trx))) {
474
+ return undefined;
475
+ }
476
+ const currentTarget = (0, utilityHelpers_1.verifyOne)(await storage.findTransactions({
477
+ partial: { transactionId: target.transactionId, userId: target.userId },
478
+ trx
479
+ }));
480
+ if (currentTarget.noSendExpiryState !== 'reclaiming' ||
481
+ currentTarget.status === 'completed' ||
482
+ currentTarget.provenTxId != null ||
483
+ currentTarget.noSendExpiryObservedAt != null ||
484
+ currentTarget.noSendExpiryReclaimTxid == null) {
485
+ return undefined;
486
+ }
487
+ const currentReclaim = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findTransactions({
488
+ partial: { userId: currentTarget.userId, txid: currentTarget.noSendExpiryReclaimTxid },
489
+ trx
490
+ }));
491
+ const currentReq = await EntityProvenTxReq_1.EntityProvenTxReq.fromStorageTxid(storage, currentTarget.noSendExpiryReclaimTxid, trx);
492
+ const currentAnchor = (0, utilityHelpers_1.verifyOne)(await storage.findOutputs({
493
+ partial: {
494
+ userId: currentTarget.userId,
495
+ txid: currentTarget.noSendExpiryAnchorTxid,
496
+ vout: currentTarget.noSendExpiryAnchorVout
497
+ },
498
+ trx
499
+ }));
500
+ if (currentReclaim?.status !== 'failed' ||
501
+ currentReclaim.provenTxId != null ||
502
+ currentReq == null ||
503
+ (currentReq.status !== 'invalid' && currentReq.status !== 'doubleSpend') ||
504
+ currentAnchor.spendable ||
505
+ currentAnchor.spentBy !== currentReclaim.transactionId) {
506
+ return undefined;
507
+ }
508
+ // Generic transaction APIs deliberately prohibit un-failing a row. This
509
+ // narrowly scoped recovery is safe because it revives only the same signed
510
+ // reclaim while retaining its input reservation and quarantined output.
511
+ await storage.updateTransaction(currentReclaim.transactionId, { status: 'unprocessed' }, trx);
512
+ currentReq.status = 'unsent';
513
+ currentReq.attempts = 0;
514
+ currentReq.rebroadcastAttempts = currentReq.rebroadcastAttempts + 1;
515
+ currentReq.notified = false;
516
+ currentReq.addHistoryNote({
517
+ what: 'brc177-reclaim-retry',
518
+ targetTxid: currentTarget.txid,
519
+ retry: currentReq.rebroadcastAttempts
520
+ });
521
+ await currentReq.updateStorageDynamicProperties(storage, trx);
522
+ return currentReq;
523
+ });
524
+ if (sendable == null)
525
+ return 'deferred';
526
+ await storage.attemptToPostReqsToNetwork([sendable]).catch(() => undefined);
527
+ return 'retried';
528
+ }
529
+ async function processReclaimRace(storage, transaction, targetStatus, result) {
530
+ if (isKnownOrMined(targetStatus) && (await noteTargetObservedDuringRace(storage, transaction))) {
531
+ result.observed++;
532
+ }
533
+ const race = await reconcileRace(storage, transaction);
534
+ if (race !== 'deferred') {
535
+ recordRaceResult(result, race);
536
+ return;
537
+ }
538
+ const recovery = await retryRejectedReclaim(storage, transaction, targetStatus);
539
+ if (recovery === 'retried')
540
+ result.reclaimRetried++;
541
+ else
542
+ result.deferred++;
543
+ }
544
+ async function processObservationOrRace(storage, transaction, targetStatus, result) {
545
+ // A service's `mined` verdict is useful evidence that reclaim must stop,
546
+ // but it is not a locally verified Merkle proof. Only storage's proven
547
+ // state may finalize the target as the winner.
548
+ if (transaction.status === 'completed' || transaction.provenTxId != null) {
549
+ if (await markObservedTarget(storage, transaction, true))
550
+ result.targetWon++;
551
+ return true;
552
+ }
553
+ if (transaction.noSendExpiryState === 'reclaiming') {
554
+ await processReclaimRace(storage, transaction, targetStatus, result);
555
+ return true;
556
+ }
557
+ const stateCanObserve = transaction.noSendExpiryState === 'signed' ||
558
+ transaction.noSendExpiryState === 'revocation-requested' ||
559
+ transaction.noSendExpiryState === 'broadcast' ||
560
+ transaction.noSendExpiryState === 'conflicted';
561
+ if (stateCanObserve && isKnownOrMined(targetStatus)) {
562
+ if (await markObservedTarget(storage, transaction, false))
563
+ result.observed++;
564
+ return true;
565
+ }
566
+ return false;
567
+ }
568
+ async function requestRevocation(storage, transaction) {
569
+ if (transaction.noSendExpiryState === 'revocation-requested')
570
+ return 'revocation-requested';
571
+ if (transaction.noSendExpiryState !== 'signed')
572
+ return undefined;
573
+ // Persist that the deadline has triggered before consulting fallible
574
+ // services. A clock correction or block-height reorganization must not
575
+ // reactivate a transaction after its expiry was already observed.
576
+ const changed = await storage.compareAndSetNoSendExpiryState(transaction.transactionId, 'signed', 'revocation-requested');
577
+ return changed ? 'revocation-requested' : undefined;
578
+ }
579
+ async function attemptReclaim(storage, transaction, result, expected = 'revocation-requested') {
580
+ const anchor = (0, utilityHelpers_1.verifyOne)(await storage.findOutputs({
581
+ partial: {
582
+ userId: transaction.userId,
583
+ txid: transaction.noSendExpiryAnchorTxid,
584
+ vout: transaction.noSendExpiryAnchorVout
585
+ }
586
+ }));
587
+ let anchorIsUtxo;
588
+ try {
589
+ anchorIsUtxo = await storage.getServices().isUtxo(anchor);
590
+ }
591
+ catch {
592
+ result.deferred++;
593
+ return;
594
+ }
595
+ if (!anchorIsUtxo) {
596
+ if (expected === 'revocation-requested') {
597
+ await storage.compareAndSetNoSendExpiryState(transaction.transactionId, expected, 'conflicted');
598
+ }
599
+ result.deferred++;
600
+ return;
601
+ }
602
+ const reclaim = await activateReclaim(storage, transaction, anchor, expected);
603
+ if (reclaim == null)
604
+ return;
605
+ result.reclaimActivated++;
606
+ await storage.attemptToPostReqsToNetwork([reclaim]).catch(() => undefined);
607
+ }
608
+ async function processTransaction(storage, transaction, targetStatus, nowSeconds, height, result) {
609
+ if (await processObservationOrRace(storage, transaction, targetStatus, result))
610
+ return;
611
+ if (!isDue(transaction, nowSeconds, height))
612
+ return;
613
+ if (transaction.noSendExpiryState === 'preparing' || transaction.noSendExpiryState === 'unsigned') {
614
+ if (await cancelUnsigned(storage, transaction))
615
+ result.cancelled++;
616
+ return;
617
+ }
618
+ if (transaction.noSendExpiryState === 'conflicted') {
619
+ // A competing mempool spend can disappear. Resume only after the target
620
+ // is still explicitly unknown and the anchor service again gives a
621
+ // conclusive unspent verdict; `attemptReclaim` advances directly to the
622
+ // higher-ranked reclaiming state without reviving the released target.
623
+ if (targetStatus !== 'unknown') {
624
+ result.deferred++;
625
+ return;
626
+ }
627
+ await attemptReclaim(storage, transaction, result, 'conflicted');
628
+ return;
629
+ }
630
+ if ((await requestRevocation(storage, transaction)) == null)
631
+ return;
632
+ // Absence of a successful, explicit "unknown" verdict is not evidence
633
+ // that the protected transaction is absent. A service outage must never
634
+ // turn into authorization to double spend the anchor.
635
+ if (targetStatus !== 'unknown') {
636
+ result.deferred++;
637
+ return;
638
+ }
639
+ await attemptReclaim(storage, transaction, result);
640
+ }
641
+ async function processPage(storage, page, context, result) {
642
+ const transactions = await activeTransactionsOnPage(storage, page);
643
+ result.inspected += transactions.length;
644
+ const needsHeight = transactions.some(transaction => transaction.noSendExpiryMode === 'blockheight');
645
+ if (!context.heightAttempted && needsHeight) {
646
+ context.heightAttempted = true;
647
+ try {
648
+ context.height = await storage.getServices().getHeight();
649
+ }
650
+ catch { }
651
+ }
652
+ const statuses = await getStatuses(storage, transactions);
653
+ for (const transaction of transactions) {
654
+ try {
655
+ const targetStatus = transaction.txid ? statuses.get(transaction.txid) : undefined;
656
+ await processTransaction(storage, transaction, targetStatus, context.nowSeconds, context.height, result);
657
+ }
658
+ catch {
659
+ // A damaged row needs operator repair, but it must not block unrelated
660
+ // users or later deadlines in this multi-user monitor pass.
661
+ result.deferred++;
662
+ result.errors++;
663
+ }
664
+ }
665
+ }
666
+ async function processState(storage, state, context, result) {
667
+ for (let offset = 0;; offset += QUERY_PAGE_SIZE) {
668
+ const page = await storage.findTransactions({
669
+ partial: { noSendExpiryState: state },
670
+ paged: { limit: QUERY_PAGE_SIZE, offset }
671
+ });
672
+ await processPage(storage, page, context, result);
673
+ // State transitions can shift later offsets and defer some rows until
674
+ // the next five-second pass, but each pass remains strictly bounded in
675
+ // memory and every pass restarts from the beginning.
676
+ if (page.length < QUERY_PAGE_SIZE)
677
+ return;
678
+ }
679
+ }
680
+ async function processNoSendExpiryLifecycle(storage) {
681
+ const result = emptyResult();
682
+ const context = {
683
+ nowSeconds: Math.floor(Date.now() / 1000),
684
+ heightAttempted: false
685
+ };
686
+ for (const state of ACTIVE_STATES) {
687
+ await processState(storage, state, context, result);
688
+ }
689
+ return result;
690
+ }
691
+ //# sourceMappingURL=noSendExpiryLifecycle.js.map