@bsv/wallet-toolbox-mobile 2.4.2 → 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 (115) 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/WalletPermissionsManager.d.ts +15 -0
  14. package/out/src/WalletPermissionsManager.d.ts.map +1 -1
  15. package/out/src/WalletPermissionsManager.js +76 -28
  16. package/out/src/WalletPermissionsManager.js.map +1 -1
  17. package/out/src/index.mobile.d.ts +1 -0
  18. package/out/src/index.mobile.d.ts.map +1 -1
  19. package/out/src/index.mobile.js +1 -0
  20. package/out/src/index.mobile.js.map +1 -1
  21. package/out/src/monitor/Monitor.d.ts.map +1 -1
  22. package/out/src/monitor/Monitor.js +4 -3
  23. package/out/src/monitor/Monitor.js.map +1 -1
  24. package/out/src/monitor/tasks/TaskCleanupActionBatches.d.ts +13 -0
  25. package/out/src/monitor/tasks/TaskCleanupActionBatches.d.ts.map +1 -0
  26. package/out/src/monitor/tasks/TaskCleanupActionBatches.js +24 -0
  27. package/out/src/monitor/tasks/TaskCleanupActionBatches.js.map +1 -0
  28. package/out/src/sdk/ActionBatch.interfaces.d.ts +105 -0
  29. package/out/src/sdk/ActionBatch.interfaces.d.ts.map +1 -0
  30. package/out/src/sdk/ActionBatch.interfaces.js +3 -0
  31. package/out/src/sdk/ActionBatch.interfaces.js.map +1 -0
  32. package/out/src/sdk/WalletStorage.interfaces.d.ts +18 -1
  33. package/out/src/sdk/WalletStorage.interfaces.d.ts.map +1 -1
  34. package/out/src/sdk/index.d.ts +1 -0
  35. package/out/src/sdk/index.d.ts.map +1 -1
  36. package/out/src/sdk/index.js +1 -0
  37. package/out/src/sdk/index.js.map +1 -1
  38. package/out/src/signer/actionBatch/ActionBatchPlanner.d.ts +28 -0
  39. package/out/src/signer/actionBatch/ActionBatchPlanner.d.ts.map +1 -0
  40. package/out/src/signer/actionBatch/ActionBatchPlanner.js +313 -0
  41. package/out/src/signer/actionBatch/ActionBatchPlanner.js.map +1 -0
  42. package/out/src/signer/actionBatch/ActionBatchWorkspace.d.ts +33 -0
  43. package/out/src/signer/actionBatch/ActionBatchWorkspace.d.ts.map +1 -0
  44. package/out/src/signer/actionBatch/ActionBatchWorkspace.js +693 -0
  45. package/out/src/signer/actionBatch/ActionBatchWorkspace.js.map +1 -0
  46. package/out/src/signer/methods/buildSignableTransaction.d.ts.map +1 -1
  47. package/out/src/signer/methods/buildSignableTransaction.js +2 -1
  48. package/out/src/signer/methods/buildSignableTransaction.js.map +1 -1
  49. package/out/src/signer/methods/createAction.d.ts.map +1 -1
  50. package/out/src/signer/methods/createAction.js +4 -1
  51. package/out/src/signer/methods/createAction.js.map +1 -1
  52. package/out/src/storage/StorageProvider.d.ts +28 -0
  53. package/out/src/storage/StorageProvider.d.ts.map +1 -1
  54. package/out/src/storage/StorageProvider.js +49 -0
  55. package/out/src/storage/StorageProvider.js.map +1 -1
  56. package/out/src/storage/WalletStorageManager.d.ts +8 -0
  57. package/out/src/storage/WalletStorageManager.d.ts.map +1 -1
  58. package/out/src/storage/WalletStorageManager.js +24 -0
  59. package/out/src/storage/WalletStorageManager.js.map +1 -1
  60. package/out/src/storage/methods/actionBatch.d.ts +14 -0
  61. package/out/src/storage/methods/actionBatch.d.ts.map +1 -0
  62. package/out/src/storage/methods/actionBatch.js +644 -0
  63. package/out/src/storage/methods/actionBatch.js.map +1 -0
  64. package/out/src/storage/methods/actionBatchBlobs.d.ts +12 -0
  65. package/out/src/storage/methods/actionBatchBlobs.d.ts.map +1 -0
  66. package/out/src/storage/methods/actionBatchBlobs.js +121 -0
  67. package/out/src/storage/methods/actionBatchBlobs.js.map +1 -0
  68. package/out/src/storage/methods/actionBatchValidation.d.ts +15 -0
  69. package/out/src/storage/methods/actionBatchValidation.d.ts.map +1 -0
  70. package/out/src/storage/methods/actionBatchValidation.js +310 -0
  71. package/out/src/storage/methods/actionBatchValidation.js.map +1 -0
  72. package/out/src/storage/methods/actionPlanning.d.ts +12 -0
  73. package/out/src/storage/methods/actionPlanning.d.ts.map +1 -0
  74. package/out/src/storage/methods/actionPlanning.js +48 -0
  75. package/out/src/storage/methods/actionPlanning.js.map +1 -0
  76. package/out/src/storage/methods/createAction.d.ts.map +1 -1
  77. package/out/src/storage/methods/createAction.js +9 -25
  78. package/out/src/storage/methods/createAction.js.map +1 -1
  79. package/out/src/storage/remoting/StorageClientBase.d.ts +9 -0
  80. package/out/src/storage/remoting/StorageClientBase.d.ts.map +1 -1
  81. package/out/src/storage/remoting/StorageClientBase.js +36 -1
  82. package/out/src/storage/remoting/StorageClientBase.js.map +1 -1
  83. package/out/src/storage/schema/tables/TableActionBatch.d.ts +24 -0
  84. package/out/src/storage/schema/tables/TableActionBatch.d.ts.map +1 -0
  85. package/out/src/storage/schema/tables/TableActionBatch.js +3 -0
  86. package/out/src/storage/schema/tables/TableActionBatch.js.map +1 -0
  87. package/out/src/storage/schema/tables/index.d.ts +1 -0
  88. package/out/src/storage/schema/tables/index.d.ts.map +1 -1
  89. package/out/src/storage/schema/tables/index.js +1 -0
  90. package/out/src/storage/schema/tables/index.js.map +1 -1
  91. package/out/src/utility/actionBatchDigest.d.ts +9 -0
  92. package/out/src/utility/actionBatchDigest.d.ts.map +1 -0
  93. package/out/src/utility/actionBatchDigest.js +48 -0
  94. package/out/src/utility/actionBatchDigest.js.map +1 -0
  95. package/out/src/utility/beefForTxids.d.ts +9 -0
  96. package/out/src/utility/beefForTxids.d.ts.map +1 -0
  97. package/out/src/utility/beefForTxids.js +35 -0
  98. package/out/src/utility/beefForTxids.js.map +1 -0
  99. package/out/src/wab-client/WABClient.d.ts +36 -90
  100. package/out/src/wab-client/WABClient.d.ts.map +1 -1
  101. package/out/src/wab-client/WABClient.js +103 -151
  102. package/out/src/wab-client/WABClient.js.map +1 -1
  103. package/out/src/wab-client/WABTransport.d.ts +53 -0
  104. package/out/src/wab-client/WABTransport.d.ts.map +1 -0
  105. package/out/src/wab-client/WABTransport.js +300 -0
  106. package/out/src/wab-client/WABTransport.js.map +1 -0
  107. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts +10 -10
  108. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts.map +1 -1
  109. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js +16 -21
  110. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js.map +1 -1
  111. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts +2 -1
  112. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts.map +1 -1
  113. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js +14 -0
  114. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js.map +1 -1
  115. package/package.json +8 -3
@@ -0,0 +1,693 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActionBatchController = void 0;
4
+ exports.additionalFundingTarget = additionalFundingTarget;
5
+ exports.fundingRunwayExtension = fundingRunwayExtension;
6
+ const sdk_1 = require("@bsv/sdk");
7
+ const WERR_errors_1 = require("../../sdk/WERR_errors");
8
+ const utilityHelpers_1 = require("../../utility/utilityHelpers");
9
+ const utilityHelpers_noBuffer_1 = require("../../utility/utilityHelpers.noBuffer");
10
+ const types_1 = require("../../sdk/types");
11
+ const actionBatchDigest_1 = require("../../utility/actionBatchDigest");
12
+ const ActionBatchPlanner_1 = require("./ActionBatchPlanner");
13
+ const beefForTxids_1 = require("../../utility/beefForTxids");
14
+ function mergeUnique(values) {
15
+ return [...new Set(values)];
16
+ }
17
+ function nextGeometricTarget(value) {
18
+ return Math.min(Number.MAX_SAFE_INTEGER, value * 2);
19
+ }
20
+ function additionalFundingTarget(error) {
21
+ return error.moreSatoshisNeeded;
22
+ }
23
+ function fundingRunwayExtension(runwayTarget, ewmaConfirmedInputs, ewmaConfirmedSatoshis, availableConfirmed) {
24
+ const availableConfirmedCount = availableConfirmed.length;
25
+ const availableConfirmedSatoshis = availableConfirmed
26
+ .reduce((sum, output) => sum + output.satoshis, 0);
27
+ const predictedByInputs = availableConfirmedCount / Math.max(1, ewmaConfirmedInputs);
28
+ const predictedBySatoshis = ewmaConfirmedSatoshis > 0
29
+ ? availableConfirmedSatoshis / ewmaConfirmedSatoshis
30
+ : Number.POSITIVE_INFINITY;
31
+ if (Math.min(predictedByInputs, predictedBySatoshis) >= 2)
32
+ return undefined;
33
+ const nextRunwayTarget = nextGeometricTarget(runwayTarget);
34
+ return {
35
+ nextRunwayTarget,
36
+ requestedOutputs: Math.max(1, Math.ceil(nextRunwayTarget * ewmaConfirmedInputs) - availableConfirmedCount),
37
+ targetSatoshis: Math.max(1, nextRunwayTarget * ewmaConfirmedSatoshis - availableConfirmedSatoshis)
38
+ };
39
+ }
40
+ function makePlannerState(begin, firstAction) {
41
+ const sharedBeef = new sdk_1.Beef();
42
+ if (begin.inputBeef != null)
43
+ sharedBeef.mergeBeef(begin.inputBeef);
44
+ if (firstAction.inputBEEF != null)
45
+ sharedBeef.mergeBeef(firstAction.inputBEEF);
46
+ for (const output of [...begin.reservedOutputs, ...begin.explicitOutputs]) {
47
+ if (output.sourceTransaction != null)
48
+ sharedBeef.mergeRawTx(output.sourceTransaction);
49
+ }
50
+ const state = {
51
+ begin,
52
+ sharedBeef,
53
+ reserved: new Map(),
54
+ explicit: new Map(),
55
+ staged: new Map(),
56
+ discardedStagedTxids: new Set(),
57
+ consumed: new Set(),
58
+ estimatedChangeCount: begin.availableChangeCount
59
+ };
60
+ (0, ActionBatchPlanner_1.addPlannerOutputs)(state.reserved, begin.reservedOutputs, begin.changeBasket.name);
61
+ (0, ActionBatchPlanner_1.addPlannerOutputs)(state.explicit, begin.explicitOutputs);
62
+ return state;
63
+ }
64
+ class ActionBatchWorkspace {
65
+ wallet;
66
+ capabilities;
67
+ batchId;
68
+ state;
69
+ planned = new Map();
70
+ actions = [];
71
+ lockingScripts = new Map();
72
+ ewmaConfirmedInputs = 1;
73
+ ewmaConfirmedSatoshis = 0;
74
+ hasConfirmedSample = false;
75
+ runwayTarget = 4;
76
+ extension;
77
+ renewal;
78
+ expiresAt;
79
+ committed = false;
80
+ constructor(wallet, begin, firstAction, capabilities) {
81
+ this.wallet = wallet;
82
+ this.capabilities = capabilities;
83
+ this.batchId = begin.batchId;
84
+ this.state = makePlannerState(begin, firstAction);
85
+ this.expiresAt = Date.parse(begin.expiresAt);
86
+ }
87
+ ownsReference(reference) {
88
+ if (this.planned.has(reference))
89
+ return true;
90
+ if (this.actions.some(action => action.reference === reference || action.txid === reference))
91
+ return true;
92
+ return Object.entries(this.wallet.pendingSignActions)
93
+ .some(([pendingReference, pending]) => this.planned.has(pendingReference) && pending.tx.id('hex') === reference);
94
+ }
95
+ get isEmpty() {
96
+ return this.planned.size === 0 && this.actions.length === 0;
97
+ }
98
+ overlayListActions(persisted, args) {
99
+ if (args.labels.includes(types_1.specOpFailedActions))
100
+ return persisted;
101
+ const controlLabels = new Set(args.labels.includes(types_1.specOpNoSendActions) ? ['abort'] : []);
102
+ const ordinaryLabels = args.labels.filter(label => !(0, types_1.isListActionsSpecOp)(label) && !controlLabels.has(label));
103
+ const staged = this.actions
104
+ .filter(action => {
105
+ if (ordinaryLabels.length === 0)
106
+ return true;
107
+ const matches = ordinaryLabels.filter(label => action.metadata.labels.includes(label)).length;
108
+ return args.labelQueryMode === 'all' ? matches === ordinaryLabels.length : matches > 0;
109
+ })
110
+ .map(action => {
111
+ const tx = this.state.sharedBeef.findTxid(action.txid)?.tx;
112
+ if (tx == null)
113
+ throw new WERR_errors_1.WERR_INVALID_OPERATION(`missing staged transaction ${action.txid}`);
114
+ const managedInputSatoshis = action.plan.inputs
115
+ .filter(input => input.vin >= action.metadata.inputs.length)
116
+ .reduce((sum, input) => sum + input.sourceSatoshis, 0);
117
+ const changeOutputSatoshis = action.plan.outputs
118
+ .filter(output => output.purpose === 'change')
119
+ .reduce((sum, output) => sum + output.satoshis, 0);
120
+ return {
121
+ txid: action.txid,
122
+ satoshis: changeOutputSatoshis - managedInputSatoshis,
123
+ status: 'nosend',
124
+ isOutgoing: true,
125
+ description: action.metadata.description,
126
+ labels: args.includeLabels ? action.metadata.labels : undefined,
127
+ version: tx.version,
128
+ lockTime: tx.lockTime,
129
+ inputs: args.includeInputs
130
+ ? action.plan.inputs.map(input => ({
131
+ sourceOutpoint: `${input.sourceTxid}.${input.sourceVout}`,
132
+ sourceSatoshis: input.sourceSatoshis,
133
+ sourceLockingScript: args.includeInputSourceLockingScripts ? input.sourceLockingScript : undefined,
134
+ unlockingScript: args.includeInputUnlockingScripts
135
+ ? tx.inputs[input.vin]?.unlockingScript?.toHex()
136
+ : undefined,
137
+ inputDescription: input.spendingDescription ?? '',
138
+ sequenceNumber: tx.inputs[input.vin]?.sequence ?? 0
139
+ }))
140
+ : undefined,
141
+ outputs: args.includeOutputs
142
+ ? action.plan.outputs.map(output => ({
143
+ satoshis: output.satoshis,
144
+ lockingScript: args.includeOutputLockingScripts
145
+ ? tx.outputs[output.vout]?.lockingScript.toHex()
146
+ : undefined,
147
+ spendable: output.purpose !== 'storage-commission',
148
+ customInstructions: undefined,
149
+ tags: output.tags,
150
+ outputIndex: output.vout,
151
+ outputDescription: output.outputDescription,
152
+ basket: output.basket ?? ''
153
+ }))
154
+ : undefined
155
+ };
156
+ });
157
+ const actions = [...persisted.actions, ...staged];
158
+ return {
159
+ totalActions: persisted.totalActions + staged.length,
160
+ actions: actions.slice(args.offset, args.offset + args.limit)
161
+ };
162
+ }
163
+ overlayListOutputs(persisted, args) {
164
+ const isBalance = args.basket === types_1.specOpWalletBalance || args.tags.includes(types_1.specOpWalletBalance);
165
+ const managedOnly = args.basket === types_1.specOpWalletManagedUtxos || isBalance;
166
+ const consumed = this.state.consumed;
167
+ const stagedLabels = new Map(this.actions.map(action => [action.txid, action.metadata.labels]));
168
+ const staged = [...this.state.staged.entries()]
169
+ .filter(([outpoint, output]) => {
170
+ if (consumed.has(outpoint))
171
+ return false;
172
+ if (managedOnly)
173
+ return output.change;
174
+ if (output.basketName !== args.basket)
175
+ return false;
176
+ const tags = output.tags ?? [];
177
+ if (args.tags.length === 0)
178
+ return true;
179
+ const matches = args.tags.filter(tag => tags.includes(tag)).length;
180
+ return args.tagQueryMode === 'all' ? matches === args.tags.length : matches > 0;
181
+ })
182
+ .map(([outpoint, output]) => ({
183
+ satoshis: output.satoshis,
184
+ lockingScript: args.includeLockingScripts
185
+ ? (0, utilityHelpers_noBuffer_1.asString)((0, ActionBatchPlanner_1.plannerOutputLockingScript)(this.state, output))
186
+ : undefined,
187
+ spendable: true,
188
+ customInstructions: args.includeCustomInstructions ? output.customInstructions : undefined,
189
+ tags: args.includeTags ? output.tags ?? [] : undefined,
190
+ outpoint,
191
+ labels: args.includeLabels && output.txid != null ? stagedLabels.get(output.txid) ?? [] : undefined
192
+ }));
193
+ if (isBalance) {
194
+ const consumedConfirmed = [...this.state.reserved.entries()]
195
+ .filter(([outpoint]) => consumed.has(outpoint))
196
+ .reduce((sum, [, output]) => sum + output.satoshis, 0);
197
+ return {
198
+ totalOutputs: persisted.totalOutputs - consumedConfirmed + staged.reduce((sum, output) => sum + output.satoshis, 0),
199
+ outputs: []
200
+ };
201
+ }
202
+ const persistedUnconsumed = persisted.outputs.filter(output => !consumed.has(output.outpoint));
203
+ const outputs = [
204
+ ...persistedUnconsumed,
205
+ ...staged
206
+ ];
207
+ const ordered = args.offset < 0 ? [...outputs].reverse() : outputs;
208
+ const offset = args.offset < 0 ? Math.max(0, -args.offset - 1) : args.offset;
209
+ const result = {
210
+ totalOutputs: persisted.totalOutputs - (persisted.outputs.length - persistedUnconsumed.length) + staged.length,
211
+ outputs: ordered.slice(offset, offset + args.limit)
212
+ };
213
+ if (args.includeTransactions) {
214
+ const beef = new sdk_1.Beef();
215
+ if (persisted.BEEF != null)
216
+ beef.mergeBeef(persisted.BEEF);
217
+ beef.mergeBeef(this.state.sharedBeef);
218
+ for (const txid of args.knownTxids)
219
+ if (beef.findTxid(txid) != null)
220
+ beef.makeTxidOnly(txid);
221
+ result.BEEF = beef.toUint8Array();
222
+ }
223
+ return result;
224
+ }
225
+ mergeExtension(extension) {
226
+ for (const output of [...extension.reservedOutputs, ...extension.explicitOutputs]) {
227
+ if (output.sourceTransaction != null) {
228
+ this.state.sharedBeef.mergeRawTx(output.sourceTransaction);
229
+ }
230
+ }
231
+ (0, ActionBatchPlanner_1.addPlannerOutputs)(this.state.reserved, extension.reservedOutputs, this.state.begin.changeBasket.name);
232
+ (0, ActionBatchPlanner_1.addPlannerOutputs)(this.state.explicit, extension.explicitOutputs);
233
+ if (extension.inputBeef != null) {
234
+ this.state.sharedBeef.mergeBeef(extension.inputBeef);
235
+ }
236
+ }
237
+ missingExplicitOutpoints(args) {
238
+ return [...args.inputs.map(input => input.outpoint), ...args.options.noSendChange]
239
+ .filter(outpoint => {
240
+ const key = `${outpoint.txid}.${outpoint.vout}`;
241
+ return !this.state.staged.has(key) && !this.state.explicit.has(key) &&
242
+ !this.state.reserved.has(key) && (this.state.discardedStagedTxids.has(outpoint.txid) ||
243
+ this.state.sharedBeef.findTxid(outpoint.txid)?.tx == null);
244
+ });
245
+ }
246
+ async extend(targetSatoshis, requestedOutputs, explicitOutpoints, includeSourceTransactions) {
247
+ const extension = await this.wallet.storage.extendActionBatch({
248
+ batchId: this.batchId,
249
+ targetSatoshis: Math.max(1, Math.ceil(targetSatoshis)),
250
+ requestedOutputs: Math.max(1, Math.ceil(requestedOutputs)),
251
+ explicitOutpoints,
252
+ includeSourceTransactions
253
+ });
254
+ this.mergeExtension(extension);
255
+ this.expiresAt = Date.parse(extension.expiresAt);
256
+ return {
257
+ outputCount: extension.reservedOutputs.length,
258
+ satoshis: extension.reservedOutputs.reduce((sum, output) => sum + output.satoshis, 0)
259
+ };
260
+ }
261
+ async renewIfNeeded() {
262
+ if (Date.now() >= this.expiresAt)
263
+ return;
264
+ const renewAt = this.expiresAt - this.capabilities.leaseMs * 0.2;
265
+ if (Date.now() < renewAt)
266
+ return;
267
+ this.renewal ??= this.wallet.storage.renewActionBatch(this.batchId)
268
+ .then(result => { this.expiresAt = Date.parse(result.expiresAt); })
269
+ .finally(() => { this.renewal = undefined; });
270
+ await this.renewal;
271
+ }
272
+ async plan(args) {
273
+ if (this.committed)
274
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('action batch is already committed');
275
+ await this.renewIfNeeded();
276
+ if (this.extension != null)
277
+ await this.extension;
278
+ if (args.inputBEEF != null) {
279
+ this.state.sharedBeef.mergeBeef(args.inputBEEF);
280
+ }
281
+ const missing = this.missingExplicitOutpoints(args);
282
+ if (missing.length > 0)
283
+ await this.extend(1, 1, missing, args.isSignAction);
284
+ let planned;
285
+ let requestedOutputs = 8;
286
+ for (;;) {
287
+ try {
288
+ planned = await (0, ActionBatchPlanner_1.planAction)(this.state, args);
289
+ break;
290
+ }
291
+ catch (error) {
292
+ if (!(error instanceof WERR_errors_1.WERR_INSUFFICIENT_FUNDS))
293
+ throw error;
294
+ // generateChangeSdk has already credited every unconsumed reservation
295
+ // supplied by planFunding. Only the reported shortfall is additional;
296
+ // requesting totalSatoshisNeeded here double-counts the held pool.
297
+ const target = additionalFundingTarget(error);
298
+ const added = await this.extend(target, requestedOutputs, [], args.isSignAction);
299
+ if (added.outputCount === 0 || added.satoshis === 0)
300
+ throw error;
301
+ requestedOutputs = nextGeometricTarget(requestedOutputs);
302
+ }
303
+ }
304
+ if (planned == null)
305
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('unable to plan action batch transaction');
306
+ this.planned.set(planned.dcr.reference, { args, planned });
307
+ return planned.dcr;
308
+ }
309
+ updateEwma(plan) {
310
+ const confirmed = plan.planned.consumedOutpoints
311
+ .map(outpoint => this.state.reserved.get(outpoint))
312
+ .filter((output) => output != null);
313
+ if (confirmed.length === 0)
314
+ return;
315
+ const satoshis = confirmed.reduce((sum, output) => sum + output.satoshis, 0);
316
+ if (!this.hasConfirmedSample) {
317
+ this.ewmaConfirmedInputs = confirmed.length;
318
+ this.ewmaConfirmedSatoshis = satoshis;
319
+ this.hasConfirmedSample = true;
320
+ return;
321
+ }
322
+ this.ewmaConfirmedInputs = 0.5 * confirmed.length + 0.5 * this.ewmaConfirmedInputs;
323
+ this.ewmaConfirmedSatoshis = 0.5 * satoshis + 0.5 * this.ewmaConfirmedSatoshis;
324
+ }
325
+ stage(prior, processArgs) {
326
+ if (this.actions.some(action => action.reference === prior.reference))
327
+ return;
328
+ const pending = this.planned.get(prior.reference);
329
+ if (pending == null)
330
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('signed action does not belong to active action batch');
331
+ const txid = prior.tx.id('hex');
332
+ const lockingScriptDigests = prior.dcr.outputs.map(output => {
333
+ if (output.lockingScript.length === 0)
334
+ return undefined;
335
+ const digest = (0, actionBatchDigest_1.actionBatchBlobDigest)((0, utilityHelpers_noBuffer_1.asArray)(output.lockingScript));
336
+ this.lockingScripts.set(digest, output.lockingScript);
337
+ return digest;
338
+ });
339
+ const compactPlan = {
340
+ ...prior.dcr,
341
+ inputBeef: undefined,
342
+ inputs: prior.dcr.inputs.map(input => ({ ...input, sourceTransaction: undefined })),
343
+ outputs: prior.dcr.outputs.map(output => ({ ...output, lockingScript: '' }))
344
+ };
345
+ const action = {
346
+ reference: prior.reference,
347
+ txid,
348
+ lockingScriptDigests,
349
+ plan: compactPlan,
350
+ metadata: {
351
+ description: pending.args.description,
352
+ labels: pending.args.labels,
353
+ isNoSend: processArgs.isNoSend,
354
+ isDelayed: processArgs.isDelayed,
355
+ inputs: pending.args.inputs,
356
+ outputs: pending.args.outputs.map(output => ({ ...output, lockingScript: '' }))
357
+ },
358
+ commissionKeyOffset: pending.planned.commissionKeyOffset
359
+ };
360
+ this.actions.push(action);
361
+ this.updateEwma(pending);
362
+ (0, ActionBatchPlanner_1.stageTransactionOutputs)(this.state, prior.tx, prior.dcr);
363
+ (0, ActionBatchPlanner_1.mergePlannerBeef)(this.state, prior.tx);
364
+ this.planned.delete(prior.reference);
365
+ this.scheduleExtensionIfNeeded();
366
+ }
367
+ scheduleExtensionIfNeeded() {
368
+ if (this.extension != null)
369
+ return;
370
+ const availableConfirmed = [...this.state.reserved.entries()]
371
+ .filter(([outpoint]) => !this.state.consumed.has(outpoint));
372
+ const request = fundingRunwayExtension(this.runwayTarget, this.ewmaConfirmedInputs, this.ewmaConfirmedSatoshis, availableConfirmed.map(([, output]) => output));
373
+ if (request == null)
374
+ return;
375
+ this.extension = this.extend(request.targetSatoshis, request.requestedOutputs, [], false)
376
+ .then(added => {
377
+ // A failed or partial extension must not geometrically inflate the
378
+ // next request against a runway that the wallet never acquired.
379
+ if (added.outputCount >= request.requestedOutputs &&
380
+ added.satoshis >= request.targetSatoshis) {
381
+ this.runwayTarget = request.nextRunwayTarget;
382
+ }
383
+ })
384
+ .catch(() => { })
385
+ .finally(() => { this.extension = undefined; });
386
+ }
387
+ buildManifest(sendWith, isDelayed) {
388
+ const stagedTxids = new Set(this.actions.map(action => action.txid));
389
+ const externalTxids = this.actions.flatMap(action => action.plan.inputs)
390
+ .map(input => input.sourceTxid)
391
+ .filter(txid => !stagedTxids.has(txid));
392
+ const dependencyBytes = (0, utilityHelpers_noBuffer_1.asArray)((0, beefForTxids_1.beefForTxids)(this.state.sharedBeef, externalTxids).toUint8Array());
393
+ const rawBytes = this.actions.map(action => {
394
+ const tx = this.state.sharedBeef.findTxid(action.txid)?.tx;
395
+ if (tx == null)
396
+ throw new WERR_errors_1.WERR_INVALID_OPERATION(`missing staged transaction ${action.txid}`);
397
+ return (0, utilityHelpers_noBuffer_1.asArray)(tx.toUint8Array());
398
+ });
399
+ const blobs = new Map();
400
+ const addBlob = (bytes) => {
401
+ const digest = (0, actionBatchDigest_1.actionBatchBlobDigest)(bytes);
402
+ if (!blobs.has(digest))
403
+ blobs.set(digest, bytes);
404
+ return digest;
405
+ };
406
+ const actions = this.actions.map((action, index) => {
407
+ return { ...action, rawTx: undefined, rawTxDigest: addBlob(rawBytes[index]) };
408
+ });
409
+ const dependencyBeefDigest = addBlob(dependencyBytes);
410
+ for (const [digest, script] of this.lockingScripts) {
411
+ if (!blobs.has(digest))
412
+ blobs.set(digest, (0, utilityHelpers_noBuffer_1.asArray)(script));
413
+ }
414
+ const totalBytes = [...blobs.values()].reduce((sum, bytes) => sum + bytes.length, 0);
415
+ const useUploads = totalBytes > this.capabilities.maxInlineBytes;
416
+ const uploadBlobs = new Map();
417
+ const blobChunks = {};
418
+ if (useUploads) {
419
+ const chunkBytes = Math.max(1, this.capabilities.maxBlobBytes);
420
+ for (const [digest, bytes] of blobs) {
421
+ if (bytes.length <= chunkBytes) {
422
+ uploadBlobs.set(digest, bytes);
423
+ continue;
424
+ }
425
+ const chunks = [];
426
+ for (let offset = 0; offset < bytes.length; offset += chunkBytes) {
427
+ const chunk = bytes.slice(offset, Math.min(bytes.length, offset + chunkBytes));
428
+ const chunkDigest = (0, actionBatchDigest_1.actionBatchBlobDigest)(chunk);
429
+ chunks.push(chunkDigest);
430
+ if (!uploadBlobs.has(chunkDigest))
431
+ uploadBlobs.set(chunkDigest, chunk);
432
+ }
433
+ blobChunks[digest] = chunks;
434
+ }
435
+ }
436
+ const withoutDigest = {
437
+ batchId: this.batchId,
438
+ actions,
439
+ dependencyBeefDigest,
440
+ inlineBlobs: useUploads
441
+ ? undefined
442
+ : Object.fromEntries([...blobs].map(([digest, bytes]) => [digest, Uint8Array.from(bytes)])),
443
+ blobChunks: Object.keys(blobChunks).length === 0 ? undefined : blobChunks,
444
+ sendWith: mergeUnique(sendWith),
445
+ isDelayed
446
+ };
447
+ return {
448
+ manifest: { ...withoutDigest, digest: (0, actionBatchDigest_1.actionBatchManifestDigest)(withoutDigest) },
449
+ uploads: [...uploadBlobs].map(([digest, bytes]) => ({ digest, bytes }))
450
+ };
451
+ }
452
+ async uploadMissing(manifest, uploads) {
453
+ const prepared = await this.wallet.storage.prepareActionBatchCommit(manifest);
454
+ const missing = new Set(prepared.missingDigests);
455
+ const pending = uploads.filter(upload => missing.has(upload.digest));
456
+ const concurrency = Math.max(1, Math.min(prepared.maxConcurrentUploads, this.capabilities.maxConcurrentUploads, pending.length));
457
+ let cursor = 0;
458
+ await Promise.all(Array.from({ length: concurrency }, async () => {
459
+ for (;;) {
460
+ const index = cursor++;
461
+ if (index >= pending.length)
462
+ return;
463
+ const upload = pending[index];
464
+ if (upload.bytes.length > prepared.maxBlobBytes) {
465
+ throw new WERR_errors_1.WERR_INVALID_OPERATION(`action batch blob ${upload.digest} exceeds provider limit`);
466
+ }
467
+ await this.wallet.storage.putActionBatchBlob({
468
+ batchId: this.batchId,
469
+ digest: upload.digest,
470
+ bytes: upload.bytes
471
+ });
472
+ }
473
+ }));
474
+ }
475
+ async commit(sendWith, isDelayed) {
476
+ if (this.extension != null)
477
+ await this.extension;
478
+ await this.renewIfNeeded();
479
+ if (this.planned.size > 0)
480
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('all two-step actions must be signed before batch commit');
481
+ const { manifest, uploads } = this.buildManifest(sendWith, isDelayed);
482
+ if (uploads.length > 0)
483
+ await this.uploadMissing(manifest, uploads);
484
+ const result = await this.wallet.storage.commitActionBatch(manifest);
485
+ this.committed = true;
486
+ return result;
487
+ }
488
+ abortAction(referenceOrTxid) {
489
+ let pendingReference;
490
+ if (this.planned.has(referenceOrTxid)) {
491
+ pendingReference = referenceOrTxid;
492
+ }
493
+ else {
494
+ pendingReference = Object.entries(this.wallet.pendingSignActions)
495
+ .find(([reference, pending]) => this.planned.has(reference) && pending.tx.id('hex') === referenceOrTxid)?.[0];
496
+ }
497
+ const actionIndex = this.actions.findIndex(action => action.reference === referenceOrTxid || action.txid === referenceOrTxid);
498
+ const action = actionIndex < 0 ? undefined : this.actions[actionIndex];
499
+ const targetTxid = action?.txid ??
500
+ (pendingReference == null ? undefined : this.wallet.pendingSignActions[pendingReference]?.tx.id('hex'));
501
+ if (targetTxid != null && this.hasDependentAction(targetTxid, action?.reference ?? pendingReference)) {
502
+ throw new WERR_errors_1.WERR_INVALID_OPERATION(`cannot abort action ${referenceOrTxid} while another staged action depends on it`);
503
+ }
504
+ if (action != null) {
505
+ this.actions.splice(actionIndex, 1);
506
+ }
507
+ else if (pendingReference != null) {
508
+ this.planned.delete(pendingReference);
509
+ Reflect.deleteProperty(this.wallet.pendingSignActions, pendingReference);
510
+ }
511
+ else {
512
+ return false;
513
+ }
514
+ if (targetTxid != null)
515
+ this.state.discardedStagedTxids.add(targetTxid);
516
+ this.rebuildStagedState();
517
+ return true;
518
+ }
519
+ hasDependentAction(txid, excludedReference) {
520
+ if (this.actions.some(action => action.reference !== excludedReference &&
521
+ action.plan.inputs.some(input => input.sourceTxid === txid)))
522
+ return true;
523
+ return [...this.planned.entries()].some(([reference, pending]) => reference !== excludedReference &&
524
+ pending.planned.dcr.inputs.some(input => input.sourceTxid === txid));
525
+ }
526
+ rebuildStagedState() {
527
+ this.state.consumed.clear();
528
+ this.state.staged.clear();
529
+ this.state.estimatedChangeCount = this.state.begin.availableChangeCount;
530
+ this.lockingScripts.clear();
531
+ for (const action of this.actions) {
532
+ const tx = this.state.sharedBeef.findTxid(action.txid)?.tx;
533
+ if (tx == null)
534
+ throw new WERR_errors_1.WERR_INVALID_OPERATION(`missing staged transaction ${action.txid}`);
535
+ for (const input of action.plan.inputs) {
536
+ this.state.consumed.add(`${input.sourceTxid}.${input.sourceVout}`);
537
+ }
538
+ const managedInputCount = action.plan.inputs.length - action.metadata.inputs.length;
539
+ const changeOutputCount = action.plan.outputs.filter(output => output.purpose === 'change').length;
540
+ this.state.estimatedChangeCount += changeOutputCount - managedInputCount;
541
+ (0, ActionBatchPlanner_1.stageTransactionOutputs)(this.state, tx, action.plan);
542
+ const lockingScriptDigests = action.lockingScriptDigests ?? [];
543
+ lockingScriptDigests.forEach((digest, outputIndex) => {
544
+ if (digest == null)
545
+ return;
546
+ const vout = action.plan.outputs[outputIndex]?.vout;
547
+ const script = vout == null ? undefined : tx.outputs[vout]?.lockingScript;
548
+ if (script == null)
549
+ throw new WERR_errors_1.WERR_INVALID_OPERATION(`missing staged output script for ${action.txid}.${vout}`);
550
+ this.lockingScripts.set(digest, script.toHex());
551
+ });
552
+ }
553
+ for (const pending of this.planned.values()) {
554
+ for (const outpoint of pending.planned.consumedOutpoints)
555
+ this.state.consumed.add(outpoint);
556
+ const managedInputCount = pending.planned.dcr.inputs.length - pending.args.inputs.length;
557
+ const changeOutputCount = pending.planned.dcr.outputs.filter(output => output.purpose === 'change').length;
558
+ this.state.estimatedChangeCount += changeOutputCount - managedInputCount;
559
+ }
560
+ }
561
+ async abort() {
562
+ if (this.committed)
563
+ return;
564
+ await this.wallet.storage.abortActionBatch(this.batchId);
565
+ for (const reference of this.planned.keys())
566
+ delete this.wallet.pendingSignActions[reference];
567
+ }
568
+ }
569
+ class ActionBatchController {
570
+ wallet;
571
+ mode;
572
+ workspace;
573
+ capabilities = new Map();
574
+ serial = Promise.resolve();
575
+ constructor(wallet, mode) {
576
+ this.wallet = wallet;
577
+ this.mode = mode;
578
+ }
579
+ get hasWorkspace() { return this.workspace != null; }
580
+ overlayListActions(persisted, args) {
581
+ return this.workspace?.overlayListActions(persisted, args) ?? persisted;
582
+ }
583
+ overlayListOutputs(persisted, args) {
584
+ return this.workspace?.overlayListOutputs(persisted, args) ?? persisted;
585
+ }
586
+ async runExclusive(operation) {
587
+ const run = this.serial.then(operation);
588
+ this.serial = run.then(() => { }, () => { });
589
+ return await run;
590
+ }
591
+ async negotiate() {
592
+ if (this.mode === 'legacy')
593
+ return undefined;
594
+ const activeStore = this.wallet.storage.getActiveStore();
595
+ let capabilities = this.capabilities.get(activeStore);
596
+ if (capabilities == null) {
597
+ capabilities = this.wallet.storage.getCapabilities()
598
+ .then(result => result.actionBatch?.version === 1 ? result.actionBatch : undefined)
599
+ .catch(() => undefined);
600
+ this.capabilities.set(activeStore, capabilities);
601
+ }
602
+ return await capabilities;
603
+ }
604
+ async begin(args) {
605
+ if (args.inputs.length > 8)
606
+ return undefined;
607
+ const capabilities = await this.negotiate();
608
+ if (capabilities == null)
609
+ return undefined;
610
+ const batchId = (0, utilityHelpers_1.randomBytesBase64)(24);
611
+ const begin = await this.wallet.storage.beginActionBatch({
612
+ batchId,
613
+ firstAction: { ...args, logger: undefined }
614
+ });
615
+ this.workspace = new ActionBatchWorkspace(this.wallet, begin, args, capabilities);
616
+ return this.workspace;
617
+ }
618
+ async plan(args) {
619
+ return await this.runExclusive(async () => {
620
+ if (!args.isNewTx)
621
+ return undefined;
622
+ let workspace = this.workspace;
623
+ let beganWorkspace = false;
624
+ if (workspace == null) {
625
+ if (!args.isNoSend)
626
+ return undefined;
627
+ workspace = await this.begin(args);
628
+ if (workspace == null)
629
+ return undefined;
630
+ beganWorkspace = true;
631
+ }
632
+ try {
633
+ return await workspace.plan(args);
634
+ }
635
+ catch (error) {
636
+ if (beganWorkspace) {
637
+ await workspace.abort();
638
+ this.workspace = undefined;
639
+ }
640
+ throw error;
641
+ }
642
+ });
643
+ }
644
+ async process(prior, args) {
645
+ return await this.runExclusive(async () => {
646
+ const workspace = this.workspace;
647
+ if (workspace == null)
648
+ return undefined;
649
+ if (prior != null && !workspace.ownsReference(prior.reference))
650
+ return undefined;
651
+ if (prior != null)
652
+ workspace.stage(prior, args);
653
+ const shouldCommit = args.isSendWith || (prior != null && !args.isNoSend);
654
+ if (!shouldCommit)
655
+ return { sendWithResults: [] };
656
+ const sendWith = [...args.options.sendWith];
657
+ if (prior != null && !args.isNoSend && !sendWith.includes(prior.tx.id('hex')))
658
+ sendWith.push(prior.tx.id('hex'));
659
+ const result = await workspace.commit(sendWith, args.isDelayed);
660
+ this.workspace = undefined;
661
+ return result;
662
+ });
663
+ }
664
+ ownsReference(reference) {
665
+ return this.workspace?.ownsReference(reference) ?? false;
666
+ }
667
+ async abort() {
668
+ return await this.runExclusive(async () => {
669
+ if (this.workspace == null)
670
+ return false;
671
+ await this.workspace.abort();
672
+ this.workspace = undefined;
673
+ return true;
674
+ });
675
+ }
676
+ async abortAction(referenceOrTxid) {
677
+ return await this.runExclusive(async () => {
678
+ const workspace = this.workspace;
679
+ if (workspace == null)
680
+ return false;
681
+ const aborted = workspace.abortAction(referenceOrTxid);
682
+ if (!aborted)
683
+ return false;
684
+ if (workspace.isEmpty) {
685
+ await workspace.abort();
686
+ this.workspace = undefined;
687
+ }
688
+ return true;
689
+ });
690
+ }
691
+ }
692
+ exports.ActionBatchController = ActionBatchController;
693
+ //# sourceMappingURL=ActionBatchWorkspace.js.map