@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.
- 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 +23 -20
- package/dist/utils/BitcoinUtils.d.ts +4 -0
- package/dist/utils/BitcoinUtils.js +31 -1
- package/package.json +2 -2
- 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 +25 -21
- 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
|
/**
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
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
|
|
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) {
|
|
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;
|
|
@@ -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
|
-
|
|
436
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
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
|
|
|
@@ -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) {
|
|
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;
|
|
@@ -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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
577
|
+
swap.btcTxId = signedTx.id;
|
|
578
|
+
swap.state = SpvVaultSwapState.SIGNED;
|
|
579
|
+
swap.sending = true;
|
|
580
|
+
await this.saveSwapData(swap);
|
|
569
581
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
+
}
|