@bsv/wallet-toolbox 2.4.21 → 2.5.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 (83) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +56 -1
  3. package/docs/authentication-observability.md +21 -9
  4. package/out/src/CWIStyleWalletManager.d.ts +31 -3
  5. package/out/src/CWIStyleWalletManager.d.ts.map +1 -1
  6. package/out/src/CWIStyleWalletManager.js +164 -56
  7. package/out/src/CWIStyleWalletManager.js.map +1 -1
  8. package/out/src/Wallet.d.ts +3 -3
  9. package/out/src/Wallet.d.ts.map +1 -1
  10. package/out/src/Wallet.js +45 -6
  11. package/out/src/Wallet.js.map +1 -1
  12. package/out/src/sdk/WalletStorage.interfaces.d.ts +1 -0
  13. package/out/src/sdk/WalletStorage.interfaces.d.ts.map +1 -1
  14. package/out/src/signer/methods/completeSignedTransaction.d.ts.map +1 -1
  15. package/out/src/signer/methods/completeSignedTransaction.js +54 -13
  16. package/out/src/signer/methods/completeSignedTransaction.js.map +1 -1
  17. package/out/src/signer/methods/createAction.d.ts.map +1 -1
  18. package/out/src/signer/methods/createAction.js +14 -10
  19. package/out/src/signer/methods/createAction.js.map +1 -1
  20. package/out/src/signer/methods/verifyUnlockScripts.d.ts.map +1 -1
  21. package/out/src/signer/methods/verifyUnlockScripts.js +257 -21
  22. package/out/src/signer/methods/verifyUnlockScripts.js.map +1 -1
  23. package/out/src/storage/StorageIdb.d.ts +14 -1
  24. package/out/src/storage/StorageIdb.d.ts.map +1 -1
  25. package/out/src/storage/StorageIdb.js +128 -17
  26. package/out/src/storage/StorageIdb.js.map +1 -1
  27. package/out/src/storage/StorageKnex.d.ts +11 -2
  28. package/out/src/storage/StorageKnex.d.ts.map +1 -1
  29. package/out/src/storage/StorageKnex.js +196 -5
  30. package/out/src/storage/StorageKnex.js.map +1 -1
  31. package/out/src/storage/StorageProvider.d.ts +30 -1
  32. package/out/src/storage/StorageProvider.d.ts.map +1 -1
  33. package/out/src/storage/StorageProvider.js +94 -2
  34. package/out/src/storage/StorageProvider.js.map +1 -1
  35. package/out/src/storage/idbHelpers.d.ts.map +1 -1
  36. package/out/src/storage/idbHelpers.js +2 -0
  37. package/out/src/storage/idbHelpers.js.map +1 -1
  38. package/out/src/storage/methods/actionBatch.d.ts.map +1 -1
  39. package/out/src/storage/methods/actionBatch.js +2 -14
  40. package/out/src/storage/methods/actionBatch.js.map +1 -1
  41. package/out/src/storage/methods/actionPlanning.d.ts +14 -0
  42. package/out/src/storage/methods/actionPlanning.d.ts.map +1 -1
  43. package/out/src/storage/methods/actionPlanning.js +55 -0
  44. package/out/src/storage/methods/actionPlanning.js.map +1 -1
  45. package/out/src/storage/methods/availableManagedChange.d.ts +11 -0
  46. package/out/src/storage/methods/availableManagedChange.d.ts.map +1 -0
  47. package/out/src/storage/methods/availableManagedChange.js +25 -0
  48. package/out/src/storage/methods/availableManagedChange.js.map +1 -0
  49. package/out/src/storage/methods/createAction.d.ts.map +1 -1
  50. package/out/src/storage/methods/createAction.js +521 -173
  51. package/out/src/storage/methods/createAction.js.map +1 -1
  52. package/out/src/storage/methods/generateChange.d.ts.map +1 -1
  53. package/out/src/storage/methods/generateChange.js +29 -14
  54. package/out/src/storage/methods/generateChange.js.map +1 -1
  55. package/out/src/storage/methods/getBeefForTransaction.d.ts +7 -0
  56. package/out/src/storage/methods/getBeefForTransaction.d.ts.map +1 -1
  57. package/out/src/storage/methods/getBeefForTransaction.js +219 -20
  58. package/out/src/storage/methods/getBeefForTransaction.js.map +1 -1
  59. package/out/src/storage/methods/processAction.d.ts.map +1 -1
  60. package/out/src/storage/methods/processAction.js +36 -15
  61. package/out/src/storage/methods/processAction.js.map +1 -1
  62. package/out/src/storage/remoting/KnexSessionManager.d.ts +20 -0
  63. package/out/src/storage/remoting/KnexSessionManager.d.ts.map +1 -1
  64. package/out/src/storage/remoting/KnexSessionManager.js +74 -16
  65. package/out/src/storage/remoting/KnexSessionManager.js.map +1 -1
  66. package/out/src/storage/schema/KnexMigrations.d.ts +1 -0
  67. package/out/src/storage/schema/KnexMigrations.d.ts.map +1 -1
  68. package/out/src/storage/schema/KnexMigrations.js +39 -1
  69. package/out/src/storage/schema/KnexMigrations.js.map +1 -1
  70. package/out/src/storage/schema/StorageIdbSchema.d.ts +2 -0
  71. package/out/src/storage/schema/StorageIdbSchema.d.ts.map +1 -1
  72. package/out/src/storage/schema/entities/EntityProvenTx.d.ts +2 -1
  73. package/out/src/storage/schema/entities/EntityProvenTx.d.ts.map +1 -1
  74. package/out/src/storage/schema/entities/EntityProvenTx.js +8 -3
  75. package/out/src/storage/schema/entities/EntityProvenTx.js.map +1 -1
  76. package/out/src/storage/storageProviderHelpers.d.ts.map +1 -1
  77. package/out/src/storage/storageProviderHelpers.js +8 -2
  78. package/out/src/storage/storageProviderHelpers.js.map +1 -1
  79. package/out/src/utility/ScriptTemplateBRC29.d.ts +3 -2
  80. package/out/src/utility/ScriptTemplateBRC29.d.ts.map +1 -1
  81. package/out/src/utility/ScriptTemplateBRC29.js +13 -5
  82. package/out/src/utility/ScriptTemplateBRC29.js.map +1 -1
  83. package/package.json +5 -2
@@ -11,7 +11,6 @@ const utilityHelpers_noBuffer_1 = require("../../utility/utilityHelpers.noBuffer
11
11
  const EntityProvenTx_1 = require("../schema/entities/EntityProvenTx");
12
12
  const Wallet_1 = require("../../Wallet");
13
13
  const offsetKey_1 = require("./offsetKey");
14
- const utils_1 = require("./utils");
15
14
  const sdk_2 = require("../../sdk");
16
15
  const managedChange_1 = require("./managedChange");
17
16
  const actionPlanning_1 = require("./actionPlanning");
@@ -20,6 +19,31 @@ function setDisableDoubleSpendCheckForTest(v) {
20
19
  disableDoubleSpendCheckForTest = v;
21
20
  }
22
21
  async function createAction(storage, auth, vargs, _originator) {
22
+ if (!storage.telemetry.enabled)
23
+ return await createActionCore(storage, auth, vargs);
24
+ return await storage.telemetry.withSpan('wallet.storage.create_action', {
25
+ component: 'wallet-storage',
26
+ carrier: vargs,
27
+ attributes: {
28
+ 'action.fixed_input_count': vargs.inputs.length,
29
+ 'action.fixed_output_count': vargs.outputs.length,
30
+ 'action.known_txid_count': vargs.options.knownTxids?.length ?? 0,
31
+ 'action.is_delayed': vargs.isDelayed,
32
+ 'action.is_no_send': vargs.isNoSend
33
+ }
34
+ }, async (span) => {
35
+ const result = await createActionCore(storage, auth, vargs, span);
36
+ span.end({
37
+ attributes: {
38
+ 'action.result_input_count': result.inputs.length,
39
+ 'action.result_output_count': result.outputs.length,
40
+ 'action.input_beef_bytes': result.inputBeef?.length ?? 0
41
+ }
42
+ });
43
+ return result;
44
+ });
45
+ }
46
+ async function createActionCore(storage, auth, vargs, parent) {
23
47
  const logger = vargs.logger;
24
48
  logger?.group('storage createAction');
25
49
  // stampLog(vargs, `start storage createTransactionSdk`)
@@ -44,60 +68,111 @@ async function createAction(storage, auth, vargs, _originator) {
44
68
  * - Create result inputs with source locking scripts
45
69
  * - Create result outputs with new locking scripts.
46
70
  * - Create and return result.
47
- */
71
+ */
48
72
  const userId = auth.userId;
49
- const { storageBeef, beef, xinputs } = await validateRequiredInputs(storage, userId, vargs);
50
- logger?.log('validated required inputs');
51
- const xoutputs = validateRequiredOutputs(storage, userId, vargs);
52
- logger?.log('validated required outputs');
53
- const changeBasketName = 'default';
54
- const changeBasket = (0, utilityHelpers_1.verifyOne)(await storage.findOutputBaskets({
55
- partial: { userId, name: changeBasketName }
56
- }), `Invalid outputGeneration basket "${changeBasketName}"`);
57
- logger?.log('found change basket');
58
- const noSendChangeIn = await validateNoSendChange(storage, userId, vargs, changeBasket);
59
- logger?.log('validated noSendChange');
60
- const availableChangeCount = await storage.countChangeInputs(userId, changeBasket.basketId, !vargs.isDelayed);
61
- logger?.log(`counted change inputs ${availableChangeCount}`);
73
+ const validated = await traceStorageStep(storage, 'wallet.storage.create_action.validate', parent, {
74
+ 'action.fixed_input_count': vargs.inputs.length,
75
+ 'action.fixed_output_count': vargs.outputs.length
76
+ }, async (span) => {
77
+ const requiredInputs = await validateRequiredInputs(storage, userId, vargs);
78
+ logger?.log('validated required inputs');
79
+ const xoutputs = validateRequiredOutputs(storage, userId, vargs);
80
+ logger?.log('validated required outputs');
81
+ const changeBasketName = 'default';
82
+ const changeBasket = (0, utilityHelpers_1.verifyOne)(await storage.findOutputBaskets({
83
+ partial: { userId, name: changeBasketName }
84
+ }), `Invalid outputGeneration basket "${changeBasketName}"`);
85
+ logger?.log('found change basket');
86
+ const noSendChangeIn = await validateNoSendChange(storage, userId, vargs, changeBasket);
87
+ logger?.log('validated noSendChange');
88
+ span?.end({
89
+ attributes: {
90
+ 'action.validated_input_count': requiredInputs.xinputs.length,
91
+ 'action.validated_output_count': xoutputs.length,
92
+ 'action.no_send_change_input_count': noSendChangeIn.length,
93
+ 'action.validated_beef_tx_count': requiredInputs.beef.txs.length
94
+ }
95
+ });
96
+ return { ...requiredInputs, xoutputs, changeBasket, noSendChangeIn };
97
+ });
98
+ const { storageBeef, beef, xinputs, xoutputs, changeBasket, noSendChangeIn } = validated;
62
99
  const feeModel = (0, StorageProvider_1.validateStorageFeeModel)(storage.feeModel);
63
100
  logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
64
- await preflightInsufficientFundsFastPath(vargs, xinputs, xoutputs, noSendChangeIn, availableChangeCount, feeModel);
65
- logger?.log('passed insufficient-funds preflight');
101
+ const initialFundingPlan = await prepareFundingPlan(storage, [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent]);
102
+ logger?.log(`planned funding from ${initialFundingPlan.availableChangeCount} change inputs`);
103
+ // The selected source txids are known before the write transaction begins.
104
+ // Start their one batched proof read now so PXC/network wait and BEEF
105
+ // assembly overlap the independent transaction-record/funding/output writes.
106
+ // A funding-claim retry is handled below by fetching only any residual roots.
107
+ const allocatedBeefPrefetch = startAllocatedChangeBeefPrefetch(storage, vargs, initialFundingPlan.selected, beef, parent);
108
+ const storageBeefBytes = storageBeef.toBinary();
66
109
  let newTx;
110
+ let newTxCommitted = false;
67
111
  try {
68
- newTx = await createNewTxRecord(storage, userId, vargs, storageBeef);
69
- logger?.log('created new transaction record');
70
- const ctx = {
71
- xinputs,
72
- xoutputs,
73
- changeBasket,
74
- noSendChangeIn,
75
- availableChangeCount,
76
- feeModel,
77
- transactionId: newTx.transactionId
78
- };
79
- const { allocatedChange, changeOutputs, derivationPrefix, maxPossibleSatoshisAdjustment } = await fundNewTransactionSdk(storage, userId, vargs, ctx);
80
- logger?.log('funded new transaction');
81
- if (maxPossibleSatoshisAdjustment != null) {
82
- const a = maxPossibleSatoshisAdjustment;
83
- if (ctx.xoutputs[a.fixedOutputIndex].satoshis !== generateChange_1.maxPossibleSatoshis)
84
- throw new WERR_errors_1.WERR_INTERNAL();
85
- ctx.xoutputs[a.fixedOutputIndex].satoshis = a.satoshis;
86
- logger?.log('adjusted change outputs to max possible');
87
- }
88
- // The satoshis of the transaction is the satoshis we get back in change minus the satoshis we spend.
89
- const satoshis = changeOutputs.reduce((a, e) => a + e.satoshis, 0) - allocatedChange.reduce((a, e) => a + e.satoshis, 0);
90
- await storage.updateTransaction(newTx.transactionId, { satoshis });
91
- const { outputs, changeVouts } = await createNewOutputs(storage, userId, vargs, ctx, changeOutputs);
112
+ const persisted = await storage.transaction(async (trx) => {
113
+ const initialSatoshis = fundingPlanSatoshis(initialFundingPlan);
114
+ newTx = await traceStorageStep(storage, 'wallet.storage.create_action.create_record', parent, {
115
+ 'action.label_count': vargs.labels.length,
116
+ 'action.storage_beef_bytes': storageBeefBytes.length
117
+ }, async (span) => {
118
+ const transaction = await createNewTxRecord(storage, userId, vargs, storageBeefBytes, initialSatoshis, trx);
119
+ span?.end({ attributes: { 'action.transaction_record_created': true } });
120
+ return transaction;
121
+ });
122
+ logger?.log('created new transaction record');
123
+ const ctx = {
124
+ xinputs,
125
+ xoutputs,
126
+ changeBasket,
127
+ noSendChangeIn,
128
+ feeModel,
129
+ transactionId: newTx.transactionId
130
+ };
131
+ const funded = await fundNewTransactionSdk(storage, userId, vargs, ctx, initialFundingPlan, parent, trx);
132
+ logger?.log('funded new transaction');
133
+ if (funded.maxPossibleSatoshisAdjustment != null) {
134
+ const adjustment = funded.maxPossibleSatoshisAdjustment;
135
+ if (ctx.xoutputs[adjustment.fixedOutputIndex].satoshis !== generateChange_1.maxPossibleSatoshis)
136
+ throw new WERR_errors_1.WERR_INTERNAL();
137
+ ctx.xoutputs[adjustment.fixedOutputIndex].satoshis = adjustment.satoshis;
138
+ logger?.log('adjusted change outputs to max possible');
139
+ }
140
+ const satoshis = funded.changeOutputs.reduce((sum, output) => sum + output.satoshis, 0) -
141
+ funded.allocatedChange.reduce((sum, output) => sum + output.satoshis, 0);
142
+ if (satoshis !== initialSatoshis) {
143
+ await storage.updateTransaction(newTx.transactionId, { satoshis }, trx);
144
+ newTx.satoshis = satoshis;
145
+ }
146
+ const storedOutputs = await traceStorageStep(storage, 'wallet.storage.create_action.persist_outputs', parent, {
147
+ 'action.fixed_output_count': ctx.xoutputs.length,
148
+ 'action.change_output_count': funded.changeOutputs.length
149
+ }, async (span) => {
150
+ const result = await createNewOutputs(storage, userId, vargs, ctx, funded.changeOutputs, trx);
151
+ span?.end({ attributes: { 'action.persisted_output_count': result.outputs.length } });
152
+ return result;
153
+ });
154
+ return { ...funded, ...storedOutputs, ctx };
155
+ });
156
+ newTxCommitted = true;
157
+ const committedTx = (0, utilityHelpers_1.verifyTruthy)(newTx);
158
+ const { allocatedChange, derivationPrefix, outputs, changeVouts, ctx } = persisted;
92
159
  logger?.log('created new output records');
93
- const inputBeef = await mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange, beef);
160
+ const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, allocatedBeefPrefetch, parent);
94
161
  logger?.log('merged allocated change beefs');
95
- const inputs = await createNewInputs(storage, userId, vargs, ctx, allocatedChange);
162
+ const inputs = await traceStorageStep(storage, 'wallet.storage.create_action.assemble_inputs', parent, {
163
+ 'action.fixed_input_count': ctx.xinputs.length,
164
+ 'action.funding_input_count': allocatedChange.length,
165
+ 'action.include_source_transactions': vargs.includeAllSourceTransactions
166
+ }, async (span) => {
167
+ const assembled = await createNewInputs(storage, userId, vargs, ctx, allocatedChange);
168
+ span?.end({ attributes: { 'action.result_input_count': assembled.length } });
169
+ return assembled;
170
+ });
96
171
  logger?.log('created new inputs');
97
172
  const r = {
98
- reference: newTx.reference,
99
- version: newTx.version,
100
- lockTime: newTx.lockTime,
173
+ reference: committedTx.reference,
174
+ version: committedTx.version,
175
+ lockTime: committedTx.lockTime,
101
176
  inputs,
102
177
  outputs,
103
178
  derivationPrefix,
@@ -108,10 +183,20 @@ async function createAction(storage, auth, vargs, _originator) {
108
183
  return r;
109
184
  }
110
185
  catch (error) {
186
+ // Always let an overlapped database read settle before cleanup destroys or
187
+ // reuses the provider. Its error is surfaced on the success path above;
188
+ // here the construction error remains authoritative.
189
+ await allocatedBeefPrefetch;
111
190
  if (newTx?.transactionId != null) {
112
191
  try {
113
- await storage.updateTransactionStatus('failed', newTx.transactionId);
114
- logger?.log(`marked failed createAction transaction ${newTx.transactionId} after construction error`);
192
+ if (newTxCommitted) {
193
+ await storage.updateTransactionStatus('failed', newTx.transactionId);
194
+ logger?.log(`marked failed createAction transaction ${newTx.transactionId} after construction error`);
195
+ }
196
+ else {
197
+ const failed = await createNewTxRecord(storage, userId, vargs, storageBeefBytes, 0, undefined, 'failed');
198
+ logger?.log(`recorded failed createAction transaction ${failed.transactionId} after rollback`);
199
+ }
115
200
  }
116
201
  catch (cleanupError) {
117
202
  logger?.log(`failed to clean up createAction transaction ${newTx.transactionId}: ${String(cleanupError)}`);
@@ -261,21 +346,9 @@ async function getCompetingBeefForReview(storage, txid) {
261
346
  throw e;
262
347
  }
263
348
  }
264
- /** Randomly reassign vout values across newOutputs using either the provided randomVals or crypto-random bytes. */
265
- /** Insert the output and attach its tags; return the SDK output descriptor. */
266
- async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
267
- o.outputId = await storage.insertOutput(o);
349
+ /** Build the SDK descriptor for a persisted output. */
350
+ function describeNewOutput(o, tags, txBaskets) {
268
351
  const changeVout = o.change && o.purpose === 'change' && o.providedBy === 'storage' ? o.vout : undefined;
269
- for (const tagName of new Set(tags)) {
270
- const tag = txTags[tagName];
271
- await storage.insertOutputTagMap({
272
- outputId: (0, utilityHelpers_1.verifyId)(o.outputId),
273
- outputTagId: (0, utilityHelpers_1.verifyId)(tag.outputTagId),
274
- created_at: new Date(),
275
- updated_at: new Date(),
276
- isDeleted: false
277
- });
278
- }
279
352
  const ro = {
280
353
  vout: (0, utilityHelpers_1.verifyInteger)(o.vout),
281
354
  satoshis: sdk_1.Validation.validateSatoshis(o.satoshis, 'o.satoshis'),
@@ -290,13 +363,28 @@ async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
290
363
  };
291
364
  return { changeVout, ro };
292
365
  }
293
- async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
366
+ /** Insert the output and attach its tags; return the SDK output descriptor. */
367
+ async function persistNewOutput(storage, o, tags, txTags, txBaskets, trx) {
368
+ o.outputId = await storage.insertOutput(o, trx);
369
+ for (const tagName of new Set(tags)) {
370
+ const tag = txTags[tagName];
371
+ await storage.insertOutputTagMap({
372
+ outputId: (0, utilityHelpers_1.verifyId)(o.outputId),
373
+ outputTagId: (0, utilityHelpers_1.verifyId)(tag.outputTagId),
374
+ created_at: new Date(),
375
+ updated_at: new Date(),
376
+ isDeleted: false
377
+ }, trx);
378
+ }
379
+ return describeNewOutput(o, tags, txBaskets);
380
+ }
381
+ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs, trx) {
294
382
  const txBaskets = {};
295
383
  const basketNames = [...new Set(ctx.xoutputs.map(x => x.basket).filter((v) => !!v))];
296
- Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames));
384
+ Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames, trx));
297
385
  const txTags = {};
298
386
  const tagNames = [...new Set(ctx.xoutputs.flatMap(x => x.tags))];
299
- Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames));
387
+ Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames, trx));
300
388
  const newOutputs = [];
301
389
  for (const xo of ctx.xoutputs) {
302
390
  const lockingScript = (0, utilityHelpers_noBuffer_1.asArray)(xo.lockingScript);
@@ -312,7 +400,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
312
400
  created_at: now,
313
401
  updated_at: now,
314
402
  commissionId: 0
315
- });
403
+ }, trx);
316
404
  const o = makeDefaultOutput(userId, ctx.transactionId, xo.satoshis, xo.vout);
317
405
  o.lockingScript = lockingScript;
318
406
  o.providedBy = 'storage';
@@ -339,17 +427,25 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
339
427
  }
340
428
  if (vargs.options.randomizeOutputs)
341
429
  (0, actionPlanning_1.randomizeOutputVouts)(newOutputs.map(output => output.o), vargs.randomVals);
430
+ // The overwhelmingly common path has no output tags. Those rows do not
431
+ // need generated outputIds, so Knex can persist fixed and change outputs in
432
+ // one multi-row statement. Tagged rows retain the established id-dependent
433
+ // insertion path below.
434
+ const untagged = newOutputs.filter(output => output.tags.length === 0);
435
+ await storage.insertOutputs(untagged.map(output => output.o), trx);
342
436
  const outputs = [];
343
437
  const changeVouts = [];
344
438
  for (const { o, tags } of newOutputs) {
345
- const { changeVout, ro } = await persistNewOutput(storage, o, tags, txTags, txBaskets);
439
+ const { changeVout, ro } = tags.length === 0
440
+ ? describeNewOutput(o, tags, txBaskets)
441
+ : await persistNewOutput(storage, o, tags, txTags, txBaskets, trx);
346
442
  if (changeVout !== undefined)
347
443
  changeVouts.push(changeVout);
348
444
  outputs.push(ro);
349
445
  }
350
446
  return { outputs, changeVouts };
351
447
  }
352
- async function createNewTxRecord(storage, userId, vargs, storageBeef) {
448
+ async function createNewTxRecord(storage, userId, vargs, storageBeef, satoshis = 0, trx, status = 'unsigned') {
353
449
  const now = new Date();
354
450
  const newTx = {
355
451
  created_at: now,
@@ -357,20 +453,22 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
357
453
  transactionId: 0,
358
454
  version: vargs.version,
359
455
  lockTime: vargs.lockTime,
360
- status: 'unsigned',
456
+ status,
361
457
  reference: (0, utilityHelpers_1.randomBytesBase64)(12),
362
- satoshis: 0, // updated after fundingTransaction
458
+ satoshis,
363
459
  userId,
364
460
  isOutgoing: true,
365
- inputBEEF: storageBeef.toBinary(),
461
+ inputBEEF: storageBeef,
366
462
  description: vargs.description,
367
463
  txid: undefined,
368
464
  rawTx: undefined
369
465
  };
370
- newTx.transactionId = await storage.insertTransaction(newTx);
371
- for (const label of vargs.labels) {
372
- const txLabel = await storage.findOrInsertTxLabel(userId, label);
373
- await storage.findOrInsertTxLabelMap((0, utilityHelpers_1.verifyId)(newTx.transactionId), (0, utilityHelpers_1.verifyId)(txLabel.txLabelId));
466
+ newTx.transactionId = await storage.insertTransaction(newTx, trx);
467
+ const labelNames = [...new Set(vargs.labels)];
468
+ const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames, trx);
469
+ for (const label of labelNames) {
470
+ const txLabel = labels[label];
471
+ await storage.findOrInsertTxLabelMap((0, utilityHelpers_1.verifyId)(newTx.transactionId), (0, utilityHelpers_1.verifyId)(txLabel.txLabelId), trx);
374
472
  }
375
473
  return newTx;
376
474
  }
@@ -574,10 +672,9 @@ async function validateNoSendChange(storage, userId, vargs, changeBasket) {
574
672
  return [];
575
673
  const noSendChange = vargs.options.noSendChange;
576
674
  if (noSendChange && noSendChange.length > 0) {
675
+ const byOutpoint = await storage.findOutputsByOutpoints(userId, noSendChange);
577
676
  for (const op of noSendChange) {
578
- const output = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findOutputs({
579
- partial: { userId, txid: op.txid, vout: op.vout }
580
- }));
677
+ const output = byOutpoint[`${op.txid}.${op.vout}`];
581
678
  // noSendChange is signed through the same BRC-29 path as allocated change.
582
679
  // It must satisfy the full managed-change policy, not merely share the
583
680
  // default basket or have a P2PKH-shaped locking script.
@@ -599,87 +696,240 @@ async function validateNoSendChange(storage, userId, vargs, changeBasket) {
599
696
  }
600
697
  return r;
601
698
  }
602
- async function preflightInsufficientFundsFastPath(vargs, xinputs, xoutputs, noSendChangeIn, availableChangeCount, feeModel) {
603
- if (feeModel.model !== 'sat/kb' || !feeModel.value)
604
- return;
605
- const fixedInputSatoshis = xinputs.reduce((a, e) => a + e.satoshis, 0);
606
- const noSendSatoshis = noSendChangeIn.reduce((a, e) => a + Number(e.satoshis || 0), 0);
607
- const spending = xoutputs.reduce((a, e) => a + e.satoshis, 0);
608
- const minSize = (0, utils_1.transactionSize)(xinputs.map(i => i.unlockingScriptLength || 0), xoutputs.map(o => Math.floor(o.lockingScript.length / 2)));
609
- const minFee = Math.ceil((minSize / 1000) * feeModel.value);
610
- const minRequired = spending + minFee;
611
- const fixedAvailable = fixedInputSatoshis + noSendSatoshis;
612
- if (fixedAvailable >= minRequired)
613
- return;
614
- // Keep common successful path cheap:
615
- // - If there are zero change candidates, failure is certain.
616
- // Otherwise, defer to the main funding allocator.
617
- const deficit = minRequired - fixedAvailable;
618
- if (availableChangeCount <= 0) {
619
- throw new WERR_errors_1.WERR_INSUFFICIENT_FUNDS(minRequired, deficit);
620
- }
621
- }
622
- async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
623
- const params = {
624
- fixedInputs: ctx.xinputs.map(xi => ({
625
- satoshis: xi.satoshis,
626
- unlockingScriptLength: xi.unlockingScriptLength
699
+ function fundingPlanSatoshis(plan) {
700
+ return plan.result.changeOutputs.reduce((sum, output) => sum + output.satoshis, 0) -
701
+ plan.selected.reduce((sum, output) => sum + output.satoshis, 0);
702
+ }
703
+ class FundingClaimConflict extends Error {
704
+ conflict;
705
+ constructor(conflict) {
706
+ super('createAction funding claim changed concurrently');
707
+ this.conflict = conflict;
708
+ }
709
+ }
710
+ async function traceStorageStep(storage, name, parent, attributes, callback) {
711
+ if (!storage.telemetry.enabled)
712
+ return await callback();
713
+ return await storage.telemetry.withSpan(name, { component: 'wallet-storage', parent: parent?.context, attributes }, async (span) => await callback(span));
714
+ }
715
+ function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, availableChangeCount) {
716
+ return {
717
+ fixedInputs: xinputs.map(input => ({
718
+ satoshis: input.satoshis,
719
+ unlockingScriptLength: input.unlockingScriptLength
627
720
  })),
628
- fixedOutputs: ctx.xoutputs.map(xo => ({
629
- satoshis: xo.satoshis,
630
- lockingScriptLength: xo.lockingScript.length / 2
721
+ fixedOutputs: xoutputs.map(output => ({
722
+ satoshis: output.satoshis,
723
+ lockingScriptLength: output.lockingScript.length / 2
631
724
  })),
632
- feeModel: ctx.feeModel,
633
- changeInitialSatoshis: Math.max(1, ctx.changeBasket.minimumDesiredUTXOValue),
634
- changeFirstSatoshis: Math.max(1, Math.round(ctx.changeBasket.minimumDesiredUTXOValue / 4)),
725
+ feeModel,
726
+ changeInitialSatoshis: Math.max(1, changeBasket.minimumDesiredUTXOValue),
727
+ changeFirstSatoshis: Math.max(1, Math.round(changeBasket.minimumDesiredUTXOValue / 4)),
635
728
  changeLockingScriptLength: 25,
636
729
  changeUnlockingScriptLength: 107,
637
- targetNetCount: ctx.changeBasket.numberOfDesiredUTXOs - ctx.availableChangeCount,
730
+ targetNetCount: changeBasket.numberOfDesiredUTXOs - availableChangeCount,
638
731
  randomVals: vargs.randomVals
639
732
  };
640
- const noSendChange = [...ctx.noSendChangeIn];
641
- const outputs = {};
642
- const allocateChangeInput = async (targetSatoshis, exactSatoshis) => {
643
- // noSendChange gets allocated first...typically only one input...just allocate in order...
644
- if (noSendChange.length > 0) {
645
- const o = noSendChange.pop();
646
- outputs[o.outputId] = o;
647
- // allocate the output in storage, noSendChange is by definition spendable false and part of noSpend transaction batch.
648
- await storage.updateOutput(o.outputId, {
649
- spendable: false,
650
- spentBy: ctx.transactionId
651
- });
652
- o.spendable = false;
653
- o.spentBy = ctx.transactionId;
654
- const r = {
655
- outputId: o.outputId,
656
- satoshis: o.satoshis
657
- };
658
- return r;
659
- }
660
- const basketId = ctx.changeBasket.basketId;
661
- const o = await storage.allocateChangeInput(userId, basketId, targetSatoshis, exactSatoshis, !vargs.isDelayed, ctx.transactionId);
662
- if (o == null)
663
- return undefined;
664
- outputs[o.outputId] = o;
665
- const r = {
666
- outputId: o.outputId,
667
- satoshis: o.satoshis
733
+ }
734
+ async function prepareFundingPlan(storage, context) {
735
+ const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent, trx] = context;
736
+ const excludeSending = !vargs.isDelayed;
737
+ const candidates = await traceStorageStep(storage, 'wallet.storage.create_action.funding_candidates', parent, { 'funding.exclude_sending': excludeSending }, async (span) => {
738
+ const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, changeBasket.basketId, excludeSending, trx);
739
+ span?.end({
740
+ attributes: {
741
+ 'funding.candidate_count': outputs.length,
742
+ 'funding.candidate_satoshis': outputs.reduce((sum, output) => sum + output.satoshis, 0)
743
+ }
744
+ });
745
+ return outputs;
746
+ });
747
+ const noSendIds = new Set(noSendChangeIn.map(output => output.outputId));
748
+ const available = candidates.filter(output => !noSendIds.has(output.outputId));
749
+ // Preserve the legacy target-net-count input: noSendChange was included in
750
+ // countChangeInputs before it was consumed by the allocator.
751
+ const params = makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, candidates.length);
752
+ return await traceStorageStep(storage, 'wallet.storage.create_action.funding_plan', parent, {
753
+ 'funding.candidate_count': available.length,
754
+ 'funding.no_send_change_count': noSendChangeIn.length
755
+ }, async (span) => {
756
+ const allocated = new Map();
757
+ const availableSelector = new actionPlanning_1.CanonicalChangeSelector(available);
758
+ const noSend = [...noSendChangeIn];
759
+ const noSendById = new Map(noSendChangeIn.map(output => [output.outputId, output]));
760
+ const allocate = async (targetSatoshis, exactSatoshis) => {
761
+ let output = noSend.pop();
762
+ output ??= availableSelector.take(targetSatoshis, exactSatoshis);
763
+ if (output == null)
764
+ return undefined;
765
+ allocated.set(output.outputId, output);
766
+ return { outputId: output.outputId, satoshis: output.satoshis };
668
767
  };
669
- return r;
768
+ const release = async (outputId) => {
769
+ const output = allocated.get(outputId);
770
+ if (output == null)
771
+ return;
772
+ allocated.delete(outputId);
773
+ availableSelector.release(outputId);
774
+ const noSendOutput = noSendById.get(outputId);
775
+ if (noSendOutput != null)
776
+ noSend.push(noSendOutput);
777
+ };
778
+ const result = await (0, generateChange_1.generateChangeSdk)(params, allocate, release, vargs.logger, storage.telemetry);
779
+ const selected = result.allocatedChangeInputs.map(input => (0, utilityHelpers_1.verifyTruthy)(allocated.get(input.outputId)));
780
+ span?.end({
781
+ attributes: {
782
+ 'funding.allocated_input_count': selected.length,
783
+ 'funding.change_output_count': result.changeOutputs.length,
784
+ 'funding.fee_satoshis': result.fee,
785
+ 'funding.transaction_size_bytes': result.size
786
+ }
787
+ });
788
+ return {
789
+ params,
790
+ result,
791
+ selected,
792
+ availableChangeCount: candidates.length
793
+ };
794
+ });
795
+ }
796
+ async function claimFundingPlan(storage, request) {
797
+ const [userId, basketId, excludeSending, transactionId, noSendChangeIn, plan, trx] = request;
798
+ if (plan.selected.length === 0) {
799
+ return { outputs: [], sourceTransactionCount: 0, hydratedScriptCount: 0, scriptSourceTransactionCount: 0 };
800
+ }
801
+ const noSendIds = new Set(noSendChangeIn.map(output => output.outputId));
802
+ const statuses = ['completed', 'unproven'];
803
+ if (!excludeSending)
804
+ statuses.push('sending');
805
+ const claim = await storage.transaction(async (claimTrx) => {
806
+ const currentById = await storage.findFundingOutputsForUpdate(userId, plan.selected.map(output => output.outputId), statuses, claimTrx);
807
+ const transactionIds = [...new Set(Object.values(currentById).map(output => output.transactionId))];
808
+ const claimed = [];
809
+ for (const planned of plan.selected) {
810
+ const current = currentById[planned.outputId];
811
+ if (current?.outputId !== planned.outputId ||
812
+ current?.satoshis !== planned.satoshis ||
813
+ current?.basketId !== basketId ||
814
+ !(0, managedChange_1.isAutoSpendableChangeOutput)(current) ||
815
+ current?.txid !== planned.txid ||
816
+ current?.vout !== planned.vout) {
817
+ return { conflict: noSendIds.has(planned.outputId) ? 'noSendChange' : 'candidate' };
818
+ }
819
+ claimed.push(current);
820
+ }
821
+ const updated = await storage.markChangeInputsSpent(claimed.map(output => output.outputId), transactionId, claimTrx);
822
+ if (updated !== claimed.length) {
823
+ throw new FundingClaimConflict(claimed.some(output => noSendIds.has(output.outputId)) ? 'noSendChange' : 'candidate');
824
+ }
825
+ for (const output of claimed) {
826
+ output.spendable = false;
827
+ output.spentBy = transactionId;
828
+ }
829
+ return { outputs: claimed, sourceTransactionCount: transactionIds.length };
830
+ }, trx).catch(error => {
831
+ if (error instanceof FundingClaimConflict)
832
+ return { conflict: error.conflict };
833
+ throw error;
834
+ });
835
+ if (claim.outputs == null)
836
+ return claim;
837
+ const hydration = await hydrateFundingInputScripts(storage, claim.outputs, trx);
838
+ return {
839
+ outputs: claim.outputs,
840
+ sourceTransactionCount: claim.sourceTransactionCount,
841
+ ...hydration
670
842
  };
671
- const releaseChangeInput = async (outputId) => {
672
- const nsco = ctx.noSendChangeIn.find(o => o.outputId === outputId);
673
- if (nsco != null) {
674
- noSendChange.push(nsco);
675
- return;
843
+ }
844
+ async function hydrateFundingInputScripts(storage, outputs, trx) {
845
+ const missing = outputs.filter(output => output.lockingScript?.length !== output.scriptLength &&
846
+ output.scriptLength != null && output.scriptLength > 0 &&
847
+ output.scriptOffset != null && output.scriptOffset > 0 &&
848
+ output.txid != null && output.txid !== '');
849
+ if (missing.length === 0)
850
+ return { hydratedScriptCount: 0, scriptSourceTransactionCount: 0 };
851
+ const byTxid = new Map();
852
+ for (const output of missing) {
853
+ const txid = (0, utilityHelpers_1.verifyTruthy)(output.txid);
854
+ const group = byTxid.get(txid) ?? [];
855
+ group.push(output);
856
+ byTxid.set(txid, group);
857
+ }
858
+ const groups = [...byTxid.entries()];
859
+ let cursor = 0;
860
+ await Promise.all(Array.from({ length: Math.min(8, groups.length) }, async () => {
861
+ while (cursor < groups.length) {
862
+ const [txid, group] = groups[cursor++];
863
+ if (group.length === 1) {
864
+ await storage.validateOutputScript(group[0], trx);
865
+ continue;
866
+ }
867
+ const rawTx = await storage.getRawTxOfKnownValidTransaction(txid, undefined, undefined, trx);
868
+ if (rawTx != null) {
869
+ for (const output of group) {
870
+ output.lockingScript = rawTx.slice(output.scriptOffset, output.scriptOffset + output.scriptLength);
871
+ }
872
+ }
873
+ else {
874
+ for (const output of group)
875
+ await storage.validateOutputScript(output, trx);
876
+ }
676
877
  }
677
- await storage.updateOutput(outputId, {
678
- spendable: true,
679
- spentBy: undefined
680
- });
878
+ }));
879
+ return {
880
+ hydratedScriptCount: missing.filter(output => output.lockingScript?.length === output.scriptLength).length,
881
+ scriptSourceTransactionCount: groups.length
681
882
  };
682
- const gcr = await (0, generateChange_1.generateChangeSdk)(params, allocateChangeInput, releaseChangeInput, vargs.logger, storage.telemetry);
883
+ }
884
+ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent, trx) {
885
+ let plan = initialPlan;
886
+ let allocatedChange;
887
+ let retryCount = 0;
888
+ await traceStorageStep(storage, 'wallet.storage.create_action.funding_claim', parent, { 'funding.planned_input_count': initialPlan.selected.length }, async (span) => {
889
+ for (let attempt = 0; attempt < 3; attempt++) {
890
+ const claim = await claimFundingPlan(storage, [
891
+ userId,
892
+ ctx.changeBasket.basketId,
893
+ !vargs.isDelayed,
894
+ ctx.transactionId,
895
+ ctx.noSendChangeIn,
896
+ plan,
897
+ trx
898
+ ]);
899
+ if (claim.outputs != null) {
900
+ allocatedChange = claim.outputs;
901
+ span?.end({
902
+ attributes: {
903
+ 'funding.claim_retry_count': retryCount,
904
+ 'funding.source_transaction_count': claim.sourceTransactionCount,
905
+ 'funding.hydrated_script_count': claim.hydratedScriptCount,
906
+ 'funding.script_source_transaction_count': claim.scriptSourceTransactionCount
907
+ }
908
+ });
909
+ return;
910
+ }
911
+ if (claim.conflict === 'noSendChange') {
912
+ throw new WERR_errors_1.WERR_INVALID_PARAMETER('noSendChange', 'outputs that remain spendable during action planning');
913
+ }
914
+ retryCount++;
915
+ plan = await prepareFundingPlan(storage, [
916
+ userId,
917
+ vargs,
918
+ ctx.xinputs,
919
+ ctx.xoutputs,
920
+ ctx.changeBasket,
921
+ ctx.noSendChangeIn,
922
+ ctx.feeModel,
923
+ parent,
924
+ trx
925
+ ]);
926
+ }
927
+ throw new WERR_errors_1.WERR_INVALID_OPERATION('wallet funding changed repeatedly during action planning; retry createAction');
928
+ });
929
+ if (allocatedChange == null)
930
+ throw new WERR_errors_1.WERR_INTERNAL('funding plan was not claimed');
931
+ const params = plan.params;
932
+ const gcr = plan.result;
683
933
  const nextRandomVal = () => {
684
934
  let val = 0;
685
935
  if (vargs.randomVals == null || vargs.randomVals.length === 0) {
@@ -715,7 +965,7 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
715
965
  const derivationPrefix = randomDerivation(16);
716
966
  const r = {
717
967
  maxPossibleSatoshisAdjustment: gcr.maxPossibleSatoshisAdjustment,
718
- allocatedChange: gcr.allocatedChangeInputs.map(i => outputs[i.outputId]),
968
+ allocatedChange,
719
969
  changeOutputs: gcr.changeOutputs.map((o, i) => ({
720
970
  // what we knnow now and can insert into the database for this new transaction's change output
721
971
  created_at: new Date(),
@@ -755,15 +1005,73 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx) {
755
1005
  function trimInputBeef(beef, vargs) {
756
1006
  if (vargs.options.returnTXIDOnly)
757
1007
  return undefined;
758
- const knownTxids = {};
759
- for (const txid of vargs.options.knownTxids || [])
760
- knownTxids[txid] = true;
761
- for (const txid of beef.txs.map(btx => btx.txid))
762
- if (knownTxids[txid])
763
- beef.makeTxidOnly(txid);
1008
+ const knownTxids = vargs.options.knownTxids ?? [];
1009
+ const hasKnownTxid = makeKnownTxidLookup(knownTxids);
1010
+ // The returned BEEF normally contains only a handful of transactions. A
1011
+ // direct scan avoids materializing a second full index of a potentially
1012
+ // very large wallet history on every successful action.
1013
+ for (const btx of beef.txs)
1014
+ if (hasKnownTxid(btx.txid))
1015
+ beef.makeTxidOnly(btx.txid);
764
1016
  return beef.toUint8Array();
765
1017
  }
766
- async function mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange, beef) {
1018
+ function makeKnownTxidLookup(knownTxids) {
1019
+ let lookups = 0;
1020
+ let indexed;
1021
+ return txid => {
1022
+ lookups++;
1023
+ if (indexed != null)
1024
+ return indexed.has(txid);
1025
+ if (knownTxids.length > 64 && lookups > 4) {
1026
+ indexed = new Set(knownTxids);
1027
+ return indexed.has(txid);
1028
+ }
1029
+ return knownTxids.includes(txid);
1030
+ };
1031
+ }
1032
+ function missingAllocatedChangeTxids(allocatedChange, beef, knownTxids) {
1033
+ const hasKnownTxid = makeKnownTxidLookup(knownTxids);
1034
+ return Array.from(new Set(allocatedChange
1035
+ .map(output => (0, utilityHelpers_1.verifyTruthy)(output.txid))
1036
+ .filter(txid => beef.findTxid(txid) == null && !hasKnownTxid(txid))));
1037
+ }
1038
+ function startAllocatedChangeBeefPrefetch(storage, vargs, allocatedChange, beef, parent) {
1039
+ if (vargs.options.returnTXIDOnly)
1040
+ return Promise.resolve({ sourceCount: 0, txids: [] });
1041
+ const knownTxids = vargs.options.knownTxids ?? [];
1042
+ const missing = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
1043
+ if (missing.length === 0)
1044
+ return Promise.resolve({ sourceCount: 0, txids: [] });
1045
+ const options = {
1046
+ trustSelf: undefined,
1047
+ knownTxids,
1048
+ ignoreStorage: false,
1049
+ ignoreServices: true,
1050
+ ignoreNewProven: false,
1051
+ minProofLevel: undefined
1052
+ };
1053
+ return traceStorageStep(storage, 'wallet.storage.create_action.beef_prefetch', parent, {
1054
+ 'beef.planned_source_count': allocatedChange.length,
1055
+ 'beef.missing_source_count': missing.length,
1056
+ 'beef.storage_batch_count': missing.length === 0 ? 0 : 1
1057
+ }, async (span) => {
1058
+ const fetched = await storage.getBeefForTransactions(missing, options);
1059
+ span?.end({
1060
+ attributes: {
1061
+ 'beef.fetched_tx_count': fetched.txs.length,
1062
+ 'beef.fetched_bump_count': fetched.bumps.length
1063
+ }
1064
+ });
1065
+ return fetched;
1066
+ }).then(prefetched => ({ beef: prefetched, sourceCount: missing.length, txids: missing }), error => ({ error, sourceCount: missing.length, txids: missing }));
1067
+ }
1068
+ function sameTxids(left, right) {
1069
+ if (left.length !== right.length)
1070
+ return false;
1071
+ const expected = new Set(left);
1072
+ return right.every(txid => expected.has(txid));
1073
+ }
1074
+ async function mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, prefetch, parent) {
767
1075
  const options = {
768
1076
  trustSelf: undefined,
769
1077
  knownTxids: vargs.options.knownTxids,
@@ -775,22 +1083,62 @@ async function mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange
775
1083
  };
776
1084
  if (vargs.options.returnTXIDOnly)
777
1085
  return undefined;
778
- const known = new Set(vargs.options.knownTxids ?? []);
779
- const missing = Array.from(new Set(allocatedChange.map(o => o.txid).filter(txid => beef.findTxid(txid) == null && !known.has(txid))));
780
- const fetched = Array.from({ length: missing.length });
781
- const concurrency = Math.min(8, Math.max(1, missing.length));
782
- let cursor = 0;
783
- await Promise.all(Array.from({ length: concurrency }, async () => {
784
- while (cursor < missing.length) {
785
- const index = cursor++;
786
- fetched[index] = await storage.getBeefForTransaction(missing[index], { ...options, mergeToBeef: undefined });
1086
+ const knownTxids = vargs.options.knownTxids ?? [];
1087
+ const requiredBeforePrefetch = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
1088
+ const prefetched = await traceStorageStep(storage, 'wallet.storage.create_action.beef_prefetch_join', parent, { 'beef.prefetch_source_count': 0 }, async (span) => {
1089
+ const result = await prefetch;
1090
+ span?.end({ attributes: { 'beef.prefetch_source_count': result.sourceCount } });
1091
+ return result;
1092
+ });
1093
+ // A claim retry may replace only part of the initial funding plan. Never
1094
+ // merge proofs for outputs the final transaction does not spend: apart from
1095
+ // unnecessary bytes, that would disclose unrelated wallet history.
1096
+ const usePrefetch = sameTxids(prefetched.txids, requiredBeforePrefetch);
1097
+ if (usePrefetch && prefetched.error != null)
1098
+ throw prefetched.error;
1099
+ if (usePrefetch && prefetched.beef != null)
1100
+ beef.mergeBeef(prefetched.beef);
1101
+ // If a concurrent spender forced the funding claim to be replanned, only
1102
+ // the newly selected roots remain. The normal uncontended path is empty.
1103
+ const missing = missingAllocatedChangeTxids(allocatedChange, beef, knownTxids);
1104
+ let fetched;
1105
+ await traceStorageStep(storage, 'wallet.storage.create_action.beef_fetch', parent, {
1106
+ 'beef.allocated_change_count': allocatedChange.length,
1107
+ 'beef.distinct_source_count': new Set(allocatedChange.map(output => output.txid)).size,
1108
+ 'beef.known_txid_count': knownTxids.length,
1109
+ 'beef.missing_source_count': missing.length,
1110
+ 'beef.fetch_concurrency': 1,
1111
+ 'beef.prefetch_reused': usePrefetch
1112
+ }, async (span) => {
1113
+ if (missing.length > 0) {
1114
+ fetched = await storage.getBeefForTransactions(missing, { ...options, mergeToBeef: undefined });
787
1115
  }
788
- }));
789
- for (const fetchedBeef of fetched) {
790
- if (fetchedBeef == null)
791
- continue;
792
- beef.mergeBeef(fetchedBeef);
793
- }
794
- return trimInputBeef(beef, vargs);
1116
+ span?.end({
1117
+ attributes: {
1118
+ 'beef.fetched_tx_count': fetched?.txs.length ?? 0,
1119
+ 'beef.fetched_bump_count': fetched?.bumps.length ?? 0,
1120
+ 'beef.storage_batch_count': missing.length === 0 ? 0 : 1
1121
+ }
1122
+ });
1123
+ });
1124
+ await traceStorageStep(storage, 'wallet.storage.create_action.beef_merge', parent, { 'beef.fragment_count': (usePrefetch && prefetched.beef != null ? 1 : 0) + (fetched == null ? 0 : 1) }, async (span) => {
1125
+ if (fetched != null)
1126
+ beef.mergeBeef(fetched);
1127
+ span?.end({
1128
+ attributes: {
1129
+ 'beef.merged_tx_count': beef.txs.length,
1130
+ 'beef.merged_bump_count': beef.bumps.length
1131
+ }
1132
+ });
1133
+ });
1134
+ return await traceStorageStep(storage, 'wallet.storage.create_action.beef_trim_serialize', parent, {
1135
+ 'beef.tx_count': beef.txs.length,
1136
+ 'beef.bump_count': beef.bumps.length,
1137
+ 'beef.known_txid_count': knownTxids.length
1138
+ }, async (span) => {
1139
+ const result = trimInputBeef(beef, vargs);
1140
+ span?.end({ attributes: { 'beef.result_bytes': result?.length ?? 0 } });
1141
+ return result;
1142
+ });
795
1143
  }
796
1144
  //# sourceMappingURL=createAction.js.map