@atomiqlabs/lp-lib 14.0.0-dev.32 → 14.0.0-dev.34

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.
@@ -1,5 +1,5 @@
1
1
  import { BitcoinRpc } from "@atomiqlabs/base";
2
- import { FromBtcLnRequestType, FromBtcRequestType, FromBtcTrustedRequestType, ISwapPrice, MultichainData, RequestData, SpvVaultSwapRequestType, SwapHandler, SwapHandlerType, ToBtcLnRequestType, ToBtcRequestType } from "..";
2
+ import { FromBtcLnRequestType, FromBtcRequestType, FromBtcTrustedRequestType, ISwapPrice, MultichainData, RequestData, SpvVaultPostQuote, SpvVaultSwap, SpvVaultSwapRequestType, SwapHandler, SwapHandlerType, ToBtcLnRequestType, ToBtcRequestType } from "..";
3
3
  import { SwapHandlerSwap } from "../swaps/SwapHandlerSwap";
4
4
  import { Command } from "@atomiqlabs/server-base";
5
5
  import { FromBtcLnTrustedRequestType } from "../swaps/trusted/frombtcln_trusted/FromBtcLnTrusted";
@@ -128,6 +128,7 @@ export interface IPlugin {
128
128
  feePPM: bigint;
129
129
  networkFeeGetter: (amount: bigint) => Promise<bigint>;
130
130
  }): Promise<QuoteThrow | QuoteSetFees | QuoteAmountTooLow | QuoteAmountTooHigh | ToBtcPluginQuote>;
131
+ onHandlePostedFromBtcQuote?(swapType: SwapHandlerType.FROM_BTC_SPV, request: RequestData<SpvVaultPostQuote>, swap: SpvVaultSwap): Promise<QuoteThrow | null>;
131
132
  onVaultSelection?(chainIdentifier: string, totalSats: bigint, requestedAmount: {
132
133
  amount: bigint;
133
134
  token: string;
@@ -1,6 +1,6 @@
1
1
  import { BitcoinRpc, SwapData } from "@atomiqlabs/base";
2
2
  import { IPlugin, PluginQuote, QuoteAmountTooHigh, QuoteAmountTooLow, QuoteSetFees, QuoteThrow, ToBtcPluginQuote } from "./IPlugin";
3
- import { FromBtcLnRequestType, FromBtcRequestType, FromBtcTrustedRequestType, ISwapPrice, MultichainData, RequestData, SpvVaultSwapRequestType, SwapHandler, SwapHandlerType, ToBtcLnRequestType, ToBtcRequestType } from "..";
3
+ import { FromBtcLnRequestType, FromBtcRequestType, FromBtcTrustedRequestType, ISwapPrice, MultichainData, RequestData, SpvVaultPostQuote, SpvVaultSwap, SpvVaultSwapRequestType, SwapHandler, SwapHandlerType, ToBtcLnRequestType, ToBtcRequestType } from "..";
4
4
  import { SwapHandlerSwap } from "../swaps/SwapHandlerSwap";
5
5
  import { FromBtcLnTrustedRequestType } from "../swaps/trusted/frombtcln_trusted/FromBtcLnTrusted";
6
6
  import { IBitcoinWallet } from "../wallets/IBitcoinWallet";
@@ -101,6 +101,7 @@ export declare class PluginManager {
101
101
  baseFeeInBtc: bigint;
102
102
  feePPM: bigint;
103
103
  }): Promise<QuoteThrow | QuoteSetFees | QuoteAmountTooLow | QuoteAmountTooHigh>;
104
+ static onHandlePostedFromBtcQuote(swapType: SwapHandlerType.FROM_BTC_SPV, request: RequestData<SpvVaultPostQuote>, swap: SpvVaultSwap): Promise<QuoteThrow | null>;
104
105
  static onVaultSelection(chainIdentifier: string, totalSats: bigint, requestedAmount: {
105
106
  amount: bigint;
106
107
  token: string;
@@ -214,6 +214,21 @@ class PluginManager {
214
214
  }
215
215
  return null;
216
216
  }
217
+ static async onHandlePostedFromBtcQuote(swapType, request, swap) {
218
+ for (let plugin of PluginManager.plugins.values()) {
219
+ try {
220
+ if (plugin.onHandlePostedFromBtcQuote != null) {
221
+ const result = await plugin.onHandlePostedFromBtcQuote(swapType, request, swap);
222
+ if (result != null && (0, IPlugin_1.isQuoteThrow)(result))
223
+ return result;
224
+ }
225
+ }
226
+ catch (e) {
227
+ pluginLogger.error(plugin, "onHandlePostedFromBtcQuote(): plugin error", e);
228
+ }
229
+ }
230
+ return null;
231
+ }
217
232
  static async onVaultSelection(chainIdentifier, totalSats, requestedAmount, gasAmount) {
218
233
  for (let plugin of PluginManager.plugins.values()) {
219
234
  try {
@@ -316,7 +316,7 @@ class ToBtcAbs extends ToBtcBaseSwapHandler_1.ToBtcBaseSwapHandler {
316
316
  if (swap.sending)
317
317
  return;
318
318
  //Bitcoin transaction was signed (maybe also sent)
319
- const tx = await (0, BitcoinUtils_1.checkTransactionReplaced)(swap.txId, swap.btcRawTx, this.bitcoin);
319
+ const tx = await (0, BitcoinUtils_1.checkTransactionReplaced)(swap.txId, swap.btcRawTx, this.bitcoinRpc);
320
320
  const isTxSent = tx != null;
321
321
  if (!isTxSent) {
322
322
  //Reset the state to COMMITED
@@ -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[];
@@ -23,6 +24,7 @@ export declare class SpvVault<D extends SpvWithdrawalTransactionData = SpvWithdr
23
24
  update(event: SpvVaultOpenEvent | SpvVaultDepositEvent | SpvVaultCloseEvent | SpvVaultClaimEvent): void;
24
25
  addWithdrawal(withdrawalData: D): void;
25
26
  removeWithdrawal(withdrawalData: D): boolean;
27
+ doubleSpendPendingWithdrawal(withdrawalData: D): boolean;
26
28
  toRawAmounts(amounts: bigint[]): bigint[];
27
29
  fromRawAmounts(rawAmounts: bigint[]): bigint[];
28
30
  /**
@@ -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.scOpenTxs = chainIdOrObj.scOpenTxs;
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
  scOpenTxs: this.scOpenTxs
87
121
  };
88
122
  }
@@ -13,6 +13,7 @@ const crypto_1 = require("crypto");
13
13
  const btc_signer_1 = require("@scure/btc-signer");
14
14
  const SpvVaults_1 = require("./SpvVaults");
15
15
  const BitcoinUtils_1 = require("../../utils/BitcoinUtils");
16
+ const AmountAssertions_1 = require("../assertions/AmountAssertions");
16
17
  const TX_MAX_VSIZE = 16 * 1024;
17
18
  class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
18
19
  constructor(storageDirectory, vaultStorage, path, chainsData, swapPricing, bitcoin, bitcoinRpc, spvVaultSigner, config) {
@@ -105,7 +106,7 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
105
106
  const foundWithdrawal = vault.pendingWithdrawals.find(val => val.btcTx.txid === swap.btcTxId);
106
107
  let tx = foundWithdrawal?.btcTx;
107
108
  if (tx == null)
108
- tx = await this.bitcoin.getWalletTransaction(swap.btcTxId);
109
+ tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
109
110
  if (tx == null) {
110
111
  await this.removeSwapData(swap, SpvVaultSwap_1.SpvVaultSwapState.FAILED);
111
112
  return;
@@ -128,7 +129,7 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
128
129
  const foundWithdrawal = vault.pendingWithdrawals.find(val => val.btcTx.txid === swap.btcTxId);
129
130
  let tx = foundWithdrawal?.btcTx;
130
131
  if (tx == null)
131
- tx = await this.bitcoin.getWalletTransaction(swap.btcTxId);
132
+ tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
132
133
  if (tx == null) {
133
134
  await this.removeSwapData(swap, SpvVaultSwap_1.SpvVaultSwapState.DOUBLE_SPENT);
134
135
  return;
@@ -258,7 +259,11 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
258
259
  metadata.times.priceCalculated = Date.now();
259
260
  const totalBtcOutput = amountBD + amountBDgas;
260
261
  //Check if we have enough funds to honor the request
261
- const vault = await this.Vaults.findVaultForSwap(chainIdentifier, totalBtcOutput, useToken, totalInToken, gasToken, totalInGasToken);
262
+ let vault;
263
+ do {
264
+ vault = await this.Vaults.findVaultForSwap(chainIdentifier, totalBtcOutput, useToken, totalInToken, gasToken, totalInGasToken);
265
+ } while (await this.Vaults.checkVaultReplacedTransactions(vault, true));
266
+ abortController.signal.throwIfAborted();
262
267
  metadata.times.vaultPicked = Date.now();
263
268
  //Create swap receive bitcoin address
264
269
  const btcFeeRate = await this.bitcoin.getFeeRate();
@@ -353,7 +358,6 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
353
358
  msg: "Error parsing PSBT, hex format required!"
354
359
  };
355
360
  }
356
- //Check correct psbt
357
361
  for (let i = 1; i < transaction.inputsLength; i++) { //Skip first vault input
358
362
  const txIn = transaction.getInput(i);
359
363
  if ((0, BitcoinUtils_1.isLegacyInput)(txIn))
@@ -361,6 +365,12 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
361
365
  code: 20514,
362
366
  msg: "Legacy (pre-segwit) inputs in tx are not allowed!"
363
367
  };
368
+ }
369
+ //Check the posted quote with the plugins
370
+ AmountAssertions_1.AmountAssertions.handlePluginErrorResponses(await PluginManager_1.PluginManager.onHandlePostedFromBtcQuote(this.type, { chainIdentifier: swap.chainIdentifier, raw: req, parsed: parsedBody, metadata }, swap));
371
+ //Check correct psbt
372
+ for (let i = 1; i < transaction.inputsLength; i++) { //Skip first vault input
373
+ const txIn = transaction.getInput(i);
364
374
  //Check UTXOs exist and are unspent
365
375
  if (await this.bitcoinRpc.isSpent(Buffer.from(txIn.txid).toString("hex") + ":" + txIn.index.toString(10)))
366
376
  throw {
@@ -426,6 +436,7 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
426
436
  code: 20516,
427
437
  msg: "Bitcoin transaction size too large, maximum: " + TX_MAX_VSIZE + " actual: " + txVsize
428
438
  };
439
+ await this.Vaults.checkVaultReplacedTransactions(vault, true);
429
440
  if (swap.vaultUtxo !== vault.getLatestUtxo()) {
430
441
  throw {
431
442
  code: 20510,
@@ -33,6 +33,13 @@ export declare class SpvVaults {
33
33
  }, import("@atomiqlabs/base").SpvVaultData<SpvWithdrawalTransactionData>>[]>;
34
34
  fundVault(vault: SpvVault, tokenAmounts: bigint[]): Promise<string>;
35
35
  withdrawFromVault(vault: SpvVault, tokenAmounts: bigint[], feeRate?: number): Promise<string>;
36
+ /**
37
+ * Call this to check whether some of the previously replaced transactions got re-introduced to the mempool
38
+ *
39
+ * @param vault
40
+ * @param save
41
+ */
42
+ checkVaultReplacedTransactions(vault: SpvVault, save?: boolean): Promise<boolean>;
36
43
  checkVaults(): Promise<void>;
37
44
  claimWithdrawals(vault: SpvVault, withdrawal: SpvWithdrawalTransactionData[]): Promise<boolean>;
38
45
  getVault(chainId: string, owner: string, vaultId: bigint): Promise<SpvVault<SpvWithdrawalTransactionData & {
@@ -177,32 +177,119 @@ class SpvVaults {
177
177
  amount: 0n,
178
178
  script: opReturnScript
179
179
  });
180
- psbt = await this.bitcoin.fundPsbt(psbt, feeRate);
181
- if (psbt.inputsLength < 2)
182
- throw new Error("PSBT needs at least 2 inputs!");
183
- psbt.updateInput(0, { sequence: 0x80000000 });
184
- psbt.updateInput(1, { sequence: 0x80000000 });
185
- psbt = await this.vaultSigner.signPsbt(vault.chainId, vault.data.getVaultId(), psbt, [0]);
186
- const res = await this.bitcoin.signPsbt(psbt);
187
- const parsedTransaction = await this.bitcoinRpc.parseTransaction(res.raw);
188
- const withdrawalData = await spvVaultContract.getWithdrawalData(parsedTransaction);
189
- if (withdrawalData.getSpentVaultUtxo() !== vault.getLatestUtxo()) {
190
- throw new Error("Latest vault UTXO already spent! Please try again later.");
180
+ let withdrawalTxId = null;
181
+ await this.bitcoin.execute(async () => {
182
+ psbt = await this.bitcoin.fundPsbt(psbt, feeRate);
183
+ if (psbt.inputsLength < 2)
184
+ throw new Error("PSBT needs at least 2 inputs!");
185
+ psbt.updateInput(0, { sequence: 0x80000000 });
186
+ psbt.updateInput(1, { sequence: 0x80000000 });
187
+ psbt = await this.vaultSigner.signPsbt(vault.chainId, vault.data.getVaultId(), psbt, [0]);
188
+ const res = await this.bitcoin.signPsbt(psbt);
189
+ withdrawalTxId = res.txId;
190
+ const parsedTransaction = await this.bitcoinRpc.parseTransaction(res.raw);
191
+ const withdrawalData = await spvVaultContract.getWithdrawalData(parsedTransaction);
192
+ if (withdrawalData.getSpentVaultUtxo() !== vault.getLatestUtxo()) {
193
+ throw new Error("Latest vault UTXO already spent! Please try again later.");
194
+ }
195
+ withdrawalData.sending = true;
196
+ vault.addWithdrawal(withdrawalData);
197
+ await this.saveVault(vault);
198
+ try {
199
+ await this.bitcoin.sendRawTransaction(res.raw);
200
+ withdrawalData.sending = false;
201
+ }
202
+ catch (e) {
203
+ withdrawalData.sending = false;
204
+ vault.removeWithdrawal(withdrawalData);
205
+ await this.saveVault(vault);
206
+ throw e;
207
+ }
208
+ });
209
+ return withdrawalTxId;
210
+ }
211
+ /**
212
+ * Call this to check whether some of the previously replaced transactions got re-introduced to the mempool
213
+ *
214
+ * @param vault
215
+ * @param save
216
+ */
217
+ async checkVaultReplacedTransactions(vault, save) {
218
+ const { spvVaultContract } = this.chains.chains[vault.chainId];
219
+ const initialVaultWithdrawalCount = vault.data.getWithdrawalCount();
220
+ let latestWithdrawalIndex = initialVaultWithdrawalCount;
221
+ const newPendingTxns = [];
222
+ const reintroducedTxIds = new Set();
223
+ for (let [withdrawalIndex, replacedWithdrawalGroup] of vault.replacedWithdrawals) {
224
+ if (withdrawalIndex <= latestWithdrawalIndex)
225
+ continue; //Don't check txns that should already be included
226
+ for (let replacedWithdrawal of replacedWithdrawalGroup) {
227
+ if (reintroducedTxIds.has(replacedWithdrawal.getTxId()))
228
+ continue;
229
+ const tx = await this.bitcoinRpc.getTransaction(replacedWithdrawal.getTxId());
230
+ if (tx == null)
231
+ continue;
232
+ //Re-introduce transaction to the pending withdrawals list
233
+ if (withdrawalIndex > latestWithdrawalIndex) {
234
+ const txChain = [replacedWithdrawal];
235
+ withdrawalIndex--;
236
+ while (withdrawalIndex > latestWithdrawalIndex) {
237
+ const tx = await this.bitcoinRpc.getTransaction(txChain[0].getSpentVaultUtxo().split(":")[0]);
238
+ if (tx == null)
239
+ break;
240
+ reintroducedTxIds.add(tx.txid);
241
+ txChain.unshift(await spvVaultContract.getWithdrawalData(tx));
242
+ withdrawalIndex--;
243
+ }
244
+ if (withdrawalIndex > latestWithdrawalIndex) {
245
+ this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Tried to re-introduce previously replaced TX, but one of txns in the chain not found!`);
246
+ continue;
247
+ }
248
+ newPendingTxns.push(...txChain);
249
+ latestWithdrawalIndex += txChain.length;
250
+ break; //Don't check other txns at the same withdrawal index
251
+ }
252
+ else {
253
+ this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Tried to re-introduce previously replaced TX, but vault has already processed such withdrawal!`);
254
+ }
255
+ }
191
256
  }
192
- withdrawalData.sending = true;
193
- vault.addWithdrawal(withdrawalData);
194
- await this.saveVault(vault);
257
+ if (newPendingTxns.length === 0)
258
+ return false;
259
+ if (initialVaultWithdrawalCount !== vault.data.getWithdrawalCount()) {
260
+ this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Not saving vault after checking replaced transactions, due to withdrawal count changed!`);
261
+ return false;
262
+ }
263
+ const backup = vault.pendingWithdrawals.splice(0, newPendingTxns.length);
264
+ const txsToAddOnTop = vault.pendingWithdrawals.splice(0, vault.pendingWithdrawals.length);
195
265
  try {
196
- await this.bitcoin.sendRawTransaction(res.raw);
197
- withdrawalData.sending = false;
266
+ newPendingTxns.forEach(val => vault.addWithdrawal(val));
267
+ txsToAddOnTop.forEach(val => vault.addWithdrawal(val));
268
+ for (let i = 0; i < newPendingTxns.length; i++) {
269
+ const withdrawalIndex = initialVaultWithdrawalCount + i + 1;
270
+ const arr = vault.replacedWithdrawals.get(withdrawalIndex);
271
+ if (arr == null)
272
+ continue;
273
+ const index = arr.indexOf(newPendingTxns[i]);
274
+ if (index === -1) {
275
+ this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Cannot remove re-introduced tx ${newPendingTxns[i].getTxId()}, not found in the respective array!`);
276
+ continue;
277
+ }
278
+ arr.splice(index, 1);
279
+ if (arr.length === 0)
280
+ vault.replacedWithdrawals.delete(withdrawalIndex);
281
+ }
282
+ this.logger.info(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Re-introduced back ${newPendingTxns.length} txns that were re-added to the mempool!`);
283
+ if (save)
284
+ await this.saveVault(vault);
285
+ return true;
198
286
  }
199
287
  catch (e) {
200
- withdrawalData.sending = false;
201
- vault.removeWithdrawal(withdrawalData);
202
- await this.saveVault(vault);
203
- throw e;
288
+ this.logger.error(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Failed to update the vault with new pending txns (rolling back): `, e);
289
+ //Rollback the pending withdrawals
290
+ vault.pendingWithdrawals.push(...backup, ...txsToAddOnTop);
291
+ return false;
204
292
  }
205
- return res.txId;
206
293
  }
207
294
  async checkVaults() {
208
295
  const vaults = Object.keys(this.vaultStorage.data).map(key => this.vaultStorage.data[key]);
@@ -268,24 +355,23 @@ class SpvVaults {
268
355
  continue;
269
356
  }
270
357
  if (vault.state === SpvVault_1.SpvVaultState.OPENED) {
271
- let changed = false;
358
+ let changed = await this.checkVaultReplacedTransactions(vault);
272
359
  //Check if some of the pendingWithdrawals got confirmed
273
360
  let latestOwnWithdrawalIndex = -1;
274
361
  let latestConfirmedWithdrawalIndex = -1;
275
- for (let i = 0; i < vault.pendingWithdrawals.length; i++) {
362
+ for (let i = vault.pendingWithdrawals.length - 1; i >= 0; i--) {
276
363
  const pendingWithdrawal = vault.pendingWithdrawals[i];
277
364
  if (pendingWithdrawal.sending)
278
365
  continue;
279
366
  //Check all the pending withdrawals that were not finalized yet
280
- const btcTx = await (0, BitcoinUtils_1.checkTransactionReplacedRpc)(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
367
+ const btcTx = await (0, BitcoinUtils_1.checkTransactionReplaced)(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
281
368
  if (btcTx == null) {
282
369
  //Probable double-spend, remove from pending withdrawals
283
- const index = vault.pendingWithdrawals.indexOf(pendingWithdrawal);
284
- if (index === -1) {
370
+ if (!vault.doubleSpendPendingWithdrawal(pendingWithdrawal)) {
285
371
  this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: " + pendingWithdrawal.btcTx.txid + ", but doesn't exist anymore!");
286
372
  }
287
373
  else {
288
- vault.pendingWithdrawals.splice(index, 1);
374
+ this.logger.info("checkVaults(): Successfully removed withdrawal txId: " + pendingWithdrawal.btcTx.txid + ", due to being replaced in the mempool!");
289
375
  }
290
376
  changed = true;
291
377
  }
@@ -608,7 +608,7 @@ class FromBtcTrusted extends SwapHandler_1.SwapHandler {
608
608
  }
609
609
  async checkDoubleSpends() {
610
610
  for (let swap of this.doubleSpendWatchdogSwaps.keys()) {
611
- const tx = await this.bitcoin.getWalletTransaction(swap.txId);
611
+ const tx = await this.bitcoinRpc.getTransaction(swap.txId);
612
612
  if (tx == null) {
613
613
  this.swapLogger.debug(swap, "checkDoubleSpends(): Swap was double spent, burning... - original txId: " + swap.txId);
614
614
  this.processPastSwap(swap, null, null);
@@ -1,6 +1,4 @@
1
1
  import { TransactionInput } from "@scure/btc-signer/psbt";
2
2
  import { BitcoinRpc, BtcTx } from "@atomiqlabs/base";
3
- import { IBitcoinWallet } from "../wallets/IBitcoinWallet";
4
3
  export declare function isLegacyInput(input: TransactionInput): boolean;
5
- export declare function checkTransactionReplaced(txId: string, txRaw: string, bitcoin: IBitcoinWallet): Promise<BtcTx>;
6
- export declare function checkTransactionReplacedRpc(txId: string, txRaw: string, bitcoin: BitcoinRpc<any>): Promise<BtcTx>;
4
+ export declare function checkTransactionReplaced(txId: string, txRaw: string, bitcoin: BitcoinRpc<any>): Promise<BtcTx>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.checkTransactionReplacedRpc = exports.checkTransactionReplaced = exports.isLegacyInput = void 0;
3
+ exports.checkTransactionReplaced = exports.isLegacyInput = void 0;
4
4
  const utxo_1 = require("@scure/btc-signer/utxo");
5
5
  const btc_signer_1 = require("@scure/btc-signer");
6
6
  const Utils_1 = require("./Utils");
@@ -46,20 +46,6 @@ function isLegacyInput(input) {
46
46
  }
47
47
  exports.isLegacyInput = isLegacyInput;
48
48
  async function checkTransactionReplaced(txId, txRaw, bitcoin) {
49
- const existingTx = await bitcoin.getWalletTransaction(txId);
50
- if (existingTx != null)
51
- return existingTx;
52
- //Try to re-broadcast
53
- try {
54
- await bitcoin.sendRawTransaction(txRaw);
55
- }
56
- catch (e) {
57
- logger.error("checkTransactionReplaced(" + txId + "): Error when trying to re-broadcast raw transaction: ", e);
58
- }
59
- return await bitcoin.getWalletTransaction(txId);
60
- }
61
- exports.checkTransactionReplaced = checkTransactionReplaced;
62
- async function checkTransactionReplacedRpc(txId, txRaw, bitcoin) {
63
49
  const existingTx = await bitcoin.getTransaction(txId);
64
50
  if (existingTx != null)
65
51
  return existingTx;
@@ -72,4 +58,4 @@ async function checkTransactionReplacedRpc(txId, txRaw, bitcoin) {
72
58
  }
73
59
  return await bitcoin.getTransaction(txId);
74
60
  }
75
- exports.checkTransactionReplacedRpc = checkTransactionReplacedRpc;
61
+ exports.checkTransactionReplaced = checkTransactionReplaced;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/lp-lib",
3
- "version": "14.0.0-dev.32",
3
+ "version": "14.0.0-dev.34",
4
4
  "description": "Main functionality implementation for atomiq LP node",
5
5
  "main": "./dist/index.js",
6
6
  "types:": "./dist/index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "license": "ISC",
24
24
  "dependencies": {
25
25
  "@atomiqlabs/base": "^10.0.0-dev.10",
26
- "@atomiqlabs/server-base": "2.0.0",
26
+ "@atomiqlabs/server-base": "^3.0.0",
27
27
  "@scure/btc-signer": "1.6.0",
28
28
  "express": "4.21.1",
29
29
  "promise-queue-ts": "0.0.1"
@@ -1,8 +1,8 @@
1
- import {BitcoinRpc, SpvWithdrawalTransactionData, SwapData} from "@atomiqlabs/base";
1
+ import {BitcoinRpc} from "@atomiqlabs/base";
2
2
  import {
3
3
  FromBtcLnRequestType,
4
4
  FromBtcRequestType, FromBtcTrustedRequestType,
5
- ISwapPrice, MultichainData, RequestData, SpvVaultSwapRequestType,
5
+ ISwapPrice, MultichainData, RequestData, SpvVaultPostQuote, SpvVaultSwap, SpvVaultSwapRequestType,
6
6
  SwapHandler, SwapHandlerType,
7
7
  ToBtcLnRequestType,
8
8
  ToBtcRequestType
@@ -151,6 +151,12 @@ export interface IPlugin {
151
151
  fees: {baseFeeInBtc: bigint, feePPM: bigint, networkFeeGetter: (amount: bigint) => Promise<bigint>}
152
152
  ): Promise<QuoteThrow | QuoteSetFees | QuoteAmountTooLow | QuoteAmountTooHigh | ToBtcPluginQuote>;
153
153
 
154
+ onHandlePostedFromBtcQuote?(
155
+ swapType: SwapHandlerType.FROM_BTC_SPV,
156
+ request: RequestData<SpvVaultPostQuote>,
157
+ swap: SpvVaultSwap
158
+ ): Promise<QuoteThrow | null>;
159
+
154
160
  onVaultSelection?(
155
161
  chainIdentifier: string,
156
162
  totalSats: bigint,
@@ -1,4 +1,4 @@
1
- import {BitcoinRpc, SpvWithdrawalTransactionData, SwapData} from "@atomiqlabs/base";
1
+ import {BitcoinRpc, SwapData} from "@atomiqlabs/base";
2
2
  import {
3
3
  IPlugin, isPluginQuote, isQuoteAmountTooHigh, isQuoteAmountTooLow, isQuoteSetFees,
4
4
  isQuoteThrow, isToBtcPluginQuote, PluginQuote,
@@ -10,7 +10,7 @@ import {
10
10
  import {
11
11
  FromBtcLnRequestType,
12
12
  FromBtcRequestType, FromBtcTrustedRequestType,
13
- ISwapPrice, MultichainData, RequestData, SpvVaultSwapRequestType,
13
+ ISwapPrice, MultichainData, RequestData, SpvVaultPostQuote, SpvVaultSwap, SpvVaultSwapRequestType,
14
14
  SwapHandler, SwapHandlerType,
15
15
  ToBtcLnRequestType,
16
16
  ToBtcRequestType
@@ -291,6 +291,24 @@ export class PluginManager {
291
291
  return null;
292
292
  }
293
293
 
294
+ static async onHandlePostedFromBtcQuote(
295
+ swapType: SwapHandlerType.FROM_BTC_SPV,
296
+ request: RequestData<SpvVaultPostQuote>,
297
+ swap: SpvVaultSwap
298
+ ): Promise<QuoteThrow | null> {
299
+ for(let plugin of PluginManager.plugins.values()) {
300
+ try {
301
+ if(plugin.onHandlePostedFromBtcQuote!=null) {
302
+ const result = await plugin.onHandlePostedFromBtcQuote(swapType, request, swap);
303
+ if(result!=null && isQuoteThrow(result)) return result;
304
+ }
305
+ } catch (e) {
306
+ pluginLogger.error(plugin, "onHandlePostedFromBtcQuote(): plugin error", e);
307
+ }
308
+ }
309
+ return null;
310
+ }
311
+
294
312
  static async onVaultSelection(
295
313
  chainIdentifier: string,
296
314
  totalSats: bigint,
@@ -404,7 +404,7 @@ export class ToBtcAbs extends ToBtcBaseSwapHandler<ToBtcSwapAbs, ToBtcSwapState>
404
404
  if(swap.state===ToBtcSwapState.BTC_SENDING) {
405
405
  if(swap.sending) return;
406
406
  //Bitcoin transaction was signed (maybe also sent)
407
- const tx = await checkTransactionReplaced(swap.txId, swap.btcRawTx, this.bitcoin);
407
+ const tx = await checkTransactionReplaced(swap.txId, swap.btcRawTx, this.bitcoinRpc);
408
408
 
409
409
  const isTxSent = tx!=null;
410
410
  if(!isTxSent) {
@@ -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.scOpenTxs = chainIdOrObj.scOpenTxs;
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
  scOpenTxs: this.scOpenTxs
117
152
  }
118
153
  }
@@ -27,9 +27,10 @@ import {ServerParamEncoder} from "../../utils/paramcoders/server/ServerParamEnco
27
27
  import {FieldTypeEnum} from "../../utils/paramcoders/SchemaVerifier";
28
28
  import {FromBtcAmountAssertions} from "../assertions/FromBtcAmountAssertions";
29
29
  import {randomBytes} from "crypto";
30
- import {getInputType, OutScript, Transaction} from "@scure/btc-signer";
30
+ import {Transaction} from "@scure/btc-signer";
31
31
  import {SpvVaults, VAULT_DUST_AMOUNT} from "./SpvVaults";
32
- import {checkTransactionReplaced, isLegacyInput} from "../../utils/BitcoinUtils";
32
+ import {isLegacyInput} from "../../utils/BitcoinUtils";
33
+ import {AmountAssertions} from "../assertions/AmountAssertions";
33
34
 
34
35
  export type SpvVaultSwapHandlerConfig = SwapBaseConfig & {
35
36
  vaultsCheckInterval: number,
@@ -170,7 +171,7 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
170
171
  const vault = await this.Vaults.getVault(swap.chainIdentifier, swap.vaultOwner, swap.vaultId);
171
172
  const foundWithdrawal = vault.pendingWithdrawals.find(val => val.btcTx.txid === swap.btcTxId);
172
173
  let tx = foundWithdrawal?.btcTx;
173
- if(tx==null) tx = await this.bitcoin.getWalletTransaction(swap.btcTxId);
174
+ if(tx==null) tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
174
175
 
175
176
  if(tx==null) {
176
177
  await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
@@ -191,7 +192,7 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
191
192
  const vault = await this.Vaults.getVault(swap.chainIdentifier, swap.vaultOwner, swap.vaultId);
192
193
  const foundWithdrawal = vault.pendingWithdrawals.find(val => val.btcTx.txid === swap.btcTxId);
193
194
  let tx = foundWithdrawal?.btcTx;
194
- if(tx==null) tx = await this.bitcoin.getWalletTransaction(swap.btcTxId);
195
+ if(tx==null) tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
195
196
 
196
197
  if(tx==null) {
197
198
  await this.removeSwapData(swap, SpvVaultSwapState.DOUBLE_SPENT);
@@ -354,7 +355,11 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
354
355
  const totalBtcOutput = amountBD + amountBDgas;
355
356
 
356
357
  //Check if we have enough funds to honor the request
357
- const vault = await this.Vaults.findVaultForSwap(chainIdentifier, totalBtcOutput, useToken, totalInToken, gasToken, totalInGasToken);
358
+ let vault: SpvVault;
359
+ do {
360
+ vault = await this.Vaults.findVaultForSwap(chainIdentifier, totalBtcOutput, useToken, totalInToken, gasToken, totalInGasToken);
361
+ } while (await this.Vaults.checkVaultReplacedTransactions(vault, true));
362
+ abortController.signal.throwIfAborted();
358
363
  metadata.times.vaultPicked = Date.now();
359
364
 
360
365
  //Create swap receive bitcoin address
@@ -480,13 +485,24 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
480
485
  };
481
486
  }
482
487
 
483
- //Check correct psbt
484
488
  for(let i=1;i<transaction.inputsLength;i++) { //Skip first vault input
485
489
  const txIn = transaction.getInput(i);
486
- if(isLegacyInput(txIn)) throw {
490
+ if (isLegacyInput(txIn)) throw {
487
491
  code: 20514,
488
492
  msg: "Legacy (pre-segwit) inputs in tx are not allowed!"
489
493
  };
494
+ }
495
+
496
+ //Check the posted quote with the plugins
497
+ AmountAssertions.handlePluginErrorResponses(await PluginManager.onHandlePostedFromBtcQuote(
498
+ this.type,
499
+ {chainIdentifier: swap.chainIdentifier, raw: req, parsed: parsedBody, metadata},
500
+ swap
501
+ ));
502
+
503
+ //Check correct psbt
504
+ for(let i=1;i<transaction.inputsLength;i++) { //Skip first vault input
505
+ const txIn = transaction.getInput(i);
490
506
  //Check UTXOs exist and are unspent
491
507
  if(await this.bitcoinRpc.isSpent(Buffer.from(txIn.txid).toString("hex")+":"+txIn.index.toString(10))) throw {
492
508
  code: 20515,
@@ -558,6 +574,7 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
558
574
  msg: "Bitcoin transaction size too large, maximum: "+TX_MAX_VSIZE+" actual: "+txVsize
559
575
  };
560
576
 
577
+ await this.Vaults.checkVaultReplacedTransactions(vault, true);
561
578
  if(swap.vaultUtxo!==vault.getLatestUtxo()) {
562
579
  throw {
563
580
  code: 20510,
@@ -15,7 +15,7 @@ import {ISpvVaultSigner} from "../../wallets/ISpvVaultSigner";
15
15
  import {AmountAssertions} from "../assertions/AmountAssertions";
16
16
  import {MultichainData} from "../SwapHandler";
17
17
  import {Transaction} from "@scure/btc-signer";
18
- import {checkTransactionReplacedRpc} from "../../utils/BitcoinUtils";
18
+ import {checkTransactionReplaced} from "../../utils/BitcoinUtils";
19
19
 
20
20
  export const VAULT_DUST_AMOUNT = 600;
21
21
  const VAULT_INIT_CONFIRMATIONS = 2;
@@ -227,34 +227,120 @@ export class SpvVaults {
227
227
  script: opReturnScript
228
228
  });
229
229
 
230
- psbt = await this.bitcoin.fundPsbt(psbt, feeRate);
231
- if(psbt.inputsLength<2) throw new Error("PSBT needs at least 2 inputs!");
232
- psbt.updateInput(0, {sequence: 0x80000000});
233
- psbt.updateInput(1, {sequence: 0x80000000});
234
- psbt = await this.vaultSigner.signPsbt(vault.chainId, vault.data.getVaultId(), psbt, [0]);
235
- const res = await this.bitcoin.signPsbt(psbt);
230
+ let withdrawalTxId: string = null;
231
+ await this.bitcoin.execute(async () => {
232
+ psbt = await this.bitcoin.fundPsbt(psbt, feeRate);
233
+ if(psbt.inputsLength<2) throw new Error("PSBT needs at least 2 inputs!");
234
+ psbt.updateInput(0, {sequence: 0x80000000});
235
+ psbt.updateInput(1, {sequence: 0x80000000});
236
+ psbt = await this.vaultSigner.signPsbt(vault.chainId, vault.data.getVaultId(), psbt, [0]);
237
+ const res = await this.bitcoin.signPsbt(psbt);
238
+ withdrawalTxId = res.txId;
239
+
240
+ const parsedTransaction = await this.bitcoinRpc.parseTransaction(res.raw);
241
+ const withdrawalData = await spvVaultContract.getWithdrawalData(parsedTransaction);
242
+
243
+ if(withdrawalData.getSpentVaultUtxo()!==vault.getLatestUtxo()) {
244
+ throw new Error("Latest vault UTXO already spent! Please try again later.");
245
+ }
246
+ (withdrawalData as any).sending = true;
247
+ vault.addWithdrawal(withdrawalData);
248
+ await this.saveVault(vault);
236
249
 
237
- const parsedTransaction = await this.bitcoinRpc.parseTransaction(res.raw);
238
- const withdrawalData = await spvVaultContract.getWithdrawalData(parsedTransaction);
250
+ try {
251
+ await this.bitcoin.sendRawTransaction(res.raw);
252
+ (withdrawalData as any).sending = false;
253
+ } catch (e) {
254
+ (withdrawalData as any).sending = false;
255
+ vault.removeWithdrawal(withdrawalData);
256
+ await this.saveVault(vault);
257
+ throw e;
258
+ }
259
+ });
260
+
261
+ return withdrawalTxId;
262
+ }
239
263
 
240
- if(withdrawalData.getSpentVaultUtxo()!==vault.getLatestUtxo()) {
241
- throw new Error("Latest vault UTXO already spent! Please try again later.");
264
+ /**
265
+ * Call this to check whether some of the previously replaced transactions got re-introduced to the mempool
266
+ *
267
+ * @param vault
268
+ * @param save
269
+ */
270
+ async checkVaultReplacedTransactions(vault: SpvVault, save?: boolean): Promise<boolean> {
271
+ const {spvVaultContract} = this.chains.chains[vault.chainId];
272
+
273
+ const initialVaultWithdrawalCount = vault.data.getWithdrawalCount();
274
+
275
+ let latestWithdrawalIndex = initialVaultWithdrawalCount;
276
+ const newPendingTxns: SpvWithdrawalTransactionData[] = [];
277
+ const reintroducedTxIds: Set<string> = new Set();
278
+ for(let [withdrawalIndex, replacedWithdrawalGroup] of vault.replacedWithdrawals) {
279
+ if(withdrawalIndex<=latestWithdrawalIndex) continue; //Don't check txns that should already be included
280
+
281
+ for(let replacedWithdrawal of replacedWithdrawalGroup) {
282
+ if(reintroducedTxIds.has(replacedWithdrawal.getTxId())) continue;
283
+ const tx = await this.bitcoinRpc.getTransaction(replacedWithdrawal.getTxId());
284
+ if(tx==null) continue;
285
+
286
+ //Re-introduce transaction to the pending withdrawals list
287
+ if(withdrawalIndex>latestWithdrawalIndex) {
288
+ const txChain: SpvWithdrawalTransactionData[] = [replacedWithdrawal];
289
+ withdrawalIndex--;
290
+ while(withdrawalIndex>latestWithdrawalIndex) {
291
+ const tx = await this.bitcoinRpc.getTransaction(txChain[0].getSpentVaultUtxo().split(":")[0]);
292
+ if(tx==null) break;
293
+ reintroducedTxIds.add(tx.txid);
294
+ txChain.unshift(await spvVaultContract.getWithdrawalData(tx));
295
+ withdrawalIndex--;
296
+ }
297
+ if(withdrawalIndex>latestWithdrawalIndex) {
298
+ this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Tried to re-introduce previously replaced TX, but one of txns in the chain not found!`);
299
+ continue;
300
+ }
301
+ newPendingTxns.push(...txChain);
302
+ latestWithdrawalIndex += txChain.length;
303
+ break; //Don't check other txns at the same withdrawal index
304
+ } else {
305
+ this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Tried to re-introduce previously replaced TX, but vault has already processed such withdrawal!`);
306
+ }
307
+ }
242
308
  }
243
- (withdrawalData as any).sending = true;
244
- vault.addWithdrawal(withdrawalData);
245
- await this.saveVault(vault);
309
+
310
+ if(newPendingTxns.length===0) return false;
311
+
312
+ if(initialVaultWithdrawalCount!==vault.data.getWithdrawalCount()) {
313
+ this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Not saving vault after checking replaced transactions, due to withdrawal count changed!`);
314
+ return false;
315
+ }
316
+
317
+ const backup = vault.pendingWithdrawals.splice(0, newPendingTxns.length);
318
+ const txsToAddOnTop = vault.pendingWithdrawals.splice(0, vault.pendingWithdrawals.length);
246
319
 
247
320
  try {
248
- await this.bitcoin.sendRawTransaction(res.raw);
249
- (withdrawalData as any).sending = false;
321
+ newPendingTxns.forEach(val => vault.addWithdrawal(val));
322
+ txsToAddOnTop.forEach(val => vault.addWithdrawal(val));
323
+ for(let i=0;i<newPendingTxns.length;i++) {
324
+ const withdrawalIndex = initialVaultWithdrawalCount+i+1;
325
+ const arr = vault.replacedWithdrawals.get(withdrawalIndex);
326
+ if(arr==null) continue;
327
+ const index = arr.indexOf(newPendingTxns[i]);
328
+ if(index===-1) {
329
+ this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Cannot remove re-introduced tx ${newPendingTxns[i].getTxId()}, not found in the respective array!`);
330
+ continue;
331
+ }
332
+ arr.splice(index, 1);
333
+ if(arr.length===0) vault.replacedWithdrawals.delete(withdrawalIndex);
334
+ }
335
+ this.logger.info(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Re-introduced back ${newPendingTxns.length} txns that were re-added to the mempool!`);
336
+ if(save) await this.saveVault(vault);
337
+ return true;
250
338
  } catch (e) {
251
- (withdrawalData as any).sending = false;
252
- vault.removeWithdrawal(withdrawalData);
253
- await this.saveVault(vault);
254
- throw e;
339
+ this.logger.error(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Failed to update the vault with new pending txns (rolling back): `, e);
340
+ //Rollback the pending withdrawals
341
+ vault.pendingWithdrawals.push(...backup, ...txsToAddOnTop);
342
+ return false;
255
343
  }
256
-
257
- return res.txId;
258
344
  }
259
345
 
260
346
  async checkVaults() {
@@ -332,23 +418,23 @@ export class SpvVaults {
332
418
  }
333
419
 
334
420
  if(vault.state===SpvVaultState.OPENED) {
335
- let changed = false;
421
+ let changed = await this.checkVaultReplacedTransactions(vault);
422
+
336
423
  //Check if some of the pendingWithdrawals got confirmed
337
424
  let latestOwnWithdrawalIndex = -1;
338
425
  let latestConfirmedWithdrawalIndex = -1;
339
- for(let i=0; i<vault.pendingWithdrawals.length; i++) {
426
+ for(let i = vault.pendingWithdrawals.length-1; i>=0; i--) {
340
427
  const pendingWithdrawal = vault.pendingWithdrawals[i];
341
428
  if(pendingWithdrawal.sending) continue;
342
429
 
343
430
  //Check all the pending withdrawals that were not finalized yet
344
- const btcTx = await checkTransactionReplacedRpc(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
431
+ const btcTx = await checkTransactionReplaced(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
345
432
  if(btcTx==null) {
346
433
  //Probable double-spend, remove from pending withdrawals
347
- const index = vault.pendingWithdrawals.indexOf(pendingWithdrawal);
348
- if(index===-1) {
349
- this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: "+pendingWithdrawal.btcTx.txid+", but doesn't exist anymore!")
434
+ if(!vault.doubleSpendPendingWithdrawal(pendingWithdrawal)) {
435
+ this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: "+pendingWithdrawal.btcTx.txid+", but doesn't exist anymore!");
350
436
  } else {
351
- vault.pendingWithdrawals.splice(index, 1);
437
+ this.logger.info("checkVaults(): Successfully removed withdrawal txId: "+pendingWithdrawal.btcTx.txid+", due to being replaced in the mempool!");
352
438
  }
353
439
  changed = true;
354
440
  } else {
@@ -702,7 +702,7 @@ export class FromBtcTrusted extends SwapHandler<FromBtcTrustedSwap, FromBtcTrust
702
702
 
703
703
  private async checkDoubleSpends(): Promise<void> {
704
704
  for(let swap of this.doubleSpendWatchdogSwaps.keys()) {
705
- const tx = await this.bitcoin.getWalletTransaction(swap.txId);
705
+ const tx = await this.bitcoinRpc.getTransaction(swap.txId);
706
706
  if(tx==null) {
707
707
  this.swapLogger.debug(swap, "checkDoubleSpends(): Swap was double spent, burning... - original txId: "+swap.txId);
708
708
  this.processPastSwap(swap, null, null);
@@ -46,19 +46,7 @@ export function isLegacyInput(input: TransactionInput): boolean {
46
46
  return true;
47
47
  }
48
48
 
49
- export async function checkTransactionReplaced(txId: string, txRaw: string, bitcoin: IBitcoinWallet): Promise<BtcTx> {
50
- const existingTx = await bitcoin.getWalletTransaction(txId);
51
- if(existingTx!=null) return existingTx;
52
- //Try to re-broadcast
53
- try {
54
- await bitcoin.sendRawTransaction(txRaw);
55
- } catch (e) {
56
- logger.error("checkTransactionReplaced("+txId+"): Error when trying to re-broadcast raw transaction: ", e);
57
- }
58
- return await bitcoin.getWalletTransaction(txId);
59
- }
60
-
61
- export async function checkTransactionReplacedRpc(txId: string, txRaw: string, bitcoin: BitcoinRpc<any>): Promise<BtcTx> {
49
+ export async function checkTransactionReplaced(txId: string, txRaw: string, bitcoin: BitcoinRpc<any>): Promise<BtcTx> {
62
50
  const existingTx = await bitcoin.getTransaction(txId);
63
51
  if(existingTx!=null) return existingTx;
64
52
  //Try to re-broadcast