@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.
- package/CHANGELOG.md +53 -0
- package/README.md +56 -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 -6
- 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 +14 -1
- package/out/src/storage/StorageIdb.d.ts.map +1 -1
- package/out/src/storage/StorageIdb.js +128 -17
- package/out/src/storage/StorageIdb.js.map +1 -1
- package/out/src/storage/StorageKnex.d.ts +11 -2
- package/out/src/storage/StorageKnex.d.ts.map +1 -1
- package/out/src/storage/StorageKnex.js +196 -5
- package/out/src/storage/StorageKnex.js.map +1 -1
- package/out/src/storage/StorageProvider.d.ts +30 -1
- package/out/src/storage/StorageProvider.d.ts.map +1 -1
- package/out/src/storage/StorageProvider.js +94 -2
- package/out/src/storage/StorageProvider.js.map +1 -1
- package/out/src/storage/idbHelpers.d.ts.map +1 -1
- package/out/src/storage/idbHelpers.js +2 -0
- package/out/src/storage/idbHelpers.js.map +1 -1
- package/out/src/storage/methods/actionBatch.d.ts.map +1 -1
- package/out/src/storage/methods/actionBatch.js +2 -14
- package/out/src/storage/methods/actionBatch.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 +11 -0
- package/out/src/storage/methods/availableManagedChange.d.ts.map +1 -0
- package/out/src/storage/methods/availableManagedChange.js +25 -0
- package/out/src/storage/methods/availableManagedChange.js.map +1 -0
- package/out/src/storage/methods/createAction.d.ts.map +1 -1
- package/out/src/storage/methods/createAction.js +521 -173
- 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 +219 -20
- 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 +1 -0
- package/out/src/storage/schema/KnexMigrations.d.ts.map +1 -1
- package/out/src/storage/schema/KnexMigrations.js +39 -1
- package/out/src/storage/schema/KnexMigrations.js.map +1 -1
- package/out/src/storage/schema/StorageIdbSchema.d.ts +2 -0
- package/out/src/storage/schema/StorageIdbSchema.d.ts.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 +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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
65
|
-
logger?.log(
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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,
|
|
160
|
+
const inputBeef = await mergeAllocatedChangeBeefs(storage, vargs, allocatedChange, beef, allocatedBeefPrefetch, parent);
|
|
94
161
|
logger?.log('merged allocated change beefs');
|
|
95
|
-
const inputs = await
|
|
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:
|
|
99
|
-
version:
|
|
100
|
-
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
|
-
|
|
114
|
-
|
|
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
|
-
/**
|
|
265
|
-
|
|
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
|
-
|
|
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 } =
|
|
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
|
|
456
|
+
status,
|
|
361
457
|
reference: (0, utilityHelpers_1.randomBytesBase64)(12),
|
|
362
|
-
satoshis
|
|
458
|
+
satoshis,
|
|
363
459
|
userId,
|
|
364
460
|
isOutgoing: true,
|
|
365
|
-
inputBEEF: storageBeef
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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 =
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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:
|
|
629
|
-
satoshis:
|
|
630
|
-
lockingScriptLength:
|
|
721
|
+
fixedOutputs: xoutputs.map(output => ({
|
|
722
|
+
satoshis: output.satoshis,
|
|
723
|
+
lockingScriptLength: output.lockingScript.length / 2
|
|
631
724
|
})),
|
|
632
|
-
feeModel
|
|
633
|
-
changeInitialSatoshis: Math.max(1,
|
|
634
|
-
changeFirstSatoshis: Math.max(1, Math.round(
|
|
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:
|
|
730
|
+
targetNetCount: changeBasket.numberOfDesiredUTXOs - availableChangeCount,
|
|
638
731
|
randomVals: vargs.randomVals
|
|
639
732
|
};
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
878
|
+
}));
|
|
879
|
+
return {
|
|
880
|
+
hydratedScriptCount: missing.filter(output => output.lockingScript?.length === output.scriptLength).length,
|
|
881
|
+
scriptSourceTransactionCount: groups.length
|
|
681
882
|
};
|
|
682
|
-
|
|
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
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
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
|
|
779
|
-
const
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|