@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.
- package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.js +19 -7
- package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.d.ts +2 -0
- package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.js +2 -0
- package/dist/swaps/spv_vault_swap/SpvVault.d.ts +3 -1
- package/dist/swaps/spv_vault_swap/SpvVaultSwap.d.ts +1 -0
- package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +47 -22
- package/dist/swaps/spv_vault_swap/SpvVaults.d.ts +6 -2
- package/dist/swaps/spv_vault_swap/SpvVaults.js +7 -4
- package/dist/utils/BitcoinUtils.d.ts +4 -0
- package/dist/utils/BitcoinUtils.js +31 -1
- package/package.json +1 -1
- package/src/swaps/escrow/tobtc_abstract/ToBtcAbs.ts +18 -7
- package/src/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.ts +6 -0
- package/src/swaps/spv_vault_swap/SpvVault.ts +1 -1
- package/src/swaps/spv_vault_swap/SpvVaultSwap.ts +3 -0
- package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +49 -23
- package/src/swaps/spv_vault_swap/SpvVaults.ts +26 -22
- package/src/utils/BitcoinUtils.ts +29 -0
|
@@ -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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
|
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
|
|
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;
|
|
@@ -99,8 +99,13 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
if (swap.state === SpvVaultSwap_1.SpvVaultSwapState.SIGNED) {
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
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
|
|
|
@@ -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
|
-
|
|
170
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
579
|
+
swap.btcTxId = signedTx.id;
|
|
580
|
+
swap.state = SpvVaultSwapState.SIGNED;
|
|
581
|
+
swap.sending = true;
|
|
582
|
+
await this.saveSwapData(swap);
|
|
571
583
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
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
|
+
}
|