@atomiqlabs/lp-lib 14.0.0-dev.29 → 14.0.0-dev.30

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.
@@ -11,6 +11,7 @@ const SchemaVerifier_1 = require("../../../utils/paramcoders/SchemaVerifier");
11
11
  const ServerParamDecoder_1 = require("../../../utils/paramcoders/server/ServerParamDecoder");
12
12
  const ToBtcBaseSwapHandler_1 = require("../ToBtcBaseSwapHandler");
13
13
  const promise_queue_ts_1 = require("promise-queue-ts");
14
+ const BitcoinUtils_1 = require("../../../utils/BitcoinUtils");
14
15
  const OUTPUT_SCRIPT_MAX_LENGTH = 200;
15
16
  const MAX_PARALLEL_TX_PROCESSED = 10;
16
17
  /**
@@ -285,12 +286,21 @@ class ToBtcAbs extends ToBtcBaseSwapHandler_1.ToBtcBaseSwapHandler {
285
286
  };
286
287
  if (swap.metadata != null)
287
288
  swap.metadata.times.paySignPSBT = Date.now();
288
- this.swapLogger.debug(swap, "sendBitcoinPayment(): signed raw transaction: " + signResult.raw);
289
- swap.txId = signResult.tx.id;
290
- swap.setRealNetworkFee(BigInt(signResult.networkFee));
291
- await swap.setState(ToBtcSwapAbs_1.ToBtcSwapState.BTC_SENDING);
292
- await this.saveSwapData(swap);
293
- await this.bitcoin.sendRawTransaction(signResult.raw);
289
+ try {
290
+ this.swapLogger.debug(swap, "sendBitcoinPayment(): signed raw transaction: " + signResult.raw);
291
+ swap.txId = signResult.tx.id;
292
+ swap.btcRawTx = signResult.raw;
293
+ swap.setRealNetworkFee(BigInt(signResult.networkFee));
294
+ swap.sending = true;
295
+ await swap.setState(ToBtcSwapAbs_1.ToBtcSwapState.BTC_SENDING);
296
+ await this.saveSwapData(swap);
297
+ await this.bitcoin.sendRawTransaction(signResult.raw);
298
+ swap.sending = false;
299
+ }
300
+ catch (e) {
301
+ swap.sending = false;
302
+ throw e;
303
+ }
294
304
  if (swap.metadata != null)
295
305
  swap.metadata.times.payTxSent = Date.now();
296
306
  this.swapLogger.info(swap, "sendBitcoinPayment(): btc transaction generated, signed & broadcasted, txId: " + swap.txId + " address: " + swap.address);
@@ -305,8 +315,10 @@ class ToBtcAbs extends ToBtcBaseSwapHandler_1.ToBtcBaseSwapHandler {
305
315
  */
306
316
  async processInitialized(swap) {
307
317
  if (swap.state === ToBtcSwapAbs_1.ToBtcSwapState.BTC_SENDING) {
318
+ if (swap.sending)
319
+ return;
308
320
  //Bitcoin transaction was signed (maybe also sent)
309
- const tx = await this.bitcoin.getWalletTransaction(swap.txId);
321
+ const tx = await (0, BitcoinUtils_1.checkTransactionReplaced)(swap.txId, swap.btcRawTx, this.bitcoin);
310
322
  const isTxSent = tx != null;
311
323
  if (!isTxSent) {
312
324
  //Reset the state to COMMITED
@@ -11,11 +11,13 @@ export declare enum ToBtcSwapState {
11
11
  CLAIMED = 4
12
12
  }
13
13
  export declare class ToBtcSwapAbs<T extends SwapData = SwapData> extends ToBtcBaseSwap<T, ToBtcSwapState> {
14
+ sending: boolean;
14
15
  readonly address: string;
15
16
  readonly satsPerVbyte: bigint;
16
17
  readonly nonce: bigint;
17
18
  readonly requiredConfirmations: number;
18
19
  readonly preferedConfirmationTarget: number;
20
+ btcRawTx: string;
19
21
  txId: string;
20
22
  constructor(chainIdentifier: string, address: string, amount: bigint, swapFee: bigint, swapFeeInToken: bigint, networkFee: bigint, networkFeeInToken: bigint, satsPerVbyte: bigint, nonce: bigint, requiredConfirmations: number, preferedConfirmationTarget: number);
21
23
  constructor(obj: any);
@@ -34,6 +34,7 @@ class ToBtcSwapAbs extends ToBtcBaseSwap_1.ToBtcBaseSwap {
34
34
  this.requiredConfirmations = chainIdOrObj.requiredConfirmations;
35
35
  this.preferedConfirmationTarget = chainIdOrObj.preferedConfirmationTarget;
36
36
  this.txId = chainIdOrObj.txId;
37
+ this.btcRawTx = chainIdOrObj.btcRawTx;
37
38
  //Compatibility
38
39
  this.quotedNetworkFee ?? (this.quotedNetworkFee = (0, Utils_1.deserializeBN)(chainIdOrObj.networkFee));
39
40
  }
@@ -47,6 +48,7 @@ class ToBtcSwapAbs extends ToBtcBaseSwap_1.ToBtcBaseSwap {
47
48
  partialSerialized.nonce = this.nonce.toString(10);
48
49
  partialSerialized.preferedConfirmationTarget = this.preferedConfirmationTarget;
49
50
  partialSerialized.txId = this.txId;
51
+ partialSerialized.btcRawTx = this.btcRawTx;
50
52
  return partialSerialized;
51
53
  }
52
54
  isInitiated() {
@@ -5,7 +5,9 @@ export declare enum SpvVaultState {
5
5
  BTC_CONFIRMED = 1,
6
6
  OPENED = 2
7
7
  }
8
- export declare class SpvVault<D extends SpvWithdrawalTransactionData = SpvWithdrawalTransactionData, T extends SpvVaultData = SpvVaultData> extends Lockable implements StorageObject {
8
+ export declare class SpvVault<D extends SpvWithdrawalTransactionData = SpvWithdrawalTransactionData & {
9
+ sending?: boolean;
10
+ }, T extends SpvVaultData = SpvVaultData> extends Lockable implements StorageObject {
9
11
  readonly chainId: string;
10
12
  readonly initialUtxo: string;
11
13
  readonly btcAddress: string;
@@ -11,6 +11,7 @@ export declare enum SpvVaultSwapState {
11
11
  CLAIMED = 4
12
12
  }
13
13
  export declare class SpvVaultSwap extends SwapHandlerSwap<SpvVaultSwapState> {
14
+ sending: boolean;
14
15
  readonly quoteId: string;
15
16
  readonly vaultOwner: string;
16
17
  readonly vaultId: bigint;
@@ -99,8 +99,13 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
99
99
  }
100
100
  }
101
101
  if (swap.state === SpvVaultSwap_1.SpvVaultSwapState.SIGNED) {
102
- //Check if sent
103
- const tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
102
+ if (swap.sending)
103
+ return;
104
+ const vault = await this.Vaults.getVault(swap.chainIdentifier, swap.vaultOwner, swap.vaultId);
105
+ const foundWithdrawal = vault.pendingWithdrawals.find(val => val.btcTx.txid === swap.btcTxId);
106
+ let tx = foundWithdrawal?.btcTx;
107
+ if (tx == null)
108
+ tx = await this.bitcoin.getWalletTransaction(swap.btcTxId);
104
109
  if (tx == null) {
105
110
  await this.removeSwapData(swap, SpvVaultSwap_1.SpvVaultSwapState.FAILED);
106
111
  return;
@@ -117,7 +122,13 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
117
122
  }
118
123
  if (swap.state === SpvVaultSwap_1.SpvVaultSwapState.SENT || swap.state === SpvVaultSwap_1.SpvVaultSwapState.BTC_CONFIRMED) {
119
124
  //Check if confirmed or double-spent
120
- const tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
125
+ if (swap.sending)
126
+ return;
127
+ const vault = await this.Vaults.getVault(swap.chainIdentifier, swap.vaultOwner, swap.vaultId);
128
+ const foundWithdrawal = vault.pendingWithdrawals.find(val => val.btcTx.txid === swap.btcTxId);
129
+ let tx = foundWithdrawal?.btcTx;
130
+ if (tx == null)
131
+ tx = await this.bitcoin.getWalletTransaction(swap.btcTxId);
121
132
  if (tx == null) {
122
133
  await this.removeSwapData(swap, SpvVaultSwap_1.SpvVaultSwapState.DOUBLE_SPENT);
123
134
  return;
@@ -421,31 +432,45 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
421
432
  msg: "Vault UTXO already spent, please try again!"
422
433
  };
423
434
  }
424
- vault.addWithdrawal(data);
425
- await this.Vaults.saveVault(vault);
426
- //Double-check the state to prevent race condition
427
- if (swap.state !== SpvVaultSwap_1.SpvVaultSwapState.CREATED)
428
- throw {
429
- code: 20505,
430
- msg: "Invalid quote ID, not found or expired!"
431
- };
432
- swap.btcTxId = signedTx.id;
433
- swap.state = SpvVaultSwap_1.SpvVaultSwapState.SIGNED;
434
- await this.saveSwapData(swap);
435
- this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: " + swap.btcTxId);
436
435
  try {
437
- await this.bitcoin.sendRawTransaction(Buffer.from(signedTx.toBytes(true, true)).toString("hex"));
438
- await swap.setState(SpvVaultSwap_1.SpvVaultSwapState.SENT);
436
+ const btcRawTx = Buffer.from(signedTx.toBytes(true, true)).toString("hex");
437
+ //Double-check the state to prevent race condition
438
+ if (swap.state !== SpvVaultSwap_1.SpvVaultSwapState.CREATED) {
439
+ throw {
440
+ code: 20505,
441
+ msg: "Invalid quote ID, not found or expired!"
442
+ };
443
+ }
444
+ swap.btcTxId = signedTx.id;
445
+ swap.state = SpvVaultSwap_1.SpvVaultSwapState.SIGNED;
446
+ swap.sending = true;
447
+ await this.saveSwapData(swap);
448
+ data.btcTx.raw = btcRawTx;
449
+ data.sending = true;
450
+ vault.addWithdrawal(data);
451
+ await this.Vaults.saveVault(vault);
452
+ this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: " + swap.btcTxId);
453
+ try {
454
+ await this.bitcoin.sendRawTransaction(btcRawTx);
455
+ await swap.setState(SpvVaultSwap_1.SpvVaultSwapState.SENT);
456
+ data.sending = false;
457
+ swap.sending = false;
458
+ }
459
+ catch (e) {
460
+ this.swapLogger.error(swap, "REST: /postQuote: Failed to send BTC transaction: ", e);
461
+ throw {
462
+ code: 20512,
463
+ msg: "Error broadcasting bitcoin transaction!"
464
+ };
465
+ }
439
466
  }
440
467
  catch (e) {
441
- this.swapLogger.error(swap, "REST: /postQuote: Failed to send BTC transaction: ", e);
468
+ data.sending = false;
469
+ swap.sending = false;
442
470
  vault.removeWithdrawal(data);
443
471
  await this.Vaults.saveVault(vault);
444
472
  await this.removeSwapData(swap, SpvVaultSwap_1.SpvVaultSwapState.FAILED);
445
- throw {
446
- code: 20512,
447
- msg: "Error broadcasting bitcoin transaction!"
448
- };
473
+ throw e;
449
474
  }
450
475
  await responseStream.writeParamsAndEnd({
451
476
  code: 20000,
@@ -28,12 +28,16 @@ export declare class SpvVaults {
28
28
  vaultsCreated: bigint[];
29
29
  btcTxId: string;
30
30
  }>;
31
- listVaults(chainId?: string, token?: string): Promise<SpvVault<SpvWithdrawalTransactionData, import("@atomiqlabs/base").SpvVaultData<SpvWithdrawalTransactionData>>[]>;
31
+ listVaults(chainId?: string, token?: string): Promise<SpvVault<SpvWithdrawalTransactionData & {
32
+ sending?: boolean;
33
+ }, import("@atomiqlabs/base").SpvVaultData<SpvWithdrawalTransactionData>>[]>;
32
34
  fundVault(vault: SpvVault, tokenAmounts: bigint[]): Promise<string>;
33
35
  withdrawFromVault(vault: SpvVault, tokenAmounts: bigint[], feeRate?: number): Promise<string>;
34
36
  checkVaults(): Promise<void>;
35
37
  claimWithdrawals(vault: SpvVault, withdrawal: SpvWithdrawalTransactionData[]): Promise<boolean>;
36
- getVault(chainId: string, owner: string, vaultId: bigint): Promise<SpvVault<SpvWithdrawalTransactionData, import("@atomiqlabs/base").SpvVaultData<SpvWithdrawalTransactionData>>>;
38
+ getVault(chainId: string, owner: string, vaultId: bigint): Promise<SpvVault<SpvWithdrawalTransactionData & {
39
+ sending?: boolean;
40
+ }, import("@atomiqlabs/base").SpvVaultData<SpvWithdrawalTransactionData>>>;
37
41
  /**
38
42
  * Returns a ready-to-use vault for a specific request
39
43
  *
@@ -6,9 +6,9 @@ const Utils_1 = require("../../utils/Utils");
6
6
  const PluginManager_1 = require("../../plugins/PluginManager");
7
7
  const AmountAssertions_1 = require("../assertions/AmountAssertions");
8
8
  const btc_signer_1 = require("@scure/btc-signer");
9
+ const BitcoinUtils_1 = require("../../utils/BitcoinUtils");
9
10
  exports.VAULT_DUST_AMOUNT = 600;
10
11
  const VAULT_INIT_CONFIRMATIONS = 2;
11
- // const BTC_FINALIZATION_CONFIRMATIONS = 6;
12
12
  const MAX_PARALLEL_VAULTS_OPENING = 10;
13
13
  class SpvVaults {
14
14
  constructor(vaultStorage, bitcoin, vaultSigner, bitcoinRpc, chains, config) {
@@ -184,12 +184,15 @@ class SpvVaults {
184
184
  if (withdrawalData.getSpentVaultUtxo() !== vault.getLatestUtxo()) {
185
185
  throw new Error("Latest vault UTXO already spent! Please try again later.");
186
186
  }
187
+ withdrawalData.sending = true;
187
188
  vault.addWithdrawal(withdrawalData);
188
189
  await this.saveVault(vault);
189
190
  try {
190
191
  await this.bitcoin.sendRawTransaction(res.raw);
192
+ withdrawalData.sending = false;
191
193
  }
192
194
  catch (e) {
195
+ withdrawalData.sending = false;
193
196
  vault.removeWithdrawal(withdrawalData);
194
197
  await this.saveVault(vault);
195
198
  throw e;
@@ -266,9 +269,10 @@ class SpvVaults {
266
269
  let latestConfirmedWithdrawalIndex = -1;
267
270
  for (let i = 0; i < vault.pendingWithdrawals.length; i++) {
268
271
  const pendingWithdrawal = vault.pendingWithdrawals[i];
272
+ if (pendingWithdrawal.sending)
273
+ continue;
269
274
  //Check all the pending withdrawals that were not finalized yet
270
- // if(pendingWithdrawal.btcTx.confirmations==null || pendingWithdrawal.btcTx.confirmations < BTC_FINALIZATION_CONFIRMATIONS) {
271
- const btcTx = await this.bitcoinRpc.getTransaction(pendingWithdrawal.btcTx.txid);
275
+ const btcTx = await (0, BitcoinUtils_1.checkTransactionReplacedRpc)(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
272
276
  if (btcTx == null) {
273
277
  //Probable double-spend, remove from pending withdrawals
274
278
  const index = vault.pendingWithdrawals.indexOf(pendingWithdrawal);
@@ -289,7 +293,6 @@ class SpvVaults {
289
293
  changed = true;
290
294
  }
291
295
  }
292
- // }
293
296
  //Check it has enough confirmations
294
297
  if (pendingWithdrawal.btcTx.confirmations >= vault.data.getConfirmations()) {
295
298
  latestConfirmedWithdrawalIndex = i;
@@ -1,2 +1,6 @@
1
1
  import { TransactionInput } from "@scure/btc-signer/psbt";
2
+ import { BitcoinRpc, BtcTx } from "@atomiqlabs/base";
3
+ import { IBitcoinWallet } from "../wallets/IBitcoinWallet";
2
4
  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>;
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isLegacyInput = void 0;
3
+ exports.checkTransactionReplacedRpc = 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
+ const Utils_1 = require("./Utils");
7
+ const logger = (0, Utils_1.getLogger)("BitcoinUtils: ");
6
8
  function parsePushOpcode(script) {
7
9
  if (script[0] === 0x00) {
8
10
  return Uint8Array.from([]);
@@ -43,3 +45,31 @@ function isLegacyInput(input) {
43
45
  return true;
44
46
  }
45
47
  exports.isLegacyInput = isLegacyInput;
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
+ const existingTx = await bitcoin.getTransaction(txId);
64
+ if (existingTx != null)
65
+ return existingTx;
66
+ //Try to re-broadcast
67
+ try {
68
+ await bitcoin.sendRawTransaction(txRaw);
69
+ }
70
+ catch (e) {
71
+ logger.error("checkTransactionReplaced(" + txId + "): Error when trying to re-broadcast raw transaction: ", e);
72
+ }
73
+ return await bitcoin.getTransaction(txId);
74
+ }
75
+ exports.checkTransactionReplacedRpc = checkTransactionReplacedRpc;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/lp-lib",
3
- "version": "14.0.0-dev.29",
3
+ "version": "14.0.0-dev.30",
4
4
  "description": "Main functionality implementation for atomiq LP node",
5
5
  "main": "./dist/index.js",
6
6
  "types:": "./dist/index.d.ts",
@@ -23,6 +23,7 @@ import {ServerParamEncoder} from "../../../utils/paramcoders/server/ServerParamE
23
23
  import {ToBtcBaseConfig, ToBtcBaseSwapHandler} from "../ToBtcBaseSwapHandler";
24
24
  import {PromiseQueue} from "promise-queue-ts";
25
25
  import {IBitcoinWallet} from "../../../wallets/IBitcoinWallet";
26
+ import {checkTransactionReplaced} from "../../../utils/BitcoinUtils";
26
27
 
27
28
  const OUTPUT_SCRIPT_MAX_LENGTH = 200;
28
29
 
@@ -372,13 +373,22 @@ export class ToBtcAbs extends ToBtcBaseSwapHandler<ToBtcSwapAbs, ToBtcSwapState>
372
373
  }
373
374
  if(swap.metadata!=null) swap.metadata.times.paySignPSBT = Date.now();
374
375
 
375
- this.swapLogger.debug(swap, "sendBitcoinPayment(): signed raw transaction: "+signResult.raw);
376
- swap.txId = signResult.tx.id;
377
- swap.setRealNetworkFee(BigInt(signResult.networkFee));
378
- await swap.setState(ToBtcSwapState.BTC_SENDING);
379
- await this.saveSwapData(swap);
376
+ try {
377
+ this.swapLogger.debug(swap, "sendBitcoinPayment(): signed raw transaction: "+signResult.raw);
378
+ swap.txId = signResult.tx.id;
379
+ swap.btcRawTx = signResult.raw;
380
+ swap.setRealNetworkFee(BigInt(signResult.networkFee));
381
+ swap.sending = true;
382
+ await swap.setState(ToBtcSwapState.BTC_SENDING);
383
+ await this.saveSwapData(swap);
384
+
385
+ await this.bitcoin.sendRawTransaction(signResult.raw);
386
+ swap.sending = false;
387
+ } catch (e) {
388
+ swap.sending = false;
389
+ throw e;
390
+ }
380
391
 
381
- await this.bitcoin.sendRawTransaction(signResult.raw);
382
392
  if(swap.metadata!=null) swap.metadata.times.payTxSent = Date.now();
383
393
  this.swapLogger.info(swap, "sendBitcoinPayment(): btc transaction generated, signed & broadcasted, txId: "+swap.txId+" address: "+swap.address);
384
394
 
@@ -394,8 +404,9 @@ export class ToBtcAbs extends ToBtcBaseSwapHandler<ToBtcSwapAbs, ToBtcSwapState>
394
404
  */
395
405
  private async processInitialized(swap: ToBtcSwapAbs) {
396
406
  if(swap.state===ToBtcSwapState.BTC_SENDING) {
407
+ if(swap.sending) return;
397
408
  //Bitcoin transaction was signed (maybe also sent)
398
- const tx = await this.bitcoin.getWalletTransaction(swap.txId);
409
+ const tx = await checkTransactionReplaced(swap.txId, swap.btcRawTx, this.bitcoin);
399
410
 
400
411
  const isTxSent = tx!=null;
401
412
  if(!isTxSent) {
@@ -16,12 +16,16 @@ export enum ToBtcSwapState {
16
16
 
17
17
  export class ToBtcSwapAbs<T extends SwapData = SwapData> extends ToBtcBaseSwap<T, ToBtcSwapState> {
18
18
 
19
+ //Unsaved sending flag
20
+ sending: boolean;
21
+
19
22
  readonly address: string;
20
23
  readonly satsPerVbyte: bigint;
21
24
  readonly nonce: bigint;
22
25
  readonly requiredConfirmations: number;
23
26
  readonly preferedConfirmationTarget: number;
24
27
 
28
+ btcRawTx: string;
25
29
  txId: string;
26
30
 
27
31
  constructor(
@@ -69,6 +73,7 @@ export class ToBtcSwapAbs<T extends SwapData = SwapData> extends ToBtcBaseSwap<T
69
73
  this.preferedConfirmationTarget = chainIdOrObj.preferedConfirmationTarget;
70
74
 
71
75
  this.txId = chainIdOrObj.txId;
76
+ this.btcRawTx = chainIdOrObj.btcRawTx;
72
77
 
73
78
  //Compatibility
74
79
  this.quotedNetworkFee ??= deserializeBN(chainIdOrObj.networkFee);
@@ -84,6 +89,7 @@ export class ToBtcSwapAbs<T extends SwapData = SwapData> extends ToBtcBaseSwap<T
84
89
  partialSerialized.nonce = this.nonce.toString(10);
85
90
  partialSerialized.preferedConfirmationTarget = this.preferedConfirmationTarget;
86
91
  partialSerialized.txId = this.txId;
92
+ partialSerialized.btcRawTx = this.btcRawTx;
87
93
  return partialSerialized;
88
94
  }
89
95
 
@@ -18,7 +18,7 @@ export enum SpvVaultState {
18
18
  }
19
19
 
20
20
  export class SpvVault<
21
- D extends SpvWithdrawalTransactionData = SpvWithdrawalTransactionData,
21
+ D extends SpvWithdrawalTransactionData = SpvWithdrawalTransactionData & {sending?: boolean},
22
22
  T extends SpvVaultData = SpvVaultData
23
23
  > extends Lockable implements StorageObject {
24
24
 
@@ -16,6 +16,9 @@ export enum SpvVaultSwapState {
16
16
 
17
17
  export class SpvVaultSwap extends SwapHandlerSwap<SpvVaultSwapState> {
18
18
 
19
+ //Unsaved sending flag
20
+ sending: boolean;
21
+
19
22
  readonly quoteId: string;
20
23
 
21
24
  readonly vaultOwner: string;
@@ -29,7 +29,7 @@ import {FromBtcAmountAssertions} from "../assertions/FromBtcAmountAssertions";
29
29
  import {randomBytes} from "crypto";
30
30
  import {getInputType, OutScript, Transaction} from "@scure/btc-signer";
31
31
  import {SpvVaults, VAULT_DUST_AMOUNT} from "./SpvVaults";
32
- import {isLegacyInput} from "../../utils/BitcoinUtils";
32
+ import {checkTransactionReplaced, isLegacyInput} from "../../utils/BitcoinUtils";
33
33
 
34
34
  export type SpvVaultSwapHandlerConfig = SwapBaseConfig & {
35
35
  vaultsCheckInterval: number,
@@ -166,8 +166,12 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
166
166
  }
167
167
 
168
168
  if(swap.state===SpvVaultSwapState.SIGNED) {
169
- //Check if sent
170
- const tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
169
+ if(swap.sending) return;
170
+ const vault = await this.Vaults.getVault(swap.chainIdentifier, swap.vaultOwner, swap.vaultId);
171
+ const foundWithdrawal = vault.pendingWithdrawals.find(val => val.btcTx.txid === swap.btcTxId);
172
+ let tx = foundWithdrawal?.btcTx;
173
+ if(tx==null) tx = await this.bitcoin.getWalletTransaction(swap.btcTxId);
174
+
171
175
  if(tx==null) {
172
176
  await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
173
177
  return;
@@ -183,7 +187,12 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
183
187
 
184
188
  if(swap.state===SpvVaultSwapState.SENT || swap.state===SpvVaultSwapState.BTC_CONFIRMED) {
185
189
  //Check if confirmed or double-spent
186
- const tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
190
+ if(swap.sending) return;
191
+ const vault = await this.Vaults.getVault(swap.chainIdentifier, swap.vaultOwner, swap.vaultId);
192
+ const foundWithdrawal = vault.pendingWithdrawals.find(val => val.btcTx.txid === swap.btcTxId);
193
+ let tx = foundWithdrawal?.btcTx;
194
+ if(tx==null) tx = await this.bitcoin.getWalletTransaction(swap.btcTxId);
195
+
187
196
  if(tx==null) {
188
197
  await this.removeSwapData(swap, SpvVaultSwapState.DOUBLE_SPENT);
189
198
  return;
@@ -555,32 +564,49 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
555
564
  msg: "Vault UTXO already spent, please try again!"
556
565
  };
557
566
  }
558
- vault.addWithdrawal(data);
559
- await this.Vaults.saveVault(vault);
560
567
 
561
- //Double-check the state to prevent race condition
562
- if(swap.state!==SpvVaultSwapState.CREATED) throw {
563
- code: 20505,
564
- msg: "Invalid quote ID, not found or expired!"
565
- };
566
- swap.btcTxId = signedTx.id;
567
- swap.state = SpvVaultSwapState.SIGNED;
568
- await this.saveSwapData(swap);
568
+ try {
569
+ const btcRawTx = Buffer.from(signedTx.toBytes(true, true)).toString("hex");
570
+
571
+ //Double-check the state to prevent race condition
572
+ if(swap.state!==SpvVaultSwapState.CREATED) {
573
+ throw {
574
+ code: 20505,
575
+ msg: "Invalid quote ID, not found or expired!"
576
+ };
577
+ }
569
578
 
570
- this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: "+swap.btcTxId);
579
+ swap.btcTxId = signedTx.id;
580
+ swap.state = SpvVaultSwapState.SIGNED;
581
+ swap.sending = true;
582
+ await this.saveSwapData(swap);
571
583
 
572
- try {
573
- await this.bitcoin.sendRawTransaction(Buffer.from(signedTx.toBytes(true, true)).toString("hex"));
574
- await swap.setState(SpvVaultSwapState.SENT);
584
+ data.btcTx.raw = btcRawTx;
585
+ (data as any).sending = true;
586
+ vault.addWithdrawal(data);
587
+ await this.Vaults.saveVault(vault);
588
+
589
+ this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: "+swap.btcTxId);
590
+
591
+ try {
592
+ await this.bitcoin.sendRawTransaction(btcRawTx);
593
+ await swap.setState(SpvVaultSwapState.SENT);
594
+ (data as any).sending = false;
595
+ swap.sending = false;
596
+ } catch (e) {
597
+ this.swapLogger.error(swap, "REST: /postQuote: Failed to send BTC transaction: ", e);
598
+ throw {
599
+ code: 20512,
600
+ msg: "Error broadcasting bitcoin transaction!"
601
+ };
602
+ }
575
603
  } catch (e) {
576
- this.swapLogger.error(swap, "REST: /postQuote: Failed to send BTC transaction: ", e);
604
+ (data as any).sending = false;
605
+ swap.sending = false;
577
606
  vault.removeWithdrawal(data);
578
607
  await this.Vaults.saveVault(vault);
579
608
  await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
580
- throw {
581
- code: 20512,
582
- msg: "Error broadcasting bitcoin transaction!"
583
- };
609
+ throw e;
584
610
  }
585
611
 
586
612
  await responseStream.writeParamsAndEnd({
@@ -15,10 +15,10 @@ 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
19
 
19
20
  export const VAULT_DUST_AMOUNT = 600;
20
21
  const VAULT_INIT_CONFIRMATIONS = 2;
21
- // const BTC_FINALIZATION_CONFIRMATIONS = 6;
22
22
  const MAX_PARALLEL_VAULTS_OPENING = 10;
23
23
 
24
24
  export class SpvVaults {
@@ -234,12 +234,15 @@ export class SpvVaults {
234
234
  if(withdrawalData.getSpentVaultUtxo()!==vault.getLatestUtxo()) {
235
235
  throw new Error("Latest vault UTXO already spent! Please try again later.");
236
236
  }
237
+ (withdrawalData as any).sending = true;
237
238
  vault.addWithdrawal(withdrawalData);
238
239
  await this.saveVault(vault);
239
240
 
240
241
  try {
241
242
  await this.bitcoin.sendRawTransaction(res.raw);
243
+ (withdrawalData as any).sending = false;
242
244
  } catch (e) {
245
+ (withdrawalData as any).sending = false;
243
246
  vault.removeWithdrawal(withdrawalData);
244
247
  await this.saveVault(vault);
245
248
  throw e;
@@ -329,30 +332,31 @@ export class SpvVaults {
329
332
  let latestConfirmedWithdrawalIndex = -1;
330
333
  for(let i=0; i<vault.pendingWithdrawals.length; i++) {
331
334
  const pendingWithdrawal = vault.pendingWithdrawals[i];
335
+ if(pendingWithdrawal.sending) continue;
336
+
332
337
  //Check all the pending withdrawals that were not finalized yet
333
- // if(pendingWithdrawal.btcTx.confirmations==null || pendingWithdrawal.btcTx.confirmations < BTC_FINALIZATION_CONFIRMATIONS) {
334
- const btcTx = await this.bitcoinRpc.getTransaction(pendingWithdrawal.btcTx.txid);
335
- if(btcTx==null) {
336
- //Probable double-spend, remove from pending withdrawals
337
- const index = vault.pendingWithdrawals.indexOf(pendingWithdrawal);
338
- if(index===-1) {
339
- this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: "+pendingWithdrawal.btcTx.txid+", but doesn't exist anymore!")
340
- } else {
341
- vault.pendingWithdrawals.splice(index, 1);
342
- }
343
- changed = true;
338
+ const btcTx = await checkTransactionReplacedRpc(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
339
+ if(btcTx==null) {
340
+ //Probable double-spend, remove from pending withdrawals
341
+ const index = vault.pendingWithdrawals.indexOf(pendingWithdrawal);
342
+ if(index===-1) {
343
+ this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: "+pendingWithdrawal.btcTx.txid+", but doesn't exist anymore!")
344
344
  } else {
345
- //Update confirmations count
346
- if(
347
- pendingWithdrawal.btcTx.confirmations !== btcTx.confirmations ||
348
- pendingWithdrawal.btcTx.blockhash !== btcTx.blockhash
349
- ) {
350
- pendingWithdrawal.btcTx.confirmations = btcTx.confirmations;
351
- pendingWithdrawal.btcTx.blockhash = btcTx.blockhash;
352
- changed = true;
353
- }
345
+ vault.pendingWithdrawals.splice(index, 1);
354
346
  }
355
- // }
347
+ changed = true;
348
+ } else {
349
+ //Update confirmations count
350
+ if(
351
+ pendingWithdrawal.btcTx.confirmations !== btcTx.confirmations ||
352
+ pendingWithdrawal.btcTx.blockhash !== btcTx.blockhash
353
+ ) {
354
+ pendingWithdrawal.btcTx.confirmations = btcTx.confirmations;
355
+ pendingWithdrawal.btcTx.blockhash = btcTx.blockhash;
356
+ changed = true;
357
+ }
358
+ }
359
+
356
360
  //Check it has enough confirmations
357
361
  if(pendingWithdrawal.btcTx.confirmations >= vault.data.getConfirmations()) {
358
362
  latestConfirmedWithdrawalIndex = i;
@@ -1,6 +1,11 @@
1
1
  import {TransactionInput} from "@scure/btc-signer/psbt";
2
2
  import {getPrevOut} from "@scure/btc-signer/utxo";
3
3
  import {OutScript} from "@scure/btc-signer";
4
+ import {BitcoinRpc, BtcTx} from "@atomiqlabs/base";
5
+ import {IBitcoinWallet} from "../wallets/IBitcoinWallet";
6
+ import {getLogger} from "./Utils";
7
+
8
+ const logger = getLogger("BitcoinUtils: ");
4
9
 
5
10
  function parsePushOpcode(script: Uint8Array): Uint8Array {
6
11
  if(script[0]===0x00) {
@@ -40,3 +45,27 @@ export function isLegacyInput(input: TransactionInput): boolean {
40
45
  }
41
46
  return true;
42
47
  }
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> {
62
+ const existingTx = await bitcoin.getTransaction(txId);
63
+ if(existingTx!=null) return existingTx;
64
+ //Try to re-broadcast
65
+ try {
66
+ await bitcoin.sendRawTransaction(txRaw);
67
+ } catch (e) {
68
+ logger.error("checkTransactionReplaced("+txId+"): Error when trying to re-broadcast raw transaction: ", e);
69
+ }
70
+ return await bitcoin.getTransaction(txId);
71
+ }