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