@bsv/wallet-toolbox 2.10.4 → 2.11.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 -0
- package/README.md +47 -0
- package/docs/managed-change-liquidity.md +7 -0
- package/docs/no-send-expiry.md +167 -0
- package/docs/prepared-beef.md +197 -0
- package/docs/setup.md +11 -0
- package/docs/storage.md +11 -0
- package/out/src/Setup.d.ts +3 -0
- package/out/src/Setup.d.ts.map +1 -1
- package/out/src/Setup.js +1 -0
- package/out/src/Setup.js.map +1 -1
- package/out/src/Wallet.d.ts.map +1 -1
- package/out/src/Wallet.js +11 -1
- package/out/src/Wallet.js.map +1 -1
- package/out/src/WalletPermissionsManager.d.ts +30 -6
- package/out/src/WalletPermissionsManager.d.ts.map +1 -1
- package/out/src/WalletPermissionsManager.js +84 -9
- package/out/src/WalletPermissionsManager.js.map +1 -1
- package/out/src/mockchain/MockServices.d.ts +2 -0
- package/out/src/mockchain/MockServices.d.ts.map +1 -1
- package/out/src/mockchain/MockServices.js +20 -0
- package/out/src/mockchain/MockServices.js.map +1 -1
- package/out/src/monitor/Monitor.d.ts +1 -0
- package/out/src/monitor/Monitor.d.ts.map +1 -1
- package/out/src/monitor/Monitor.js +19 -3
- package/out/src/monitor/Monitor.js.map +1 -1
- package/out/src/monitor/tasks/TaskNoSendExpiry.d.ts +19 -0
- package/out/src/monitor/tasks/TaskNoSendExpiry.d.ts.map +1 -0
- package/out/src/monitor/tasks/TaskNoSendExpiry.js +47 -0
- package/out/src/monitor/tasks/TaskNoSendExpiry.js.map +1 -0
- 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 +4 -0
- package/out/src/sdk/ActionBatch.interfaces.d.ts.map +1 -1
- package/out/src/sdk/WalletStorage.interfaces.d.ts +35 -0
- package/out/src/sdk/WalletStorage.interfaces.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageKnex.d.ts +0 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageKnex.d.ts.map +1 -1
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageKnex.js +23 -12
- package/out/src/services/chaintracker/chaintracks/Storage/ChaintracksStorageKnex.js.map +1 -1
- package/out/src/signer/methods/createNoSendExpiryAction.d.ts +7 -0
- package/out/src/signer/methods/createNoSendExpiryAction.d.ts.map +1 -0
- package/out/src/signer/methods/createNoSendExpiryAction.js +249 -0
- package/out/src/signer/methods/createNoSendExpiryAction.js.map +1 -0
- package/out/src/signer/methods/signAction.d.ts.map +1 -1
- package/out/src/signer/methods/signAction.js +9 -0
- package/out/src/signer/methods/signAction.js.map +1 -1
- package/out/src/storage/StorageIdb.d.ts +3 -0
- package/out/src/storage/StorageIdb.d.ts.map +1 -1
- package/out/src/storage/StorageIdb.js +37 -1
- package/out/src/storage/StorageIdb.js.map +1 -1
- package/out/src/storage/StorageKnex.d.ts +48 -3
- package/out/src/storage/StorageKnex.d.ts.map +1 -1
- package/out/src/storage/StorageKnex.js +234 -14
- package/out/src/storage/StorageKnex.js.map +1 -1
- package/out/src/storage/StorageProvider.d.ts +6 -1
- package/out/src/storage/StorageProvider.d.ts.map +1 -1
- package/out/src/storage/StorageProvider.js +147 -5
- package/out/src/storage/StorageProvider.js.map +1 -1
- package/out/src/storage/StorageReaderWriter.d.ts +2 -0
- package/out/src/storage/StorageReaderWriter.d.ts.map +1 -1
- package/out/src/storage/StorageReaderWriter.js +4 -0
- package/out/src/storage/StorageReaderWriter.js.map +1 -1
- package/out/src/storage/WalletStorageManager.d.ts +11 -0
- package/out/src/storage/WalletStorageManager.d.ts.map +1 -1
- package/out/src/storage/WalletStorageManager.js +77 -12
- package/out/src/storage/WalletStorageManager.js.map +1 -1
- package/out/src/storage/idbHelpers.d.ts.map +1 -1
- package/out/src/storage/idbHelpers.js +14 -2
- package/out/src/storage/idbHelpers.js.map +1 -1
- package/out/src/storage/index.all.d.ts +2 -0
- package/out/src/storage/index.all.d.ts.map +1 -1
- package/out/src/storage/index.all.js +2 -0
- package/out/src/storage/index.all.js.map +1 -1
- package/out/src/storage/methods/createAction.d.ts +57 -1
- package/out/src/storage/methods/createAction.d.ts.map +1 -1
- package/out/src/storage/methods/createAction.js +195 -49
- package/out/src/storage/methods/createAction.js.map +1 -1
- package/out/src/storage/methods/noSendExpiry.d.ts +11 -0
- package/out/src/storage/methods/noSendExpiry.d.ts.map +1 -0
- package/out/src/storage/methods/noSendExpiry.js +357 -0
- package/out/src/storage/methods/noSendExpiry.js.map +1 -0
- package/out/src/storage/methods/noSendExpiryLifecycle.d.ts +14 -0
- package/out/src/storage/methods/noSendExpiryLifecycle.d.ts.map +1 -0
- package/out/src/storage/methods/noSendExpiryLifecycle.js +691 -0
- package/out/src/storage/methods/noSendExpiryLifecycle.js.map +1 -0
- package/out/src/storage/methods/preparedBeef.d.ts +104 -0
- package/out/src/storage/methods/preparedBeef.d.ts.map +1 -0
- package/out/src/storage/methods/preparedBeef.js +525 -0
- package/out/src/storage/methods/preparedBeef.js.map +1 -0
- package/out/src/storage/methods/processAction.d.ts.map +1 -1
- package/out/src/storage/methods/processAction.js +109 -28
- package/out/src/storage/methods/processAction.js.map +1 -1
- package/out/src/storage/methods/purgeData.d.ts.map +1 -1
- package/out/src/storage/methods/purgeData.js +9 -0
- package/out/src/storage/methods/purgeData.js.map +1 -1
- package/out/src/storage/methods/validateSyncProof.d.ts +24 -0
- package/out/src/storage/methods/validateSyncProof.d.ts.map +1 -0
- package/out/src/storage/methods/validateSyncProof.js +116 -0
- package/out/src/storage/methods/validateSyncProof.js.map +1 -0
- package/out/src/storage/remoting/StorageClientBase.d.ts +4 -1
- package/out/src/storage/remoting/StorageClientBase.d.ts.map +1 -1
- package/out/src/storage/remoting/StorageClientBase.js +9 -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 +16 -3
- package/out/src/storage/remoting/StorageServer.js.map +1 -1
- package/out/src/storage/schema/KnexMigrations.d.ts +2 -0
- package/out/src/storage/schema/KnexMigrations.d.ts.map +1 -1
- package/out/src/storage/schema/KnexMigrations.js +66 -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/EntityOutput.d.ts.map +1 -1
- package/out/src/storage/schema/entities/EntityOutput.js +13 -0
- package/out/src/storage/schema/entities/EntityOutput.js.map +1 -1
- package/out/src/storage/schema/entities/EntityProvenTx.d.ts +2 -0
- package/out/src/storage/schema/entities/EntityProvenTx.d.ts.map +1 -1
- package/out/src/storage/schema/entities/EntityProvenTx.js +33 -3
- package/out/src/storage/schema/entities/EntityProvenTx.js.map +1 -1
- package/out/src/storage/schema/entities/EntityTransaction.d.ts +26 -0
- package/out/src/storage/schema/entities/EntityTransaction.d.ts.map +1 -1
- package/out/src/storage/schema/entities/EntityTransaction.js +221 -21
- package/out/src/storage/schema/entities/EntityTransaction.js.map +1 -1
- package/out/src/storage/schema/tables/TablePreparedBeef.interfaces.d.ts +23 -0
- package/out/src/storage/schema/tables/TablePreparedBeef.interfaces.d.ts.map +1 -0
- package/out/src/storage/schema/tables/TablePreparedBeef.interfaces.js +3 -0
- package/out/src/storage/schema/tables/TablePreparedBeef.interfaces.js.map +1 -0
- package/out/src/storage/schema/tables/TableTransaction.d.ts +16 -0
- package/out/src/storage/schema/tables/TableTransaction.d.ts.map +1 -1
- package/out/src/storage/schema/tables/TableTransaction.js +14 -1
- package/out/src/storage/schema/tables/TableTransaction.js.map +1 -1
- package/out/src/utility/Argon2idBackend.d.ts +36 -0
- package/out/src/utility/Argon2idBackend.d.ts.map +1 -0
- package/out/src/utility/Argon2idBackend.js +60 -0
- package/out/src/utility/Argon2idBackend.js.map +1 -0
- package/out/src/utility/brc177NoSendExpiry.d.ts +34 -0
- package/out/src/utility/brc177NoSendExpiry.d.ts.map +1 -0
- package/out/src/utility/brc177NoSendExpiry.js +81 -0
- package/out/src/utility/brc177NoSendExpiry.js.map +1 -0
- package/out/src/utility/hashWasm.d.ts +2 -1
- package/out/src/utility/hashWasm.d.ts.map +1 -1
- package/out/src/utility/hashWasm.js +83 -1
- package/out/src/utility/hashWasm.js.map +1 -1
- package/out/src/utility/index.all.d.ts +2 -0
- package/out/src/utility/index.all.d.ts.map +1 -1
- package/out/src/utility/index.all.js +2 -0
- package/out/src/utility/index.all.js.map +1 -1
- package/out/src/utility/index.client.d.ts +2 -0
- package/out/src/utility/index.client.d.ts.map +1 -1
- package/out/src/utility/index.client.js +2 -0
- package/out/src/utility/index.client.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.processNoSendExpiryLifecycle = processNoSendExpiryLifecycle;
|
|
4
|
+
const sdk_1 = require("@bsv/sdk");
|
|
5
|
+
const WERR_errors_1 = require("../../sdk/WERR_errors");
|
|
6
|
+
const parseTxScriptOffsets_1 = require("../../utility/parseTxScriptOffsets");
|
|
7
|
+
const utilityHelpers_1 = require("../../utility/utilityHelpers");
|
|
8
|
+
const utilityHelpers_noBuffer_1 = require("../../utility/utilityHelpers.noBuffer");
|
|
9
|
+
const EntityProvenTxReq_1 = require("../schema/entities/EntityProvenTxReq");
|
|
10
|
+
// Descending lifecycle order ensures a row advanced by this pass moves only
|
|
11
|
+
// into a state whose page has already run, so it is never counted or acted on
|
|
12
|
+
// twice without retaining an unbounded set of transaction IDs.
|
|
13
|
+
const ACTIVE_STATES = [
|
|
14
|
+
'reclaiming',
|
|
15
|
+
'broadcast',
|
|
16
|
+
'conflicted',
|
|
17
|
+
'revocation-requested',
|
|
18
|
+
'signed',
|
|
19
|
+
'unsigned',
|
|
20
|
+
'preparing'
|
|
21
|
+
];
|
|
22
|
+
const QUERY_PAGE_SIZE = 100;
|
|
23
|
+
const STATUS_BATCH_SIZE = 100;
|
|
24
|
+
const RECLAIM_RETRY_BASE_MSECS = 30_000;
|
|
25
|
+
const RECLAIM_RETRY_MAX_MSECS = 60 * 60_000;
|
|
26
|
+
function emptyResult() {
|
|
27
|
+
return {
|
|
28
|
+
inspected: 0,
|
|
29
|
+
cancelled: 0,
|
|
30
|
+
observed: 0,
|
|
31
|
+
reclaimActivated: 0,
|
|
32
|
+
reclaimRetried: 0,
|
|
33
|
+
reclaimed: 0,
|
|
34
|
+
targetWon: 0,
|
|
35
|
+
deferred: 0,
|
|
36
|
+
errors: 0
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function activeTransactionsOnPage(storage, page) {
|
|
40
|
+
// A remote database may be retained as a synchronized backup while another
|
|
41
|
+
// provider is authoritative for a user. Check only the bounded set of users
|
|
42
|
+
// represented by this page rather than materializing every active user or
|
|
43
|
+
// expiry in a multi-user service.
|
|
44
|
+
const activeStorage = storage.getSettings().storageIdentityKey;
|
|
45
|
+
const activeUsers = new Set();
|
|
46
|
+
for (const userId of new Set(page.map(transaction => transaction.userId))) {
|
|
47
|
+
const user = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findUsers({
|
|
48
|
+
partial: { userId, activeStorage },
|
|
49
|
+
paged: { limit: 1 }
|
|
50
|
+
}));
|
|
51
|
+
if (user != null)
|
|
52
|
+
activeUsers.add(userId);
|
|
53
|
+
}
|
|
54
|
+
return page.filter(transaction => activeUsers.has(transaction.userId));
|
|
55
|
+
}
|
|
56
|
+
function isDue(transaction, nowSeconds, height) {
|
|
57
|
+
if (transaction.noSendExpiryState === 'revocation-requested')
|
|
58
|
+
return true;
|
|
59
|
+
if (transaction.noSendExpiryDeadline == null)
|
|
60
|
+
return false;
|
|
61
|
+
return transaction.noSendExpiryMode === 'blockheight'
|
|
62
|
+
? height != null && height >= transaction.noSendExpiryDeadline
|
|
63
|
+
: nowSeconds >= transaction.noSendExpiryDeadline;
|
|
64
|
+
}
|
|
65
|
+
async function getStatuses(storage, transactions) {
|
|
66
|
+
const txids = [
|
|
67
|
+
...new Set(transactions.flatMap(transaction => {
|
|
68
|
+
const txids = [];
|
|
69
|
+
if (transaction.txid)
|
|
70
|
+
txids.push(transaction.txid);
|
|
71
|
+
if (transaction.noSendExpiryState === 'reclaiming' && transaction.noSendExpiryReclaimTxid) {
|
|
72
|
+
txids.push(transaction.noSendExpiryReclaimTxid);
|
|
73
|
+
}
|
|
74
|
+
return txids;
|
|
75
|
+
}))
|
|
76
|
+
];
|
|
77
|
+
if (txids.length === 0)
|
|
78
|
+
return new Map();
|
|
79
|
+
const statuses = new Map();
|
|
80
|
+
for (let offset = 0; offset < txids.length; offset += STATUS_BATCH_SIZE) {
|
|
81
|
+
try {
|
|
82
|
+
const result = await storage.getServices().getStatusForTxids(txids.slice(offset, offset + STATUS_BATCH_SIZE));
|
|
83
|
+
if (result.status !== 'success')
|
|
84
|
+
continue;
|
|
85
|
+
for (const item of result.results)
|
|
86
|
+
statuses.set(item.txid, item.status);
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
}
|
|
90
|
+
return statuses;
|
|
91
|
+
}
|
|
92
|
+
async function updateReqStatus(storage, txid, status, note, trx) {
|
|
93
|
+
if (!txid)
|
|
94
|
+
return;
|
|
95
|
+
const req = await EntityProvenTxReq_1.EntityProvenTxReq.fromStorageTxid(storage, txid, trx);
|
|
96
|
+
if (req == null || req.status === 'completed')
|
|
97
|
+
return;
|
|
98
|
+
req.status = status;
|
|
99
|
+
req.addHistoryNote({ what: note });
|
|
100
|
+
await req.updateStorageDynamicProperties(storage, trx);
|
|
101
|
+
}
|
|
102
|
+
async function quarantineTransactionOutputs(storage, transactionId, trx) {
|
|
103
|
+
const outputs = await storage.findOutputs({ partial: { transactionId }, trx });
|
|
104
|
+
for (const output of outputs) {
|
|
105
|
+
if (output.spendable)
|
|
106
|
+
await storage.updateOutput(output.outputId, { spendable: false }, trx);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function markObservedTarget(storage, transaction, final) {
|
|
110
|
+
return await storage.transaction(async (trx) => {
|
|
111
|
+
const current = (0, utilityHelpers_1.verifyOne)(await storage.findTransactions({
|
|
112
|
+
partial: { transactionId: (0, utilityHelpers_1.verifyId)(transaction.transactionId) },
|
|
113
|
+
trx
|
|
114
|
+
}));
|
|
115
|
+
if (current.noSendExpiryState === 'reclaimed' || current.noSendExpiryState === 'cancelled')
|
|
116
|
+
return false;
|
|
117
|
+
if (final) {
|
|
118
|
+
return await finishTargetWinner(storage, current, trx);
|
|
119
|
+
}
|
|
120
|
+
if (current.noSendExpiryState !== 'signed' &&
|
|
121
|
+
current.noSendExpiryState !== 'revocation-requested' &&
|
|
122
|
+
current.noSendExpiryState !== 'broadcast' &&
|
|
123
|
+
current.noSendExpiryState !== 'conflicted') {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
// Claim even an existing broadcast state. Besides making conflicted ->
|
|
127
|
+
// broadcast monotonic, the conditional write holds this row for the rest
|
|
128
|
+
// of the transaction so a concurrent reclaim/proof winner cannot be
|
|
129
|
+
// overwritten by the observation metadata update below.
|
|
130
|
+
if (!(await storage.compareAndSetNoSendExpiryState(current.transactionId, current.noSendExpiryState, 'broadcast', trx))) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
await storage.updateTransaction(current.transactionId, {
|
|
134
|
+
noSendExpiryObservedAt: Date.now(),
|
|
135
|
+
...(current.status === 'nosend' ? { status: 'unproven' } : {})
|
|
136
|
+
}, trx);
|
|
137
|
+
await updateReqStatus(storage, current.txid, 'unmined', 'brc177-target-observed', trx);
|
|
138
|
+
return true;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async function noteTargetObservedDuringRace(storage, transaction) {
|
|
142
|
+
return await storage.transaction(async (trx) => {
|
|
143
|
+
const current = (0, utilityHelpers_1.verifyOne)(await storage.findTransactions({
|
|
144
|
+
partial: { transactionId: (0, utilityHelpers_1.verifyId)(transaction.transactionId) },
|
|
145
|
+
trx
|
|
146
|
+
}));
|
|
147
|
+
if (current.noSendExpiryState !== 'reclaiming')
|
|
148
|
+
return false;
|
|
149
|
+
// Lock the lifecycle row before changing transaction/request metadata. If
|
|
150
|
+
// a proof winner committed after our read, this self-transition fails and
|
|
151
|
+
// prevents stale observation data from reviving the losing transaction.
|
|
152
|
+
if (!(await storage.compareAndSetNoSendExpiryState(current.transactionId, 'reclaiming', 'reclaiming', trx))) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
let changed = false;
|
|
156
|
+
if (current.status === 'nosend') {
|
|
157
|
+
await storage.updateTransaction(current.transactionId, {
|
|
158
|
+
status: 'unproven',
|
|
159
|
+
noSendExpiryObservedAt: current.noSendExpiryObservedAt ?? Date.now()
|
|
160
|
+
}, trx);
|
|
161
|
+
changed = true;
|
|
162
|
+
}
|
|
163
|
+
if (current.txid != null) {
|
|
164
|
+
const req = await EntityProvenTxReq_1.EntityProvenTxReq.fromStorageTxid(storage, current.txid, trx);
|
|
165
|
+
if (req?.status === 'nosend') {
|
|
166
|
+
req.status = 'unmined';
|
|
167
|
+
req.addHistoryNote({ what: 'brc177-target-observed-during-reclaim-race' });
|
|
168
|
+
await req.updateStorageDynamicProperties(storage, trx);
|
|
169
|
+
changed = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (current.noSendExpiryReclaimTxid != null) {
|
|
173
|
+
const reclaim = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findTransactions({
|
|
174
|
+
partial: { userId: current.userId, txid: current.noSendExpiryReclaimTxid },
|
|
175
|
+
trx
|
|
176
|
+
}));
|
|
177
|
+
const reclaimReq = await EntityProvenTxReq_1.EntityProvenTxReq.fromStorageTxid(storage, current.noSendExpiryReclaimTxid, trx);
|
|
178
|
+
// Once the target is positively observed, BRC-177 stops future reclaim
|
|
179
|
+
// submissions. Keep the request in proof tracking because a prior send
|
|
180
|
+
// may still have reached a processor and either side can win the race.
|
|
181
|
+
if (reclaimReq != null && (reclaimReq.status === 'unsent' || reclaimReq.status === 'sending')) {
|
|
182
|
+
reclaimReq.status = 'unmined';
|
|
183
|
+
reclaimReq.addHistoryNote({ what: 'brc177-reclaim-retry-suppressed-target-observed' });
|
|
184
|
+
await reclaimReq.updateStorageDynamicProperties(storage, trx);
|
|
185
|
+
if (reclaim != null && (reclaim.status === 'unprocessed' || reclaim.status === 'sending')) {
|
|
186
|
+
await storage.updateTransaction(reclaim.transactionId, { status: 'unproven' }, trx);
|
|
187
|
+
}
|
|
188
|
+
changed = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return changed;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
async function cancelUnsigned(storage, transaction) {
|
|
195
|
+
return await storage.transaction(async (trx) => {
|
|
196
|
+
const expected = transaction.noSendExpiryState;
|
|
197
|
+
if (expected !== 'preparing' && expected !== 'unsigned')
|
|
198
|
+
return false;
|
|
199
|
+
if (!(await storage.compareAndSetNoSendExpiryState(transaction.transactionId, expected, 'cancelled', trx))) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
await storage.updateTransactionStatus('failed', transaction.transactionId, undefined, undefined, trx);
|
|
203
|
+
return true;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function validateReclaim(transaction) {
|
|
207
|
+
if (transaction.noSendExpiryReclaimRawTx == null ||
|
|
208
|
+
!transaction.noSendExpiryReclaimTxid ||
|
|
209
|
+
!transaction.noSendExpiryReclaimDerivationPrefix ||
|
|
210
|
+
!transaction.noSendExpiryReclaimDerivationSuffix ||
|
|
211
|
+
transaction.noSendExpiryReclaimSatoshis == null ||
|
|
212
|
+
!transaction.noSendExpiryAnchorTxid ||
|
|
213
|
+
transaction.noSendExpiryAnchorVout == null) {
|
|
214
|
+
throw new WERR_errors_1.WERR_INVALID_OPERATION('BRC-177 reclaim metadata is incomplete');
|
|
215
|
+
}
|
|
216
|
+
const reclaim = sdk_1.Transaction.fromBinary((0, utilityHelpers_noBuffer_1.asArray)(transaction.noSendExpiryReclaimRawTx));
|
|
217
|
+
if (reclaim.id('hex') !== transaction.noSendExpiryReclaimTxid ||
|
|
218
|
+
reclaim.inputs.length !== 1 ||
|
|
219
|
+
reclaim.outputs.length !== 1 ||
|
|
220
|
+
reclaim.inputs[0].sourceTXID !== transaction.noSendExpiryAnchorTxid ||
|
|
221
|
+
reclaim.inputs[0].sourceOutputIndex !== transaction.noSendExpiryAnchorVout ||
|
|
222
|
+
reclaim.outputs[0].satoshis !== transaction.noSendExpiryReclaimSatoshis) {
|
|
223
|
+
throw new WERR_errors_1.WERR_INVALID_OPERATION('BRC-177 reclaim metadata does not describe the signed reclaim transaction');
|
|
224
|
+
}
|
|
225
|
+
return reclaim;
|
|
226
|
+
}
|
|
227
|
+
async function insertReclaim(storage, target, anchor, trx) {
|
|
228
|
+
const reclaim = validateReclaim(target);
|
|
229
|
+
const rawTx = (0, utilityHelpers_noBuffer_1.asArray)(target.noSendExpiryReclaimRawTx);
|
|
230
|
+
const reclaimTxid = target.noSendExpiryReclaimTxid;
|
|
231
|
+
const existingTransaction = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findTransactions({
|
|
232
|
+
partial: { userId: target.userId, txid: reclaimTxid },
|
|
233
|
+
trx
|
|
234
|
+
}));
|
|
235
|
+
let reclaimTransaction = existingTransaction;
|
|
236
|
+
if (reclaimTransaction == null) {
|
|
237
|
+
const now = new Date();
|
|
238
|
+
reclaimTransaction = {
|
|
239
|
+
created_at: now,
|
|
240
|
+
updated_at: now,
|
|
241
|
+
transactionId: 0,
|
|
242
|
+
userId: target.userId,
|
|
243
|
+
status: 'unprocessed',
|
|
244
|
+
reference: (0, utilityHelpers_1.randomBytesBase64)(12),
|
|
245
|
+
isOutgoing: true,
|
|
246
|
+
satoshis: target.noSendExpiryReclaimSatoshis - anchor.satoshis,
|
|
247
|
+
description: 'BRC-177 expiry reclaim',
|
|
248
|
+
version: reclaim.version,
|
|
249
|
+
lockTime: reclaim.lockTime,
|
|
250
|
+
txid: reclaimTxid
|
|
251
|
+
};
|
|
252
|
+
reclaimTransaction.transactionId = await storage.insertTransaction(reclaimTransaction, trx);
|
|
253
|
+
const basket = (0, utilityHelpers_1.verifyOne)(await storage.findOutputBaskets({
|
|
254
|
+
partial: { userId: target.userId, name: 'default' },
|
|
255
|
+
trx
|
|
256
|
+
}));
|
|
257
|
+
const offsets = (0, parseTxScriptOffsets_1.parseTxScriptOffsets)(rawTx);
|
|
258
|
+
const scriptOffset = offsets.outputs[0];
|
|
259
|
+
const lockingScript = rawTx.slice(scriptOffset.offset, scriptOffset.offset + scriptOffset.length);
|
|
260
|
+
await storage.insertOutput({
|
|
261
|
+
created_at: now,
|
|
262
|
+
updated_at: now,
|
|
263
|
+
outputId: 0,
|
|
264
|
+
userId: target.userId,
|
|
265
|
+
transactionId: reclaimTransaction.transactionId,
|
|
266
|
+
basketId: basket.basketId,
|
|
267
|
+
// The reclaim is deliberately racing the released target. Do not expose
|
|
268
|
+
// its output as wallet liquidity until a locally validated proof wins.
|
|
269
|
+
spendable: false,
|
|
270
|
+
change: true,
|
|
271
|
+
outputDescription: '',
|
|
272
|
+
vout: 0,
|
|
273
|
+
satoshis: target.noSendExpiryReclaimSatoshis,
|
|
274
|
+
providedBy: 'storage',
|
|
275
|
+
purpose: 'change',
|
|
276
|
+
type: 'P2PKH',
|
|
277
|
+
txid: reclaimTxid,
|
|
278
|
+
derivationPrefix: target.noSendExpiryReclaimDerivationPrefix,
|
|
279
|
+
derivationSuffix: target.noSendExpiryReclaimDerivationSuffix,
|
|
280
|
+
scriptLength: scriptOffset.length,
|
|
281
|
+
scriptOffset: scriptOffset.offset,
|
|
282
|
+
lockingScript: scriptOffset.length > storage.getSettings().maxOutputScript ? undefined : lockingScript
|
|
283
|
+
}, trx);
|
|
284
|
+
}
|
|
285
|
+
await storage.updateOutput(anchor.outputId, {
|
|
286
|
+
spendable: false,
|
|
287
|
+
spentBy: reclaimTransaction.transactionId
|
|
288
|
+
}, trx);
|
|
289
|
+
const targetReq = target.txid == null ? undefined : await EntityProvenTxReq_1.EntityProvenTxReq.fromStorageTxid(storage, target.txid, trx);
|
|
290
|
+
if (targetReq == null)
|
|
291
|
+
throw new WERR_errors_1.WERR_INTERNAL('BRC-177 protected transaction request is missing');
|
|
292
|
+
const req = EntityProvenTxReq_1.EntityProvenTxReq.fromTxid(reclaimTxid, rawTx, targetReq.api.inputBEEF);
|
|
293
|
+
req.status = 'unsent';
|
|
294
|
+
req.addNotifyTransactionId(reclaimTransaction.transactionId);
|
|
295
|
+
req.addHistoryNote({ what: 'brc177-reclaim-activated', targetTxid: target.txid });
|
|
296
|
+
return await req.insertOrMerge(storage, trx);
|
|
297
|
+
}
|
|
298
|
+
async function activateReclaim(storage, transaction, anchor, expected) {
|
|
299
|
+
return await storage.transaction(async (trx) => {
|
|
300
|
+
if (!(await storage.compareAndSetNoSendExpiryState(transaction.transactionId, expected, 'reclaiming', trx)))
|
|
301
|
+
return undefined;
|
|
302
|
+
return await insertReclaim(storage, transaction, anchor, trx);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
async function finishReclaimWinner(storage, target, reclaimTransaction, trx) {
|
|
306
|
+
const currentTarget = (0, utilityHelpers_1.verifyOne)(await storage.findTransactions({
|
|
307
|
+
partial: { transactionId: target.transactionId, userId: target.userId },
|
|
308
|
+
trx
|
|
309
|
+
}));
|
|
310
|
+
const currentReclaim = (0, utilityHelpers_1.verifyOne)(await storage.findTransactions({
|
|
311
|
+
partial: { transactionId: reclaimTransaction.transactionId, userId: target.userId },
|
|
312
|
+
trx
|
|
313
|
+
}));
|
|
314
|
+
if (currentTarget.noSendExpiryState !== 'reclaiming' ||
|
|
315
|
+
currentTarget.status === 'completed' ||
|
|
316
|
+
currentTarget.provenTxId != null ||
|
|
317
|
+
currentReclaim.provenTxId == null) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
if (!(await storage.compareAndSetNoSendExpiryState(currentTarget.transactionId, 'reclaiming', 'reclaimed', trx))) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
if (currentTarget.status !== 'failed') {
|
|
324
|
+
await storage.updateTransactionStatus('failed', currentTarget.transactionId, undefined, undefined, trx);
|
|
325
|
+
}
|
|
326
|
+
await updateReqStatus(storage, currentTarget.txid, 'invalid', 'brc177-reclaim-won', trx);
|
|
327
|
+
const anchor = (0, utilityHelpers_1.verifyOne)(await storage.findOutputs({
|
|
328
|
+
partial: {
|
|
329
|
+
userId: currentTarget.userId,
|
|
330
|
+
txid: currentTarget.noSendExpiryAnchorTxid,
|
|
331
|
+
vout: currentTarget.noSendExpiryAnchorVout
|
|
332
|
+
},
|
|
333
|
+
trx
|
|
334
|
+
}));
|
|
335
|
+
await storage.updateOutput(anchor.outputId, {
|
|
336
|
+
spendable: false,
|
|
337
|
+
spentBy: currentReclaim.transactionId
|
|
338
|
+
}, trx);
|
|
339
|
+
const reclaimOutput = (0, utilityHelpers_1.verifyOne)(await storage.findOutputs({
|
|
340
|
+
partial: { transactionId: currentReclaim.transactionId, vout: 0 },
|
|
341
|
+
trx
|
|
342
|
+
}));
|
|
343
|
+
await storage.updateOutput(reclaimOutput.outputId, { spendable: true }, trx);
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
async function finishTargetWinner(storage, target, trx) {
|
|
347
|
+
const current = (0, utilityHelpers_1.verifyOne)(await storage.findTransactions({
|
|
348
|
+
partial: { transactionId: target.transactionId, userId: target.userId },
|
|
349
|
+
trx
|
|
350
|
+
}));
|
|
351
|
+
if (current.noSendExpiryState == null ||
|
|
352
|
+
current.noSendExpiryState === 'reclaimed' ||
|
|
353
|
+
current.noSendExpiryState === 'cancelled' ||
|
|
354
|
+
current.noSendExpiryState === 'target-won') {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
const reclaimTxid = current.noSendExpiryReclaimTxid;
|
|
358
|
+
const reclaimTransaction = reclaimTxid == null
|
|
359
|
+
? undefined
|
|
360
|
+
: (0, utilityHelpers_1.verifyOneOrNone)(await storage.findTransactions({
|
|
361
|
+
partial: { userId: current.userId, txid: reclaimTxid },
|
|
362
|
+
trx
|
|
363
|
+
}));
|
|
364
|
+
// Contradictory completion/proof signals for competing spends cannot both
|
|
365
|
+
// describe the same best chain. Keep every output quarantined until the
|
|
366
|
+
// ordinary proof reconciliation machinery resolves that inconsistency.
|
|
367
|
+
if (reclaimTransaction?.status === 'completed' || reclaimTransaction?.provenTxId != null) {
|
|
368
|
+
await quarantineTransactionOutputs(storage, current.transactionId, trx);
|
|
369
|
+
await quarantineTransactionOutputs(storage, reclaimTransaction.transactionId, trx);
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
// A status flag alone suppresses reclaim but never releases value or
|
|
373
|
+
// finalizes a winner; only the linked, locally validated proof may do that.
|
|
374
|
+
if (current.provenTxId == null)
|
|
375
|
+
return false;
|
|
376
|
+
if (!(await storage.compareAndSetNoSendExpiryState(current.transactionId, current.noSendExpiryState, 'target-won', trx))) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
if (reclaimTransaction != null && reclaimTransaction.status !== 'failed') {
|
|
380
|
+
await storage.updateTransactionStatus('failed', reclaimTransaction.transactionId, undefined, undefined, trx);
|
|
381
|
+
await updateReqStatus(storage, reclaimTxid, 'doubleSpend', 'brc177-target-won', trx);
|
|
382
|
+
}
|
|
383
|
+
const anchor = (0, utilityHelpers_1.verifyOne)(await storage.findOutputs({
|
|
384
|
+
partial: {
|
|
385
|
+
userId: current.userId,
|
|
386
|
+
txid: current.noSendExpiryAnchorTxid,
|
|
387
|
+
vout: current.noSendExpiryAnchorVout
|
|
388
|
+
},
|
|
389
|
+
trx
|
|
390
|
+
}));
|
|
391
|
+
await storage.updateOutput(anchor.outputId, {
|
|
392
|
+
spendable: false,
|
|
393
|
+
spentBy: current.transactionId
|
|
394
|
+
}, trx);
|
|
395
|
+
await storage.updateTransaction(current.transactionId, {
|
|
396
|
+
noSendExpiryObservedAt: current.noSendExpiryObservedAt ?? Date.now(),
|
|
397
|
+
...(current.status === 'nosend' ? { status: 'unproven' } : {})
|
|
398
|
+
}, trx);
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
async function reconcileRace(storage, target) {
|
|
402
|
+
const reclaim = target.noSendExpiryReclaimTxid == null
|
|
403
|
+
? undefined
|
|
404
|
+
: (0, utilityHelpers_1.verifyOneOrNone)(await storage.findTransactions({
|
|
405
|
+
partial: { userId: target.userId, txid: target.noSendExpiryReclaimTxid }
|
|
406
|
+
}));
|
|
407
|
+
if (target.status === 'completed' || target.provenTxId != null) {
|
|
408
|
+
const won = await storage.transaction(async (trx) => await finishTargetWinner(storage, target, trx));
|
|
409
|
+
return won ? 'target' : 'deferred';
|
|
410
|
+
}
|
|
411
|
+
if (reclaim?.status === 'completed' || reclaim?.provenTxId != null) {
|
|
412
|
+
const won = await storage.transaction(async (trx) => await finishReclaimWinner(storage, target, reclaim, trx));
|
|
413
|
+
return won ? 'reclaimed' : 'deferred';
|
|
414
|
+
}
|
|
415
|
+
return 'deferred';
|
|
416
|
+
}
|
|
417
|
+
function isKnownOrMined(status) {
|
|
418
|
+
return status === 'known' || status === 'mined';
|
|
419
|
+
}
|
|
420
|
+
function recordRaceResult(result, race) {
|
|
421
|
+
if (race === 'reclaimed')
|
|
422
|
+
result.reclaimed++;
|
|
423
|
+
else if (race === 'target')
|
|
424
|
+
result.targetWon++;
|
|
425
|
+
else
|
|
426
|
+
result.deferred++;
|
|
427
|
+
}
|
|
428
|
+
function reclaimRetryDelay(rebroadcastAttempts) {
|
|
429
|
+
return Math.min(RECLAIM_RETRY_BASE_MSECS * 2 ** Math.min(rebroadcastAttempts, 30), RECLAIM_RETRY_MAX_MSECS);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* A processor may reject a reclaim even though the same anchor later becomes a
|
|
433
|
+
* conclusive UTXO again (for example, after a conflicting mempool spend is
|
|
434
|
+
* evicted). Re-submit only the already validated, pre-signed reclaim and never
|
|
435
|
+
* release its anchor or output based on a rejection response.
|
|
436
|
+
*/
|
|
437
|
+
async function retryRejectedReclaim(storage, target, targetStatus) {
|
|
438
|
+
if (targetStatus !== 'unknown' ||
|
|
439
|
+
target.noSendExpiryObservedAt != null ||
|
|
440
|
+
target.noSendExpiryReclaimTxid == null) {
|
|
441
|
+
return 'not-applicable';
|
|
442
|
+
}
|
|
443
|
+
const reclaim = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findTransactions({
|
|
444
|
+
partial: { userId: target.userId, txid: target.noSendExpiryReclaimTxid }
|
|
445
|
+
}));
|
|
446
|
+
const req = await EntityProvenTxReq_1.EntityProvenTxReq.fromStorageTxid(storage, target.noSendExpiryReclaimTxid);
|
|
447
|
+
if (reclaim?.status !== 'failed' ||
|
|
448
|
+
reclaim.provenTxId != null ||
|
|
449
|
+
req == null ||
|
|
450
|
+
(req.status !== 'invalid' && req.status !== 'doubleSpend')) {
|
|
451
|
+
return 'not-applicable';
|
|
452
|
+
}
|
|
453
|
+
const retryAfter = (0, utilityHelpers_1.verifyTruthy)(req.updated_at).getTime() + reclaimRetryDelay(req.rebroadcastAttempts);
|
|
454
|
+
if (Date.now() < retryAfter)
|
|
455
|
+
return 'deferred';
|
|
456
|
+
const anchor = (0, utilityHelpers_1.verifyOne)(await storage.findOutputs({
|
|
457
|
+
partial: {
|
|
458
|
+
userId: target.userId,
|
|
459
|
+
txid: target.noSendExpiryAnchorTxid,
|
|
460
|
+
vout: target.noSendExpiryAnchorVout
|
|
461
|
+
}
|
|
462
|
+
}));
|
|
463
|
+
let anchorIsUtxo;
|
|
464
|
+
try {
|
|
465
|
+
anchorIsUtxo = await storage.getServices().isUtxo(anchor);
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
return 'deferred';
|
|
469
|
+
}
|
|
470
|
+
if (!anchorIsUtxo)
|
|
471
|
+
return 'deferred';
|
|
472
|
+
const sendable = await storage.transaction(async (trx) => {
|
|
473
|
+
if (!(await storage.compareAndSetNoSendExpiryState(target.transactionId, 'reclaiming', 'reclaiming', trx))) {
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
const currentTarget = (0, utilityHelpers_1.verifyOne)(await storage.findTransactions({
|
|
477
|
+
partial: { transactionId: target.transactionId, userId: target.userId },
|
|
478
|
+
trx
|
|
479
|
+
}));
|
|
480
|
+
if (currentTarget.noSendExpiryState !== 'reclaiming' ||
|
|
481
|
+
currentTarget.status === 'completed' ||
|
|
482
|
+
currentTarget.provenTxId != null ||
|
|
483
|
+
currentTarget.noSendExpiryObservedAt != null ||
|
|
484
|
+
currentTarget.noSendExpiryReclaimTxid == null) {
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
const currentReclaim = (0, utilityHelpers_1.verifyOneOrNone)(await storage.findTransactions({
|
|
488
|
+
partial: { userId: currentTarget.userId, txid: currentTarget.noSendExpiryReclaimTxid },
|
|
489
|
+
trx
|
|
490
|
+
}));
|
|
491
|
+
const currentReq = await EntityProvenTxReq_1.EntityProvenTxReq.fromStorageTxid(storage, currentTarget.noSendExpiryReclaimTxid, trx);
|
|
492
|
+
const currentAnchor = (0, utilityHelpers_1.verifyOne)(await storage.findOutputs({
|
|
493
|
+
partial: {
|
|
494
|
+
userId: currentTarget.userId,
|
|
495
|
+
txid: currentTarget.noSendExpiryAnchorTxid,
|
|
496
|
+
vout: currentTarget.noSendExpiryAnchorVout
|
|
497
|
+
},
|
|
498
|
+
trx
|
|
499
|
+
}));
|
|
500
|
+
if (currentReclaim?.status !== 'failed' ||
|
|
501
|
+
currentReclaim.provenTxId != null ||
|
|
502
|
+
currentReq == null ||
|
|
503
|
+
(currentReq.status !== 'invalid' && currentReq.status !== 'doubleSpend') ||
|
|
504
|
+
currentAnchor.spendable ||
|
|
505
|
+
currentAnchor.spentBy !== currentReclaim.transactionId) {
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
// Generic transaction APIs deliberately prohibit un-failing a row. This
|
|
509
|
+
// narrowly scoped recovery is safe because it revives only the same signed
|
|
510
|
+
// reclaim while retaining its input reservation and quarantined output.
|
|
511
|
+
await storage.updateTransaction(currentReclaim.transactionId, { status: 'unprocessed' }, trx);
|
|
512
|
+
currentReq.status = 'unsent';
|
|
513
|
+
currentReq.attempts = 0;
|
|
514
|
+
currentReq.rebroadcastAttempts = currentReq.rebroadcastAttempts + 1;
|
|
515
|
+
currentReq.notified = false;
|
|
516
|
+
currentReq.addHistoryNote({
|
|
517
|
+
what: 'brc177-reclaim-retry',
|
|
518
|
+
targetTxid: currentTarget.txid,
|
|
519
|
+
retry: currentReq.rebroadcastAttempts
|
|
520
|
+
});
|
|
521
|
+
await currentReq.updateStorageDynamicProperties(storage, trx);
|
|
522
|
+
return currentReq;
|
|
523
|
+
});
|
|
524
|
+
if (sendable == null)
|
|
525
|
+
return 'deferred';
|
|
526
|
+
await storage.attemptToPostReqsToNetwork([sendable]).catch(() => undefined);
|
|
527
|
+
return 'retried';
|
|
528
|
+
}
|
|
529
|
+
async function processReclaimRace(storage, transaction, targetStatus, result) {
|
|
530
|
+
if (isKnownOrMined(targetStatus) && (await noteTargetObservedDuringRace(storage, transaction))) {
|
|
531
|
+
result.observed++;
|
|
532
|
+
}
|
|
533
|
+
const race = await reconcileRace(storage, transaction);
|
|
534
|
+
if (race !== 'deferred') {
|
|
535
|
+
recordRaceResult(result, race);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const recovery = await retryRejectedReclaim(storage, transaction, targetStatus);
|
|
539
|
+
if (recovery === 'retried')
|
|
540
|
+
result.reclaimRetried++;
|
|
541
|
+
else
|
|
542
|
+
result.deferred++;
|
|
543
|
+
}
|
|
544
|
+
async function processObservationOrRace(storage, transaction, targetStatus, result) {
|
|
545
|
+
// A service's `mined` verdict is useful evidence that reclaim must stop,
|
|
546
|
+
// but it is not a locally verified Merkle proof. Only storage's proven
|
|
547
|
+
// state may finalize the target as the winner.
|
|
548
|
+
if (transaction.status === 'completed' || transaction.provenTxId != null) {
|
|
549
|
+
if (await markObservedTarget(storage, transaction, true))
|
|
550
|
+
result.targetWon++;
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
if (transaction.noSendExpiryState === 'reclaiming') {
|
|
554
|
+
await processReclaimRace(storage, transaction, targetStatus, result);
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
const stateCanObserve = transaction.noSendExpiryState === 'signed' ||
|
|
558
|
+
transaction.noSendExpiryState === 'revocation-requested' ||
|
|
559
|
+
transaction.noSendExpiryState === 'broadcast' ||
|
|
560
|
+
transaction.noSendExpiryState === 'conflicted';
|
|
561
|
+
if (stateCanObserve && isKnownOrMined(targetStatus)) {
|
|
562
|
+
if (await markObservedTarget(storage, transaction, false))
|
|
563
|
+
result.observed++;
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
async function requestRevocation(storage, transaction) {
|
|
569
|
+
if (transaction.noSendExpiryState === 'revocation-requested')
|
|
570
|
+
return 'revocation-requested';
|
|
571
|
+
if (transaction.noSendExpiryState !== 'signed')
|
|
572
|
+
return undefined;
|
|
573
|
+
// Persist that the deadline has triggered before consulting fallible
|
|
574
|
+
// services. A clock correction or block-height reorganization must not
|
|
575
|
+
// reactivate a transaction after its expiry was already observed.
|
|
576
|
+
const changed = await storage.compareAndSetNoSendExpiryState(transaction.transactionId, 'signed', 'revocation-requested');
|
|
577
|
+
return changed ? 'revocation-requested' : undefined;
|
|
578
|
+
}
|
|
579
|
+
async function attemptReclaim(storage, transaction, result, expected = 'revocation-requested') {
|
|
580
|
+
const anchor = (0, utilityHelpers_1.verifyOne)(await storage.findOutputs({
|
|
581
|
+
partial: {
|
|
582
|
+
userId: transaction.userId,
|
|
583
|
+
txid: transaction.noSendExpiryAnchorTxid,
|
|
584
|
+
vout: transaction.noSendExpiryAnchorVout
|
|
585
|
+
}
|
|
586
|
+
}));
|
|
587
|
+
let anchorIsUtxo;
|
|
588
|
+
try {
|
|
589
|
+
anchorIsUtxo = await storage.getServices().isUtxo(anchor);
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
result.deferred++;
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (!anchorIsUtxo) {
|
|
596
|
+
if (expected === 'revocation-requested') {
|
|
597
|
+
await storage.compareAndSetNoSendExpiryState(transaction.transactionId, expected, 'conflicted');
|
|
598
|
+
}
|
|
599
|
+
result.deferred++;
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const reclaim = await activateReclaim(storage, transaction, anchor, expected);
|
|
603
|
+
if (reclaim == null)
|
|
604
|
+
return;
|
|
605
|
+
result.reclaimActivated++;
|
|
606
|
+
await storage.attemptToPostReqsToNetwork([reclaim]).catch(() => undefined);
|
|
607
|
+
}
|
|
608
|
+
async function processTransaction(storage, transaction, targetStatus, nowSeconds, height, result) {
|
|
609
|
+
if (await processObservationOrRace(storage, transaction, targetStatus, result))
|
|
610
|
+
return;
|
|
611
|
+
if (!isDue(transaction, nowSeconds, height))
|
|
612
|
+
return;
|
|
613
|
+
if (transaction.noSendExpiryState === 'preparing' || transaction.noSendExpiryState === 'unsigned') {
|
|
614
|
+
if (await cancelUnsigned(storage, transaction))
|
|
615
|
+
result.cancelled++;
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (transaction.noSendExpiryState === 'conflicted') {
|
|
619
|
+
// A competing mempool spend can disappear. Resume only after the target
|
|
620
|
+
// is still explicitly unknown and the anchor service again gives a
|
|
621
|
+
// conclusive unspent verdict; `attemptReclaim` advances directly to the
|
|
622
|
+
// higher-ranked reclaiming state without reviving the released target.
|
|
623
|
+
if (targetStatus !== 'unknown') {
|
|
624
|
+
result.deferred++;
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
await attemptReclaim(storage, transaction, result, 'conflicted');
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if ((await requestRevocation(storage, transaction)) == null)
|
|
631
|
+
return;
|
|
632
|
+
// Absence of a successful, explicit "unknown" verdict is not evidence
|
|
633
|
+
// that the protected transaction is absent. A service outage must never
|
|
634
|
+
// turn into authorization to double spend the anchor.
|
|
635
|
+
if (targetStatus !== 'unknown') {
|
|
636
|
+
result.deferred++;
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
await attemptReclaim(storage, transaction, result);
|
|
640
|
+
}
|
|
641
|
+
async function processPage(storage, page, context, result) {
|
|
642
|
+
const transactions = await activeTransactionsOnPage(storage, page);
|
|
643
|
+
result.inspected += transactions.length;
|
|
644
|
+
const needsHeight = transactions.some(transaction => transaction.noSendExpiryMode === 'blockheight');
|
|
645
|
+
if (!context.heightAttempted && needsHeight) {
|
|
646
|
+
context.heightAttempted = true;
|
|
647
|
+
try {
|
|
648
|
+
context.height = await storage.getServices().getHeight();
|
|
649
|
+
}
|
|
650
|
+
catch { }
|
|
651
|
+
}
|
|
652
|
+
const statuses = await getStatuses(storage, transactions);
|
|
653
|
+
for (const transaction of transactions) {
|
|
654
|
+
try {
|
|
655
|
+
const targetStatus = transaction.txid ? statuses.get(transaction.txid) : undefined;
|
|
656
|
+
await processTransaction(storage, transaction, targetStatus, context.nowSeconds, context.height, result);
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
// A damaged row needs operator repair, but it must not block unrelated
|
|
660
|
+
// users or later deadlines in this multi-user monitor pass.
|
|
661
|
+
result.deferred++;
|
|
662
|
+
result.errors++;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async function processState(storage, state, context, result) {
|
|
667
|
+
for (let offset = 0;; offset += QUERY_PAGE_SIZE) {
|
|
668
|
+
const page = await storage.findTransactions({
|
|
669
|
+
partial: { noSendExpiryState: state },
|
|
670
|
+
paged: { limit: QUERY_PAGE_SIZE, offset }
|
|
671
|
+
});
|
|
672
|
+
await processPage(storage, page, context, result);
|
|
673
|
+
// State transitions can shift later offsets and defer some rows until
|
|
674
|
+
// the next five-second pass, but each pass remains strictly bounded in
|
|
675
|
+
// memory and every pass restarts from the beginning.
|
|
676
|
+
if (page.length < QUERY_PAGE_SIZE)
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function processNoSendExpiryLifecycle(storage) {
|
|
681
|
+
const result = emptyResult();
|
|
682
|
+
const context = {
|
|
683
|
+
nowSeconds: Math.floor(Date.now() / 1000),
|
|
684
|
+
heightAttempted: false
|
|
685
|
+
};
|
|
686
|
+
for (const state of ACTIVE_STATES) {
|
|
687
|
+
await processState(storage, state, context, result);
|
|
688
|
+
}
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
691
|
+
//# sourceMappingURL=noSendExpiryLifecycle.js.map
|