@atomiqlabs/lp-lib 15.0.4 → 15.0.6

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
  /**
16
17
  * Handler for to BTC swaps, utilizing PTLCs (proof-time locked contracts) using btc relay (on-chain bitcoin SPV)
@@ -276,12 +277,21 @@ class ToBtcAbs extends ToBtcBaseSwapHandler_1.ToBtcBaseSwapHandler {
276
277
  };
277
278
  if (swap.metadata != null)
278
279
  swap.metadata.times.paySignPSBT = Date.now();
279
- this.swapLogger.debug(swap, "sendBitcoinPayment(): signed raw transaction: " + signResult.raw);
280
- swap.txId = signResult.tx.id;
281
- swap.setRealNetworkFee(BigInt(signResult.networkFee));
282
- await swap.setState(ToBtcSwapAbs_1.ToBtcSwapState.BTC_SENDING);
283
- await this.saveSwapData(swap);
284
- await this.bitcoin.sendRawTransaction(signResult.raw);
280
+ try {
281
+ this.swapLogger.debug(swap, "sendBitcoinPayment(): signed raw transaction: " + signResult.raw);
282
+ swap.txId = signResult.tx.id;
283
+ swap.btcRawTx = signResult.raw;
284
+ swap.setRealNetworkFee(BigInt(signResult.networkFee));
285
+ swap.sending = true;
286
+ await swap.setState(ToBtcSwapAbs_1.ToBtcSwapState.BTC_SENDING);
287
+ await this.saveSwapData(swap);
288
+ await this.bitcoin.sendRawTransaction(signResult.raw);
289
+ swap.sending = false;
290
+ }
291
+ catch (e) {
292
+ swap.sending = false;
293
+ throw e;
294
+ }
285
295
  if (swap.metadata != null)
286
296
  swap.metadata.times.payTxSent = Date.now();
287
297
  this.swapLogger.info(swap, "sendBitcoinPayment(): btc transaction generated, signed & broadcasted, txId: " + swap.txId + " address: " + swap.address);
@@ -296,8 +306,10 @@ class ToBtcAbs extends ToBtcBaseSwapHandler_1.ToBtcBaseSwapHandler {
296
306
  */
297
307
  async processInitialized(swap) {
298
308
  if (swap.state === ToBtcSwapAbs_1.ToBtcSwapState.BTC_SENDING) {
309
+ if (swap.sending)
310
+ return;
299
311
  //Bitcoin transaction was signed (maybe also sent)
300
- const tx = await this.bitcoin.getWalletTransaction(swap.txId);
312
+ const tx = await (0, BitcoinUtils_1.checkTransactionReplaced)(swap.txId, swap.btcRawTx, this.bitcoin);
301
313
  const isTxSent = tx != null;
302
314
  if (!isTxSent) {
303
315
  //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) {
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;
@@ -419,31 +430,45 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
419
430
  msg: "Vault UTXO already spent, please try again!"
420
431
  };
421
432
  }
422
- vault.addWithdrawal(data);
423
- await this.Vaults.saveVault(vault);
424
- //Double-check the state to prevent race condition
425
- if (swap.state !== SpvVaultSwap_1.SpvVaultSwapState.CREATED)
426
- throw {
427
- code: 20505,
428
- msg: "Invalid quote ID, not found or expired!"
429
- };
430
- swap.btcTxId = signedTx.id;
431
- swap.state = SpvVaultSwap_1.SpvVaultSwapState.SIGNED;
432
- await this.saveSwapData(swap);
433
- this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: " + swap.btcTxId);
434
433
  try {
435
- await this.bitcoin.sendRawTransaction(Buffer.from(signedTx.toBytes(true, true)).toString("hex"));
436
- await swap.setState(SpvVaultSwap_1.SpvVaultSwapState.SENT);
434
+ const btcRawTx = Buffer.from(signedTx.toBytes(true, true)).toString("hex");
435
+ //Double-check the state to prevent race condition
436
+ if (swap.state !== SpvVaultSwap_1.SpvVaultSwapState.CREATED) {
437
+ throw {
438
+ code: 20505,
439
+ msg: "Invalid quote ID, not found or expired!"
440
+ };
441
+ }
442
+ swap.btcTxId = signedTx.id;
443
+ swap.state = SpvVaultSwap_1.SpvVaultSwapState.SIGNED;
444
+ swap.sending = true;
445
+ await this.saveSwapData(swap);
446
+ data.btcTx.raw = btcRawTx;
447
+ data.sending = true;
448
+ vault.addWithdrawal(data);
449
+ await this.Vaults.saveVault(vault);
450
+ this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: " + swap.btcTxId);
451
+ try {
452
+ await this.bitcoin.sendRawTransaction(btcRawTx);
453
+ await swap.setState(SpvVaultSwap_1.SpvVaultSwapState.SENT);
454
+ data.sending = false;
455
+ swap.sending = false;
456
+ }
457
+ catch (e) {
458
+ this.swapLogger.error(swap, "REST: /postQuote: Failed to send BTC transaction: ", e);
459
+ throw {
460
+ code: 20512,
461
+ msg: "Error broadcasting bitcoin transaction!"
462
+ };
463
+ }
437
464
  }
438
465
  catch (e) {
439
- this.swapLogger.error(swap, "REST: /postQuote: Failed to send BTC transaction: ", e);
466
+ data.sending = false;
467
+ swap.sending = false;
440
468
  vault.removeWithdrawal(data);
441
469
  await this.Vaults.saveVault(vault);
442
470
  await this.removeSwapData(swap, SpvVaultSwap_1.SpvVaultSwapState.FAILED);
443
- throw {
444
- code: 20512,
445
- msg: "Error broadcasting bitcoin transaction!"
446
- };
471
+ throw e;
447
472
  }
448
473
  await responseStream.writeParamsAndEnd({
449
474
  code: 20000,
@@ -33,12 +33,16 @@ export declare class SpvVaults {
33
33
  vaultsCreated: bigint[];
34
34
  btcTxId: string;
35
35
  }>;
36
- listVaults(chainId?: string, token?: string): Promise<SpvVault<SpvWithdrawalTransactionData, import("@atomiqlabs/base").SpvVaultData<SpvWithdrawalTransactionData>>[]>;
36
+ listVaults(chainId?: string, token?: string): Promise<SpvVault<SpvWithdrawalTransactionData & {
37
+ sending?: boolean;
38
+ }, import("@atomiqlabs/base").SpvVaultData<SpvWithdrawalTransactionData>>[]>;
37
39
  fundVault(vault: SpvVault, tokenAmounts: bigint[]): Promise<string>;
38
40
  withdrawFromVault(vault: SpvVault, tokenAmounts: bigint[], feeRate?: number): Promise<string>;
39
41
  checkVaults(): Promise<void>;
40
42
  claimWithdrawals(vault: SpvVault, withdrawal: SpvWithdrawalTransactionData[]): Promise<boolean>;
41
- getVault(chainId: string, owner: string, vaultId: bigint): Promise<SpvVault<SpvWithdrawalTransactionData, import("@atomiqlabs/base").SpvVaultData<SpvWithdrawalTransactionData>>>;
43
+ getVault(chainId: string, owner: string, vaultId: bigint): Promise<SpvVault<SpvWithdrawalTransactionData & {
44
+ sending?: boolean;
45
+ }, import("@atomiqlabs/base").SpvVaultData<SpvWithdrawalTransactionData>>>;
42
46
  /**
43
47
  * Returns a ready-to-use vault for a specific request
44
48
  *
@@ -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
  class SpvVaults {
13
13
  constructor(vaultStorage, bitcoin, vaultSigner, bitcoinRpc, getChain, config) {
14
14
  this.logger = {
@@ -175,12 +175,15 @@ class SpvVaults {
175
175
  if (withdrawalData.getSpentVaultUtxo() !== vault.getLatestUtxo()) {
176
176
  throw new Error("Latest vault UTXO already spent! Please try again later.");
177
177
  }
178
+ withdrawalData.sending = true;
178
179
  vault.addWithdrawal(withdrawalData);
179
180
  await this.saveVault(vault);
180
181
  try {
181
182
  await this.bitcoin.sendRawTransaction(res.raw);
183
+ withdrawalData.sending = false;
182
184
  }
183
185
  catch (e) {
186
+ withdrawalData.sending = false;
184
187
  vault.removeWithdrawal(withdrawalData);
185
188
  await this.saveVault(vault);
186
189
  throw e;
@@ -241,28 +244,28 @@ class SpvVaults {
241
244
  let latestConfirmedWithdrawalIndex = -1;
242
245
  for (let i = 0; i < vault.pendingWithdrawals.length; i++) {
243
246
  const pendingWithdrawal = vault.pendingWithdrawals[i];
247
+ if (pendingWithdrawal.sending)
248
+ continue;
244
249
  //Check all the pending withdrawals that were not finalized yet
245
- if (pendingWithdrawal.btcTx.confirmations == null || pendingWithdrawal.btcTx.confirmations < BTC_FINALIZATION_CONFIRMATIONS) {
246
- const btcTx = await this.bitcoinRpc.getTransaction(pendingWithdrawal.btcTx.txid);
247
- if (btcTx == null) {
248
- //Probable double-spend, remove from pending withdrawals
249
- const index = vault.pendingWithdrawals.indexOf(pendingWithdrawal);
250
- if (index === -1) {
251
- this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: " + pendingWithdrawal.btcTx.txid + ", but doesn't exist anymore!");
252
- }
253
- else {
254
- vault.pendingWithdrawals.splice(index, 1);
255
- }
256
- changed = true;
250
+ const btcTx = await (0, BitcoinUtils_1.checkTransactionReplacedRpc)(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
251
+ if (btcTx == null) {
252
+ //Probable double-spend, remove from pending withdrawals
253
+ const index = vault.pendingWithdrawals.indexOf(pendingWithdrawal);
254
+ if (index === -1) {
255
+ this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: " + pendingWithdrawal.btcTx.txid + ", but doesn't exist anymore!");
257
256
  }
258
257
  else {
259
- //Update confirmations count
260
- if (pendingWithdrawal.btcTx.confirmations !== btcTx.confirmations ||
261
- pendingWithdrawal.btcTx.blockhash !== btcTx.blockhash) {
262
- pendingWithdrawal.btcTx.confirmations = btcTx.confirmations;
263
- pendingWithdrawal.btcTx.blockhash = btcTx.blockhash;
264
- changed = true;
265
- }
258
+ vault.pendingWithdrawals.splice(index, 1);
259
+ }
260
+ changed = true;
261
+ }
262
+ else {
263
+ //Update confirmations count
264
+ if (pendingWithdrawal.btcTx.confirmations !== btcTx.confirmations ||
265
+ pendingWithdrawal.btcTx.blockhash !== btcTx.blockhash) {
266
+ pendingWithdrawal.btcTx.confirmations = btcTx.confirmations;
267
+ pendingWithdrawal.btcTx.blockhash = btcTx.blockhash;
268
+ changed = true;
266
269
  }
267
270
  }
268
271
  //Check it has enough confirmations
@@ -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": "15.0.4",
3
+ "version": "15.0.6",
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": "^11.0.0",
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"
@@ -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
 
@@ -362,13 +363,22 @@ export class ToBtcAbs extends ToBtcBaseSwapHandler<ToBtcSwapAbs, ToBtcSwapState>
362
363
  }
363
364
  if(swap.metadata!=null) swap.metadata.times.paySignPSBT = Date.now();
364
365
 
365
- this.swapLogger.debug(swap, "sendBitcoinPayment(): signed raw transaction: "+signResult.raw);
366
- swap.txId = signResult.tx.id;
367
- swap.setRealNetworkFee(BigInt(signResult.networkFee));
368
- await swap.setState(ToBtcSwapState.BTC_SENDING);
369
- await this.saveSwapData(swap);
366
+ try {
367
+ this.swapLogger.debug(swap, "sendBitcoinPayment(): signed raw transaction: "+signResult.raw);
368
+ swap.txId = signResult.tx.id;
369
+ swap.btcRawTx = signResult.raw;
370
+ swap.setRealNetworkFee(BigInt(signResult.networkFee));
371
+ swap.sending = true;
372
+ await swap.setState(ToBtcSwapState.BTC_SENDING);
373
+ await this.saveSwapData(swap);
374
+
375
+ await this.bitcoin.sendRawTransaction(signResult.raw);
376
+ swap.sending = false;
377
+ } catch (e) {
378
+ swap.sending = false;
379
+ throw e;
380
+ }
370
381
 
371
- await this.bitcoin.sendRawTransaction(signResult.raw);
372
382
  if(swap.metadata!=null) swap.metadata.times.payTxSent = Date.now();
373
383
  this.swapLogger.info(swap, "sendBitcoinPayment(): btc transaction generated, signed & broadcasted, txId: "+swap.txId+" address: "+swap.address);
374
384
 
@@ -384,8 +394,9 @@ export class ToBtcAbs extends ToBtcBaseSwapHandler<ToBtcSwapAbs, ToBtcSwapState>
384
394
  */
385
395
  private async processInitialized(swap: ToBtcSwapAbs) {
386
396
  if(swap.state===ToBtcSwapState.BTC_SENDING) {
397
+ if(swap.sending) return;
387
398
  //Bitcoin transaction was signed (maybe also sent)
388
- const tx = await this.bitcoin.getWalletTransaction(swap.txId);
399
+ const tx = await checkTransactionReplaced(swap.txId, swap.btcRawTx, this.bitcoin);
389
400
 
390
401
  const isTxSent = tx!=null;
391
402
  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) {
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;
@@ -553,32 +562,49 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
553
562
  msg: "Vault UTXO already spent, please try again!"
554
563
  };
555
564
  }
556
- vault.addWithdrawal(data);
557
- await this.Vaults.saveVault(vault);
558
565
 
559
- //Double-check the state to prevent race condition
560
- if(swap.state!==SpvVaultSwapState.CREATED) throw {
561
- code: 20505,
562
- msg: "Invalid quote ID, not found or expired!"
563
- };
564
- swap.btcTxId = signedTx.id;
565
- swap.state = SpvVaultSwapState.SIGNED;
566
- await this.saveSwapData(swap);
566
+ try {
567
+ const btcRawTx = Buffer.from(signedTx.toBytes(true, true)).toString("hex");
568
+
569
+ //Double-check the state to prevent race condition
570
+ if(swap.state!==SpvVaultSwapState.CREATED) {
571
+ throw {
572
+ code: 20505,
573
+ msg: "Invalid quote ID, not found or expired!"
574
+ };
575
+ }
567
576
 
568
- this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: "+swap.btcTxId);
577
+ swap.btcTxId = signedTx.id;
578
+ swap.state = SpvVaultSwapState.SIGNED;
579
+ swap.sending = true;
580
+ await this.saveSwapData(swap);
569
581
 
570
- try {
571
- await this.bitcoin.sendRawTransaction(Buffer.from(signedTx.toBytes(true, true)).toString("hex"));
572
- await swap.setState(SpvVaultSwapState.SENT);
582
+ data.btcTx.raw = btcRawTx;
583
+ (data as any).sending = true;
584
+ vault.addWithdrawal(data);
585
+ await this.Vaults.saveVault(vault);
586
+
587
+ this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: "+swap.btcTxId);
588
+
589
+ try {
590
+ await this.bitcoin.sendRawTransaction(btcRawTx);
591
+ await swap.setState(SpvVaultSwapState.SENT);
592
+ (data as any).sending = false;
593
+ swap.sending = false;
594
+ } catch (e) {
595
+ this.swapLogger.error(swap, "REST: /postQuote: Failed to send BTC transaction: ", e);
596
+ throw {
597
+ code: 20512,
598
+ msg: "Error broadcasting bitcoin transaction!"
599
+ };
600
+ }
573
601
  } catch (e) {
574
- this.swapLogger.error(swap, "REST: /postQuote: Failed to send BTC transaction: ", e);
602
+ (data as any).sending = false;
603
+ swap.sending = false;
575
604
  vault.removeWithdrawal(data);
576
605
  await this.Vaults.saveVault(vault);
577
606
  await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
578
- throw {
579
- code: 20512,
580
- msg: "Error broadcasting bitcoin transaction!"
581
- };
607
+ throw e;
582
608
  }
583
609
 
584
610
  await responseStream.writeParamsAndEnd({
@@ -15,10 +15,10 @@ import {ISpvVaultSigner} from "../../wallets/ISpvVaultSigner";
15
15
  import {AmountAssertions} from "../assertions/AmountAssertions";
16
16
  import {ChainData} 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
 
23
23
  export class SpvVaults {
24
24
 
@@ -224,12 +224,15 @@ export class SpvVaults {
224
224
  if(withdrawalData.getSpentVaultUtxo()!==vault.getLatestUtxo()) {
225
225
  throw new Error("Latest vault UTXO already spent! Please try again later.");
226
226
  }
227
+ (withdrawalData as any).sending = true;
227
228
  vault.addWithdrawal(withdrawalData);
228
229
  await this.saveVault(vault);
229
230
 
230
231
  try {
231
232
  await this.bitcoin.sendRawTransaction(res.raw);
233
+ (withdrawalData as any).sending = false;
232
234
  } catch (e) {
235
+ (withdrawalData as any).sending = false;
233
236
  vault.removeWithdrawal(withdrawalData);
234
237
  await this.saveVault(vault);
235
238
  throw e;
@@ -300,30 +303,31 @@ export class SpvVaults {
300
303
  let latestConfirmedWithdrawalIndex = -1;
301
304
  for(let i=0; i<vault.pendingWithdrawals.length; i++) {
302
305
  const pendingWithdrawal = vault.pendingWithdrawals[i];
306
+ if(pendingWithdrawal.sending) continue;
307
+
303
308
  //Check all the pending withdrawals that were not finalized yet
304
- if(pendingWithdrawal.btcTx.confirmations==null || pendingWithdrawal.btcTx.confirmations < BTC_FINALIZATION_CONFIRMATIONS) {
305
- const btcTx = await this.bitcoinRpc.getTransaction(pendingWithdrawal.btcTx.txid);
306
- if(btcTx==null) {
307
- //Probable double-spend, remove from pending withdrawals
308
- const index = vault.pendingWithdrawals.indexOf(pendingWithdrawal);
309
- if(index===-1) {
310
- this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: "+pendingWithdrawal.btcTx.txid+", but doesn't exist anymore!")
311
- } else {
312
- vault.pendingWithdrawals.splice(index, 1);
313
- }
314
- changed = true;
309
+ const btcTx = await checkTransactionReplacedRpc(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
310
+ if(btcTx==null) {
311
+ //Probable double-spend, remove from pending withdrawals
312
+ const index = vault.pendingWithdrawals.indexOf(pendingWithdrawal);
313
+ if(index===-1) {
314
+ this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: "+pendingWithdrawal.btcTx.txid+", but doesn't exist anymore!")
315
315
  } else {
316
- //Update confirmations count
317
- if(
318
- pendingWithdrawal.btcTx.confirmations !== btcTx.confirmations ||
319
- pendingWithdrawal.btcTx.blockhash !== btcTx.blockhash
320
- ) {
321
- pendingWithdrawal.btcTx.confirmations = btcTx.confirmations;
322
- pendingWithdrawal.btcTx.blockhash = btcTx.blockhash;
323
- changed = true;
324
- }
316
+ vault.pendingWithdrawals.splice(index, 1);
317
+ }
318
+ changed = true;
319
+ } else {
320
+ //Update confirmations count
321
+ if(
322
+ pendingWithdrawal.btcTx.confirmations !== btcTx.confirmations ||
323
+ pendingWithdrawal.btcTx.blockhash !== btcTx.blockhash
324
+ ) {
325
+ pendingWithdrawal.btcTx.confirmations = btcTx.confirmations;
326
+ pendingWithdrawal.btcTx.blockhash = btcTx.blockhash;
327
+ changed = true;
325
328
  }
326
329
  }
330
+
327
331
  //Check it has enough confirmations
328
332
  if(pendingWithdrawal.btcTx.confirmations >= vault.data.getConfirmations()) {
329
333
  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
+ }