@bsv/wallet-toolbox 2.6.4 → 2.7.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 +68 -3
- package/README.md +68 -12
- package/docs/action-batch-planning.md +80 -37
- package/docs/managed-change-liquidity.md +251 -0
- package/out/src/Setup.d.ts +9 -0
- package/out/src/Setup.d.ts.map +1 -1
- package/out/src/Setup.js +2 -0
- package/out/src/Setup.js.map +1 -1
- package/out/src/SetupClient.d.ts +3 -0
- package/out/src/SetupClient.d.ts.map +1 -1
- package/out/src/SetupClient.js +1 -0
- package/out/src/SetupClient.js.map +1 -1
- package/out/src/SetupWallet.d.ts +3 -0
- package/out/src/SetupWallet.d.ts.map +1 -1
- package/out/src/Wallet.d.ts +4 -3
- package/out/src/Wallet.d.ts.map +1 -1
- package/out/src/Wallet.js +4 -3
- package/out/src/Wallet.js.map +1 -1
- package/out/src/WalletPermissionsManager.d.ts.map +1 -1
- package/out/src/WalletPermissionsManager.js +5 -8
- package/out/src/WalletPermissionsManager.js.map +1 -1
- package/out/src/mockchain/MockServices.d.ts.map +1 -1
- package/out/src/mockchain/MockServices.js +2 -6
- package/out/src/mockchain/MockServices.js.map +1 -1
- package/out/src/monitor/Monitor.d.ts.map +1 -1
- package/out/src/monitor/Monitor.js +5 -4
- package/out/src/monitor/Monitor.js.map +1 -1
- package/out/src/monitor/tasks/TaskArcSSE.d.ts +4 -1
- package/out/src/monitor/tasks/TaskArcSSE.d.ts.map +1 -1
- package/out/src/monitor/tasks/TaskArcSSE.js +92 -65
- package/out/src/monitor/tasks/TaskArcSSE.js.map +1 -1
- package/out/src/monitor/tasks/TaskReconcilePendingTransactions.d.ts +25 -0
- package/out/src/monitor/tasks/TaskReconcilePendingTransactions.d.ts.map +1 -0
- package/out/src/monitor/tasks/TaskReconcilePendingTransactions.js +181 -0
- package/out/src/monitor/tasks/TaskReconcilePendingTransactions.js.map +1 -0
- package/out/src/monitor/tasks/TaskReviewDoubleSpends.d.ts.map +1 -1
- package/out/src/monitor/tasks/TaskReviewDoubleSpends.js +15 -3
- package/out/src/monitor/tasks/TaskReviewDoubleSpends.js.map +1 -1
- package/out/src/monitor/tasks/TaskReviewUtxos.d.ts +29 -2
- package/out/src/monitor/tasks/TaskReviewUtxos.d.ts.map +1 -1
- package/out/src/monitor/tasks/TaskReviewUtxos.js +155 -7
- package/out/src/monitor/tasks/TaskReviewUtxos.js.map +1 -1
- package/out/src/monitor/tasks/index.all.d.ts +1 -0
- package/out/src/monitor/tasks/index.all.d.ts.map +1 -1
- package/out/src/monitor/tasks/index.all.js +1 -0
- package/out/src/monitor/tasks/index.all.js.map +1 -1
- package/out/src/sdk/ActionBatch.interfaces.d.ts +19 -0
- package/out/src/sdk/ActionBatch.interfaces.d.ts.map +1 -1
- package/out/src/sdk/WERR_errors.d.ts +23 -0
- package/out/src/sdk/WERR_errors.d.ts.map +1 -1
- package/out/src/sdk/WERR_errors.js +44 -1
- package/out/src/sdk/WERR_errors.js.map +1 -1
- package/out/src/sdk/WalletError.d.ts.map +1 -1
- package/out/src/sdk/WalletError.js +7 -21
- package/out/src/sdk/WalletError.js.map +1 -1
- package/out/src/sdk/WalletErrorFromJson.d.ts.map +1 -1
- package/out/src/sdk/WalletErrorFromJson.js +9 -1
- package/out/src/sdk/WalletErrorFromJson.js.map +1 -1
- package/out/src/sdk/WalletServices.interfaces.d.ts +14 -1
- package/out/src/sdk/WalletServices.interfaces.d.ts.map +1 -1
- package/out/src/sdk/WalletStorage.interfaces.d.ts +3 -1
- package/out/src/sdk/WalletStorage.interfaces.d.ts.map +1 -1
- package/out/src/sdk/types.d.ts +3 -2
- package/out/src/sdk/types.d.ts.map +1 -1
- package/out/src/sdk/types.js +3 -2
- package/out/src/sdk/types.js.map +1 -1
- package/out/src/services/Services.d.ts.map +1 -1
- package/out/src/services/Services.js +73 -15
- package/out/src/services/Services.js.map +1 -1
- package/out/src/services/classifyOutputUtxo.d.ts +25 -0
- package/out/src/services/classifyOutputUtxo.d.ts.map +1 -0
- package/out/src/services/classifyOutputUtxo.js +87 -0
- package/out/src/services/classifyOutputUtxo.js.map +1 -0
- package/out/src/services/createDefaultWalletServicesOptions.d.ts +16 -3
- package/out/src/services/createDefaultWalletServicesOptions.d.ts.map +1 -1
- package/out/src/services/createDefaultWalletServicesOptions.js +38 -8
- package/out/src/services/createDefaultWalletServicesOptions.js.map +1 -1
- package/out/src/services/providers/ArcSSEClient.d.ts +13 -4
- package/out/src/services/providers/ArcSSEClient.d.ts.map +1 -1
- package/out/src/services/providers/ArcSSEClient.js +30 -7
- package/out/src/services/providers/ArcSSEClient.js.map +1 -1
- package/out/src/services/providers/Arcade.d.ts +10 -1
- package/out/src/services/providers/Arcade.d.ts.map +1 -1
- package/out/src/services/providers/Arcade.js +63 -0
- package/out/src/services/providers/Arcade.js.map +1 -1
- package/out/src/services/providers/arcadeStatus.d.ts +18 -0
- package/out/src/services/providers/arcadeStatus.d.ts.map +1 -0
- package/out/src/services/providers/arcadeStatus.js +56 -0
- package/out/src/services/providers/arcadeStatus.js.map +1 -0
- package/out/src/signer/actionBatch/ActionBatchPlanner.d.ts.map +1 -1
- package/out/src/signer/actionBatch/ActionBatchPlanner.js +25 -3
- package/out/src/signer/actionBatch/ActionBatchPlanner.js.map +1 -1
- package/out/src/signer/actionBatch/ActionBatchWorkspace.d.ts +2 -0
- package/out/src/signer/actionBatch/ActionBatchWorkspace.d.ts.map +1 -1
- package/out/src/signer/actionBatch/ActionBatchWorkspace.js +135 -16
- package/out/src/signer/actionBatch/ActionBatchWorkspace.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 +24 -1
- package/out/src/storage/StorageIdb.js.map +1 -1
- package/out/src/storage/StorageKnex.d.ts +1 -1
- package/out/src/storage/StorageKnex.d.ts.map +1 -1
- package/out/src/storage/StorageKnex.js +4 -4
- package/out/src/storage/StorageKnex.js.map +1 -1
- package/out/src/storage/StorageProvider.d.ts +18 -2
- package/out/src/storage/StorageProvider.d.ts.map +1 -1
- package/out/src/storage/StorageProvider.js +45 -14
- package/out/src/storage/StorageProvider.js.map +1 -1
- package/out/src/storage/StorageReaderWriter.d.ts.map +1 -1
- package/out/src/storage/StorageReaderWriter.js +3 -2
- package/out/src/storage/StorageReaderWriter.js.map +1 -1
- package/out/src/storage/WalletStorageManager.d.ts +12 -0
- package/out/src/storage/WalletStorageManager.d.ts.map +1 -1
- package/out/src/storage/WalletStorageManager.js +22 -2
- package/out/src/storage/WalletStorageManager.js.map +1 -1
- package/out/src/storage/adminServer/adminServer.d.ts +2 -0
- package/out/src/storage/adminServer/adminServer.d.ts.map +1 -1
- package/out/src/storage/adminServer/adminServer.js +25 -8
- package/out/src/storage/adminServer/adminServer.js.map +1 -1
- package/out/src/storage/adminServer/adminUi.d.ts.map +1 -1
- package/out/src/storage/adminServer/adminUi.js +32 -9
- package/out/src/storage/adminServer/adminUi.js.map +1 -1
- package/out/src/storage/adminServer/reviewUtxos.d.ts +27 -0
- package/out/src/storage/adminServer/reviewUtxos.d.ts.map +1 -0
- package/out/src/storage/adminServer/reviewUtxos.js +62 -0
- package/out/src/storage/adminServer/reviewUtxos.js.map +1 -0
- package/out/src/storage/index.all.d.ts +1 -0
- package/out/src/storage/index.all.d.ts.map +1 -1
- package/out/src/storage/index.all.js +1 -0
- package/out/src/storage/index.all.js.map +1 -1
- package/out/src/storage/index.client.d.ts +1 -0
- package/out/src/storage/index.client.d.ts.map +1 -1
- package/out/src/storage/index.client.js +1 -0
- package/out/src/storage/index.client.js.map +1 -1
- package/out/src/storage/index.mobile.d.ts +1 -0
- package/out/src/storage/index.mobile.d.ts.map +1 -1
- package/out/src/storage/index.mobile.js +1 -0
- package/out/src/storage/index.mobile.js.map +1 -1
- package/out/src/storage/methods/ListOutputsSpecOp.d.ts.map +1 -1
- package/out/src/storage/methods/ListOutputsSpecOp.js +4 -44
- package/out/src/storage/methods/ListOutputsSpecOp.js.map +1 -1
- package/out/src/storage/methods/actionBatch.d.ts +9 -2
- package/out/src/storage/methods/actionBatch.d.ts.map +1 -1
- package/out/src/storage/methods/actionBatch.js +258 -66
- package/out/src/storage/methods/actionBatch.js.map +1 -1
- package/out/src/storage/methods/actionBatchBlobs.js +7 -4
- package/out/src/storage/methods/actionBatchBlobs.js.map +1 -1
- package/out/src/storage/methods/attemptToPostReqsToNetwork.d.ts.map +1 -1
- package/out/src/storage/methods/attemptToPostReqsToNetwork.js +2 -66
- package/out/src/storage/methods/attemptToPostReqsToNetwork.js.map +1 -1
- package/out/src/storage/methods/availableManagedChange.d.ts +8 -1
- 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 +183 -51
- package/out/src/storage/methods/createAction.js.map +1 -1
- package/out/src/storage/methods/generateChange.d.ts +13 -1
- package/out/src/storage/methods/generateChange.d.ts.map +1 -1
- package/out/src/storage/methods/generateChange.js +169 -12
- package/out/src/storage/methods/generateChange.js.map +1 -1
- package/out/src/storage/methods/listActionsIdb.d.ts.map +1 -1
- package/out/src/storage/methods/listActionsIdb.js +4 -0
- package/out/src/storage/methods/listActionsIdb.js.map +1 -1
- package/out/src/storage/methods/listActionsKnex.d.ts.map +1 -1
- package/out/src/storage/methods/listActionsKnex.js +4 -0
- package/out/src/storage/methods/listActionsKnex.js.map +1 -1
- package/out/src/storage/methods/listOutputsIdb.d.ts.map +1 -1
- package/out/src/storage/methods/listOutputsIdb.js +2 -1
- package/out/src/storage/methods/listOutputsIdb.js.map +1 -1
- package/out/src/storage/methods/managedChangePolicy.d.ts +44 -0
- package/out/src/storage/methods/managedChangePolicy.d.ts.map +1 -0
- package/out/src/storage/methods/managedChangePolicy.js +58 -0
- package/out/src/storage/methods/managedChangePolicy.js.map +1 -0
- package/out/src/storage/methods/reconcileFailedTransactionInputs.d.ts +26 -0
- package/out/src/storage/methods/reconcileFailedTransactionInputs.d.ts.map +1 -0
- package/out/src/storage/methods/reconcileFailedTransactionInputs.js +124 -0
- package/out/src/storage/methods/reconcileFailedTransactionInputs.js.map +1 -0
- package/out/src/storage/methods/reviewUtxoOutputs.d.ts +37 -0
- package/out/src/storage/methods/reviewUtxoOutputs.d.ts.map +1 -0
- package/out/src/storage/methods/reviewUtxoOutputs.js +150 -0
- package/out/src/storage/methods/reviewUtxoOutputs.js.map +1 -0
- package/out/src/storage/portable/index.d.ts.map +1 -1
- package/out/src/storage/portable/index.js +4 -2
- package/out/src/storage/portable/index.js.map +1 -1
- package/out/src/storage/remoting/StorageClientBase.d.ts +2 -1
- package/out/src/storage/remoting/StorageClientBase.d.ts.map +1 -1
- package/out/src/storage/remoting/StorageClientBase.js +3 -0
- package/out/src/storage/remoting/StorageClientBase.js.map +1 -1
- package/out/src/storage/remoting/StorageServer.d.ts.map +1 -1
- package/out/src/storage/remoting/StorageServer.js +4 -1
- package/out/src/storage/remoting/StorageServer.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 +29 -3
- package/out/src/storage/schema/KnexMigrations.js.map +1 -1
- package/out/src/storage/schema/entities/EntityOutputBasket.d.ts.map +1 -1
- package/out/src/storage/schema/entities/EntityOutputBasket.js +5 -2
- package/out/src/storage/schema/entities/EntityOutputBasket.js.map +1 -1
- package/out/src/utility/brc153ReferenceLabels.d.ts +21 -0
- package/out/src/utility/brc153ReferenceLabels.d.ts.map +1 -0
- package/out/src/utility/brc153ReferenceLabels.js +45 -0
- package/out/src/utility/brc153ReferenceLabels.js.map +1 -0
- package/out/src/utility/index.all.d.ts +1 -0
- package/out/src/utility/index.all.d.ts.map +1 -1
- package/out/src/utility/index.all.js +1 -0
- package/out/src/utility/index.all.js.map +1 -1
- package/out/src/utility/index.client.d.ts +1 -0
- package/out/src/utility/index.client.d.ts.map +1 -1
- package/out/src/utility/index.client.js +1 -0
- package/out/src/utility/index.client.js.map +1 -1
- package/out/src/utility/utilityHelpers.d.ts +1 -1
- package/out/src/utility/utilityHelpers.d.ts.map +1 -1
- package/out/src/utility/utilityHelpers.js +2 -1
- package/out/src/utility/utilityHelpers.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ACTION_BATCH_MAX_RESERVATION_EXTENSION_OUTPUTS = exports.ACTION_BATCH_HARD_LIFETIME_MS = exports.ACTION_BATCH_LEASE_MS = void 0;
|
|
3
|
+
exports.ACTION_BATCH_MAX_RESERVED_OUTPUTS = exports.ACTION_BATCH_MAX_RESERVATION_EXTENSION_OUTPUTS = exports.ACTION_BATCH_HARD_LIFETIME_MS = exports.ACTION_BATCH_LEASE_MS = void 0;
|
|
4
4
|
exports.getActionBatchCapabilities = getActionBatchCapabilities;
|
|
5
5
|
exports.cleanupExpiredActionBatches = cleanupExpiredActionBatches;
|
|
6
6
|
exports.beginActionBatch = beginActionBatch;
|
|
7
7
|
exports.extendActionBatch = extendActionBatch;
|
|
8
8
|
exports.renewActionBatch = renewActionBatch;
|
|
9
|
+
exports.resumeActionBatch = resumeActionBatch;
|
|
9
10
|
exports.commitActionBatch = commitActionBatch;
|
|
10
11
|
exports.commitActionBatchByDigest = commitActionBatchByDigest;
|
|
11
12
|
exports.abortActionBatch = abortActionBatch;
|
|
@@ -29,7 +30,14 @@ exports.ACTION_BATCH_HARD_LIFETIME_MS = 60 * 60 * 1000;
|
|
|
29
30
|
const INITIAL_RESERVATION_LIMIT = 8;
|
|
30
31
|
const INITIAL_EXTRA_OUTPUTS = 3;
|
|
31
32
|
exports.ACTION_BATCH_MAX_RESERVATION_EXTENSION_OUTPUTS = 64;
|
|
32
|
-
|
|
33
|
+
exports.ACTION_BATCH_MAX_RESERVED_OUTPUTS = 256;
|
|
34
|
+
function isValidOutpoint(outpoint) {
|
|
35
|
+
// Storage keys and canonical transaction IDs are lowercase. Rejecting an
|
|
36
|
+
// alternate spelling here is preferable to validating it and then silently
|
|
37
|
+
// missing the case-sensitive lookup below.
|
|
38
|
+
return /^[0-9a-f]{64}$/.test(outpoint.txid) && Number.isSafeInteger(outpoint.vout) && outpoint.vout >= 0;
|
|
39
|
+
}
|
|
40
|
+
function getActionBatchCapabilities(maxReservedOutputs = exports.ACTION_BATCH_MAX_RESERVED_OUTPUTS, supportsResume = false) {
|
|
33
41
|
return {
|
|
34
42
|
actionBatch: {
|
|
35
43
|
version: 1,
|
|
@@ -38,6 +46,8 @@ function getActionBatchCapabilities() {
|
|
|
38
46
|
maxConcurrentUploads: actionBatchBlobs_1.ACTION_BATCH_MAX_CONCURRENT_UPLOADS,
|
|
39
47
|
leaseMs: exports.ACTION_BATCH_LEASE_MS,
|
|
40
48
|
hardLifetimeMs: exports.ACTION_BATCH_HARD_LIFETIME_MS,
|
|
49
|
+
resume: supportsResume ? true : undefined,
|
|
50
|
+
maxReservedOutputs,
|
|
41
51
|
compactBegin: true,
|
|
42
52
|
manifestVersion: 2,
|
|
43
53
|
commitByDigest: true,
|
|
@@ -55,13 +65,28 @@ function getActionBatchCapabilities() {
|
|
|
55
65
|
};
|
|
56
66
|
}
|
|
57
67
|
function activeExpiry(batch, now = new Date()) {
|
|
58
|
-
return batch.status !== 'active' && batch.status !== 'prepared' ||
|
|
68
|
+
return ((batch.status !== 'active' && batch.status !== 'prepared') ||
|
|
59
69
|
batch.expiresAt.getTime() <= now.getTime() ||
|
|
60
|
-
batch.hardExpiresAt.getTime() <= now.getTime();
|
|
70
|
+
batch.hardExpiresAt.getTime() <= now.getTime());
|
|
71
|
+
}
|
|
72
|
+
function actionBatchErrorState(batch, now = new Date()) {
|
|
73
|
+
if (batch == null)
|
|
74
|
+
return 'missing';
|
|
75
|
+
if (batch.hardExpiresAt.getTime() <= now.getTime())
|
|
76
|
+
return 'hard-expired';
|
|
77
|
+
if (batch.status === 'aborted')
|
|
78
|
+
return 'aborted';
|
|
79
|
+
if (batch.status === 'committed')
|
|
80
|
+
return 'committed';
|
|
81
|
+
if (batch.status === 'expired' || batch.expiresAt.getTime() <= now.getTime())
|
|
82
|
+
return 'expired';
|
|
83
|
+
if (batch.status !== 'active' && batch.status !== 'prepared')
|
|
84
|
+
return 'inactive';
|
|
85
|
+
return undefined;
|
|
61
86
|
}
|
|
62
87
|
function canExpire(batch, now = new Date()) {
|
|
63
|
-
return (batch.status === 'active' || batch.status === 'prepared') &&
|
|
64
|
-
(batch.expiresAt.getTime() <= now.getTime() || batch.hardExpiresAt.getTime() <= now.getTime());
|
|
88
|
+
return ((batch.status === 'active' || batch.status === 'prepared') &&
|
|
89
|
+
(batch.expiresAt.getTime() <= now.getTime() || batch.hardExpiresAt.getTime() <= now.getTime()));
|
|
65
90
|
}
|
|
66
91
|
async function releaseBatchState(storage, batch, status, trx) {
|
|
67
92
|
await storage.deleteActionBatchOutputReservations(batch.actionBatchId, trx);
|
|
@@ -92,7 +117,10 @@ function sourceOutputFromBeef(beef, outpoint) {
|
|
|
92
117
|
const output = tx?.outputs[outpoint.vout];
|
|
93
118
|
if (output == null)
|
|
94
119
|
return undefined;
|
|
95
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
satoshis: sdk_1.Validation.validateSatoshis(output.satoshis, 'source output satoshis'),
|
|
122
|
+
lockingScript: output.lockingScript
|
|
123
|
+
};
|
|
96
124
|
}
|
|
97
125
|
async function resolveExplicitOutputs(storage, userId, args, allowDeferredProofs) {
|
|
98
126
|
const byOutpoint = await storage.findOutputsByOutpoints(userId, args.inputs.map(input => input.outpoint));
|
|
@@ -142,12 +170,19 @@ function estimateFirstActionTarget(storage, args, inputSatoshis, outputScriptLen
|
|
|
142
170
|
outputLengths.push(25);
|
|
143
171
|
outputLengths.push(25);
|
|
144
172
|
const fee = (0, StorageProvider_1.validateStorageFeeModel)(storage.feeModel).value ?? 0;
|
|
145
|
-
const minFee = Math.ceil((0, utils_1.transactionSize)([...inputLengths, 107], outputLengths) * fee / 1000);
|
|
173
|
+
const minFee = Math.ceil(((0, utils_1.transactionSize)([...inputLengths, 107], outputLengths) * fee) / 1000);
|
|
146
174
|
return Math.max(1, outputSatoshis + minFee - inputSatoshis);
|
|
147
175
|
}
|
|
176
|
+
function selectReservationChange(candidates, targetSatoshis) {
|
|
177
|
+
for (const status of ['completed', 'unproven', 'sending']) {
|
|
178
|
+
const selected = (0, actionPlanning_1.selectCanonicalChange)(candidates.filter(candidate => candidate.transactionStatus === status), targetSatoshis);
|
|
179
|
+
if (selected != null)
|
|
180
|
+
return selected;
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
148
184
|
function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLimit, planningCosts) {
|
|
149
|
-
const remaining = candidates
|
|
150
|
-
.filter(output => output.satoshis > planningCosts.marginalInputFee);
|
|
185
|
+
const remaining = candidates.filter(output => output.satoshis > planningCosts.marginalInputFee);
|
|
151
186
|
const chosen = [];
|
|
152
187
|
// A reservation is useful to generateChangeSdk only after it also covers
|
|
153
188
|
// the marginal funding-input fee and leaves an economically viable first
|
|
@@ -155,7 +190,7 @@ function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLi
|
|
|
155
190
|
// that satisfies the nominal deficit but can never close the real plan.
|
|
156
191
|
let deficit = targetSatoshis + planningCosts.firstChangeCost;
|
|
157
192
|
while (deficit > 0 && chosen.length < limit) {
|
|
158
|
-
const output = (
|
|
193
|
+
const output = selectReservationChange(remaining, deficit + planningCosts.marginalInputFee);
|
|
159
194
|
if (output == null)
|
|
160
195
|
break;
|
|
161
196
|
chosen.push(output);
|
|
@@ -164,7 +199,7 @@ function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLi
|
|
|
164
199
|
}
|
|
165
200
|
const desiredCount = fillLimit ? limit : Math.min(limit, chosen.length + extras);
|
|
166
201
|
while (remaining.length > 0 && chosen.length < desiredCount) {
|
|
167
|
-
const output = (
|
|
202
|
+
const output = selectReservationChange(remaining, targetSatoshis);
|
|
168
203
|
if (output == null)
|
|
169
204
|
break;
|
|
170
205
|
chosen.push(output);
|
|
@@ -172,17 +207,24 @@ function chooseReservationPool(candidates, targetSatoshis, limit, extras, fillLi
|
|
|
172
207
|
}
|
|
173
208
|
return chosen;
|
|
174
209
|
}
|
|
175
|
-
function
|
|
210
|
+
function reservationOutput(candidate) {
|
|
211
|
+
const { transactionStatus: _transactionStatus, ...output } = candidate;
|
|
212
|
+
return output;
|
|
213
|
+
}
|
|
214
|
+
function reservationPlanningCosts(storage, _basket) {
|
|
176
215
|
const satsPerKb = (0, StorageProvider_1.validateStorageFeeModel)(storage.feeModel).value ?? 0;
|
|
177
216
|
const minimumSpendSize = (0, utils_1.transactionSize)([107], [25]);
|
|
178
|
-
const minimumSpendFee = Math.ceil(minimumSpendSize * satsPerKb / 1000);
|
|
217
|
+
const minimumSpendFee = Math.ceil((minimumSpendSize * satsPerKb) / 1000);
|
|
179
218
|
const dustFloor = Math.max(1, minimumSpendFee * 2);
|
|
180
219
|
const marginalInputSize = (0, utils_1.transactionSize)([107], []) - (0, utils_1.transactionSize)([], []);
|
|
181
220
|
const marginalOutputSize = (0, utils_1.transactionSize)([], [25]) - (0, utils_1.transactionSize)([], []);
|
|
182
|
-
|
|
221
|
+
// Reserving inputs is compulsory-funding work, not pool shaping. Requiring
|
|
222
|
+
// the preferred liquidity value here could consume reservation capacity or
|
|
223
|
+
// reject a batch solely to manufacture optional change.
|
|
224
|
+
const desiredFirstChange = dustFloor;
|
|
183
225
|
return {
|
|
184
|
-
firstChangeCost: desiredFirstChange + Math.ceil(marginalOutputSize * satsPerKb / 1000),
|
|
185
|
-
marginalInputFee: Math.ceil(marginalInputSize * satsPerKb / 1000)
|
|
226
|
+
firstChangeCost: desiredFirstChange + Math.ceil((marginalOutputSize * satsPerKb) / 1000),
|
|
227
|
+
marginalInputFee: Math.ceil((marginalInputSize * satsPerKb) / 1000)
|
|
186
228
|
};
|
|
187
229
|
}
|
|
188
230
|
async function reserveOutputs(storage, batch, outputs, trx) {
|
|
@@ -251,33 +293,48 @@ function newBatch(userId, batchId) {
|
|
|
251
293
|
async function beginActionBatch(storage, auth, args) {
|
|
252
294
|
const userId = (0, utilityHelpers_1.verifyId)(auth.userId);
|
|
253
295
|
await cleanupExpiredActionBatches(storage);
|
|
254
|
-
if (await storage.findActionBatch(userId, args.batchId) != null) {
|
|
296
|
+
if ((await storage.findActionBatch(userId, args.batchId)) != null) {
|
|
255
297
|
throw new WERR_errors_1.WERR_INVALID_PARAMETER('batchId', 'unique');
|
|
256
298
|
}
|
|
257
299
|
const outputScriptLengths = args.firstActionOutputScriptLengths;
|
|
258
|
-
if (outputScriptLengths != null &&
|
|
259
|
-
outputScriptLengths.
|
|
260
|
-
|
|
261
|
-
output.lockingScript.length / 2 !== outputScriptLengths[index]))) {
|
|
300
|
+
if (outputScriptLengths != null &&
|
|
301
|
+
(outputScriptLengths.length !== args.firstAction.outputs.length ||
|
|
302
|
+
outputScriptLengths.some(length => !Number.isSafeInteger(length) || length < 0) ||
|
|
303
|
+
args.firstAction.outputs.some((output, index) => output.lockingScript.length > 0 && output.lockingScript.length / 2 !== outputScriptLengths[index]))) {
|
|
262
304
|
throw new WERR_errors_1.WERR_INVALID_PARAMETER('firstActionOutputScriptLengths', 'valid byte lengths aligned with firstAction outputs');
|
|
263
305
|
}
|
|
264
306
|
const changeBasket = (0, utilityHelpers_1.verifyOne)(await storage.findOutputBaskets({ partial: { userId, name: 'default' } }));
|
|
265
307
|
const explicit = await resolveExplicitOutputs(storage, userId, args.firstAction, outputScriptLengths != null);
|
|
266
308
|
const noSendChange = await resolveNoSendChangeOutputs(storage, userId, args.firstAction);
|
|
267
309
|
const fixedOutputIds = new Set([...explicit.outputs, ...noSendChange.outputs].map(output => output.outputId));
|
|
268
|
-
const
|
|
310
|
+
const availableOutputs = (await storage.findAvailableManagedChangeInputs(userId, changeBasket.basketId, false)).filter(output => !fixedOutputIds.has(output.outputId));
|
|
311
|
+
const availableStatuses = await storage.findTransactionStatusesByIds(userId, availableOutputs.map(output => output.transactionId));
|
|
312
|
+
const available = availableOutputs.map(output => ({
|
|
313
|
+
...output,
|
|
314
|
+
// An orphaned/missing status is never promoted ahead of known settled
|
|
315
|
+
// ancestry. Retain it only in the final last-resort reservation tier.
|
|
316
|
+
transactionStatus: availableStatuses.get(output.transactionId) ?? 'sending'
|
|
317
|
+
}));
|
|
269
318
|
const target = estimateFirstActionTarget(storage, args.firstAction, explicit.inputSatoshis + noSendChange.inputSatoshis, outputScriptLengths);
|
|
270
|
-
const fixedOutputs = [
|
|
271
|
-
|
|
319
|
+
const fixedOutputs = [
|
|
320
|
+
...new Map([...explicit.outputs, ...noSendChange.outputs].map(output => [output.outputId, output])).values()
|
|
321
|
+
];
|
|
322
|
+
const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
|
|
323
|
+
if (maxReservedOutputs >= 0 && fixedOutputs.length > maxReservedOutputs) {
|
|
324
|
+
throw new WERR_errors_1.WERR_INVALID_PARAMETER('firstAction', `no more than ${maxReservedOutputs} persisted inputs`);
|
|
325
|
+
}
|
|
326
|
+
const initialCapacity = maxReservedOutputs < 0 ? INITIAL_RESERVATION_LIMIT : Math.min(INITIAL_RESERVATION_LIMIT, maxReservedOutputs);
|
|
327
|
+
const requiredCapacity = Math.max(0, initialCapacity - fixedOutputs.length);
|
|
272
328
|
const funding = chooseReservationPool(available, target, requiredCapacity, INITIAL_EXTRA_OUTPUTS, false, reservationPlanningCosts(storage, changeBasket));
|
|
329
|
+
const fundingOutputs = funding.map(reservationOutput);
|
|
273
330
|
const batch = newBatch(userId, args.batchId);
|
|
274
331
|
await storage.transaction(async (trx) => {
|
|
275
332
|
await storage.insertActionBatch(batch, trx);
|
|
276
|
-
await reserveOutputs(storage, batch, [...fixedOutputs, ...
|
|
333
|
+
await reserveOutputs(storage, batch, [...fixedOutputs, ...fundingOutputs], trx);
|
|
277
334
|
});
|
|
278
335
|
let fundingResult;
|
|
279
336
|
try {
|
|
280
|
-
fundingResult = await makeFundingResult(storage, args.firstAction, [...
|
|
337
|
+
fundingResult = await makeFundingResult(storage, args.firstAction, [...fundingOutputs, ...fixedOutputs]);
|
|
281
338
|
}
|
|
282
339
|
catch (error) {
|
|
283
340
|
await storage.transaction(async (trx) => await releaseBatchState(storage, batch, 'aborted', trx));
|
|
@@ -292,55 +349,110 @@ async function beginActionBatch(storage, auth, args) {
|
|
|
292
349
|
feeModel: (0, StorageProvider_1.validateStorageFeeModel)(storage.feeModel),
|
|
293
350
|
commissionSatoshis: storage.commissionSatoshis,
|
|
294
351
|
commissionPubKeyHex: storage.commissionPubKeyHex,
|
|
295
|
-
availableChangeCount: available.length,
|
|
296
|
-
|
|
352
|
+
availableChangeCount: available.filter(output => output.satoshis >= Math.max(1, changeBasket.minimumDesiredUTXOValue)).length,
|
|
353
|
+
managedChangePolicy: {
|
|
354
|
+
maxOutputsPerAction: storage.managedChangePolicy.maxOutputsPerAction,
|
|
355
|
+
migrationInputsPerAction: storage.managedChangePolicy.migrationInputsPerAction
|
|
356
|
+
},
|
|
357
|
+
reservedOutputs: fundingResult.outputs.filter(output => fundingOutputs.some(candidate => candidate.outputId === output.outputId)),
|
|
297
358
|
explicitOutputs: fundingResult.outputs.filter(output => explicitIds.has(output.outputId)),
|
|
298
359
|
inputBeef: fundingResult.beef
|
|
299
360
|
};
|
|
300
361
|
}
|
|
301
|
-
function requireLiveBatch(batch) {
|
|
302
|
-
|
|
303
|
-
|
|
362
|
+
function requireLiveBatch(batch, batchId) {
|
|
363
|
+
const state = actionBatchErrorState(batch);
|
|
364
|
+
if (state != null)
|
|
365
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE(state, batchId ?? batch?.batchId);
|
|
366
|
+
if (batch == null)
|
|
367
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE('missing', batchId);
|
|
304
368
|
return batch;
|
|
305
369
|
}
|
|
306
370
|
async function extendActionBatch(storage, auth, args) {
|
|
307
371
|
const userId = (0, utilityHelpers_1.verifyId)(auth.userId);
|
|
308
372
|
await cleanupExpiredActionBatches(storage);
|
|
309
|
-
const batch = requireLiveBatch(await storage.findActionBatch(userId, args.batchId));
|
|
373
|
+
const batch = requireLiveBatch(await storage.findActionBatch(userId, args.batchId), args.batchId);
|
|
310
374
|
const basket = (0, utilityHelpers_1.verifyOne)(await storage.findOutputBaskets({ partial: { userId, name: 'default' } }));
|
|
311
375
|
const alreadyReserved = await storage.findActionBatchOutputIds(batch.actionBatchId);
|
|
312
|
-
const available = await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false);
|
|
313
376
|
if (!Number.isSafeInteger(args.requestedOutputs) || args.requestedOutputs < 0) {
|
|
314
377
|
throw new WERR_errors_1.WERR_INVALID_PARAMETER('requestedOutputs', 'non-negative safe integer');
|
|
315
378
|
}
|
|
316
|
-
|
|
317
|
-
|
|
379
|
+
if (!Number.isSafeInteger(args.targetSatoshis) || args.targetSatoshis < 0) {
|
|
380
|
+
throw new WERR_errors_1.WERR_INVALID_PARAMETER('targetSatoshis', 'non-negative safe integer');
|
|
381
|
+
}
|
|
382
|
+
const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
|
|
383
|
+
if ((maxReservedOutputs >= 0 && args.explicitOutpoints.length > maxReservedOutputs) ||
|
|
384
|
+
new Set(args.explicitOutpoints.map(outpoint => `${outpoint.txid}.${outpoint.vout}`)).size !==
|
|
385
|
+
args.explicitOutpoints.length ||
|
|
386
|
+
args.explicitOutpoints.some(outpoint => !isValidOutpoint(outpoint))) {
|
|
387
|
+
throw new WERR_errors_1.WERR_INVALID_PARAMETER('explicitOutpoints', `at most ${maxReservedOutputs < 0 ? 'the configured request capacity' : maxReservedOutputs} valid outpoints`);
|
|
388
|
+
}
|
|
389
|
+
const remainingCapacity = maxReservedOutputs < 0 ? Number.MAX_SAFE_INTEGER : maxReservedOutputs - alreadyReserved.length;
|
|
390
|
+
if (remainingCapacity <= 0 && (args.requestedOutputs > 0 || args.explicitOutpoints.length > 0)) {
|
|
391
|
+
throw new WERR_errors_1.WERR_INVALID_OPERATION(`action batch already holds the maximum of ${maxReservedOutputs} outputs`);
|
|
392
|
+
}
|
|
318
393
|
const explicitByOutpoint = await storage.findOutputsByOutpoints(userId, args.explicitOutpoints);
|
|
319
|
-
const explicit =
|
|
320
|
-
|
|
394
|
+
const explicit = [
|
|
395
|
+
...new Map(Object.values(explicitByOutpoint)
|
|
396
|
+
.filter(output => !alreadyReserved.includes(output.outputId))
|
|
397
|
+
.map(output => [output.outputId, output])).values()
|
|
398
|
+
];
|
|
399
|
+
if (explicit.length > remainingCapacity) {
|
|
400
|
+
throw new WERR_errors_1.WERR_INVALID_PARAMETER('explicitOutpoints', `no more than ${remainingCapacity} additional reserved outputs`);
|
|
401
|
+
}
|
|
402
|
+
const explicitIds = new Set(explicit.map(output => output.outputId));
|
|
403
|
+
const availableOutputs = (await storage.findAvailableManagedChangeInputs(userId, basket.basketId, false)).filter(output => !explicitIds.has(output.outputId));
|
|
404
|
+
const availableStatuses = await storage.findTransactionStatusesByIds(userId, availableOutputs.map(output => output.transactionId));
|
|
405
|
+
const available = availableOutputs.map(output => ({
|
|
406
|
+
...output,
|
|
407
|
+
transactionStatus: availableStatuses.get(output.transactionId) ?? 'sending'
|
|
408
|
+
}));
|
|
409
|
+
const requestedCount = Math.min(args.requestedOutputs, exports.ACTION_BATCH_MAX_RESERVATION_EXTENSION_OUTPUTS, remainingCapacity - explicit.length);
|
|
410
|
+
const funding = chooseReservationPool(available, Math.max(1, args.targetSatoshis), requestedCount, 0, true, reservationPlanningCosts(storage, basket));
|
|
411
|
+
const fundingOutputs = funding.map(reservationOutput);
|
|
321
412
|
const fundingShape = argsToFundingShape(args.includeSourceTransactions);
|
|
322
|
-
const fundingResult = await makeFundingResult(storage, fundingShape, [...
|
|
413
|
+
const fundingResult = await makeFundingResult(storage, fundingShape, [...fundingOutputs, ...explicit]);
|
|
323
414
|
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + exports.ACTION_BATCH_LEASE_MS));
|
|
324
415
|
await storage.transaction(async (trx) => {
|
|
325
|
-
const current = requireLiveBatch(await storage.findActionBatchForUpdate(userId, args.batchId, trx));
|
|
326
|
-
|
|
416
|
+
const current = requireLiveBatch(await storage.findActionBatchForUpdate(userId, args.batchId, trx), args.batchId);
|
|
417
|
+
const additions = [...new Map([...fundingOutputs, ...explicit].map(output => [output.outputId, output])).values()];
|
|
418
|
+
const currentReserved = new Set(await storage.findActionBatchOutputIds(current.actionBatchId, trx));
|
|
419
|
+
const newAdditions = additions.filter(output => !currentReserved.has(output.outputId));
|
|
420
|
+
if (maxReservedOutputs >= 0 && currentReserved.size + newAdditions.length > maxReservedOutputs) {
|
|
421
|
+
throw new WERR_errors_1.WERR_INVALID_OPERATION(`action batch cannot reserve more than ${maxReservedOutputs} outputs`);
|
|
422
|
+
}
|
|
423
|
+
await reserveOutputs(storage, current, newAdditions, trx);
|
|
327
424
|
await storage.updateActionBatch(current.actionBatchId, { expiresAt }, trx);
|
|
328
425
|
});
|
|
329
426
|
return {
|
|
330
427
|
expiresAt: expiresAt.toISOString(),
|
|
331
|
-
reservedOutputs: fundingResult.outputs.filter(output =>
|
|
428
|
+
reservedOutputs: fundingResult.outputs.filter(output => fundingOutputs.some(candidate => candidate.outputId === output.outputId)),
|
|
332
429
|
explicitOutputs: fundingResult.outputs.filter(output => explicit.some(candidate => candidate.outputId === output.outputId)),
|
|
333
430
|
inputBeef: fundingResult.beef
|
|
334
431
|
};
|
|
335
432
|
}
|
|
336
433
|
function argsToFundingShape(includeSourceTransactions) {
|
|
337
434
|
return {
|
|
338
|
-
inputs: [],
|
|
435
|
+
inputs: [],
|
|
436
|
+
outputs: [],
|
|
437
|
+
labels: [],
|
|
438
|
+
description: 'action batch extension',
|
|
439
|
+
version: 1,
|
|
440
|
+
lockTime: 0,
|
|
339
441
|
options: {
|
|
340
|
-
acceptDelayedBroadcast: true,
|
|
341
|
-
|
|
442
|
+
acceptDelayedBroadcast: true,
|
|
443
|
+
returnTXIDOnly: false,
|
|
444
|
+
noSend: true,
|
|
445
|
+
sendWith: [],
|
|
446
|
+
signAndProcess: true,
|
|
447
|
+
knownTxids: [],
|
|
448
|
+
noSendChange: [],
|
|
449
|
+
randomizeOutputs: true
|
|
342
450
|
},
|
|
343
|
-
isSendWith: false,
|
|
451
|
+
isSendWith: false,
|
|
452
|
+
isNewTx: true,
|
|
453
|
+
isRemixChange: false,
|
|
454
|
+
isNoSend: true,
|
|
455
|
+
isDelayed: true,
|
|
344
456
|
isTestWerrReviewActions: false,
|
|
345
457
|
isSignAction: includeSourceTransactions,
|
|
346
458
|
includeAllSourceTransactions: includeSourceTransactions
|
|
@@ -349,24 +461,95 @@ function argsToFundingShape(includeSourceTransactions) {
|
|
|
349
461
|
async function renewActionBatch(storage, auth, batchId) {
|
|
350
462
|
const userId = (0, utilityHelpers_1.verifyId)(auth.userId);
|
|
351
463
|
return await storage.transaction(async (trx) => {
|
|
352
|
-
const batch = requireLiveBatch(await storage.findActionBatchForUpdate(userId, batchId, trx));
|
|
464
|
+
const batch = requireLiveBatch(await storage.findActionBatchForUpdate(userId, batchId, trx), batchId);
|
|
353
465
|
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + exports.ACTION_BATCH_LEASE_MS));
|
|
354
466
|
await storage.updateActionBatch(batch.actionBatchId, { expiresAt }, trx);
|
|
355
467
|
return { expiresAt: expiresAt.toISOString() };
|
|
356
468
|
});
|
|
357
469
|
}
|
|
470
|
+
function validateResumeOutpoints(storage, args) {
|
|
471
|
+
const unique = new Set(args.outpoints.map(outpoint => `${outpoint.txid}.${outpoint.vout}`));
|
|
472
|
+
const maxReservedOutputs = storage.actionBatchMaxReservedOutputs;
|
|
473
|
+
if ((maxReservedOutputs >= 0 && args.outpoints.length > maxReservedOutputs) ||
|
|
474
|
+
unique.size !== args.outpoints.length ||
|
|
475
|
+
args.outpoints.some(outpoint => !isValidOutpoint(outpoint))) {
|
|
476
|
+
throw new WERR_errors_1.WERR_INVALID_PARAMETER('outpoints', `at most ${maxReservedOutputs < 0 ? 'the configured request capacity' : maxReservedOutputs} unique valid outpoints`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Reacquire exactly the persisted inputs owned by one expired client
|
|
481
|
+
* workspace. This is deliberately explicit: another action may not become a
|
|
482
|
+
* member merely because it happens to use the same Wallet instance.
|
|
483
|
+
*/
|
|
484
|
+
async function resumeActionBatch(storage, auth, args) {
|
|
485
|
+
const userId = (0, utilityHelpers_1.verifyId)(auth.userId);
|
|
486
|
+
validateResumeOutpoints(storage, args);
|
|
487
|
+
await cleanupExpiredActionBatches(storage);
|
|
488
|
+
return await storage.transaction(async (trx) => {
|
|
489
|
+
const batch = await storage.findActionBatchForUpdate(userId, args.batchId, trx);
|
|
490
|
+
const state = actionBatchErrorState(batch);
|
|
491
|
+
if (state != null && state !== 'expired') {
|
|
492
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE(state, args.batchId);
|
|
493
|
+
}
|
|
494
|
+
if (batch == null)
|
|
495
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE('missing', args.batchId);
|
|
496
|
+
const expiresAt = new Date(Math.min(batch.hardExpiresAt.getTime(), Date.now() + exports.ACTION_BATCH_LEASE_MS));
|
|
497
|
+
if (state == null) {
|
|
498
|
+
const reserved = new Set(await storage.findActionBatchOutputIds(batch.actionBatchId, trx));
|
|
499
|
+
const current = await storage.findOutputsByOutpointsForUpdate(userId, args.outpoints, trx);
|
|
500
|
+
if (Object.values(current).some(output => !reserved.has(output.outputId)) ||
|
|
501
|
+
Object.keys(current).length !== args.outpoints.length ||
|
|
502
|
+
reserved.size !== args.outpoints.length) {
|
|
503
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE('conflicted', args.batchId);
|
|
504
|
+
}
|
|
505
|
+
await storage.updateActionBatch(batch.actionBatchId, { expiresAt }, trx);
|
|
506
|
+
return { expiresAt: expiresAt.toISOString() };
|
|
507
|
+
}
|
|
508
|
+
const stored = await storage.findOutputsByOutpointsForUpdate(userId, args.outpoints, trx);
|
|
509
|
+
const outputs = args.outpoints.map(outpoint => stored[`${outpoint.txid}.${outpoint.vout}`]);
|
|
510
|
+
if (outputs.some(output => output == null || !output.spendable || output.spentBy != null)) {
|
|
511
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE('conflicted', args.batchId);
|
|
512
|
+
}
|
|
513
|
+
await storage.deleteActionBatchOutputReservations(batch.actionBatchId, trx);
|
|
514
|
+
const exactOutputs = outputs;
|
|
515
|
+
const conflicts = await storage.findReservedActionBatchOutputIds(exactOutputs.map(output => output.outputId), trx);
|
|
516
|
+
if (conflicts.length > 0)
|
|
517
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE('conflicted', args.batchId);
|
|
518
|
+
const now = new Date();
|
|
519
|
+
await storage.reserveActionBatchOutputs(exactOutputs.map(output => ({
|
|
520
|
+
actionBatchId: batch.actionBatchId,
|
|
521
|
+
outputId: output.outputId,
|
|
522
|
+
created_at: now,
|
|
523
|
+
updated_at: now
|
|
524
|
+
})), trx);
|
|
525
|
+
await storage.updateActionBatch(batch.actionBatchId, {
|
|
526
|
+
status: 'active',
|
|
527
|
+
expiresAt,
|
|
528
|
+
manifest: undefined,
|
|
529
|
+
manifestDigest: undefined,
|
|
530
|
+
uploadDigests: undefined
|
|
531
|
+
}, trx);
|
|
532
|
+
return { expiresAt: expiresAt.toISOString() };
|
|
533
|
+
});
|
|
534
|
+
}
|
|
358
535
|
async function reacquireManifestInputs(storage, userId, batch, validated, trx) {
|
|
359
536
|
if (batch.hardExpiresAt.getTime() <= Date.now()) {
|
|
360
|
-
throw new WERR_errors_1.
|
|
537
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE('hard-expired', batch.batchId);
|
|
361
538
|
}
|
|
362
539
|
const stagedTxids = new Set(validated.actions.map(({ action }) => action.txid));
|
|
363
|
-
const outpoints = [
|
|
540
|
+
const outpoints = [
|
|
541
|
+
...new Map(validated.actions
|
|
542
|
+
.flatMap(({ action }) => action.plan.inputs)
|
|
364
543
|
.filter(input => !stagedTxids.has(input.sourceTxid))
|
|
365
|
-
.map(input => [
|
|
544
|
+
.map(input => [
|
|
545
|
+
`${input.sourceTxid}.${input.sourceVout}`,
|
|
546
|
+
{
|
|
366
547
|
txid: input.sourceTxid,
|
|
367
548
|
vout: input.sourceVout,
|
|
368
549
|
providedBy: input.providedBy
|
|
369
|
-
}
|
|
550
|
+
}
|
|
551
|
+
])).values()
|
|
552
|
+
];
|
|
370
553
|
const stored = await storage.findOutputsByOutpointsForUpdate(userId, outpoints, trx);
|
|
371
554
|
await storage.deleteActionBatchOutputReservations(batch.actionBatchId, trx);
|
|
372
555
|
const outputs = [];
|
|
@@ -545,7 +728,7 @@ async function persistManifestAtomically(storage, userId, batch, manifest, valid
|
|
|
545
728
|
return await storage.transaction(async (trx) => {
|
|
546
729
|
const current = await storage.findActionBatchForUpdate(userId, batch.batchId, trx);
|
|
547
730
|
if (current == null)
|
|
548
|
-
throw new WERR_errors_1.
|
|
731
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE('missing', batch.batchId);
|
|
549
732
|
if (current.status === 'committed') {
|
|
550
733
|
if (current.manifestDigest !== manifest.digest) {
|
|
551
734
|
throw new WERR_errors_1.WERR_INVALID_OPERATION('batch committed with another manifest');
|
|
@@ -553,30 +736,36 @@ async function persistManifestAtomically(storage, userId, batch, manifest, valid
|
|
|
553
736
|
return { batch: current, alreadyCommitted: true };
|
|
554
737
|
}
|
|
555
738
|
if (current.status === 'aborted')
|
|
556
|
-
throw new WERR_errors_1.
|
|
739
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE('aborted', batch.batchId);
|
|
557
740
|
if (current.manifestDigest != null && current.manifestDigest !== manifest.digest) {
|
|
558
741
|
throw new WERR_errors_1.WERR_INVALID_OPERATION('batch prepared with another manifest');
|
|
559
742
|
}
|
|
560
743
|
const needsReacquire = current.status === 'expired' || activeExpiry(current);
|
|
561
744
|
if (needsReacquire) {
|
|
562
745
|
if (current.hardExpiresAt.getTime() <= Date.now()) {
|
|
563
|
-
throw new WERR_errors_1.
|
|
746
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE('hard-expired', batch.batchId);
|
|
564
747
|
}
|
|
565
748
|
await reacquireManifestInputs(storage, userId, current, validated, trx);
|
|
566
749
|
}
|
|
567
750
|
else {
|
|
568
|
-
requireLiveBatch(current);
|
|
751
|
+
requireLiveBatch(current, batch.batchId);
|
|
569
752
|
}
|
|
570
753
|
const reservedOutputIds = new Set(await storage.findActionBatchOutputIds(current.actionBatchId, trx));
|
|
571
754
|
const stagedTxids = new Set(validated.actions.map(({ action }) => action.txid));
|
|
572
|
-
const inputOutpoints = [
|
|
755
|
+
const inputOutpoints = [
|
|
756
|
+
...new Map(validated.actions
|
|
757
|
+
.flatMap(({ action }) => action.plan.inputs)
|
|
573
758
|
.filter(input => !stagedTxids.has(input.sourceTxid))
|
|
574
|
-
.map(input => [
|
|
759
|
+
.map(input => [
|
|
760
|
+
`${input.sourceTxid}.${input.sourceVout}`,
|
|
761
|
+
{
|
|
575
762
|
txid: input.sourceTxid,
|
|
576
763
|
vout: input.sourceVout
|
|
577
|
-
}
|
|
764
|
+
}
|
|
765
|
+
])).values()
|
|
766
|
+
];
|
|
578
767
|
const storedByOutpoint = await storage.findOutputsByOutpointsForUpdate(userId, inputOutpoints, trx);
|
|
579
|
-
const allBasketNames = validated.actions.flatMap(({ action }) => action.plan.outputs.flatMap(output => output.basket == null ? [] : [output.basket]));
|
|
768
|
+
const allBasketNames = validated.actions.flatMap(({ action }) => action.plan.outputs.flatMap(output => (output.basket == null ? [] : [output.basket])));
|
|
580
769
|
if (validated.actions.some(({ action }) => action.plan.outputs.some(output => output.purpose === 'change')))
|
|
581
770
|
allBasketNames.push('default');
|
|
582
771
|
const baskets = await storage.findOrInsertOutputBasketsBulk(userId, [...new Set(allBasketNames)], trx);
|
|
@@ -651,19 +840,19 @@ async function completeCommittedBatch(storage, userId, batch, manifest, alreadyC
|
|
|
651
840
|
async function commitActionBatchOnce(storage, userId, manifest) {
|
|
652
841
|
const batch = await storage.findActionBatch(userId, manifest.batchId);
|
|
653
842
|
if (batch == null)
|
|
654
|
-
throw new WERR_errors_1.
|
|
843
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE('missing', manifest.batchId);
|
|
655
844
|
if (batch.status === 'committed') {
|
|
656
845
|
if (batch.manifestDigest !== manifest.digest)
|
|
657
846
|
throw new WERR_errors_1.WERR_INVALID_OPERATION('batch committed with another manifest');
|
|
658
847
|
return await completeCommittedBatch(storage, userId, batch, manifest, true);
|
|
659
848
|
}
|
|
660
849
|
if (batch.status === 'aborted')
|
|
661
|
-
throw new WERR_errors_1.
|
|
850
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE('aborted', manifest.batchId);
|
|
662
851
|
const needsReacquire = batch.status === 'expired' || activeExpiry(batch);
|
|
663
852
|
if (!needsReacquire)
|
|
664
|
-
requireLiveBatch(batch);
|
|
853
|
+
requireLiveBatch(batch, manifest.batchId);
|
|
665
854
|
else if (batch.hardExpiresAt.getTime() <= Date.now()) {
|
|
666
|
-
throw new WERR_errors_1.
|
|
855
|
+
throw new WERR_errors_1.WERR_ACTION_BATCH_STATE('hard-expired', manifest.batchId);
|
|
667
856
|
}
|
|
668
857
|
if (batch.manifestDigest != null && batch.manifestDigest !== manifest.digest) {
|
|
669
858
|
throw new WERR_errors_1.WERR_INVALID_OPERATION('batch prepared with another manifest');
|
|
@@ -692,8 +881,9 @@ async function commitActionBatch(storage, auth, manifest) {
|
|
|
692
881
|
}
|
|
693
882
|
return await active.promise;
|
|
694
883
|
}
|
|
695
|
-
const promise = commitActionBatchOnce(storage, userId, manifest)
|
|
696
|
-
|
|
884
|
+
const promise = commitActionBatchOnce(storage, userId, manifest).finally(() => {
|
|
885
|
+
commits?.delete(key);
|
|
886
|
+
});
|
|
697
887
|
commits.set(key, { digest: manifest.digest, promise });
|
|
698
888
|
return await promise;
|
|
699
889
|
}
|
|
@@ -704,7 +894,9 @@ async function commitActionBatchByDigest(storage, auth, args) {
|
|
|
704
894
|
throw new WERR_errors_1.WERR_INVALID_OPERATION('prepared action batch manifest was not found');
|
|
705
895
|
}
|
|
706
896
|
const manifest = JSON.parse(batch.manifest);
|
|
707
|
-
if (manifest.format !== 2 ||
|
|
897
|
+
if (manifest.format !== 2 ||
|
|
898
|
+
manifest.batchId !== args.batchId ||
|
|
899
|
+
manifest.digest !== args.digest ||
|
|
708
900
|
!(0, actionBatchDigest_1.verifyActionBatchManifestDigest)(manifest)) {
|
|
709
901
|
throw new WERR_errors_1.WERR_INVALID_OPERATION('prepared action batch manifest is invalid');
|
|
710
902
|
}
|