@bsv/wallet-toolbox 2.4.22 → 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.
- package/CHANGELOG.md +44 -0
- package/README.md +24 -1
- package/docs/authentication-observability.md +21 -9
- package/out/src/CWIStyleWalletManager.d.ts +31 -3
- package/out/src/CWIStyleWalletManager.d.ts.map +1 -1
- package/out/src/CWIStyleWalletManager.js +164 -56
- package/out/src/CWIStyleWalletManager.js.map +1 -1
- package/out/src/Wallet.d.ts +3 -3
- package/out/src/Wallet.d.ts.map +1 -1
- package/out/src/Wallet.js +45 -19
- package/out/src/Wallet.js.map +1 -1
- package/out/src/sdk/WalletStorage.interfaces.d.ts +1 -0
- package/out/src/sdk/WalletStorage.interfaces.d.ts.map +1 -1
- package/out/src/signer/methods/completeSignedTransaction.d.ts.map +1 -1
- package/out/src/signer/methods/completeSignedTransaction.js +54 -13
- package/out/src/signer/methods/completeSignedTransaction.js.map +1 -1
- package/out/src/signer/methods/createAction.d.ts.map +1 -1
- package/out/src/signer/methods/createAction.js +14 -10
- package/out/src/signer/methods/createAction.js.map +1 -1
- package/out/src/signer/methods/verifyUnlockScripts.d.ts.map +1 -1
- package/out/src/signer/methods/verifyUnlockScripts.js +257 -21
- package/out/src/signer/methods/verifyUnlockScripts.js.map +1 -1
- package/out/src/storage/StorageIdb.d.ts +2 -0
- package/out/src/storage/StorageIdb.d.ts.map +1 -1
- package/out/src/storage/StorageIdb.js +49 -3
- package/out/src/storage/StorageIdb.js.map +1 -1
- package/out/src/storage/StorageKnex.d.ts +6 -0
- package/out/src/storage/StorageKnex.d.ts.map +1 -1
- package/out/src/storage/StorageKnex.js +140 -5
- package/out/src/storage/StorageKnex.js.map +1 -1
- package/out/src/storage/StorageProvider.d.ts +23 -0
- package/out/src/storage/StorageProvider.d.ts.map +1 -1
- package/out/src/storage/StorageProvider.js +64 -1
- package/out/src/storage/StorageProvider.js.map +1 -1
- package/out/src/storage/methods/actionPlanning.d.ts +14 -0
- package/out/src/storage/methods/actionPlanning.d.ts.map +1 -1
- package/out/src/storage/methods/actionPlanning.js +55 -0
- package/out/src/storage/methods/actionPlanning.js.map +1 -1
- package/out/src/storage/methods/availableManagedChange.d.ts +1 -0
- package/out/src/storage/methods/availableManagedChange.d.ts.map +1 -1
- package/out/src/storage/methods/availableManagedChange.js.map +1 -1
- package/out/src/storage/methods/createAction.d.ts.map +1 -1
- package/out/src/storage/methods/createAction.js +225 -131
- package/out/src/storage/methods/createAction.js.map +1 -1
- package/out/src/storage/methods/generateChange.d.ts.map +1 -1
- package/out/src/storage/methods/generateChange.js +29 -14
- package/out/src/storage/methods/generateChange.js.map +1 -1
- package/out/src/storage/methods/getBeefForTransaction.d.ts +7 -0
- package/out/src/storage/methods/getBeefForTransaction.d.ts.map +1 -1
- package/out/src/storage/methods/getBeefForTransaction.js +196 -16
- package/out/src/storage/methods/getBeefForTransaction.js.map +1 -1
- package/out/src/storage/methods/processAction.d.ts.map +1 -1
- package/out/src/storage/methods/processAction.js +36 -15
- package/out/src/storage/methods/processAction.js.map +1 -1
- package/out/src/storage/remoting/KnexSessionManager.d.ts +20 -0
- package/out/src/storage/remoting/KnexSessionManager.d.ts.map +1 -1
- package/out/src/storage/remoting/KnexSessionManager.js +74 -16
- package/out/src/storage/remoting/KnexSessionManager.js.map +1 -1
- package/out/src/storage/schema/KnexMigrations.d.ts.map +1 -1
- package/out/src/storage/schema/KnexMigrations.js +25 -0
- package/out/src/storage/schema/KnexMigrations.js.map +1 -1
- package/out/src/storage/schema/entities/EntityProvenTx.d.ts +2 -1
- package/out/src/storage/schema/entities/EntityProvenTx.d.ts.map +1 -1
- package/out/src/storage/schema/entities/EntityProvenTx.js +8 -3
- package/out/src/storage/schema/entities/EntityProvenTx.js.map +1 -1
- package/out/src/storage/storageProviderHelpers.d.ts.map +1 -1
- package/out/src/storage/storageProviderHelpers.js +8 -2
- package/out/src/storage/storageProviderHelpers.js.map +1 -1
- package/out/src/utility/ScriptTemplateBRC29.d.ts +3 -2
- package/out/src/utility/ScriptTemplateBRC29.d.ts.map +1 -1
- package/out/src/utility/ScriptTemplateBRC29.js +13 -5
- package/out/src/utility/ScriptTemplateBRC29.js.map +1 -1
- package/package.json +4 -2
|
@@ -98,50 +98,66 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
98
98
|
const { storageBeef, beef, xinputs, xoutputs, changeBasket, noSendChangeIn } = validated;
|
|
99
99
|
const feeModel = (0, StorageProvider_1.validateStorageFeeModel)(storage.feeModel);
|
|
100
100
|
logger?.log(`validated fee model ${JSON.stringify(feeModel)}`);
|
|
101
|
-
const initialFundingPlan = await prepareFundingPlan(storage,
|
|
101
|
+
const initialFundingPlan = await prepareFundingPlan(storage, [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent]);
|
|
102
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();
|
|
103
109
|
let newTx;
|
|
110
|
+
let newTxCommitted = false;
|
|
104
111
|
try {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
'
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
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 };
|
|
142
155
|
});
|
|
156
|
+
newTxCommitted = true;
|
|
157
|
+
const committedTx = (0, utilityHelpers_1.verifyTruthy)(newTx);
|
|
158
|
+
const { allocatedChange, derivationPrefix, outputs, changeVouts, ctx } = persisted;
|
|
143
159
|
logger?.log('created new output records');
|
|
144
|
-
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, parent);
|
|
160
|
+
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, allocatedBeefPrefetch, parent);
|
|
145
161
|
logger?.log('merged allocated change beefs');
|
|
146
162
|
const inputs = await traceStorageStep(storage, 'wallet.storage.create_action.assemble_inputs', parent, {
|
|
147
163
|
'action.fixed_input_count': ctx.xinputs.length,
|
|
@@ -154,9 +170,9 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
154
170
|
});
|
|
155
171
|
logger?.log('created new inputs');
|
|
156
172
|
const r = {
|
|
157
|
-
reference:
|
|
158
|
-
version:
|
|
159
|
-
lockTime:
|
|
173
|
+
reference: committedTx.reference,
|
|
174
|
+
version: committedTx.version,
|
|
175
|
+
lockTime: committedTx.lockTime,
|
|
160
176
|
inputs,
|
|
161
177
|
outputs,
|
|
162
178
|
derivationPrefix,
|
|
@@ -167,10 +183,20 @@ async function createActionCore(storage, auth, vargs, parent) {
|
|
|
167
183
|
return r;
|
|
168
184
|
}
|
|
169
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;
|
|
170
190
|
if (newTx?.transactionId != null) {
|
|
171
191
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
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
|
+
}
|
|
174
200
|
}
|
|
175
201
|
catch (cleanupError) {
|
|
176
202
|
logger?.log(`failed to clean up createAction transaction ${newTx.transactionId}: ${String(cleanupError)}`);
|
|
@@ -320,21 +346,9 @@ async function getCompetingBeefForReview(storage, txid) {
|
|
|
320
346
|
throw e;
|
|
321
347
|
}
|
|
322
348
|
}
|
|
323
|
-
/**
|
|
324
|
-
|
|
325
|
-
async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
|
|
326
|
-
o.outputId = await storage.insertOutput(o);
|
|
349
|
+
/** Build the SDK descriptor for a persisted output. */
|
|
350
|
+
function describeNewOutput(o, tags, txBaskets) {
|
|
327
351
|
const changeVout = o.change && o.purpose === 'change' && o.providedBy === 'storage' ? o.vout : undefined;
|
|
328
|
-
for (const tagName of new Set(tags)) {
|
|
329
|
-
const tag = txTags[tagName];
|
|
330
|
-
await storage.insertOutputTagMap({
|
|
331
|
-
outputId: (0, utilityHelpers_1.verifyId)(o.outputId),
|
|
332
|
-
outputTagId: (0, utilityHelpers_1.verifyId)(tag.outputTagId),
|
|
333
|
-
created_at: new Date(),
|
|
334
|
-
updated_at: new Date(),
|
|
335
|
-
isDeleted: false
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
352
|
const ro = {
|
|
339
353
|
vout: (0, utilityHelpers_1.verifyInteger)(o.vout),
|
|
340
354
|
satoshis: sdk_1.Validation.validateSatoshis(o.satoshis, 'o.satoshis'),
|
|
@@ -349,13 +363,28 @@ async function persistNewOutput(storage, o, tags, txTags, txBaskets) {
|
|
|
349
363
|
};
|
|
350
364
|
return { changeVout, ro };
|
|
351
365
|
}
|
|
352
|
-
|
|
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) {
|
|
353
382
|
const txBaskets = {};
|
|
354
383
|
const basketNames = [...new Set(ctx.xoutputs.map(x => x.basket).filter((v) => !!v))];
|
|
355
|
-
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames));
|
|
384
|
+
Object.assign(txBaskets, await storage.findOrInsertOutputBasketsBulk(userId, basketNames, trx));
|
|
356
385
|
const txTags = {};
|
|
357
386
|
const tagNames = [...new Set(ctx.xoutputs.flatMap(x => x.tags))];
|
|
358
|
-
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames));
|
|
387
|
+
Object.assign(txTags, await storage.findOrInsertOutputTagsBulk(userId, tagNames, trx));
|
|
359
388
|
const newOutputs = [];
|
|
360
389
|
for (const xo of ctx.xoutputs) {
|
|
361
390
|
const lockingScript = (0, utilityHelpers_noBuffer_1.asArray)(xo.lockingScript);
|
|
@@ -371,7 +400,7 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
371
400
|
created_at: now,
|
|
372
401
|
updated_at: now,
|
|
373
402
|
commissionId: 0
|
|
374
|
-
});
|
|
403
|
+
}, trx);
|
|
375
404
|
const o = makeDefaultOutput(userId, ctx.transactionId, xo.satoshis, xo.vout);
|
|
376
405
|
o.lockingScript = lockingScript;
|
|
377
406
|
o.providedBy = 'storage';
|
|
@@ -398,17 +427,25 @@ async function createNewOutputs(storage, userId, vargs, ctx, changeOutputs) {
|
|
|
398
427
|
}
|
|
399
428
|
if (vargs.options.randomizeOutputs)
|
|
400
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);
|
|
401
436
|
const outputs = [];
|
|
402
437
|
const changeVouts = [];
|
|
403
438
|
for (const { o, tags } of newOutputs) {
|
|
404
|
-
const { changeVout, ro } =
|
|
439
|
+
const { changeVout, ro } = tags.length === 0
|
|
440
|
+
? describeNewOutput(o, tags, txBaskets)
|
|
441
|
+
: await persistNewOutput(storage, o, tags, txTags, txBaskets, trx);
|
|
405
442
|
if (changeVout !== undefined)
|
|
406
443
|
changeVouts.push(changeVout);
|
|
407
444
|
outputs.push(ro);
|
|
408
445
|
}
|
|
409
446
|
return { outputs, changeVouts };
|
|
410
447
|
}
|
|
411
|
-
async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
448
|
+
async function createNewTxRecord(storage, userId, vargs, storageBeef, satoshis = 0, trx, status = 'unsigned') {
|
|
412
449
|
const now = new Date();
|
|
413
450
|
const newTx = {
|
|
414
451
|
created_at: now,
|
|
@@ -416,9 +453,9 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
416
453
|
transactionId: 0,
|
|
417
454
|
version: vargs.version,
|
|
418
455
|
lockTime: vargs.lockTime,
|
|
419
|
-
status
|
|
456
|
+
status,
|
|
420
457
|
reference: (0, utilityHelpers_1.randomBytesBase64)(12),
|
|
421
|
-
satoshis
|
|
458
|
+
satoshis,
|
|
422
459
|
userId,
|
|
423
460
|
isOutgoing: true,
|
|
424
461
|
inputBEEF: storageBeef,
|
|
@@ -426,12 +463,12 @@ async function createNewTxRecord(storage, userId, vargs, storageBeef) {
|
|
|
426
463
|
txid: undefined,
|
|
427
464
|
rawTx: undefined
|
|
428
465
|
};
|
|
429
|
-
newTx.transactionId = await storage.insertTransaction(newTx);
|
|
466
|
+
newTx.transactionId = await storage.insertTransaction(newTx, trx);
|
|
430
467
|
const labelNames = [...new Set(vargs.labels)];
|
|
431
|
-
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames);
|
|
468
|
+
const labels = await storage.findOrInsertTxLabelsBulk(userId, labelNames, trx);
|
|
432
469
|
for (const label of labelNames) {
|
|
433
470
|
const txLabel = labels[label];
|
|
434
|
-
await storage.findOrInsertTxLabelMap((0, utilityHelpers_1.verifyId)(newTx.transactionId), (0, utilityHelpers_1.verifyId)(txLabel.txLabelId));
|
|
471
|
+
await storage.findOrInsertTxLabelMap((0, utilityHelpers_1.verifyId)(newTx.transactionId), (0, utilityHelpers_1.verifyId)(txLabel.txLabelId), trx);
|
|
435
472
|
}
|
|
436
473
|
return newTx;
|
|
437
474
|
}
|
|
@@ -659,6 +696,10 @@ async function validateNoSendChange(storage, userId, vargs, changeBasket) {
|
|
|
659
696
|
}
|
|
660
697
|
return r;
|
|
661
698
|
}
|
|
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
|
+
}
|
|
662
703
|
class FundingClaimConflict extends Error {
|
|
663
704
|
conflict;
|
|
664
705
|
constructor(conflict) {
|
|
@@ -691,10 +732,10 @@ function makeFundingParams(vargs, xinputs, xoutputs, changeBasket, feeModel, ava
|
|
|
691
732
|
};
|
|
692
733
|
}
|
|
693
734
|
async function prepareFundingPlan(storage, context) {
|
|
694
|
-
const
|
|
735
|
+
const [userId, vargs, xinputs, xoutputs, changeBasket, noSendChangeIn, feeModel, parent, trx] = context;
|
|
695
736
|
const excludeSending = !vargs.isDelayed;
|
|
696
737
|
const candidates = await traceStorageStep(storage, 'wallet.storage.create_action.funding_candidates', parent, { 'funding.exclude_sending': excludeSending }, async (span) => {
|
|
697
|
-
const outputs = await storage.
|
|
738
|
+
const outputs = await storage.findAvailableManagedChangeInputCandidates(userId, changeBasket.basketId, excludeSending, trx);
|
|
698
739
|
span?.end({
|
|
699
740
|
attributes: {
|
|
700
741
|
'funding.candidate_count': outputs.length,
|
|
@@ -713,10 +754,12 @@ async function prepareFundingPlan(storage, context) {
|
|
|
713
754
|
'funding.no_send_change_count': noSendChangeIn.length
|
|
714
755
|
}, async (span) => {
|
|
715
756
|
const allocated = new Map();
|
|
757
|
+
const availableSelector = new actionPlanning_1.CanonicalChangeSelector(available);
|
|
716
758
|
const noSend = [...noSendChangeIn];
|
|
759
|
+
const noSendById = new Map(noSendChangeIn.map(output => [output.outputId, output]));
|
|
717
760
|
const allocate = async (targetSatoshis, exactSatoshis) => {
|
|
718
761
|
let output = noSend.pop();
|
|
719
|
-
output ??=
|
|
762
|
+
output ??= availableSelector.take(targetSatoshis, exactSatoshis);
|
|
720
763
|
if (output == null)
|
|
721
764
|
return undefined;
|
|
722
765
|
allocated.set(output.outputId, output);
|
|
@@ -727,8 +770,10 @@ async function prepareFundingPlan(storage, context) {
|
|
|
727
770
|
if (output == null)
|
|
728
771
|
return;
|
|
729
772
|
allocated.delete(outputId);
|
|
730
|
-
|
|
731
|
-
|
|
773
|
+
availableSelector.release(outputId);
|
|
774
|
+
const noSendOutput = noSendById.get(outputId);
|
|
775
|
+
if (noSendOutput != null)
|
|
776
|
+
noSend.push(noSendOutput);
|
|
732
777
|
};
|
|
733
778
|
const result = await (0, generateChange_1.generateChangeSdk)(params, allocate, release, vargs.logger, storage.telemetry);
|
|
734
779
|
const selected = result.allocatedChangeInputs.map(input => (0, utilityHelpers_1.verifyTruthy)(allocated.get(input.outputId)));
|
|
@@ -748,7 +793,8 @@ async function prepareFundingPlan(storage, context) {
|
|
|
748
793
|
};
|
|
749
794
|
});
|
|
750
795
|
}
|
|
751
|
-
async function claimFundingPlan(storage,
|
|
796
|
+
async function claimFundingPlan(storage, request) {
|
|
797
|
+
const [userId, basketId, excludeSending, transactionId, noSendChangeIn, plan, trx] = request;
|
|
752
798
|
if (plan.selected.length === 0) {
|
|
753
799
|
return { outputs: [], sourceTransactionCount: 0, hydratedScriptCount: 0, scriptSourceTransactionCount: 0 };
|
|
754
800
|
}
|
|
@@ -756,33 +802,23 @@ async function claimFundingPlan(storage, userId, basketId, excludeSending, trans
|
|
|
756
802
|
const statuses = ['completed', 'unproven'];
|
|
757
803
|
if (!excludeSending)
|
|
758
804
|
statuses.push('sending');
|
|
759
|
-
const claim = await storage.transaction(async (
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
throw new WERR_errors_1.WERR_INTERNAL('planned change input is missing txid');
|
|
763
|
-
return { txid: output.txid, vout: output.vout };
|
|
764
|
-
});
|
|
765
|
-
const currentByOutpoint = await storage.findOutputsByOutpointsForUpdate(userId, outpoints, trx, true);
|
|
766
|
-
const reserved = new Set(await storage.findReservedActionBatchOutputIds(plan.selected.map(output => output.outputId), trx));
|
|
767
|
-
const transactionIds = [...new Set(Object.values(currentByOutpoint).map(output => output.transactionId))];
|
|
768
|
-
const transactionStatuses = await storage.findTransactionStatusesByIds(userId, transactionIds, trx);
|
|
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))];
|
|
769
808
|
const claimed = [];
|
|
770
809
|
for (const planned of plan.selected) {
|
|
771
|
-
const
|
|
772
|
-
const current = currentByOutpoint[key];
|
|
773
|
-
const currentStatus = current == null ? undefined : transactionStatuses.get(current.transactionId);
|
|
774
|
-
const validTransaction = currentStatus != null && statuses.includes(currentStatus);
|
|
810
|
+
const current = currentById[planned.outputId];
|
|
775
811
|
if (current?.outputId !== planned.outputId ||
|
|
776
812
|
current?.satoshis !== planned.satoshis ||
|
|
777
813
|
current?.basketId !== basketId ||
|
|
778
814
|
!(0, managedChange_1.isAutoSpendableChangeOutput)(current) ||
|
|
779
|
-
|
|
780
|
-
|
|
815
|
+
current?.txid !== planned.txid ||
|
|
816
|
+
current?.vout !== planned.vout) {
|
|
781
817
|
return { conflict: noSendIds.has(planned.outputId) ? 'noSendChange' : 'candidate' };
|
|
782
818
|
}
|
|
783
819
|
claimed.push(current);
|
|
784
820
|
}
|
|
785
|
-
const updated = await storage.markChangeInputsSpent(claimed.map(output => output.outputId), transactionId,
|
|
821
|
+
const updated = await storage.markChangeInputsSpent(claimed.map(output => output.outputId), transactionId, claimTrx);
|
|
786
822
|
if (updated !== claimed.length) {
|
|
787
823
|
throw new FundingClaimConflict(claimed.some(output => noSendIds.has(output.outputId)) ? 'noSendChange' : 'candidate');
|
|
788
824
|
}
|
|
@@ -791,21 +827,21 @@ async function claimFundingPlan(storage, userId, basketId, excludeSending, trans
|
|
|
791
827
|
output.spentBy = transactionId;
|
|
792
828
|
}
|
|
793
829
|
return { outputs: claimed, sourceTransactionCount: transactionIds.length };
|
|
794
|
-
}).catch(error => {
|
|
830
|
+
}, trx).catch(error => {
|
|
795
831
|
if (error instanceof FundingClaimConflict)
|
|
796
832
|
return { conflict: error.conflict };
|
|
797
833
|
throw error;
|
|
798
834
|
});
|
|
799
835
|
if (claim.outputs == null)
|
|
800
836
|
return claim;
|
|
801
|
-
const hydration = await hydrateFundingInputScripts(storage, claim.outputs);
|
|
837
|
+
const hydration = await hydrateFundingInputScripts(storage, claim.outputs, trx);
|
|
802
838
|
return {
|
|
803
839
|
outputs: claim.outputs,
|
|
804
840
|
sourceTransactionCount: claim.sourceTransactionCount,
|
|
805
841
|
...hydration
|
|
806
842
|
};
|
|
807
843
|
}
|
|
808
|
-
async function hydrateFundingInputScripts(storage, outputs) {
|
|
844
|
+
async function hydrateFundingInputScripts(storage, outputs, trx) {
|
|
809
845
|
const missing = outputs.filter(output => output.lockingScript?.length !== output.scriptLength &&
|
|
810
846
|
output.scriptLength != null && output.scriptLength > 0 &&
|
|
811
847
|
output.scriptOffset != null && output.scriptOffset > 0 &&
|
|
@@ -825,10 +861,10 @@ async function hydrateFundingInputScripts(storage, outputs) {
|
|
|
825
861
|
while (cursor < groups.length) {
|
|
826
862
|
const [txid, group] = groups[cursor++];
|
|
827
863
|
if (group.length === 1) {
|
|
828
|
-
await storage.validateOutputScript(group[0]);
|
|
864
|
+
await storage.validateOutputScript(group[0], trx);
|
|
829
865
|
continue;
|
|
830
866
|
}
|
|
831
|
-
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid);
|
|
867
|
+
const rawTx = await storage.getRawTxOfKnownValidTransaction(txid, undefined, undefined, trx);
|
|
832
868
|
if (rawTx != null) {
|
|
833
869
|
for (const output of group) {
|
|
834
870
|
output.lockingScript = rawTx.slice(output.scriptOffset, output.scriptOffset + output.scriptLength);
|
|
@@ -836,7 +872,7 @@ async function hydrateFundingInputScripts(storage, outputs) {
|
|
|
836
872
|
}
|
|
837
873
|
else {
|
|
838
874
|
for (const output of group)
|
|
839
|
-
await storage.validateOutputScript(output);
|
|
875
|
+
await storage.validateOutputScript(output, trx);
|
|
840
876
|
}
|
|
841
877
|
}
|
|
842
878
|
}));
|
|
@@ -845,13 +881,21 @@ async function hydrateFundingInputScripts(storage, outputs) {
|
|
|
845
881
|
scriptSourceTransactionCount: groups.length
|
|
846
882
|
};
|
|
847
883
|
}
|
|
848
|
-
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent) {
|
|
884
|
+
async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, parent, trx) {
|
|
849
885
|
let plan = initialPlan;
|
|
850
886
|
let allocatedChange;
|
|
851
887
|
let retryCount = 0;
|
|
852
888
|
await traceStorageStep(storage, 'wallet.storage.create_action.funding_claim', parent, { 'funding.planned_input_count': initialPlan.selected.length }, async (span) => {
|
|
853
889
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
854
|
-
const claim = await claimFundingPlan(storage,
|
|
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
|
+
]);
|
|
855
899
|
if (claim.outputs != null) {
|
|
856
900
|
allocatedChange = claim.outputs;
|
|
857
901
|
span?.end({
|
|
@@ -868,16 +912,17 @@ async function fundNewTransactionSdk(storage, userId, vargs, ctx, initialPlan, p
|
|
|
868
912
|
throw new WERR_errors_1.WERR_INVALID_PARAMETER('noSendChange', 'outputs that remain spendable during action planning');
|
|
869
913
|
}
|
|
870
914
|
retryCount++;
|
|
871
|
-
plan = await prepareFundingPlan(storage,
|
|
915
|
+
plan = await prepareFundingPlan(storage, [
|
|
872
916
|
userId,
|
|
873
917
|
vargs,
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
parent
|
|
880
|
-
|
|
918
|
+
ctx.xinputs,
|
|
919
|
+
ctx.xoutputs,
|
|
920
|
+
ctx.changeBasket,
|
|
921
|
+
ctx.noSendChangeIn,
|
|
922
|
+
ctx.feeModel,
|
|
923
|
+
parent,
|
|
924
|
+
trx
|
|
925
|
+
]);
|
|
881
926
|
}
|
|
882
927
|
throw new WERR_errors_1.WERR_INVALID_OPERATION('wallet funding changed repeatedly during action planning; retry createAction');
|
|
883
928
|
});
|
|
@@ -984,7 +1029,49 @@ function makeKnownTxidLookup(knownTxids) {
|
|
|
984
1029
|
return knownTxids.includes(txid);
|
|
985
1030
|
};
|
|
986
1031
|
}
|
|
987
|
-
|
|
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) {
|
|
988
1075
|
const options = {
|
|
989
1076
|
trustSelf: undefined,
|
|
990
1077
|
knownTxids: vargs.options.knownTxids,
|
|
@@ -997,39 +1084,46 @@ async function mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef,
|
|
|
997
1084
|
if (vargs.options.returnTXIDOnly)
|
|
998
1085
|
return undefined;
|
|
999
1086
|
const knownTxids = vargs.options.knownTxids ?? [];
|
|
1000
|
-
const
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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;
|
|
1007
1105
|
await traceStorageStep(storage, 'wallet.storage.create_action.beef_fetch', parent, {
|
|
1008
1106
|
'beef.allocated_change_count': allocatedChange.length,
|
|
1009
1107
|
'beef.distinct_source_count': new Set(allocatedChange.map(output => output.txid)).size,
|
|
1010
1108
|
'beef.known_txid_count': knownTxids.length,
|
|
1011
1109
|
'beef.missing_source_count': missing.length,
|
|
1012
|
-
'beef.fetch_concurrency':
|
|
1110
|
+
'beef.fetch_concurrency': 1,
|
|
1111
|
+
'beef.prefetch_reused': usePrefetch
|
|
1013
1112
|
}, async (span) => {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
fetched[index] = await storage.getBeefForTransaction(missing[index], { ...options, mergeToBeef: undefined });
|
|
1018
|
-
}
|
|
1019
|
-
}));
|
|
1113
|
+
if (missing.length > 0) {
|
|
1114
|
+
fetched = await storage.getBeefForTransactions(missing, { ...options, mergeToBeef: undefined });
|
|
1115
|
+
}
|
|
1020
1116
|
span?.end({
|
|
1021
1117
|
attributes: {
|
|
1022
|
-
'beef.fetched_tx_count': fetched
|
|
1023
|
-
'beef.fetched_bump_count': fetched
|
|
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
|
|
1024
1121
|
}
|
|
1025
1122
|
});
|
|
1026
1123
|
});
|
|
1027
|
-
await traceStorageStep(storage, 'wallet.storage.create_action.beef_merge', parent, { 'beef.fragment_count':
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
continue;
|
|
1031
|
-
beef.mergeBeef(fetchedBeef);
|
|
1032
|
-
}
|
|
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);
|
|
1033
1127
|
span?.end({
|
|
1034
1128
|
attributes: {
|
|
1035
1129
|
'beef.merged_tx_count': beef.txs.length,
|