@bsv/wallet-toolbox-mobile 2.4.3 → 2.4.4

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 (111) hide show
  1. package/out/src/CWIStyleWalletManager.d.ts +52 -5
  2. package/out/src/CWIStyleWalletManager.d.ts.map +1 -1
  3. package/out/src/CWIStyleWalletManager.js +500 -128
  4. package/out/src/CWIStyleWalletManager.js.map +1 -1
  5. package/out/src/Wallet.d.ts +8 -0
  6. package/out/src/Wallet.d.ts.map +1 -1
  7. package/out/src/Wallet.js +23 -9
  8. package/out/src/Wallet.js.map +1 -1
  9. package/out/src/WalletAuthenticationManager.d.ts +20 -6
  10. package/out/src/WalletAuthenticationManager.d.ts.map +1 -1
  11. package/out/src/WalletAuthenticationManager.js +214 -34
  12. package/out/src/WalletAuthenticationManager.js.map +1 -1
  13. package/out/src/index.mobile.d.ts +1 -0
  14. package/out/src/index.mobile.d.ts.map +1 -1
  15. package/out/src/index.mobile.js +1 -0
  16. package/out/src/index.mobile.js.map +1 -1
  17. package/out/src/monitor/Monitor.d.ts.map +1 -1
  18. package/out/src/monitor/Monitor.js +4 -3
  19. package/out/src/monitor/Monitor.js.map +1 -1
  20. package/out/src/monitor/tasks/TaskCleanupActionBatches.d.ts +13 -0
  21. package/out/src/monitor/tasks/TaskCleanupActionBatches.d.ts.map +1 -0
  22. package/out/src/monitor/tasks/TaskCleanupActionBatches.js +24 -0
  23. package/out/src/monitor/tasks/TaskCleanupActionBatches.js.map +1 -0
  24. package/out/src/sdk/ActionBatch.interfaces.d.ts +105 -0
  25. package/out/src/sdk/ActionBatch.interfaces.d.ts.map +1 -0
  26. package/out/src/sdk/ActionBatch.interfaces.js +3 -0
  27. package/out/src/sdk/ActionBatch.interfaces.js.map +1 -0
  28. package/out/src/sdk/WalletStorage.interfaces.d.ts +18 -1
  29. package/out/src/sdk/WalletStorage.interfaces.d.ts.map +1 -1
  30. package/out/src/sdk/index.d.ts +1 -0
  31. package/out/src/sdk/index.d.ts.map +1 -1
  32. package/out/src/sdk/index.js +1 -0
  33. package/out/src/sdk/index.js.map +1 -1
  34. package/out/src/signer/actionBatch/ActionBatchPlanner.d.ts +28 -0
  35. package/out/src/signer/actionBatch/ActionBatchPlanner.d.ts.map +1 -0
  36. package/out/src/signer/actionBatch/ActionBatchPlanner.js +313 -0
  37. package/out/src/signer/actionBatch/ActionBatchPlanner.js.map +1 -0
  38. package/out/src/signer/actionBatch/ActionBatchWorkspace.d.ts +33 -0
  39. package/out/src/signer/actionBatch/ActionBatchWorkspace.d.ts.map +1 -0
  40. package/out/src/signer/actionBatch/ActionBatchWorkspace.js +693 -0
  41. package/out/src/signer/actionBatch/ActionBatchWorkspace.js.map +1 -0
  42. package/out/src/signer/methods/buildSignableTransaction.d.ts.map +1 -1
  43. package/out/src/signer/methods/buildSignableTransaction.js +2 -1
  44. package/out/src/signer/methods/buildSignableTransaction.js.map +1 -1
  45. package/out/src/signer/methods/createAction.d.ts.map +1 -1
  46. package/out/src/signer/methods/createAction.js +4 -1
  47. package/out/src/signer/methods/createAction.js.map +1 -1
  48. package/out/src/storage/StorageProvider.d.ts +28 -0
  49. package/out/src/storage/StorageProvider.d.ts.map +1 -1
  50. package/out/src/storage/StorageProvider.js +49 -0
  51. package/out/src/storage/StorageProvider.js.map +1 -1
  52. package/out/src/storage/WalletStorageManager.d.ts +8 -0
  53. package/out/src/storage/WalletStorageManager.d.ts.map +1 -1
  54. package/out/src/storage/WalletStorageManager.js +24 -0
  55. package/out/src/storage/WalletStorageManager.js.map +1 -1
  56. package/out/src/storage/methods/actionBatch.d.ts +14 -0
  57. package/out/src/storage/methods/actionBatch.d.ts.map +1 -0
  58. package/out/src/storage/methods/actionBatch.js +644 -0
  59. package/out/src/storage/methods/actionBatch.js.map +1 -0
  60. package/out/src/storage/methods/actionBatchBlobs.d.ts +12 -0
  61. package/out/src/storage/methods/actionBatchBlobs.d.ts.map +1 -0
  62. package/out/src/storage/methods/actionBatchBlobs.js +121 -0
  63. package/out/src/storage/methods/actionBatchBlobs.js.map +1 -0
  64. package/out/src/storage/methods/actionBatchValidation.d.ts +15 -0
  65. package/out/src/storage/methods/actionBatchValidation.d.ts.map +1 -0
  66. package/out/src/storage/methods/actionBatchValidation.js +310 -0
  67. package/out/src/storage/methods/actionBatchValidation.js.map +1 -0
  68. package/out/src/storage/methods/actionPlanning.d.ts +12 -0
  69. package/out/src/storage/methods/actionPlanning.d.ts.map +1 -0
  70. package/out/src/storage/methods/actionPlanning.js +48 -0
  71. package/out/src/storage/methods/actionPlanning.js.map +1 -0
  72. package/out/src/storage/methods/createAction.d.ts.map +1 -1
  73. package/out/src/storage/methods/createAction.js +9 -25
  74. package/out/src/storage/methods/createAction.js.map +1 -1
  75. package/out/src/storage/remoting/StorageClientBase.d.ts +9 -0
  76. package/out/src/storage/remoting/StorageClientBase.d.ts.map +1 -1
  77. package/out/src/storage/remoting/StorageClientBase.js +36 -1
  78. package/out/src/storage/remoting/StorageClientBase.js.map +1 -1
  79. package/out/src/storage/schema/tables/TableActionBatch.d.ts +24 -0
  80. package/out/src/storage/schema/tables/TableActionBatch.d.ts.map +1 -0
  81. package/out/src/storage/schema/tables/TableActionBatch.js +3 -0
  82. package/out/src/storage/schema/tables/TableActionBatch.js.map +1 -0
  83. package/out/src/storage/schema/tables/index.d.ts +1 -0
  84. package/out/src/storage/schema/tables/index.d.ts.map +1 -1
  85. package/out/src/storage/schema/tables/index.js +1 -0
  86. package/out/src/storage/schema/tables/index.js.map +1 -1
  87. package/out/src/utility/actionBatchDigest.d.ts +9 -0
  88. package/out/src/utility/actionBatchDigest.d.ts.map +1 -0
  89. package/out/src/utility/actionBatchDigest.js +48 -0
  90. package/out/src/utility/actionBatchDigest.js.map +1 -0
  91. package/out/src/utility/beefForTxids.d.ts +9 -0
  92. package/out/src/utility/beefForTxids.d.ts.map +1 -0
  93. package/out/src/utility/beefForTxids.js +35 -0
  94. package/out/src/utility/beefForTxids.js.map +1 -0
  95. package/out/src/wab-client/WABClient.d.ts +36 -90
  96. package/out/src/wab-client/WABClient.d.ts.map +1 -1
  97. package/out/src/wab-client/WABClient.js +103 -151
  98. package/out/src/wab-client/WABClient.js.map +1 -1
  99. package/out/src/wab-client/WABTransport.d.ts +53 -0
  100. package/out/src/wab-client/WABTransport.d.ts.map +1 -0
  101. package/out/src/wab-client/WABTransport.js +300 -0
  102. package/out/src/wab-client/WABTransport.js.map +1 -0
  103. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts +10 -10
  104. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts.map +1 -1
  105. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js +16 -21
  106. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js.map +1 -1
  107. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts +2 -1
  108. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts.map +1 -1
  109. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js +14 -0
  110. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js.map +1 -1
  111. package/package.json +8 -3
@@ -0,0 +1,644 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ACTION_BATCH_MAX_RESERVATION_EXTENSION_OUTPUTS = exports.ACTION_BATCH_HARD_LIFETIME_MS = exports.ACTION_BATCH_LEASE_MS = void 0;
4
+ exports.getActionBatchCapabilities = getActionBatchCapabilities;
5
+ exports.cleanupExpiredActionBatches = cleanupExpiredActionBatches;
6
+ exports.beginActionBatch = beginActionBatch;
7
+ exports.extendActionBatch = extendActionBatch;
8
+ exports.renewActionBatch = renewActionBatch;
9
+ exports.commitActionBatch = commitActionBatch;
10
+ exports.abortActionBatch = abortActionBatch;
11
+ const sdk_1 = require("@bsv/sdk");
12
+ const WERR_errors_1 = require("../../sdk/WERR_errors");
13
+ const utilityHelpers_1 = require("../../utility/utilityHelpers");
14
+ const actionBatchDigest_1 = require("../../utility/actionBatchDigest");
15
+ const StorageProvider_1 = require("../StorageProvider");
16
+ const EntityProvenTxReq_1 = require("../schema/entities/EntityProvenTxReq");
17
+ const managedChange_1 = require("./managedChange");
18
+ const parseTxScriptOffsets_1 = require("../../utility/parseTxScriptOffsets");
19
+ const processAction_1 = require("./processAction");
20
+ const utils_1 = require("./utils");
21
+ const actionPlanning_1 = require("./actionPlanning");
22
+ const actionBatchBlobs_1 = require("./actionBatchBlobs");
23
+ const actionBatchValidation_1 = require("./actionBatchValidation");
24
+ exports.ACTION_BATCH_LEASE_MS = 15 * 60 * 1000;
25
+ exports.ACTION_BATCH_HARD_LIFETIME_MS = 60 * 60 * 1000;
26
+ const INITIAL_RESERVATION_LIMIT = 8;
27
+ const INITIAL_EXTRA_OUTPUTS = 3;
28
+ exports.ACTION_BATCH_MAX_RESERVATION_EXTENSION_OUTPUTS = 64;
29
+ function getActionBatchCapabilities() {
30
+ return {
31
+ actionBatch: {
32
+ version: 1,
33
+ maxInlineBytes: actionBatchBlobs_1.ACTION_BATCH_MAX_INLINE_BYTES,
34
+ maxBlobBytes: actionBatchBlobs_1.ACTION_BATCH_MAX_BLOB_BYTES,
35
+ maxConcurrentUploads: actionBatchBlobs_1.ACTION_BATCH_MAX_CONCURRENT_UPLOADS,
36
+ leaseMs: exports.ACTION_BATCH_LEASE_MS,
37
+ hardLifetimeMs: exports.ACTION_BATCH_HARD_LIFETIME_MS
38
+ }
39
+ };
40
+ }
41
+ function activeExpiry(batch, now = new Date()) {
42
+ return batch.status !== 'active' && batch.status !== 'prepared' ||
43
+ batch.expiresAt.getTime() <= now.getTime() ||
44
+ batch.hardExpiresAt.getTime() <= now.getTime();
45
+ }
46
+ function canExpire(batch, now = new Date()) {
47
+ return (batch.status === 'active' || batch.status === 'prepared') &&
48
+ (batch.expiresAt.getTime() <= now.getTime() || batch.hardExpiresAt.getTime() <= now.getTime());
49
+ }
50
+ async function releaseBatchState(storage, batch, status, trx) {
51
+ await storage.deleteActionBatchOutputReservations(batch.actionBatchId, trx);
52
+ await storage.deleteActionBatchBlobRecords(batch.actionBatchId, trx);
53
+ await storage.updateActionBatch(batch.actionBatchId, { status }, trx);
54
+ }
55
+ async function cleanupExpiredActionBatches(storage) {
56
+ const now = new Date();
57
+ const expired = await storage.findExpiredActionBatches(now);
58
+ let released = 0;
59
+ for (const batch of expired) {
60
+ await storage.transaction(async (trx) => {
61
+ const current = await storage.findActionBatchForUpdate(batch.userId, batch.batchId, trx);
62
+ if (current == null || !canExpire(current, now))
63
+ return;
64
+ await releaseBatchState(storage, current, 'expired', trx);
65
+ released++;
66
+ });
67
+ }
68
+ return released;
69
+ }
70
+ async function availableManagedChange(storage, userId, basketId, excludeSending, trx) {
71
+ const statuses = ['completed', 'unproven'];
72
+ if (!excludeSending)
73
+ statuses.push('sending');
74
+ const outputs = (await storage.findOutputs({
75
+ partial: { userId, basketId, spendable: true },
76
+ txStatus: statuses,
77
+ trx
78
+ })).filter(managedChange_1.isAutoSpendableChangeOutput);
79
+ const reserved = new Set(await storage.findReservedActionBatchOutputIds(outputs.map(o => o.outputId), trx));
80
+ return outputs.filter(output => output.spentBy == null && !reserved.has(output.outputId));
81
+ }
82
+ function sourceOutputFromBeef(beef, outpoint) {
83
+ const tx = beef.findTxid(outpoint.txid)?.tx;
84
+ const output = tx?.outputs[outpoint.vout];
85
+ if (output == null)
86
+ return undefined;
87
+ return { satoshis: sdk_1.Validation.validateSatoshis(output.satoshis, 'source output satoshis'), lockingScript: output.lockingScript };
88
+ }
89
+ async function resolveExplicitOutputs(storage, userId, args) {
90
+ const byOutpoint = await storage.findOutputsByOutpoints(userId, args.inputs.map(input => input.outpoint));
91
+ const beef = args.inputBEEF == null ? new sdk_1.Beef() : sdk_1.Beef.fromBinary(args.inputBEEF);
92
+ const outputs = [];
93
+ let inputSatoshis = 0;
94
+ for (const input of args.inputs) {
95
+ const output = byOutpoint[`${input.outpoint.txid}.${input.outpoint.vout}`];
96
+ if (output != null) {
97
+ await storage.validateOutputScript(output);
98
+ outputs.push(output);
99
+ inputSatoshis += output.satoshis;
100
+ continue;
101
+ }
102
+ const source = sourceOutputFromBeef(beef, input.outpoint);
103
+ if (source == null) {
104
+ throw new WERR_errors_1.WERR_INVALID_PARAMETER('inputBEEF', `proof data for ${input.outpoint.txid}.${input.outpoint.vout}`);
105
+ }
106
+ inputSatoshis += source.satoshis;
107
+ }
108
+ return { outputs, inputSatoshis };
109
+ }
110
+ async function resolveNoSendChangeOutputs(storage, userId, args) {
111
+ const outpoints = args.options.noSendChange;
112
+ const byOutpoint = await storage.findOutputsByOutpoints(userId, outpoints);
113
+ const outputs = [];
114
+ let inputSatoshis = 0;
115
+ for (const outpoint of outpoints) {
116
+ const key = `${outpoint.txid}.${outpoint.vout}`;
117
+ const output = byOutpoint[key];
118
+ if (output == null || !output.spendable || output.spentBy != null || !(0, managedChange_1.isAutoSpendableChangeOutput)(output)) {
119
+ throw new WERR_errors_1.WERR_INVALID_PARAMETER('noSendChange', `spendable wallet-managed output ${key}`);
120
+ }
121
+ await storage.validateOutputScript(output);
122
+ outputs.push(output);
123
+ inputSatoshis += output.satoshis;
124
+ }
125
+ return { outputs, inputSatoshis };
126
+ }
127
+ function estimateFirstActionTarget(storage, args, inputSatoshis) {
128
+ const outputSatoshis = args.outputs.reduce((sum, output) => sum + output.satoshis, 0) + storage.commissionSatoshis;
129
+ const inputLengths = args.inputs.map(input => input.unlockingScriptLength);
130
+ const outputLengths = args.outputs.map(output => output.lockingScript.length / 2);
131
+ if (storage.commissionSatoshis > 0)
132
+ outputLengths.push(25);
133
+ outputLengths.push(25);
134
+ const fee = (0, StorageProvider_1.validateStorageFeeModel)(storage.feeModel).value ?? 0;
135
+ const minFee = Math.ceil((0, utils_1.transactionSize)([...inputLengths, 107], outputLengths) * fee / 1000);
136
+ return Math.max(1, outputSatoshis + minFee - inputSatoshis);
137
+ }
138
+ function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLimit, planningCosts) {
139
+ const remaining = candidates
140
+ .filter(output => output.satoshis > planningCosts.marginalInputFee);
141
+ const chosen = [];
142
+ // A reservation is useful to generateChangeSdk only after it also covers
143
+ // the marginal funding-input fee and leaves an economically viable first
144
+ // change output. Without this buffer, a tiny target repeatedly selects dust
145
+ // that satisfies the nominal deficit but can never close the real plan.
146
+ let deficit = targetSatoshis + planningCosts.firstChangeCost;
147
+ while (deficit > 0 && chosen.length < limit) {
148
+ const output = (0, actionPlanning_1.selectCanonicalChange)(remaining, deficit + planningCosts.marginalInputFee);
149
+ if (output == null)
150
+ break;
151
+ chosen.push(output);
152
+ remaining.splice(remaining.indexOf(output), 1);
153
+ deficit -= output.satoshis - planningCosts.marginalInputFee;
154
+ }
155
+ const desiredCount = fillLimit ? limit : Math.min(limit, chosen.length + extras);
156
+ while (remaining.length > 0 && chosen.length < desiredCount) {
157
+ const output = (0, actionPlanning_1.selectCanonicalChange)(remaining, targetSatoshis);
158
+ if (output == null)
159
+ break;
160
+ chosen.push(output);
161
+ remaining.splice(remaining.indexOf(output), 1);
162
+ }
163
+ return chosen;
164
+ }
165
+ function reservationPlanningCosts(storage, basket) {
166
+ const satsPerKb = (0, StorageProvider_1.validateStorageFeeModel)(storage.feeModel).value ?? 0;
167
+ const minimumSpendSize = (0, utils_1.transactionSize)([107], [25]);
168
+ const minimumSpendFee = Math.ceil(minimumSpendSize * satsPerKb / 1000);
169
+ const dustFloor = Math.max(1, minimumSpendFee * 2);
170
+ const marginalInputSize = (0, utils_1.transactionSize)([107], []) - (0, utils_1.transactionSize)([], []);
171
+ const marginalOutputSize = (0, utils_1.transactionSize)([], [25]) - (0, utils_1.transactionSize)([], []);
172
+ const desiredFirstChange = Math.max(dustFloor, Math.max(1, Math.round(basket.minimumDesiredUTXOValue / 4)));
173
+ return {
174
+ firstChangeCost: desiredFirstChange + Math.ceil(marginalOutputSize * satsPerKb / 1000),
175
+ marginalInputFee: Math.ceil(marginalInputSize * satsPerKb / 1000)
176
+ };
177
+ }
178
+ async function reserveOutputs(storage, batch, outputs, trx) {
179
+ const now = new Date();
180
+ const unique = [...new Map(outputs.map(output => [output.outputId, output])).values()];
181
+ const reserve = async (transaction) => {
182
+ const outpoints = unique.map(output => {
183
+ if (output.txid == null)
184
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('action batch output is missing its txid');
185
+ return { txid: output.txid, vout: output.vout };
186
+ });
187
+ const current = await storage.findOutputsByOutpointsForUpdate(batch.userId, outpoints, transaction);
188
+ for (const output of unique) {
189
+ const stored = output.txid == null ? undefined : current[`${output.txid}.${output.vout}`];
190
+ if (stored == null || !stored.spendable || stored.spentBy != null) {
191
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('one or more action batch outputs are no longer spendable');
192
+ }
193
+ }
194
+ const conflicts = await storage.findReservedActionBatchOutputIds(unique.map(output => output.outputId), transaction);
195
+ if (conflicts.length > 0)
196
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('one or more action batch outputs were concurrently reserved');
197
+ await storage.reserveActionBatchOutputs(unique.map(output => ({
198
+ actionBatchId: batch.actionBatchId,
199
+ outputId: output.outputId,
200
+ created_at: now,
201
+ updated_at: now
202
+ })), transaction);
203
+ };
204
+ if (trx != null)
205
+ await reserve(trx);
206
+ else
207
+ await storage.transaction(reserve);
208
+ }
209
+ async function makeFundingResult(storage, args, outputs) {
210
+ const beef = new sdk_1.Beef();
211
+ const result = [];
212
+ for (const output of outputs) {
213
+ await storage.validateOutputScript(output);
214
+ const copy = { ...output };
215
+ if (args.isSignAction && args.includeAllSourceTransactions) {
216
+ copy.sourceTransaction = await storage.getRawTxOfKnownValidTransaction(output.txid);
217
+ }
218
+ if (output.txid != null && beef.findTxid(output.txid) == null) {
219
+ beef.mergeBeef(await storage.getBeefForTransaction(output.txid, {
220
+ knownTxids: args.options.knownTxids,
221
+ ignoreServices: true
222
+ }));
223
+ }
224
+ result.push(copy);
225
+ }
226
+ return { outputs: result, beef: beef.toUint8Array() };
227
+ }
228
+ function newBatch(userId, batchId) {
229
+ const now = new Date();
230
+ return {
231
+ actionBatchId: 0,
232
+ userId,
233
+ batchId,
234
+ status: 'active',
235
+ expiresAt: new Date(now.getTime() + exports.ACTION_BATCH_LEASE_MS),
236
+ hardExpiresAt: new Date(now.getTime() + exports.ACTION_BATCH_HARD_LIFETIME_MS),
237
+ created_at: now,
238
+ updated_at: now
239
+ };
240
+ }
241
+ async function beginActionBatch(storage, auth, args) {
242
+ const userId = (0, utilityHelpers_1.verifyId)(auth.userId);
243
+ await cleanupExpiredActionBatches(storage);
244
+ if (await storage.findActionBatch(userId, args.batchId) != null) {
245
+ throw new WERR_errors_1.WERR_INVALID_PARAMETER('batchId', 'unique');
246
+ }
247
+ const changeBasket = (0, utilityHelpers_1.verifyOne)(await storage.findOutputBaskets({ partial: { userId, name: 'default' } }));
248
+ const explicit = await resolveExplicitOutputs(storage, userId, args.firstAction);
249
+ const noSendChange = await resolveNoSendChangeOutputs(storage, userId, args.firstAction);
250
+ const fixedOutputIds = new Set([...explicit.outputs, ...noSendChange.outputs].map(output => output.outputId));
251
+ const available = (await availableManagedChange(storage, userId, changeBasket.basketId, !args.firstAction.isDelayed)).filter(output => !fixedOutputIds.has(output.outputId));
252
+ const target = estimateFirstActionTarget(storage, args.firstAction, explicit.inputSatoshis + noSendChange.inputSatoshis);
253
+ const fixedOutputs = [...explicit.outputs, ...noSendChange.outputs];
254
+ const requiredCapacity = Math.max(0, INITIAL_RESERVATION_LIMIT - fixedOutputs.length);
255
+ const funding = chooseReservationPool(available, target, requiredCapacity, INITIAL_EXTRA_OUTPUTS, false, reservationPlanningCosts(storage, changeBasket));
256
+ const batch = newBatch(userId, args.batchId);
257
+ await storage.transaction(async (trx) => {
258
+ await storage.insertActionBatch(batch, trx);
259
+ await reserveOutputs(storage, batch, [...fixedOutputs, ...funding], trx);
260
+ });
261
+ let fundingResult;
262
+ try {
263
+ fundingResult = await makeFundingResult(storage, args.firstAction, [...funding, ...fixedOutputs]);
264
+ }
265
+ catch (error) {
266
+ await storage.transaction(async (trx) => await releaseBatchState(storage, batch, 'aborted', trx));
267
+ throw error;
268
+ }
269
+ const explicitIds = new Set(fixedOutputs.map(output => output.outputId));
270
+ return {
271
+ batchId: batch.batchId,
272
+ expiresAt: batch.expiresAt.toISOString(),
273
+ hardExpiresAt: batch.hardExpiresAt.toISOString(),
274
+ changeBasket,
275
+ feeModel: (0, StorageProvider_1.validateStorageFeeModel)(storage.feeModel),
276
+ commissionSatoshis: storage.commissionSatoshis,
277
+ commissionPubKeyHex: storage.commissionPubKeyHex,
278
+ availableChangeCount: available.length,
279
+ reservedOutputs: fundingResult.outputs.filter(output => funding.some(candidate => candidate.outputId === output.outputId)),
280
+ explicitOutputs: fundingResult.outputs.filter(output => explicitIds.has(output.outputId)),
281
+ inputBeef: fundingResult.beef
282
+ };
283
+ }
284
+ function requireLiveBatch(batch) {
285
+ if (batch == null || activeExpiry(batch))
286
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('action batch is not active');
287
+ return batch;
288
+ }
289
+ async function extendActionBatch(storage, auth, args) {
290
+ const userId = (0, utilityHelpers_1.verifyId)(auth.userId);
291
+ await cleanupExpiredActionBatches(storage);
292
+ const batch = requireLiveBatch(await storage.findActionBatch(userId, args.batchId));
293
+ const basket = (0, utilityHelpers_1.verifyOne)(await storage.findOutputBaskets({ partial: { userId, name: 'default' } }));
294
+ const alreadyReserved = await storage.findActionBatchOutputIds(batch.actionBatchId);
295
+ const available = await availableManagedChange(storage, userId, basket.basketId, false);
296
+ if (!Number.isSafeInteger(args.requestedOutputs) || args.requestedOutputs < 0) {
297
+ throw new WERR_errors_1.WERR_INVALID_PARAMETER('requestedOutputs', 'non-negative safe integer');
298
+ }
299
+ const requestedCount = Math.min(args.requestedOutputs, exports.ACTION_BATCH_MAX_RESERVATION_EXTENSION_OUTPUTS);
300
+ const funding = chooseReservationPool(available, Math.max(1, args.targetSatoshis), requestedCount, 0, true, reservationPlanningCosts(storage, basket));
301
+ const explicitByOutpoint = await storage.findOutputsByOutpoints(userId, args.explicitOutpoints);
302
+ const explicit = Object.values(explicitByOutpoint)
303
+ .filter(output => !alreadyReserved.includes(output.outputId));
304
+ const fundingShape = argsToFundingShape(args.includeSourceTransactions);
305
+ const fundingResult = await makeFundingResult(storage, fundingShape, [...funding, ...explicit]);
306
+ const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + exports.ACTION_BATCH_LEASE_MS));
307
+ await storage.transaction(async (trx) => {
308
+ const current = requireLiveBatch(await storage.findActionBatchForUpdate(userId, args.batchId, trx));
309
+ await reserveOutputs(storage, current, [...funding, ...explicit], trx);
310
+ await storage.updateActionBatch(current.actionBatchId, { expiresAt }, trx);
311
+ });
312
+ return {
313
+ expiresAt: expiresAt.toISOString(),
314
+ reservedOutputs: fundingResult.outputs.filter(output => funding.some(candidate => candidate.outputId === output.outputId)),
315
+ explicitOutputs: fundingResult.outputs.filter(output => explicit.some(candidate => candidate.outputId === output.outputId)),
316
+ inputBeef: fundingResult.beef
317
+ };
318
+ }
319
+ function argsToFundingShape(includeSourceTransactions) {
320
+ return {
321
+ inputs: [], outputs: [], labels: [], description: 'action batch extension', version: 1, lockTime: 0,
322
+ options: {
323
+ acceptDelayedBroadcast: true, returnTXIDOnly: false, noSend: true, sendWith: [], signAndProcess: true,
324
+ knownTxids: [], noSendChange: [], randomizeOutputs: true
325
+ },
326
+ isSendWith: false, isNewTx: true, isRemixChange: false, isNoSend: true, isDelayed: true,
327
+ isTestWerrReviewActions: false,
328
+ isSignAction: includeSourceTransactions,
329
+ includeAllSourceTransactions: includeSourceTransactions
330
+ };
331
+ }
332
+ async function renewActionBatch(storage, auth, batchId) {
333
+ const userId = (0, utilityHelpers_1.verifyId)(auth.userId);
334
+ return await storage.transaction(async (trx) => {
335
+ const batch = requireLiveBatch(await storage.findActionBatchForUpdate(userId, batchId, trx));
336
+ const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + exports.ACTION_BATCH_LEASE_MS));
337
+ await storage.updateActionBatch(batch.actionBatchId, { expiresAt }, trx);
338
+ return { expiresAt: expiresAt.toISOString() };
339
+ });
340
+ }
341
+ async function reacquireManifestInputs(storage, userId, batch, validated, trx) {
342
+ if (batch.hardExpiresAt.getTime() <= Date.now()) {
343
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('action batch hard lifetime has expired');
344
+ }
345
+ const stagedTxids = new Set(validated.actions.map(({ action }) => action.txid));
346
+ const outpoints = [...new Map(validated.actions.flatMap(({ action }) => action.plan.inputs)
347
+ .filter(input => !stagedTxids.has(input.sourceTxid))
348
+ .map(input => [`${input.sourceTxid}.${input.sourceVout}`, {
349
+ txid: input.sourceTxid,
350
+ vout: input.sourceVout,
351
+ providedBy: input.providedBy
352
+ }])).values()];
353
+ const stored = await storage.findOutputsByOutpointsForUpdate(userId, outpoints, trx);
354
+ await storage.deleteActionBatchOutputReservations(batch.actionBatchId, trx);
355
+ const outputs = [];
356
+ for (const outpoint of outpoints) {
357
+ const key = `${outpoint.txid}.${outpoint.vout}`;
358
+ const output = stored[key];
359
+ if (output == null) {
360
+ if (outpoint.providedBy === 'storage' || outpoint.providedBy === 'you-and-storage') {
361
+ throw new WERR_errors_1.WERR_INVALID_OPERATION(`reserved input ${key} is no longer available`);
362
+ }
363
+ continue;
364
+ }
365
+ if (!output.spendable || output.spentBy != null) {
366
+ throw new WERR_errors_1.WERR_INVALID_OPERATION(`input ${key} is no longer spendable`);
367
+ }
368
+ outputs.push(output);
369
+ }
370
+ const conflicts = await storage.findReservedActionBatchOutputIds(outputs.map(output => output.outputId), trx);
371
+ if (conflicts.length > 0) {
372
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('one or more expired action batch inputs were reserved elsewhere');
373
+ }
374
+ const now = new Date();
375
+ await storage.reserveActionBatchOutputs(outputs.map(output => ({
376
+ actionBatchId: batch.actionBatchId,
377
+ outputId: output.outputId,
378
+ created_at: now,
379
+ updated_at: now
380
+ })), trx);
381
+ await storage.updateActionBatch(batch.actionBatchId, {
382
+ status: 'active',
383
+ expiresAt: new Date(Math.min(batch.hardExpiresAt.getTime(), now.getTime() + exports.ACTION_BATCH_LEASE_MS))
384
+ }, trx);
385
+ }
386
+ function transactionStatuses(action) {
387
+ if (action.metadata.isNoSend)
388
+ return { tx: 'nosend', req: 'nosend' };
389
+ if (action.metadata.isDelayed)
390
+ return { tx: 'unprocessed', req: 'unsent' };
391
+ return { tx: 'unprocessed', req: 'unprocessed' };
392
+ }
393
+ async function persistLabels(storage, userId, transactionId, labels, trx) {
394
+ for (const label of new Set(labels)) {
395
+ const row = await storage.findOrInsertTxLabel(userId, label, trx);
396
+ await storage.findOrInsertTxLabelMap(transactionId, (0, utilityHelpers_1.verifyId)(row.txLabelId), trx);
397
+ }
398
+ }
399
+ async function persistOutputs(storage, userId, transactionId, validated, trx) {
400
+ const { action, tx, rawTx } = validated;
401
+ const basketNames = [...new Set(action.plan.outputs.map(output => output.basket).filter((name) => name != null))];
402
+ if (action.plan.outputs.some(output => output.purpose === 'change'))
403
+ basketNames.push('default');
404
+ const baskets = await storage.findOrInsertOutputBasketsBulk(userId, [...new Set(basketNames)], trx);
405
+ const tagNames = [...new Set(action.plan.outputs.flatMap(output => output.tags))];
406
+ const tags = await storage.findOrInsertOutputTagsBulk(userId, tagNames, trx);
407
+ const offsets = (0, parseTxScriptOffsets_1.parseTxScriptOffsets)(rawTx);
408
+ const rows = [];
409
+ for (let index = 0; index < action.plan.outputs.length; index++) {
410
+ const planned = action.plan.outputs[index];
411
+ const output = tx.outputs[planned.vout];
412
+ const isChange = planned.providedBy === 'storage' && planned.purpose === 'change';
413
+ const isCommission = planned.providedBy === 'storage' &&
414
+ (planned.purpose === 'storage-commission' || planned.purpose === 'service-charge');
415
+ const offset = offsets.outputs[planned.vout];
416
+ const lockingScript = output.lockingScript.toBinary();
417
+ const now = new Date();
418
+ const row = {
419
+ outputId: 0,
420
+ userId,
421
+ transactionId,
422
+ basketId: isChange ? baskets.default?.basketId : (planned.basket == null ? undefined : baskets[planned.basket].basketId),
423
+ spendable: !isCommission,
424
+ change: isChange,
425
+ outputDescription: planned.outputDescription,
426
+ vout: planned.vout,
427
+ satoshis: planned.satoshis,
428
+ providedBy: planned.providedBy,
429
+ purpose: isCommission ? 'storage-commission' : (planned.purpose ?? ''),
430
+ type: isChange ? 'P2PKH' : 'custom',
431
+ txid: action.txid,
432
+ derivationPrefix: isChange ? action.plan.derivationPrefix : undefined,
433
+ derivationSuffix: isChange ? planned.derivationSuffix : undefined,
434
+ customInstructions: planned.customInstructions,
435
+ scriptLength: offset.length,
436
+ scriptOffset: offset.offset,
437
+ lockingScript: offset.length > storage.getSettings().maxOutputScript ? undefined : lockingScript,
438
+ created_at: now,
439
+ updated_at: now
440
+ };
441
+ row.outputId = await storage.insertOutput(row, trx);
442
+ for (const tagName of new Set(planned.tags)) {
443
+ const tag = tags[tagName];
444
+ await storage.findOrInsertOutputTagMap(row.outputId, (0, utilityHelpers_1.verifyId)(tag.outputTagId), trx);
445
+ }
446
+ if (isCommission) {
447
+ const commission = {
448
+ commissionId: 0,
449
+ userId,
450
+ transactionId,
451
+ satoshis: planned.satoshis,
452
+ keyOffset: action.commissionKeyOffset ?? '',
453
+ isRedeemed: false,
454
+ lockingScript,
455
+ created_at: now,
456
+ updated_at: now
457
+ };
458
+ await storage.insertCommission(commission, trx);
459
+ }
460
+ rows.push(row);
461
+ }
462
+ return rows;
463
+ }
464
+ async function persistAction(storage, userId, validated, reservedOutputIds, stagedByOutpoint, trx) {
465
+ const { action, tx, rawTx } = validated;
466
+ const statuses = transactionStatuses(action);
467
+ const managedInputSatoshis = action.plan.inputs
468
+ .filter(input => input.vin >= action.metadata.inputs.length)
469
+ .reduce((sum, input) => sum + input.sourceSatoshis, 0);
470
+ const changeOutputSatoshis = action.plan.outputs
471
+ .filter(output => output.purpose === 'change')
472
+ .reduce((sum, output) => sum + output.satoshis, 0);
473
+ const now = new Date();
474
+ const transaction = {
475
+ transactionId: 0,
476
+ userId,
477
+ status: statuses.tx,
478
+ reference: action.reference,
479
+ isOutgoing: true,
480
+ satoshis: changeOutputSatoshis - managedInputSatoshis,
481
+ description: action.metadata.description,
482
+ version: tx.version,
483
+ lockTime: tx.lockTime,
484
+ txid: action.txid,
485
+ created_at: now,
486
+ updated_at: now
487
+ };
488
+ transaction.transactionId = await storage.insertTransaction(transaction, trx);
489
+ await persistLabels(storage, userId, transaction.transactionId, action.metadata.labels, trx);
490
+ for (const input of action.plan.inputs) {
491
+ const outpoint = `${input.sourceTxid}.${input.sourceVout}`;
492
+ let output = stagedByOutpoint.get(outpoint);
493
+ output ??= (await storage.findOutputsByOutpoints(userId, [{ txid: input.sourceTxid, vout: input.sourceVout }], trx))[outpoint];
494
+ if (output == null)
495
+ continue;
496
+ if (output.spentBy != null || !output.spendable)
497
+ throw new WERR_errors_1.WERR_INVALID_OPERATION(`input ${outpoint} is no longer spendable`);
498
+ if (!stagedByOutpoint.has(outpoint) && !reservedOutputIds.has(output.outputId)) {
499
+ throw new WERR_errors_1.WERR_INVALID_OPERATION(`input ${outpoint} was not reserved by this action batch`);
500
+ }
501
+ await storage.updateOutput(output.outputId, {
502
+ spendable: false,
503
+ spentBy: transaction.transactionId,
504
+ spendingDescription: action.metadata.inputs[input.vin]?.inputDescription
505
+ }, trx);
506
+ output.spendable = false;
507
+ output.spentBy = transaction.transactionId;
508
+ }
509
+ const outputRows = await persistOutputs(storage, userId, transaction.transactionId, validated, trx);
510
+ for (const output of outputRows)
511
+ stagedByOutpoint.set(`${action.txid}.${output.vout}`, output);
512
+ const req = EntityProvenTxReq_1.EntityProvenTxReq.fromTxid(action.txid, rawTx, validated.inputBeef);
513
+ req.status = statuses.req;
514
+ req.addNotifyTransactionId(transaction.transactionId);
515
+ await req.insertOrMerge(storage, trx);
516
+ }
517
+ async function persistManifestAtomically(storage, userId, batch, manifest, validated) {
518
+ return await storage.transaction(async (trx) => {
519
+ const current = await storage.findActionBatchForUpdate(userId, batch.batchId, trx);
520
+ if (current == null)
521
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('action batch was not found');
522
+ if (current.status === 'committed') {
523
+ if (current.manifestDigest !== manifest.digest) {
524
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('batch committed with another manifest');
525
+ }
526
+ return { batch: current, alreadyCommitted: true };
527
+ }
528
+ if (current.status === 'aborted')
529
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('aborted action batch cannot be committed');
530
+ if (current.manifestDigest != null && current.manifestDigest !== manifest.digest) {
531
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('batch prepared with another manifest');
532
+ }
533
+ const needsReacquire = current.status === 'expired' || activeExpiry(current);
534
+ if (needsReacquire) {
535
+ if (current.hardExpiresAt.getTime() <= Date.now()) {
536
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('action batch hard lifetime has expired');
537
+ }
538
+ await reacquireManifestInputs(storage, userId, current, validated, trx);
539
+ }
540
+ else {
541
+ requireLiveBatch(current);
542
+ }
543
+ const reservedOutputIds = new Set(await storage.findActionBatchOutputIds(current.actionBatchId, trx));
544
+ const stagedByOutpoint = new Map();
545
+ for (const action of validated.actions) {
546
+ await persistAction(storage, userId, action, reservedOutputIds, stagedByOutpoint, trx);
547
+ }
548
+ await storage.deleteActionBatchOutputReservations(current.actionBatchId, trx);
549
+ await storage.deleteActionBatchBlobRecords(current.actionBatchId, trx);
550
+ await storage.updateActionBatch(current.actionBatchId, {
551
+ status: 'committed',
552
+ manifestDigest: manifest.digest
553
+ }, trx);
554
+ current.status = 'committed';
555
+ current.manifestDigest = manifest.digest;
556
+ return { batch: current, alreadyCommitted: false };
557
+ });
558
+ }
559
+ async function completeCommittedBatch(storage, userId, batch, manifest, alreadyCommitted) {
560
+ if (batch.result != null) {
561
+ const saved = JSON.parse(batch.result);
562
+ return {
563
+ batchId: saved.batchId,
564
+ manifestDigest: saved.manifestDigest,
565
+ committedTxids: saved.committedTxids,
566
+ alreadyCommitted: true,
567
+ sendWithResults: saved.sendWithResults,
568
+ notDelayedResults: saved.notDelayedResults,
569
+ log: saved.log
570
+ };
571
+ }
572
+ const { swr, ndr } = await (0, processAction_1.shareReqsWithWorld)(storage, userId, manifest.sendWith, manifest.isDelayed);
573
+ const result = {
574
+ batchId: manifest.batchId,
575
+ manifestDigest: manifest.digest,
576
+ committedTxids: manifest.actions.map(action => action.txid),
577
+ alreadyCommitted,
578
+ sendWithResults: swr,
579
+ notDelayedResults: ndr
580
+ };
581
+ await storage.updateActionBatch(batch.actionBatchId, { result: JSON.stringify(result) });
582
+ return result;
583
+ }
584
+ async function commitActionBatchOnce(storage, userId, manifest) {
585
+ const batch = await storage.findActionBatch(userId, manifest.batchId);
586
+ if (batch == null)
587
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('action batch was not found');
588
+ if (batch.status === 'committed') {
589
+ if (batch.manifestDigest !== manifest.digest)
590
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('batch committed with another manifest');
591
+ return await completeCommittedBatch(storage, userId, batch, manifest, true);
592
+ }
593
+ if (batch.status === 'aborted')
594
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('aborted action batch cannot be committed');
595
+ const needsReacquire = batch.status === 'expired' || activeExpiry(batch);
596
+ if (!needsReacquire)
597
+ requireLiveBatch(batch);
598
+ else if (batch.hardExpiresAt.getTime() <= Date.now()) {
599
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('action batch hard lifetime has expired');
600
+ }
601
+ if (batch.manifestDigest != null && batch.manifestDigest !== manifest.digest) {
602
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('batch prepared with another manifest');
603
+ }
604
+ const validated = await (0, actionBatchValidation_1.validateManifestActions)(storage, batch, manifest);
605
+ const persisted = await persistManifestAtomically(storage, userId, batch, manifest, validated);
606
+ return await completeCommittedBatch(storage, userId, persisted.batch, manifest, persisted.alreadyCommitted);
607
+ }
608
+ const activeBatchCommits = new WeakMap();
609
+ async function commitActionBatch(storage, auth, manifest) {
610
+ (0, actionBatchBlobs_1.validateActionBatchInlinePayload)(manifest);
611
+ if (!(0, actionBatchDigest_1.verifyActionBatchManifestDigest)(manifest))
612
+ throw new WERR_errors_1.WERR_INVALID_PARAMETER('manifest.digest', 'valid');
613
+ const userId = (0, utilityHelpers_1.verifyId)(auth.userId);
614
+ let commits = activeBatchCommits.get(storage);
615
+ if (commits == null) {
616
+ commits = new Map();
617
+ activeBatchCommits.set(storage, commits);
618
+ }
619
+ const key = `${userId}:${manifest.batchId}`;
620
+ const active = commits.get(key);
621
+ if (active != null) {
622
+ if (active.digest !== manifest.digest) {
623
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('action batch commit is active with another manifest');
624
+ }
625
+ return await active.promise;
626
+ }
627
+ const promise = commitActionBatchOnce(storage, userId, manifest)
628
+ .finally(() => { commits?.delete(key); });
629
+ commits.set(key, { digest: manifest.digest, promise });
630
+ return await promise;
631
+ }
632
+ async function abortActionBatch(storage, auth, batchId) {
633
+ const userId = (0, utilityHelpers_1.verifyId)(auth.userId);
634
+ return await storage.transaction(async (trx) => {
635
+ const batch = await storage.findActionBatchForUpdate(userId, batchId, trx);
636
+ if (batch == null || batch.status === 'aborted')
637
+ return { aborted: true };
638
+ if (batch.status === 'committed')
639
+ return { aborted: false };
640
+ await releaseBatchState(storage, batch, 'aborted', trx);
641
+ return { aborted: true };
642
+ });
643
+ }
644
+ //# sourceMappingURL=actionBatch.js.map