@atomiqlabs/lp-lib 15.0.7 → 15.0.8
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/dist/swaps/spv_vault_swap/SpvVault.d.ts +2 -0
- package/dist/swaps/spv_vault_swap/SpvVault.js +34 -0
- package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +5 -1
- package/dist/swaps/spv_vault_swap/SpvVaults.d.ts +7 -0
- package/dist/swaps/spv_vault_swap/SpvVaults.js +89 -3
- package/package.json +1 -1
- package/src/swaps/spv_vault_swap/SpvVault.ts +35 -0
- package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +5 -1
- package/src/swaps/spv_vault_swap/SpvVaults.ts +89 -4
|
@@ -12,6 +12,7 @@ export declare class SpvVault<D extends SpvWithdrawalTransactionData = SpvWithdr
|
|
|
12
12
|
readonly initialUtxo: string;
|
|
13
13
|
readonly btcAddress: string;
|
|
14
14
|
readonly pendingWithdrawals: D[];
|
|
15
|
+
readonly replacedWithdrawals: Map<number, D[]>;
|
|
15
16
|
data: T;
|
|
16
17
|
state: SpvVaultState;
|
|
17
18
|
balances: SpvVaultTokenBalance[];
|
|
@@ -24,6 +25,7 @@ export declare class SpvVault<D extends SpvWithdrawalTransactionData = SpvWithdr
|
|
|
24
25
|
update(event: SpvVaultOpenEvent | SpvVaultDepositEvent | SpvVaultCloseEvent | SpvVaultClaimEvent): void;
|
|
25
26
|
addWithdrawal(withdrawalData: D): void;
|
|
26
27
|
removeWithdrawal(withdrawalData: D): boolean;
|
|
28
|
+
doubleSpendPendingWithdrawal(withdrawalData: D): boolean;
|
|
27
29
|
toRawAmounts(amounts: bigint[]): bigint[];
|
|
28
30
|
fromRawAmounts(rawAmounts: bigint[]): bigint[];
|
|
29
31
|
/**
|
|
@@ -19,6 +19,7 @@ class SpvVault extends base_1.Lockable {
|
|
|
19
19
|
this.initialUtxo = vault.getUtxo();
|
|
20
20
|
this.btcAddress = btcAddress;
|
|
21
21
|
this.pendingWithdrawals = [];
|
|
22
|
+
this.replacedWithdrawals = new Map();
|
|
22
23
|
}
|
|
23
24
|
else {
|
|
24
25
|
this.state = chainIdOrObj.state;
|
|
@@ -28,6 +29,12 @@ class SpvVault extends base_1.Lockable {
|
|
|
28
29
|
this.btcAddress = chainIdOrObj.btcAddress;
|
|
29
30
|
this.pendingWithdrawals = chainIdOrObj.pendingWithdrawals.map((base_1.SpvWithdrawalTransactionData.deserialize));
|
|
30
31
|
this.scOpenTx = chainIdOrObj.scOpenTx;
|
|
32
|
+
this.replacedWithdrawals = new Map();
|
|
33
|
+
if (chainIdOrObj.replacedWithdrawals != null) {
|
|
34
|
+
chainIdOrObj.replacedWithdrawals.forEach((val) => {
|
|
35
|
+
this.replacedWithdrawals.set(val[0], val[1].map((base_1.SpvWithdrawalTransactionData.deserialize)));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
31
38
|
}
|
|
32
39
|
this.balances = this.data.calculateStateAfter(this.pendingWithdrawals).balances;
|
|
33
40
|
}
|
|
@@ -36,6 +43,15 @@ class SpvVault extends base_1.Lockable {
|
|
|
36
43
|
const processedWithdrawalIndex = this.pendingWithdrawals.findIndex(val => val.btcTx.txid === event.btcTxId);
|
|
37
44
|
if (processedWithdrawalIndex !== -1)
|
|
38
45
|
this.pendingWithdrawals.splice(0, processedWithdrawalIndex + 1);
|
|
46
|
+
if (event instanceof base_1.SpvVaultClaimEvent) {
|
|
47
|
+
for (let key of this.replacedWithdrawals.keys()) {
|
|
48
|
+
if (key <= event.withdrawCount)
|
|
49
|
+
this.replacedWithdrawals.delete(key);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (event instanceof base_1.SpvVaultCloseEvent) {
|
|
53
|
+
this.replacedWithdrawals.clear();
|
|
54
|
+
}
|
|
39
55
|
}
|
|
40
56
|
this.data.updateState(event);
|
|
41
57
|
this.balances = this.data.calculateStateAfter(this.pendingWithdrawals).balances;
|
|
@@ -53,6 +69,19 @@ class SpvVault extends base_1.Lockable {
|
|
|
53
69
|
this.balances = this.data.calculateStateAfter(this.pendingWithdrawals).balances;
|
|
54
70
|
return true;
|
|
55
71
|
}
|
|
72
|
+
doubleSpendPendingWithdrawal(withdrawalData) {
|
|
73
|
+
const index = this.pendingWithdrawals.indexOf(withdrawalData);
|
|
74
|
+
if (index === -1)
|
|
75
|
+
return false;
|
|
76
|
+
this.pendingWithdrawals.splice(index, 1);
|
|
77
|
+
this.balances = this.data.calculateStateAfter(this.pendingWithdrawals).balances;
|
|
78
|
+
const withdrawalIndex = this.data.getWithdrawalCount() + index + 1;
|
|
79
|
+
let arr = this.replacedWithdrawals.get(withdrawalIndex);
|
|
80
|
+
if (arr == null)
|
|
81
|
+
this.replacedWithdrawals.set(withdrawalIndex, arr = []);
|
|
82
|
+
arr.push(withdrawalData);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
56
85
|
toRawAmounts(amounts) {
|
|
57
86
|
return amounts.map((amt, index) => {
|
|
58
87
|
const tokenData = this.data.getTokenData()[index];
|
|
@@ -76,6 +105,10 @@ class SpvVault extends base_1.Lockable {
|
|
|
76
105
|
return this.data.calculateStateAfter(this.pendingWithdrawals.filter(val => val.btcTx.confirmations >= 1)).balances;
|
|
77
106
|
}
|
|
78
107
|
serialize() {
|
|
108
|
+
const replacedWithdrawals = [];
|
|
109
|
+
this.replacedWithdrawals.forEach((value, key) => {
|
|
110
|
+
replacedWithdrawals.push([key, value.map(val => val.serialize())]);
|
|
111
|
+
});
|
|
79
112
|
return {
|
|
80
113
|
state: this.state,
|
|
81
114
|
chainId: this.chainId,
|
|
@@ -83,6 +116,7 @@ class SpvVault extends base_1.Lockable {
|
|
|
83
116
|
initialUtxo: this.initialUtxo,
|
|
84
117
|
btcAddress: this.btcAddress,
|
|
85
118
|
pendingWithdrawals: this.pendingWithdrawals.map(val => val.serialize()),
|
|
119
|
+
replacedWithdrawals,
|
|
86
120
|
scOpenTx: this.scOpenTx
|
|
87
121
|
};
|
|
88
122
|
}
|
|
@@ -256,7 +256,11 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
|
|
|
256
256
|
metadata.times.priceCalculated = Date.now();
|
|
257
257
|
const totalBtcOutput = amountBD + amountBDgas;
|
|
258
258
|
//Check if we have enough funds to honor the request
|
|
259
|
-
|
|
259
|
+
let vault;
|
|
260
|
+
do {
|
|
261
|
+
vault = await this.Vaults.findVaultForSwap(chainIdentifier, totalBtcOutput, useToken, totalInToken, gasToken, totalInGasToken);
|
|
262
|
+
} while (await this.Vaults.checkVaultReplacedTransactions(vault, true));
|
|
263
|
+
abortController.signal.throwIfAborted();
|
|
260
264
|
metadata.times.vaultPicked = Date.now();
|
|
261
265
|
//Create swap receive bitcoin address
|
|
262
266
|
const btcFeeRate = await this.bitcoin.getFeeRate();
|
|
@@ -38,6 +38,13 @@ export declare class SpvVaults {
|
|
|
38
38
|
}, import("@atomiqlabs/base").SpvVaultData<SpvWithdrawalTransactionData>>[]>;
|
|
39
39
|
fundVault(vault: SpvVault, tokenAmounts: bigint[]): Promise<string>;
|
|
40
40
|
withdrawFromVault(vault: SpvVault, tokenAmounts: bigint[], feeRate?: number): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Call this to check whether some of the previously replaced transactions got re-introduced to the mempool
|
|
43
|
+
*
|
|
44
|
+
* @param vault
|
|
45
|
+
* @param save
|
|
46
|
+
*/
|
|
47
|
+
checkVaultReplacedTransactions(vault: SpvVault, save?: boolean): Promise<boolean>;
|
|
41
48
|
checkVaults(): Promise<void>;
|
|
42
49
|
claimWithdrawals(vault: SpvVault, withdrawal: SpvWithdrawalTransactionData[]): Promise<boolean>;
|
|
43
50
|
getVault(chainId: string, owner: string, vaultId: bigint): Promise<SpvVault<SpvWithdrawalTransactionData & {
|
|
@@ -190,6 +190,89 @@ class SpvVaults {
|
|
|
190
190
|
}
|
|
191
191
|
return res.txId;
|
|
192
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Call this to check whether some of the previously replaced transactions got re-introduced to the mempool
|
|
195
|
+
*
|
|
196
|
+
* @param vault
|
|
197
|
+
* @param save
|
|
198
|
+
*/
|
|
199
|
+
async checkVaultReplacedTransactions(vault, save) {
|
|
200
|
+
const { spvVaultContract } = this.getChain(vault.chainId);
|
|
201
|
+
const initialVaultWithdrawalCount = vault.data.getWithdrawalCount();
|
|
202
|
+
let latestWithdrawalIndex = initialVaultWithdrawalCount;
|
|
203
|
+
const newPendingTxns = [];
|
|
204
|
+
const reintroducedTxIds = new Set();
|
|
205
|
+
for (let [withdrawalIndex, replacedWithdrawalGroup] of vault.replacedWithdrawals) {
|
|
206
|
+
if (withdrawalIndex <= latestWithdrawalIndex)
|
|
207
|
+
continue; //Don't check txns that should already be included
|
|
208
|
+
for (let replacedWithdrawal of replacedWithdrawalGroup) {
|
|
209
|
+
if (reintroducedTxIds.has(replacedWithdrawal.getTxId()))
|
|
210
|
+
continue;
|
|
211
|
+
const tx = await this.bitcoinRpc.getTransaction(replacedWithdrawal.getTxId());
|
|
212
|
+
if (tx == null)
|
|
213
|
+
continue;
|
|
214
|
+
//Re-introduce transaction to the pending withdrawals list
|
|
215
|
+
if (withdrawalIndex > latestWithdrawalIndex) {
|
|
216
|
+
const txChain = [replacedWithdrawal];
|
|
217
|
+
withdrawalIndex--;
|
|
218
|
+
while (withdrawalIndex > latestWithdrawalIndex) {
|
|
219
|
+
const tx = await this.bitcoinRpc.getTransaction(txChain[0].getSpentVaultUtxo().split(":")[0]);
|
|
220
|
+
if (tx == null)
|
|
221
|
+
break;
|
|
222
|
+
reintroducedTxIds.add(tx.txid);
|
|
223
|
+
txChain.unshift(await spvVaultContract.getWithdrawalData(tx));
|
|
224
|
+
withdrawalIndex--;
|
|
225
|
+
}
|
|
226
|
+
if (withdrawalIndex > latestWithdrawalIndex) {
|
|
227
|
+
this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Tried to re-introduce previously replaced TX, but one of txns in the chain not found!`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
newPendingTxns.push(...txChain);
|
|
231
|
+
latestWithdrawalIndex += txChain.length;
|
|
232
|
+
break; //Don't check other txns at the same withdrawal index
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Tried to re-introduce previously replaced TX, but vault has already processed such withdrawal!`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (newPendingTxns.length === 0)
|
|
240
|
+
return false;
|
|
241
|
+
if (initialVaultWithdrawalCount !== vault.data.getWithdrawalCount()) {
|
|
242
|
+
this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Not saving vault after checking replaced transactions, due to withdrawal count changed!`);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
const backup = vault.pendingWithdrawals.splice(0, newPendingTxns.length);
|
|
246
|
+
const txsToAddOnTop = vault.pendingWithdrawals.splice(0, vault.pendingWithdrawals.length);
|
|
247
|
+
try {
|
|
248
|
+
newPendingTxns.forEach(val => vault.addWithdrawal(val));
|
|
249
|
+
txsToAddOnTop.forEach(val => vault.addWithdrawal(val));
|
|
250
|
+
for (let i = 0; i < newPendingTxns.length; i++) {
|
|
251
|
+
const withdrawalIndex = initialVaultWithdrawalCount + i + 1;
|
|
252
|
+
const arr = vault.replacedWithdrawals.get(withdrawalIndex);
|
|
253
|
+
if (arr == null)
|
|
254
|
+
continue;
|
|
255
|
+
const index = arr.indexOf(newPendingTxns[i]);
|
|
256
|
+
if (index === -1) {
|
|
257
|
+
this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Cannot remove re-introduced tx ${newPendingTxns[i].getTxId()}, not found in the respective array!`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
arr.splice(index, 1);
|
|
261
|
+
if (arr.length === 0)
|
|
262
|
+
vault.replacedWithdrawals.delete(withdrawalIndex);
|
|
263
|
+
}
|
|
264
|
+
this.logger.info(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Re-introduced back ${newPendingTxns.length} txns that were re-added to the mempool!`);
|
|
265
|
+
if (save)
|
|
266
|
+
await this.saveVault(vault);
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
this.logger.error(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Failed to update the vault with new pending txns (rolling back): `, e);
|
|
271
|
+
//Rollback the pending withdrawals
|
|
272
|
+
vault.pendingWithdrawals.push(...backup, ...txsToAddOnTop);
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
193
276
|
async checkVaults() {
|
|
194
277
|
const vaults = Object.keys(this.vaultStorage.data).map(key => this.vaultStorage.data[key]);
|
|
195
278
|
const claimWithdrawals = [];
|
|
@@ -238,11 +321,11 @@ class SpvVaults {
|
|
|
238
321
|
await this.saveVault(vault);
|
|
239
322
|
}
|
|
240
323
|
if (vault.state === SpvVault_1.SpvVaultState.OPENED) {
|
|
241
|
-
let changed =
|
|
324
|
+
let changed = await this.checkVaultReplacedTransactions(vault);
|
|
242
325
|
//Check if some of the pendingWithdrawals got confirmed
|
|
243
326
|
let latestOwnWithdrawalIndex = -1;
|
|
244
327
|
let latestConfirmedWithdrawalIndex = -1;
|
|
245
|
-
for (let i =
|
|
328
|
+
for (let i = vault.pendingWithdrawals.length - 1; i >= 0; i--) {
|
|
246
329
|
const pendingWithdrawal = vault.pendingWithdrawals[i];
|
|
247
330
|
if (pendingWithdrawal.sending)
|
|
248
331
|
continue;
|
|
@@ -250,9 +333,12 @@ class SpvVaults {
|
|
|
250
333
|
const btcTx = await (0, BitcoinUtils_1.checkTransactionReplacedRpc)(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
|
|
251
334
|
if (btcTx == null) {
|
|
252
335
|
//Probable double-spend, remove from pending withdrawals
|
|
253
|
-
if (!vault.
|
|
336
|
+
if (!vault.doubleSpendPendingWithdrawal(pendingWithdrawal)) {
|
|
254
337
|
this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: " + pendingWithdrawal.btcTx.txid + ", but doesn't exist anymore!");
|
|
255
338
|
}
|
|
339
|
+
else {
|
|
340
|
+
this.logger.info("checkVaults(): Successfully removed withdrawal txId: " + pendingWithdrawal.btcTx.txid + ", due to being replaced in the mempool!");
|
|
341
|
+
}
|
|
256
342
|
changed = true;
|
|
257
343
|
}
|
|
258
344
|
else {
|
package/package.json
CHANGED
|
@@ -28,6 +28,7 @@ export class SpvVault<
|
|
|
28
28
|
readonly btcAddress: string;
|
|
29
29
|
|
|
30
30
|
readonly pendingWithdrawals: D[];
|
|
31
|
+
readonly replacedWithdrawals: Map<number, D[]>;
|
|
31
32
|
data: T;
|
|
32
33
|
|
|
33
34
|
state: SpvVaultState;
|
|
@@ -47,6 +48,7 @@ export class SpvVault<
|
|
|
47
48
|
this.initialUtxo = vault.getUtxo();
|
|
48
49
|
this.btcAddress = btcAddress;
|
|
49
50
|
this.pendingWithdrawals = [];
|
|
51
|
+
this.replacedWithdrawals = new Map();
|
|
50
52
|
} else {
|
|
51
53
|
this.state = chainIdOrObj.state;
|
|
52
54
|
this.chainId = chainIdOrObj.chainId;
|
|
@@ -55,6 +57,12 @@ export class SpvVault<
|
|
|
55
57
|
this.btcAddress = chainIdOrObj.btcAddress;
|
|
56
58
|
this.pendingWithdrawals = chainIdOrObj.pendingWithdrawals.map(SpvWithdrawalTransactionData.deserialize<D>);
|
|
57
59
|
this.scOpenTx = chainIdOrObj.scOpenTx;
|
|
60
|
+
this.replacedWithdrawals = new Map();
|
|
61
|
+
if(chainIdOrObj.replacedWithdrawals!=null) {
|
|
62
|
+
chainIdOrObj.replacedWithdrawals.forEach((val: [number, any[]]) => {
|
|
63
|
+
this.replacedWithdrawals.set(val[0], val[1].map(SpvWithdrawalTransactionData.deserialize<D>));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
58
66
|
}
|
|
59
67
|
this.balances = this.data.calculateStateAfter(this.pendingWithdrawals).balances;
|
|
60
68
|
}
|
|
@@ -63,6 +71,14 @@ export class SpvVault<
|
|
|
63
71
|
if(event instanceof SpvVaultClaimEvent || event instanceof SpvVaultCloseEvent) {
|
|
64
72
|
const processedWithdrawalIndex = this.pendingWithdrawals.findIndex(val => val.btcTx.txid === event.btcTxId);
|
|
65
73
|
if(processedWithdrawalIndex!==-1) this.pendingWithdrawals.splice(0, processedWithdrawalIndex + 1);
|
|
74
|
+
if(event instanceof SpvVaultClaimEvent) {
|
|
75
|
+
for(let key of this.replacedWithdrawals.keys()) {
|
|
76
|
+
if(key<=event.withdrawCount) this.replacedWithdrawals.delete(key);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if(event instanceof SpvVaultCloseEvent) {
|
|
80
|
+
this.replacedWithdrawals.clear();
|
|
81
|
+
}
|
|
66
82
|
}
|
|
67
83
|
this.data.updateState(event);
|
|
68
84
|
this.balances = this.data.calculateStateAfter(this.pendingWithdrawals).balances;
|
|
@@ -82,6 +98,19 @@ export class SpvVault<
|
|
|
82
98
|
return true;
|
|
83
99
|
}
|
|
84
100
|
|
|
101
|
+
doubleSpendPendingWithdrawal(withdrawalData: D): boolean {
|
|
102
|
+
const index = this.pendingWithdrawals.indexOf(withdrawalData);
|
|
103
|
+
if(index===-1) return false;
|
|
104
|
+
this.pendingWithdrawals.splice(index, 1);
|
|
105
|
+
this.balances = this.data.calculateStateAfter(this.pendingWithdrawals).balances;
|
|
106
|
+
|
|
107
|
+
const withdrawalIndex = this.data.getWithdrawalCount()+index+1;
|
|
108
|
+
let arr = this.replacedWithdrawals.get(withdrawalIndex);
|
|
109
|
+
if(arr==null) this.replacedWithdrawals.set(withdrawalIndex, arr = []);
|
|
110
|
+
arr.push(withdrawalData);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
85
114
|
toRawAmounts(amounts: bigint[]): bigint[] {
|
|
86
115
|
return amounts.map((amt, index) => {
|
|
87
116
|
const tokenData = this.data.getTokenData()[index];
|
|
@@ -106,6 +135,11 @@ export class SpvVault<
|
|
|
106
135
|
}
|
|
107
136
|
|
|
108
137
|
serialize(): any {
|
|
138
|
+
const replacedWithdrawals: [number, any[]][] = [];
|
|
139
|
+
this.replacedWithdrawals.forEach((value, key) => {
|
|
140
|
+
replacedWithdrawals.push([key, value.map(val => val.serialize())])
|
|
141
|
+
});
|
|
142
|
+
|
|
109
143
|
return {
|
|
110
144
|
state: this.state,
|
|
111
145
|
chainId: this.chainId,
|
|
@@ -113,6 +147,7 @@ export class SpvVault<
|
|
|
113
147
|
initialUtxo: this.initialUtxo,
|
|
114
148
|
btcAddress: this.btcAddress,
|
|
115
149
|
pendingWithdrawals: this.pendingWithdrawals.map(val => val.serialize()),
|
|
150
|
+
replacedWithdrawals,
|
|
116
151
|
scOpenTx: this.scOpenTx
|
|
117
152
|
}
|
|
118
153
|
}
|
|
@@ -352,7 +352,11 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
|
|
|
352
352
|
const totalBtcOutput = amountBD + amountBDgas;
|
|
353
353
|
|
|
354
354
|
//Check if we have enough funds to honor the request
|
|
355
|
-
|
|
355
|
+
let vault: SpvVault;
|
|
356
|
+
do {
|
|
357
|
+
vault = await this.Vaults.findVaultForSwap(chainIdentifier, totalBtcOutput, useToken, totalInToken, gasToken, totalInGasToken);
|
|
358
|
+
} while (await this.Vaults.checkVaultReplacedTransactions(vault, true));
|
|
359
|
+
abortController.signal.throwIfAborted();
|
|
356
360
|
metadata.times.vaultPicked = Date.now();
|
|
357
361
|
|
|
358
362
|
//Create swap receive bitcoin address
|
|
@@ -241,6 +241,88 @@ export class SpvVaults {
|
|
|
241
241
|
return res.txId;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Call this to check whether some of the previously replaced transactions got re-introduced to the mempool
|
|
246
|
+
*
|
|
247
|
+
* @param vault
|
|
248
|
+
* @param save
|
|
249
|
+
*/
|
|
250
|
+
async checkVaultReplacedTransactions(vault: SpvVault, save?: boolean): Promise<boolean> {
|
|
251
|
+
const {spvVaultContract} = this.getChain(vault.chainId);
|
|
252
|
+
|
|
253
|
+
const initialVaultWithdrawalCount = vault.data.getWithdrawalCount();
|
|
254
|
+
|
|
255
|
+
let latestWithdrawalIndex = initialVaultWithdrawalCount;
|
|
256
|
+
const newPendingTxns: SpvWithdrawalTransactionData[] = [];
|
|
257
|
+
const reintroducedTxIds: Set<string> = new Set();
|
|
258
|
+
for(let [withdrawalIndex, replacedWithdrawalGroup] of vault.replacedWithdrawals) {
|
|
259
|
+
if(withdrawalIndex<=latestWithdrawalIndex) continue; //Don't check txns that should already be included
|
|
260
|
+
|
|
261
|
+
for(let replacedWithdrawal of replacedWithdrawalGroup) {
|
|
262
|
+
if(reintroducedTxIds.has(replacedWithdrawal.getTxId())) continue;
|
|
263
|
+
const tx = await this.bitcoinRpc.getTransaction(replacedWithdrawal.getTxId());
|
|
264
|
+
if(tx==null) continue;
|
|
265
|
+
|
|
266
|
+
//Re-introduce transaction to the pending withdrawals list
|
|
267
|
+
if(withdrawalIndex>latestWithdrawalIndex) {
|
|
268
|
+
const txChain: SpvWithdrawalTransactionData[] = [replacedWithdrawal];
|
|
269
|
+
withdrawalIndex--;
|
|
270
|
+
while(withdrawalIndex>latestWithdrawalIndex) {
|
|
271
|
+
const tx = await this.bitcoinRpc.getTransaction(txChain[0].getSpentVaultUtxo().split(":")[0]);
|
|
272
|
+
if(tx==null) break;
|
|
273
|
+
reintroducedTxIds.add(tx.txid);
|
|
274
|
+
txChain.unshift(await spvVaultContract.getWithdrawalData(tx));
|
|
275
|
+
withdrawalIndex--;
|
|
276
|
+
}
|
|
277
|
+
if(withdrawalIndex>latestWithdrawalIndex) {
|
|
278
|
+
this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Tried to re-introduce previously replaced TX, but one of txns in the chain not found!`);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
newPendingTxns.push(...txChain);
|
|
282
|
+
latestWithdrawalIndex += txChain.length;
|
|
283
|
+
break; //Don't check other txns at the same withdrawal index
|
|
284
|
+
} else {
|
|
285
|
+
this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Tried to re-introduce previously replaced TX, but vault has already processed such withdrawal!`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if(newPendingTxns.length===0) return false;
|
|
291
|
+
|
|
292
|
+
if(initialVaultWithdrawalCount!==vault.data.getWithdrawalCount()) {
|
|
293
|
+
this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Not saving vault after checking replaced transactions, due to withdrawal count changed!`);
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const backup = vault.pendingWithdrawals.splice(0, newPendingTxns.length);
|
|
298
|
+
const txsToAddOnTop = vault.pendingWithdrawals.splice(0, vault.pendingWithdrawals.length);
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
newPendingTxns.forEach(val => vault.addWithdrawal(val));
|
|
302
|
+
txsToAddOnTop.forEach(val => vault.addWithdrawal(val));
|
|
303
|
+
for(let i=0;i<newPendingTxns.length;i++) {
|
|
304
|
+
const withdrawalIndex = initialVaultWithdrawalCount+i+1;
|
|
305
|
+
const arr = vault.replacedWithdrawals.get(withdrawalIndex);
|
|
306
|
+
if(arr==null) continue;
|
|
307
|
+
const index = arr.indexOf(newPendingTxns[i]);
|
|
308
|
+
if(index===-1) {
|
|
309
|
+
this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Cannot remove re-introduced tx ${newPendingTxns[i].getTxId()}, not found in the respective array!`);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
arr.splice(index, 1);
|
|
313
|
+
if(arr.length===0) vault.replacedWithdrawals.delete(withdrawalIndex);
|
|
314
|
+
}
|
|
315
|
+
this.logger.info(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Re-introduced back ${newPendingTxns.length} txns that were re-added to the mempool!`);
|
|
316
|
+
if(save) await this.saveVault(vault);
|
|
317
|
+
return true;
|
|
318
|
+
} catch (e) {
|
|
319
|
+
this.logger.error(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Failed to update the vault with new pending txns (rolling back): `, e);
|
|
320
|
+
//Rollback the pending withdrawals
|
|
321
|
+
vault.pendingWithdrawals.push(...backup, ...txsToAddOnTop);
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
244
326
|
async checkVaults() {
|
|
245
327
|
const vaults = Object.keys(this.vaultStorage.data).map(key => this.vaultStorage.data[key]);
|
|
246
328
|
|
|
@@ -297,11 +379,12 @@ export class SpvVaults {
|
|
|
297
379
|
}
|
|
298
380
|
|
|
299
381
|
if(vault.state===SpvVaultState.OPENED) {
|
|
300
|
-
let changed =
|
|
382
|
+
let changed = await this.checkVaultReplacedTransactions(vault);
|
|
383
|
+
|
|
301
384
|
//Check if some of the pendingWithdrawals got confirmed
|
|
302
385
|
let latestOwnWithdrawalIndex = -1;
|
|
303
386
|
let latestConfirmedWithdrawalIndex = -1;
|
|
304
|
-
for(let i=
|
|
387
|
+
for(let i = vault.pendingWithdrawals.length-1; i>=0; i--) {
|
|
305
388
|
const pendingWithdrawal = vault.pendingWithdrawals[i];
|
|
306
389
|
if(pendingWithdrawal.sending) continue;
|
|
307
390
|
|
|
@@ -309,8 +392,10 @@ export class SpvVaults {
|
|
|
309
392
|
const btcTx = await checkTransactionReplacedRpc(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
|
|
310
393
|
if(btcTx==null) {
|
|
311
394
|
//Probable double-spend, remove from pending withdrawals
|
|
312
|
-
if(!vault.
|
|
313
|
-
this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: "+pendingWithdrawal.btcTx.txid+", but doesn't exist anymore!")
|
|
395
|
+
if(!vault.doubleSpendPendingWithdrawal(pendingWithdrawal)) {
|
|
396
|
+
this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: "+pendingWithdrawal.btcTx.txid+", but doesn't exist anymore!");
|
|
397
|
+
} else {
|
|
398
|
+
this.logger.info("checkVaults(): Successfully removed withdrawal txId: "+pendingWithdrawal.btcTx.txid+", due to being replaced in the mempool!");
|
|
314
399
|
}
|
|
315
400
|
changed = true;
|
|
316
401
|
} else {
|