@atomiqlabs/lp-lib 16.2.0 → 16.2.1
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/LICENSE +201 -201
- package/dist/fees/IBtcFeeEstimator.d.ts +3 -3
- package/dist/fees/IBtcFeeEstimator.js +2 -2
- package/dist/index.d.ts +42 -42
- package/dist/index.js +58 -58
- package/dist/info/InfoHandler.d.ts +17 -17
- package/dist/info/InfoHandler.js +60 -60
- package/dist/plugins/IPlugin.d.ts +144 -144
- package/dist/plugins/IPlugin.js +34 -34
- package/dist/plugins/PluginManager.d.ts +113 -113
- package/dist/plugins/PluginManager.js +274 -274
- package/dist/prices/BinanceSwapPrice.d.ts +29 -29
- package/dist/prices/BinanceSwapPrice.js +79 -79
- package/dist/prices/CoinGeckoSwapPrice.d.ts +33 -33
- package/dist/prices/CoinGeckoSwapPrice.js +51 -51
- package/dist/prices/ISwapPrice.d.ts +43 -43
- package/dist/prices/ISwapPrice.js +55 -55
- package/dist/prices/OKXSwapPrice.d.ts +29 -29
- package/dist/prices/OKXSwapPrice.js +79 -79
- package/dist/storage/IIntermediaryStorage.d.ts +18 -18
- package/dist/storage/IIntermediaryStorage.js +2 -2
- package/dist/storagemanager/IntermediaryStorageManager.d.ts +19 -19
- package/dist/storagemanager/IntermediaryStorageManager.js +111 -111
- package/dist/storagemanager/StorageManager.d.ts +13 -13
- package/dist/storagemanager/StorageManager.js +64 -64
- package/dist/swaps/SwapHandler.d.ts +171 -171
- package/dist/swaps/SwapHandler.js +217 -217
- package/dist/swaps/SwapHandlerSwap.d.ts +79 -79
- package/dist/swaps/SwapHandlerSwap.js +78 -78
- package/dist/swaps/assertions/AmountAssertions.d.ts +28 -28
- package/dist/swaps/assertions/AmountAssertions.js +74 -74
- package/dist/swaps/assertions/FromBtcAmountAssertions.d.ts +76 -76
- package/dist/swaps/assertions/FromBtcAmountAssertions.js +185 -185
- package/dist/swaps/assertions/LightningAssertions.d.ts +44 -44
- package/dist/swaps/assertions/LightningAssertions.js +86 -86
- package/dist/swaps/assertions/ToBtcAmountAssertions.d.ts +53 -53
- package/dist/swaps/assertions/ToBtcAmountAssertions.js +150 -150
- package/dist/swaps/escrow/EscrowHandler.d.ts +50 -50
- package/dist/swaps/escrow/EscrowHandler.js +151 -151
- package/dist/swaps/escrow/EscrowHandlerSwap.d.ts +35 -35
- package/dist/swaps/escrow/EscrowHandlerSwap.js +69 -69
- package/dist/swaps/escrow/FromBtcBaseSwap.d.ts +14 -14
- package/dist/swaps/escrow/FromBtcBaseSwap.js +32 -32
- package/dist/swaps/escrow/FromBtcBaseSwapHandler.d.ts +102 -102
- package/dist/swaps/escrow/FromBtcBaseSwapHandler.js +210 -210
- package/dist/swaps/escrow/ToBtcBaseSwap.d.ts +36 -36
- package/dist/swaps/escrow/ToBtcBaseSwap.js +67 -67
- package/dist/swaps/escrow/ToBtcBaseSwapHandler.d.ts +53 -53
- package/dist/swaps/escrow/ToBtcBaseSwapHandler.js +81 -81
- package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.d.ts +84 -84
- package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.js +322 -322
- package/dist/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.d.ts +21 -21
- package/dist/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.js +50 -50
- package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.d.ts +108 -108
- package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.js +695 -695
- package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.d.ts +33 -33
- package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.js +91 -91
- package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.d.ts +112 -112
- package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.js +708 -708
- package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.d.ts +55 -55
- package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.js +120 -120
- package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.d.ts +170 -170
- package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.js +745 -745
- package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.d.ts +28 -28
- package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.js +64 -64
- package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.d.ts +178 -178
- package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.js +900 -899
- package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.d.ts +24 -24
- package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.js +58 -58
- package/dist/swaps/spv_vault_swap/SpvVault.d.ts +44 -44
- package/dist/swaps/spv_vault_swap/SpvVault.js +145 -145
- package/dist/swaps/spv_vault_swap/SpvVaultSwap.d.ts +68 -68
- package/dist/swaps/spv_vault_swap/SpvVaultSwap.js +158 -158
- package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.d.ts +68 -68
- package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +561 -561
- package/dist/swaps/spv_vault_swap/SpvVaults.d.ts +63 -63
- package/dist/swaps/spv_vault_swap/SpvVaults.js +491 -491
- package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.d.ts +52 -52
- package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.js +662 -662
- package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.d.ts +52 -52
- package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.js +118 -118
- package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.d.ts +77 -77
- package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.js +504 -504
- package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.d.ts +34 -34
- package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.js +81 -81
- package/dist/utils/BitcoinUtils.d.ts +4 -4
- package/dist/utils/BitcoinUtils.js +61 -61
- package/dist/utils/Utils.d.ts +32 -32
- package/dist/utils/Utils.js +129 -129
- package/dist/utils/paramcoders/IParamReader.d.ts +5 -5
- package/dist/utils/paramcoders/IParamReader.js +2 -2
- package/dist/utils/paramcoders/IParamWriter.d.ts +4 -4
- package/dist/utils/paramcoders/IParamWriter.js +2 -2
- package/dist/utils/paramcoders/LegacyParamEncoder.d.ts +10 -10
- package/dist/utils/paramcoders/LegacyParamEncoder.js +22 -22
- package/dist/utils/paramcoders/ParamDecoder.d.ts +25 -25
- package/dist/utils/paramcoders/ParamDecoder.js +222 -222
- package/dist/utils/paramcoders/ParamEncoder.d.ts +9 -9
- package/dist/utils/paramcoders/ParamEncoder.js +22 -22
- package/dist/utils/paramcoders/SchemaVerifier.d.ts +21 -21
- package/dist/utils/paramcoders/SchemaVerifier.js +84 -84
- package/dist/utils/paramcoders/server/ServerParamDecoder.d.ts +8 -8
- package/dist/utils/paramcoders/server/ServerParamDecoder.js +107 -107
- package/dist/utils/paramcoders/server/ServerParamEncoder.d.ts +11 -11
- package/dist/utils/paramcoders/server/ServerParamEncoder.js +65 -65
- package/dist/wallets/IBitcoinWallet.d.ts +149 -149
- package/dist/wallets/IBitcoinWallet.js +97 -97
- package/dist/wallets/ILightningWallet.d.ts +136 -136
- package/dist/wallets/ILightningWallet.js +37 -37
- package/dist/wallets/ISpvVaultSigner.d.ts +7 -7
- package/dist/wallets/ISpvVaultSigner.js +2 -2
- package/package.json +36 -36
- package/src/fees/IBtcFeeEstimator.ts +6 -6
- package/src/index.ts +53 -53
- package/src/info/InfoHandler.ts +103 -103
- package/src/plugins/IPlugin.ts +174 -174
- package/src/plugins/PluginManager.ts +354 -354
- package/src/prices/BinanceSwapPrice.ts +101 -101
- package/src/prices/CoinGeckoSwapPrice.ts +75 -75
- package/src/prices/ISwapPrice.ts +88 -88
- package/src/prices/OKXSwapPrice.ts +101 -101
- package/src/storage/IIntermediaryStorage.ts +19 -19
- package/src/storagemanager/IntermediaryStorageManager.ts +118 -118
- package/src/storagemanager/StorageManager.ts +78 -78
- package/src/swaps/SwapHandler.ts +323 -323
- package/src/swaps/SwapHandlerSwap.ts +141 -141
- package/src/swaps/assertions/AmountAssertions.ts +77 -77
- package/src/swaps/assertions/FromBtcAmountAssertions.ts +251 -251
- package/src/swaps/assertions/LightningAssertions.ts +103 -103
- package/src/swaps/assertions/ToBtcAmountAssertions.ts +203 -203
- package/src/swaps/escrow/EscrowHandler.ts +172 -172
- package/src/swaps/escrow/EscrowHandlerSwap.ts +86 -86
- package/src/swaps/escrow/FromBtcBaseSwap.ts +38 -38
- package/src/swaps/escrow/FromBtcBaseSwapHandler.ts +286 -286
- package/src/swaps/escrow/ToBtcBaseSwap.ts +85 -85
- package/src/swaps/escrow/ToBtcBaseSwapHandler.ts +129 -129
- package/src/swaps/escrow/frombtc_abstract/FromBtcAbs.ts +457 -457
- package/src/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.ts +61 -61
- package/src/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.ts +873 -873
- package/src/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.ts +141 -141
- package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.ts +866 -866
- package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.ts +196 -196
- package/src/swaps/escrow/tobtc_abstract/ToBtcAbs.ts +920 -920
- package/src/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.ts +108 -108
- package/src/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.ts +1150 -1149
- package/src/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.ts +80 -80
- package/src/swaps/spv_vault_swap/SpvVault.ts +178 -178
- package/src/swaps/spv_vault_swap/SpvVaultSwap.ts +228 -228
- package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +718 -718
- package/src/swaps/spv_vault_swap/SpvVaults.ts +567 -567
- package/src/swaps/trusted/frombtc_trusted/FromBtcTrusted.ts +762 -762
- package/src/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.ts +185 -185
- package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.ts +603 -603
- package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.ts +121 -121
- package/src/utils/BitcoinUtils.ts +59 -59
- package/src/utils/Utils.ts +150 -150
- package/src/utils/paramcoders/IParamReader.ts +7 -7
- package/src/utils/paramcoders/IParamWriter.ts +8 -8
- package/src/utils/paramcoders/LegacyParamEncoder.ts +27 -27
- package/src/utils/paramcoders/ParamDecoder.ts +218 -218
- package/src/utils/paramcoders/ParamEncoder.ts +29 -29
- package/src/utils/paramcoders/SchemaVerifier.ts +96 -96
- package/src/utils/paramcoders/server/ServerParamDecoder.ts +118 -118
- package/src/utils/paramcoders/server/ServerParamEncoder.ts +75 -75
- package/src/wallets/IBitcoinWallet.ts +237 -237
- package/src/wallets/ILightningWallet.ts +200 -200
- package/src/wallets/ISpvVaultSigner.ts +10 -10
|
@@ -1,568 +1,568 @@
|
|
|
1
|
-
import {SpvVault, SpvVaultState} from "./SpvVault";
|
|
2
|
-
import {
|
|
3
|
-
BitcoinRpc,
|
|
4
|
-
IStorageManager,
|
|
5
|
-
SpvVaultClaimEvent,
|
|
6
|
-
SpvVaultCloseEvent,
|
|
7
|
-
SpvVaultDepositEvent,
|
|
8
|
-
SpvVaultOpenEvent, SpvWithdrawalTransactionData
|
|
9
|
-
} from "@atomiqlabs/base";
|
|
10
|
-
import {SpvVaultSwap} from "./SpvVaultSwap";
|
|
11
|
-
import {bigIntSorter, getLogger} from "../../utils/Utils";
|
|
12
|
-
import {PluginManager} from "../../plugins/PluginManager";
|
|
13
|
-
import {IBitcoinWallet} from "../../wallets/IBitcoinWallet";
|
|
14
|
-
import {ISpvVaultSigner} from "../../wallets/ISpvVaultSigner";
|
|
15
|
-
import {AmountAssertions} from "../assertions/AmountAssertions";
|
|
16
|
-
import {MultichainData} from "../SwapHandler";
|
|
17
|
-
import {Transaction} from "@scure/btc-signer";
|
|
18
|
-
import {checkTransactionReplaced} from "../../utils/BitcoinUtils";
|
|
19
|
-
|
|
20
|
-
export const VAULT_DUST_AMOUNT = 600;
|
|
21
|
-
const VAULT_INIT_CONFIRMATIONS = 2;
|
|
22
|
-
const MAX_PARALLEL_VAULTS_OPENING = 10;
|
|
23
|
-
|
|
24
|
-
export class SpvVaults {
|
|
25
|
-
|
|
26
|
-
readonly vaultStorage: IStorageManager<SpvVault>;
|
|
27
|
-
|
|
28
|
-
readonly bitcoin: IBitcoinWallet;
|
|
29
|
-
readonly vaultSigner: ISpvVaultSigner;
|
|
30
|
-
readonly bitcoinRpc: BitcoinRpc<any>;
|
|
31
|
-
readonly config: {vaultsCheckInterval: number, maxUnclaimedWithdrawals?: number};
|
|
32
|
-
readonly chains: MultichainData;
|
|
33
|
-
|
|
34
|
-
readonly logger = getLogger("SpvVaults: ");
|
|
35
|
-
|
|
36
|
-
constructor(
|
|
37
|
-
vaultStorage: IStorageManager<SpvVault>,
|
|
38
|
-
bitcoin: IBitcoinWallet,
|
|
39
|
-
vaultSigner: ISpvVaultSigner,
|
|
40
|
-
bitcoinRpc: BitcoinRpc<any>,
|
|
41
|
-
chains: MultichainData,
|
|
42
|
-
config: {vaultsCheckInterval: number, maxUnclaimedWithdrawals?: number}
|
|
43
|
-
) {
|
|
44
|
-
this.vaultStorage = vaultStorage;
|
|
45
|
-
this.bitcoin = bitcoin;
|
|
46
|
-
this.vaultSigner = vaultSigner;
|
|
47
|
-
this.bitcoinRpc = bitcoinRpc;
|
|
48
|
-
this.chains = chains;
|
|
49
|
-
this.config = config;
|
|
50
|
-
|
|
51
|
-
for(let chainId in chains.chains) {
|
|
52
|
-
const {chainInterface} = chains.chains[chainId];
|
|
53
|
-
chainInterface.onBeforeTxReplace(async (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => {
|
|
54
|
-
for(let key in this.vaultStorage.data) {
|
|
55
|
-
const vaultData = this.vaultStorage.data[key];
|
|
56
|
-
if(vaultData.scOpenTxs!=null && vaultData.scOpenTxs[oldTxId]!=null){
|
|
57
|
-
vaultData.scOpenTxs[newTxId] = newTx;
|
|
58
|
-
await this.saveVault(vaultData);
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async processDepositEvent(vault: SpvVault, event: SpvVaultDepositEvent): Promise<void> {
|
|
67
|
-
vault.update(event);
|
|
68
|
-
await this.saveVault(vault);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async processOpenEvent(vault: SpvVault, event: SpvVaultOpenEvent): Promise<void> {
|
|
72
|
-
if(vault.state===SpvVaultState.BTC_CONFIRMED) {
|
|
73
|
-
vault.state = SpvVaultState.OPENED;
|
|
74
|
-
}
|
|
75
|
-
vault.update(event);
|
|
76
|
-
await this.saveVault(vault);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async processCloseEvent(vault: SpvVault, event: SpvVaultCloseEvent): Promise<void> {
|
|
80
|
-
if(vault.state===SpvVaultState.OPENED) {
|
|
81
|
-
vault.state = SpvVaultState.CLOSED;
|
|
82
|
-
}
|
|
83
|
-
vault.update(event);
|
|
84
|
-
await this.saveVault(vault);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async processClaimEvent(vault: SpvVault, swap: SpvVaultSwap | null, event: SpvVaultClaimEvent): Promise<void> {
|
|
88
|
-
//Update vault
|
|
89
|
-
const foundPendingWithdrawal = vault.pendingWithdrawals.findIndex(val => val.btcTx.txid===event.btcTxId);
|
|
90
|
-
if(foundPendingWithdrawal!==-1) vault.pendingWithdrawals.splice(foundPendingWithdrawal, 1);
|
|
91
|
-
vault.update(event);
|
|
92
|
-
await this.saveVault(vault);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async createVaults(chainId: string, count: number, token: string, confirmations: number = 2, feeRate?: number): Promise<{vaultsCreated: bigint[], btcTxId: string}> {
|
|
96
|
-
const {signer, chainInterface, tokenMultipliers, spvVaultContract} = this.chains.chains[chainId];
|
|
97
|
-
|
|
98
|
-
const signerAddress = signer.getAddress();
|
|
99
|
-
|
|
100
|
-
//Check vaultId of the latest saved vault
|
|
101
|
-
let latestVaultId: bigint = -1n;
|
|
102
|
-
for(let key in this.vaultStorage.data) {
|
|
103
|
-
const vault = this.vaultStorage.data[key];
|
|
104
|
-
if(vault.chainId!==chainId) continue;
|
|
105
|
-
if(vault.data.getOwner()!==signerAddress) continue;
|
|
106
|
-
if(vault.data.getVaultId() > latestVaultId) latestVaultId = vault.data.getVaultId();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
latestVaultId++;
|
|
110
|
-
|
|
111
|
-
const vaultAddreses: {vaultId: bigint, address: string}[] = [];
|
|
112
|
-
for(let i=0;i<count;i++) {
|
|
113
|
-
const vaultId = latestVaultId + BigInt(i);
|
|
114
|
-
const address = await this.vaultSigner.getAddress(chainId, vaultId);
|
|
115
|
-
vaultAddreses.push({vaultId, address});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const nativeToken = chainInterface.getNativeCurrencyAddress();
|
|
119
|
-
|
|
120
|
-
let txId: string = null;
|
|
121
|
-
let vaults: SpvVault[] = null;
|
|
122
|
-
await this.bitcoin.execute(async () => {
|
|
123
|
-
//Construct transaction
|
|
124
|
-
const txResult = await this.bitcoin.getSignedMultiTransaction(vaultAddreses.map(val => {
|
|
125
|
-
return {address: val.address, amount: VAULT_DUST_AMOUNT}
|
|
126
|
-
}), feeRate);
|
|
127
|
-
|
|
128
|
-
vaults = await Promise.all(vaultAddreses.map(async (val, index) => {
|
|
129
|
-
const vaultData = await spvVaultContract.createVaultData(signerAddress, val.vaultId, txResult.txId+":"+index, confirmations, [
|
|
130
|
-
{token, multiplier: tokenMultipliers?.[token] ?? 1n},
|
|
131
|
-
{token: nativeToken, multiplier: tokenMultipliers?.[nativeToken] ?? 1n}
|
|
132
|
-
]);
|
|
133
|
-
return new SpvVault(chainId, vaultData, val.address);
|
|
134
|
-
}));
|
|
135
|
-
|
|
136
|
-
//Save vaults
|
|
137
|
-
if(this.vaultStorage.saveDataArr!=null) {
|
|
138
|
-
await this.vaultStorage.saveDataArr(vaults.map(val => {
|
|
139
|
-
return {id: val.getIdentifier(), object: val}
|
|
140
|
-
}));
|
|
141
|
-
} else {
|
|
142
|
-
for(let vault of vaults) {
|
|
143
|
-
await this.vaultStorage.saveData(vault.getIdentifier(), vault);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
//Send bitcoin tx
|
|
148
|
-
await this.bitcoin.sendRawTransaction(txResult.raw);
|
|
149
|
-
|
|
150
|
-
txId = txResult.txId;
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
this.logger.info("createVaults(): Funding "+count+" vaults, bitcoin txId: "+txId);
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
vaultsCreated: vaults.map(val => val.data.getVaultId()),
|
|
157
|
-
btcTxId: txId
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async listVaults(chainId?: string, token?: string) {
|
|
162
|
-
return Object.keys(this.vaultStorage.data)
|
|
163
|
-
.map(key => this.vaultStorage.data[key])
|
|
164
|
-
.filter(val => chainId==null ? true : val.chainId===chainId)
|
|
165
|
-
.filter(val => this.chains.chains[val.chainId]!=null && val.data.getOwner()===this.chains.chains[val.chainId]?.signer?.getAddress())
|
|
166
|
-
.filter(val => token==null ? true : val.data.getTokenData()[0].token===token);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async fundVault(vault: SpvVault, tokenAmounts: bigint[]): Promise<string> {
|
|
170
|
-
if(vault.state!==SpvVaultState.OPENED) throw new Error("Vault not opened!");
|
|
171
|
-
|
|
172
|
-
this.logger.info("fundVault(): Depositing tokens to the vault "+vault.data.getVaultId().toString(10)+", amounts: "+tokenAmounts.map(val => val.toString(10)).join(", "));
|
|
173
|
-
|
|
174
|
-
const {signer, spvVaultContract} = this.chains.chains[vault.chainId];
|
|
175
|
-
|
|
176
|
-
const txId = await spvVaultContract.deposit(signer, vault.data, tokenAmounts, {waitForConfirmation: true});
|
|
177
|
-
|
|
178
|
-
this.logger.info("fundVault(): Tokens deposited to vault "+vault.data.getVaultId().toString(10)+", amounts: "+tokenAmounts.map(val => val.toString(10)).join(", ")+", txId: "+txId);
|
|
179
|
-
|
|
180
|
-
return txId;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async withdrawFromVault(vault: SpvVault, tokenAmounts: bigint[], feeRate?: number): Promise<string> {
|
|
184
|
-
tokenAmounts.forEach((rawAmount, index) => {
|
|
185
|
-
if(vault.balances[index]==null) throw new Error("Token not found in the vault");
|
|
186
|
-
if(vault.balances[index].rawAmount<rawAmount) throw new Error("Not enough balance in the vault");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
if(!vault.isReady()) throw new Error("Vault not ready, wait for the latest swap to get at least 1 confirmation!");
|
|
190
|
-
|
|
191
|
-
const {signer, spvVaultContract} = this.chains.chains[vault.chainId];
|
|
192
|
-
|
|
193
|
-
const latestUtxo = vault.getLatestUtxo();
|
|
194
|
-
const [txId, voutStr] = latestUtxo.split(":");
|
|
195
|
-
|
|
196
|
-
const opReturnData = spvVaultContract.toOpReturnData(signer.getAddress(), tokenAmounts);
|
|
197
|
-
let opReturnScript: Buffer;
|
|
198
|
-
if(opReturnData.length<76) {
|
|
199
|
-
opReturnScript = Buffer.concat([
|
|
200
|
-
Buffer.from([0x6a, opReturnData.length]),
|
|
201
|
-
opReturnData
|
|
202
|
-
]);
|
|
203
|
-
} else {
|
|
204
|
-
opReturnScript = Buffer.concat([
|
|
205
|
-
Buffer.from([0x6a, 0x4c, opReturnData.length]),
|
|
206
|
-
opReturnData
|
|
207
|
-
]);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
let psbt = new Transaction({
|
|
211
|
-
allowUnknownOutputs: true
|
|
212
|
-
});
|
|
213
|
-
psbt.addInput({
|
|
214
|
-
txid: txId,
|
|
215
|
-
index: parseInt(voutStr),
|
|
216
|
-
witnessUtxo: {
|
|
217
|
-
amount: BigInt(VAULT_DUST_AMOUNT),
|
|
218
|
-
script: this.bitcoin.toOutputScript(vault.btcAddress)
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
psbt.addOutput({
|
|
222
|
-
amount: BigInt(VAULT_DUST_AMOUNT),
|
|
223
|
-
script: this.bitcoin.toOutputScript(vault.btcAddress)
|
|
224
|
-
});
|
|
225
|
-
psbt.addOutput({
|
|
226
|
-
amount: 0n,
|
|
227
|
-
script: opReturnScript
|
|
228
|
-
});
|
|
229
|
-
|
|
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);
|
|
249
|
-
|
|
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
|
-
}
|
|
263
|
-
|
|
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
|
-
}
|
|
308
|
-
}
|
|
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);
|
|
319
|
-
|
|
320
|
-
try {
|
|
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;
|
|
338
|
-
} catch (e) {
|
|
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;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
async checkVaults() {
|
|
347
|
-
const vaults = Object.keys(this.vaultStorage.data).map(key => this.vaultStorage.data[key]);
|
|
348
|
-
|
|
349
|
-
const claimWithdrawals: {vault: SpvVault, withdrawals: SpvWithdrawalTransactionData[]}[] = [];
|
|
350
|
-
|
|
351
|
-
let promises: Promise<void>[] = [];
|
|
352
|
-
|
|
353
|
-
for(let vault of vaults) {
|
|
354
|
-
const chainData = this.chains.chains[vault.chainId];
|
|
355
|
-
if(chainData==null) continue;
|
|
356
|
-
const {signer, spvVaultContract, chainInterface} = chainData;
|
|
357
|
-
if(vault.data.getOwner()!==signer.getAddress()) continue;
|
|
358
|
-
|
|
359
|
-
if(vault.state===SpvVaultState.BTC_INITIATED) {
|
|
360
|
-
//Check if btc tx confirmed
|
|
361
|
-
const txId = vault.initialUtxo.split(":")[0];
|
|
362
|
-
const btcTx = await this.bitcoinRpc.getTransaction(txId);
|
|
363
|
-
if(btcTx.confirmations >= VAULT_INIT_CONFIRMATIONS) {
|
|
364
|
-
//Double-check the state here to prevent race condition
|
|
365
|
-
if(vault.state===SpvVaultState.BTC_INITIATED) {
|
|
366
|
-
vault.state = SpvVaultState.BTC_CONFIRMED;
|
|
367
|
-
await this.saveVault(vault);
|
|
368
|
-
}
|
|
369
|
-
this.logger.info("checkVaults(): Vault ID "+vault.data.getVaultId().toString(10)+" confirmed on bitcoin, opening vault on "+vault.chainId);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if(vault.state===SpvVaultState.BTC_CONFIRMED) {
|
|
374
|
-
//Check if open txs were sent already
|
|
375
|
-
if(vault.scOpenTxs!=null) {
|
|
376
|
-
//Check if confirmed
|
|
377
|
-
let _continue = false;
|
|
378
|
-
for(let txId in vault.scOpenTxs) {
|
|
379
|
-
const tx = vault.scOpenTxs[txId];
|
|
380
|
-
const status = await chainInterface.getTxStatus(tx);
|
|
381
|
-
if(status==="pending") {
|
|
382
|
-
_continue = true;
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
if(status==="success") {
|
|
386
|
-
vault.state = SpvVaultState.OPENED;
|
|
387
|
-
await this.saveVault(vault);
|
|
388
|
-
_continue = true;
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if(_continue) continue;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const txs = await spvVaultContract.txsOpen(signer.getAddress(), vault.data);
|
|
396
|
-
let numTx = 0;
|
|
397
|
-
promises.push(
|
|
398
|
-
chainInterface.sendAndConfirm(
|
|
399
|
-
signer, txs, true, undefined, true,
|
|
400
|
-
async (txId: string, rawTx: string) => {
|
|
401
|
-
numTx++;
|
|
402
|
-
if(numTx===txs.length) {
|
|
403
|
-
//Final tx
|
|
404
|
-
vault.scOpenTxs = {[txId]: rawTx};
|
|
405
|
-
await this.saveVault(vault);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
).then(txIds => {
|
|
409
|
-
this.logger.info("checkVaults(): Vault ID "+vault.data.getVaultId().toString(10)+" opened on "+vault.chainId+" txId: "+txIds.join(", "));
|
|
410
|
-
|
|
411
|
-
vault.state = SpvVaultState.OPENED;
|
|
412
|
-
return this.saveVault(vault);
|
|
413
|
-
})
|
|
414
|
-
);
|
|
415
|
-
if(promises.length>=MAX_PARALLEL_VAULTS_OPENING) {
|
|
416
|
-
await Promise.all(promises);
|
|
417
|
-
promises = [];
|
|
418
|
-
}
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if(vault.state===SpvVaultState.OPENED) {
|
|
423
|
-
let changed = await this.checkVaultReplacedTransactions(vault);
|
|
424
|
-
|
|
425
|
-
//Check if some of the pendingWithdrawals got confirmed
|
|
426
|
-
let latestOwnWithdrawalIndex = -1;
|
|
427
|
-
let latestConfirmedWithdrawalIndex = -1;
|
|
428
|
-
for(let i = vault.pendingWithdrawals.length-1; i>=0; i--) {
|
|
429
|
-
const pendingWithdrawal = vault.pendingWithdrawals[i];
|
|
430
|
-
if(pendingWithdrawal.sending) continue;
|
|
431
|
-
|
|
432
|
-
//Check all the pending withdrawals that were not finalized yet
|
|
433
|
-
const btcTx = await checkTransactionReplaced(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
|
|
434
|
-
if(btcTx==null) {
|
|
435
|
-
//Probable double-spend, remove from pending withdrawals
|
|
436
|
-
if(!vault.doubleSpendPendingWithdrawal(pendingWithdrawal)) {
|
|
437
|
-
this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: "+pendingWithdrawal.btcTx.txid+", but doesn't exist anymore!");
|
|
438
|
-
} else {
|
|
439
|
-
this.logger.info("checkVaults(): Successfully removed withdrawal txId: "+pendingWithdrawal.btcTx.txid+", due to being replaced in the mempool!");
|
|
440
|
-
}
|
|
441
|
-
changed = true;
|
|
442
|
-
} else {
|
|
443
|
-
//Update confirmations count
|
|
444
|
-
if(
|
|
445
|
-
pendingWithdrawal.btcTx.confirmations !== btcTx.confirmations ||
|
|
446
|
-
pendingWithdrawal.btcTx.blockhash !== btcTx.blockhash
|
|
447
|
-
) {
|
|
448
|
-
pendingWithdrawal.btcTx.confirmations = btcTx.confirmations;
|
|
449
|
-
pendingWithdrawal.btcTx.blockhash = btcTx.blockhash;
|
|
450
|
-
changed = true;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
//Check it has enough confirmations
|
|
455
|
-
if(pendingWithdrawal.btcTx.confirmations >= vault.data.getConfirmations()) {
|
|
456
|
-
latestConfirmedWithdrawalIndex = i;
|
|
457
|
-
//Check if the pending withdrawals contain a withdrawal to our own address
|
|
458
|
-
if (pendingWithdrawal.isRecipient(signer.getAddress())) {
|
|
459
|
-
latestOwnWithdrawalIndex = i;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
if(changed) {
|
|
464
|
-
await this.saveVault(vault);
|
|
465
|
-
}
|
|
466
|
-
if(this.config.maxUnclaimedWithdrawals!=null && latestConfirmedWithdrawalIndex+1 >= this.config.maxUnclaimedWithdrawals) {
|
|
467
|
-
this.logger.info("checkVaults(): Processing withdrawals by self, because a lot of them are unclaimed!");
|
|
468
|
-
claimWithdrawals.push({vault, withdrawals: vault.pendingWithdrawals.slice(0, latestConfirmedWithdrawalIndex+1)});
|
|
469
|
-
} else if(latestOwnWithdrawalIndex!==-1) {
|
|
470
|
-
claimWithdrawals.push({vault, withdrawals: vault.pendingWithdrawals.slice(0, latestOwnWithdrawalIndex+1)});
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
for(let {vault, withdrawals} of claimWithdrawals) {
|
|
476
|
-
if(!await this.claimWithdrawals(vault, withdrawals)) {
|
|
477
|
-
this.logger.error("checkVaults(): Cannot process withdrawals "+withdrawals.map(val => val.btcTx.txid).join(", ")+" for vault: "+vault.data.getVaultId());
|
|
478
|
-
break;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
async claimWithdrawals(vault: SpvVault, withdrawal: SpvWithdrawalTransactionData[]): Promise<boolean> {
|
|
484
|
-
const {signer, spvVaultContract} = this.chains.chains[vault.chainId];
|
|
485
|
-
|
|
486
|
-
try {
|
|
487
|
-
const txId = await spvVaultContract.claim(signer, vault.data, withdrawal.map(tx => {
|
|
488
|
-
return {tx};
|
|
489
|
-
}), undefined, true, {waitForConfirmation: true});
|
|
490
|
-
this.logger.info("claimWithdrawal(): Successfully claimed withdrawals, btcTxIds: "+withdrawal.map(val => val.btcTx.txid).join(", ")+" smartChainTxId: "+txId);
|
|
491
|
-
return true;
|
|
492
|
-
} catch (e) {
|
|
493
|
-
this.logger.error("claimWithdrawal(): Tried to claim but got error: ", e);
|
|
494
|
-
return false;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
async getVault(chainId: string, owner: string, vaultId: bigint) {
|
|
499
|
-
return this.vaultStorage.data[chainId+"_"+owner+"_"+vaultId.toString(10)];
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Returns a ready-to-use vault for a specific request
|
|
504
|
-
*
|
|
505
|
-
* @param chainIdentifier
|
|
506
|
-
* @param totalSats
|
|
507
|
-
* @param token
|
|
508
|
-
* @param amount
|
|
509
|
-
* @param gasToken
|
|
510
|
-
* @param gasTokenAmount
|
|
511
|
-
* @protected
|
|
512
|
-
*/
|
|
513
|
-
async findVaultForSwap(chainIdentifier: string, totalSats: bigint, token: string, amount: bigint, gasToken: string, gasTokenAmount: bigint): Promise<SpvVault | null> {
|
|
514
|
-
const {signer} = this.chains.chains[chainIdentifier];
|
|
515
|
-
|
|
516
|
-
const pluginResponse = await PluginManager.onVaultSelection(
|
|
517
|
-
chainIdentifier, totalSats, {token, amount}, {token: gasToken, amount: gasTokenAmount}
|
|
518
|
-
);
|
|
519
|
-
if(pluginResponse!=null) {
|
|
520
|
-
AmountAssertions.handlePluginErrorResponses(pluginResponse);
|
|
521
|
-
return pluginResponse as SpvVault;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const candidates = Object.keys(this.vaultStorage.data)
|
|
525
|
-
.map(key => this.vaultStorage.data[key])
|
|
526
|
-
.filter(vault =>
|
|
527
|
-
vault.chainId===chainIdentifier && vault.data.getOwner()===signer.getAddress() && vault.isReady()
|
|
528
|
-
)
|
|
529
|
-
.filter(vault => {
|
|
530
|
-
const token0 = vault.balances[0];
|
|
531
|
-
if(token0.token!==token || token0.scaledAmount < amount) return false;
|
|
532
|
-
if(gasToken!=null && gasTokenAmount!==0n) {
|
|
533
|
-
const token1 = vault.balances[1];
|
|
534
|
-
if(token1.token!==gasToken || token1.scaledAmount < gasTokenAmount) return false;
|
|
535
|
-
}
|
|
536
|
-
return true;
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
candidates.sort((a, b) => bigIntSorter(a.balances[0].scaledAmount, b.balances[0].scaledAmount));
|
|
540
|
-
|
|
541
|
-
const result = candidates[0];
|
|
542
|
-
|
|
543
|
-
if(result==null) throw {
|
|
544
|
-
code: 20301,
|
|
545
|
-
msg: "No suitable swap vault found, try again later!"
|
|
546
|
-
};
|
|
547
|
-
|
|
548
|
-
return result;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
saveVault(vault: SpvVault) {
|
|
552
|
-
return this.vaultStorage.saveData(vault.getIdentifier(), vault);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
async startVaultsWatchdog() {
|
|
556
|
-
let rerun: () => Promise<void>;
|
|
557
|
-
rerun = async () => {
|
|
558
|
-
await this.checkVaults().catch( e => this.logger.error("startVaultsWatchdog(): Error when periodically checking SPV vaults: ", e));
|
|
559
|
-
setTimeout(rerun, this.config.vaultsCheckInterval);
|
|
560
|
-
};
|
|
561
|
-
await rerun();
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
async init() {
|
|
565
|
-
const vaults = await this.vaultStorage.loadData(SpvVault);
|
|
566
|
-
}
|
|
567
|
-
|
|
1
|
+
import {SpvVault, SpvVaultState} from "./SpvVault";
|
|
2
|
+
import {
|
|
3
|
+
BitcoinRpc,
|
|
4
|
+
IStorageManager,
|
|
5
|
+
SpvVaultClaimEvent,
|
|
6
|
+
SpvVaultCloseEvent,
|
|
7
|
+
SpvVaultDepositEvent,
|
|
8
|
+
SpvVaultOpenEvent, SpvWithdrawalTransactionData
|
|
9
|
+
} from "@atomiqlabs/base";
|
|
10
|
+
import {SpvVaultSwap} from "./SpvVaultSwap";
|
|
11
|
+
import {bigIntSorter, getLogger} from "../../utils/Utils";
|
|
12
|
+
import {PluginManager} from "../../plugins/PluginManager";
|
|
13
|
+
import {IBitcoinWallet} from "../../wallets/IBitcoinWallet";
|
|
14
|
+
import {ISpvVaultSigner} from "../../wallets/ISpvVaultSigner";
|
|
15
|
+
import {AmountAssertions} from "../assertions/AmountAssertions";
|
|
16
|
+
import {MultichainData} from "../SwapHandler";
|
|
17
|
+
import {Transaction} from "@scure/btc-signer";
|
|
18
|
+
import {checkTransactionReplaced} from "../../utils/BitcoinUtils";
|
|
19
|
+
|
|
20
|
+
export const VAULT_DUST_AMOUNT = 600;
|
|
21
|
+
const VAULT_INIT_CONFIRMATIONS = 2;
|
|
22
|
+
const MAX_PARALLEL_VAULTS_OPENING = 10;
|
|
23
|
+
|
|
24
|
+
export class SpvVaults {
|
|
25
|
+
|
|
26
|
+
readonly vaultStorage: IStorageManager<SpvVault>;
|
|
27
|
+
|
|
28
|
+
readonly bitcoin: IBitcoinWallet;
|
|
29
|
+
readonly vaultSigner: ISpvVaultSigner;
|
|
30
|
+
readonly bitcoinRpc: BitcoinRpc<any>;
|
|
31
|
+
readonly config: {vaultsCheckInterval: number, maxUnclaimedWithdrawals?: number};
|
|
32
|
+
readonly chains: MultichainData;
|
|
33
|
+
|
|
34
|
+
readonly logger = getLogger("SpvVaults: ");
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
vaultStorage: IStorageManager<SpvVault>,
|
|
38
|
+
bitcoin: IBitcoinWallet,
|
|
39
|
+
vaultSigner: ISpvVaultSigner,
|
|
40
|
+
bitcoinRpc: BitcoinRpc<any>,
|
|
41
|
+
chains: MultichainData,
|
|
42
|
+
config: {vaultsCheckInterval: number, maxUnclaimedWithdrawals?: number}
|
|
43
|
+
) {
|
|
44
|
+
this.vaultStorage = vaultStorage;
|
|
45
|
+
this.bitcoin = bitcoin;
|
|
46
|
+
this.vaultSigner = vaultSigner;
|
|
47
|
+
this.bitcoinRpc = bitcoinRpc;
|
|
48
|
+
this.chains = chains;
|
|
49
|
+
this.config = config;
|
|
50
|
+
|
|
51
|
+
for(let chainId in chains.chains) {
|
|
52
|
+
const {chainInterface} = chains.chains[chainId];
|
|
53
|
+
chainInterface.onBeforeTxReplace(async (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => {
|
|
54
|
+
for(let key in this.vaultStorage.data) {
|
|
55
|
+
const vaultData = this.vaultStorage.data[key];
|
|
56
|
+
if(vaultData.scOpenTxs!=null && vaultData.scOpenTxs[oldTxId]!=null){
|
|
57
|
+
vaultData.scOpenTxs[newTxId] = newTx;
|
|
58
|
+
await this.saveVault(vaultData);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async processDepositEvent(vault: SpvVault, event: SpvVaultDepositEvent): Promise<void> {
|
|
67
|
+
vault.update(event);
|
|
68
|
+
await this.saveVault(vault);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async processOpenEvent(vault: SpvVault, event: SpvVaultOpenEvent): Promise<void> {
|
|
72
|
+
if(vault.state===SpvVaultState.BTC_CONFIRMED) {
|
|
73
|
+
vault.state = SpvVaultState.OPENED;
|
|
74
|
+
}
|
|
75
|
+
vault.update(event);
|
|
76
|
+
await this.saveVault(vault);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async processCloseEvent(vault: SpvVault, event: SpvVaultCloseEvent): Promise<void> {
|
|
80
|
+
if(vault.state===SpvVaultState.OPENED) {
|
|
81
|
+
vault.state = SpvVaultState.CLOSED;
|
|
82
|
+
}
|
|
83
|
+
vault.update(event);
|
|
84
|
+
await this.saveVault(vault);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async processClaimEvent(vault: SpvVault, swap: SpvVaultSwap | null, event: SpvVaultClaimEvent): Promise<void> {
|
|
88
|
+
//Update vault
|
|
89
|
+
const foundPendingWithdrawal = vault.pendingWithdrawals.findIndex(val => val.btcTx.txid===event.btcTxId);
|
|
90
|
+
if(foundPendingWithdrawal!==-1) vault.pendingWithdrawals.splice(foundPendingWithdrawal, 1);
|
|
91
|
+
vault.update(event);
|
|
92
|
+
await this.saveVault(vault);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async createVaults(chainId: string, count: number, token: string, confirmations: number = 2, feeRate?: number): Promise<{vaultsCreated: bigint[], btcTxId: string}> {
|
|
96
|
+
const {signer, chainInterface, tokenMultipliers, spvVaultContract} = this.chains.chains[chainId];
|
|
97
|
+
|
|
98
|
+
const signerAddress = signer.getAddress();
|
|
99
|
+
|
|
100
|
+
//Check vaultId of the latest saved vault
|
|
101
|
+
let latestVaultId: bigint = -1n;
|
|
102
|
+
for(let key in this.vaultStorage.data) {
|
|
103
|
+
const vault = this.vaultStorage.data[key];
|
|
104
|
+
if(vault.chainId!==chainId) continue;
|
|
105
|
+
if(vault.data.getOwner()!==signerAddress) continue;
|
|
106
|
+
if(vault.data.getVaultId() > latestVaultId) latestVaultId = vault.data.getVaultId();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
latestVaultId++;
|
|
110
|
+
|
|
111
|
+
const vaultAddreses: {vaultId: bigint, address: string}[] = [];
|
|
112
|
+
for(let i=0;i<count;i++) {
|
|
113
|
+
const vaultId = latestVaultId + BigInt(i);
|
|
114
|
+
const address = await this.vaultSigner.getAddress(chainId, vaultId);
|
|
115
|
+
vaultAddreses.push({vaultId, address});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const nativeToken = chainInterface.getNativeCurrencyAddress();
|
|
119
|
+
|
|
120
|
+
let txId: string = null;
|
|
121
|
+
let vaults: SpvVault[] = null;
|
|
122
|
+
await this.bitcoin.execute(async () => {
|
|
123
|
+
//Construct transaction
|
|
124
|
+
const txResult = await this.bitcoin.getSignedMultiTransaction(vaultAddreses.map(val => {
|
|
125
|
+
return {address: val.address, amount: VAULT_DUST_AMOUNT}
|
|
126
|
+
}), feeRate);
|
|
127
|
+
|
|
128
|
+
vaults = await Promise.all(vaultAddreses.map(async (val, index) => {
|
|
129
|
+
const vaultData = await spvVaultContract.createVaultData(signerAddress, val.vaultId, txResult.txId+":"+index, confirmations, [
|
|
130
|
+
{token, multiplier: tokenMultipliers?.[token] ?? 1n},
|
|
131
|
+
{token: nativeToken, multiplier: tokenMultipliers?.[nativeToken] ?? 1n}
|
|
132
|
+
]);
|
|
133
|
+
return new SpvVault(chainId, vaultData, val.address);
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
//Save vaults
|
|
137
|
+
if(this.vaultStorage.saveDataArr!=null) {
|
|
138
|
+
await this.vaultStorage.saveDataArr(vaults.map(val => {
|
|
139
|
+
return {id: val.getIdentifier(), object: val}
|
|
140
|
+
}));
|
|
141
|
+
} else {
|
|
142
|
+
for(let vault of vaults) {
|
|
143
|
+
await this.vaultStorage.saveData(vault.getIdentifier(), vault);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
//Send bitcoin tx
|
|
148
|
+
await this.bitcoin.sendRawTransaction(txResult.raw);
|
|
149
|
+
|
|
150
|
+
txId = txResult.txId;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.logger.info("createVaults(): Funding "+count+" vaults, bitcoin txId: "+txId);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
vaultsCreated: vaults.map(val => val.data.getVaultId()),
|
|
157
|
+
btcTxId: txId
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async listVaults(chainId?: string, token?: string) {
|
|
162
|
+
return Object.keys(this.vaultStorage.data)
|
|
163
|
+
.map(key => this.vaultStorage.data[key])
|
|
164
|
+
.filter(val => chainId==null ? true : val.chainId===chainId)
|
|
165
|
+
.filter(val => this.chains.chains[val.chainId]!=null && val.data.getOwner()===this.chains.chains[val.chainId]?.signer?.getAddress())
|
|
166
|
+
.filter(val => token==null ? true : val.data.getTokenData()[0].token===token);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async fundVault(vault: SpvVault, tokenAmounts: bigint[]): Promise<string> {
|
|
170
|
+
if(vault.state!==SpvVaultState.OPENED) throw new Error("Vault not opened!");
|
|
171
|
+
|
|
172
|
+
this.logger.info("fundVault(): Depositing tokens to the vault "+vault.data.getVaultId().toString(10)+", amounts: "+tokenAmounts.map(val => val.toString(10)).join(", "));
|
|
173
|
+
|
|
174
|
+
const {signer, spvVaultContract} = this.chains.chains[vault.chainId];
|
|
175
|
+
|
|
176
|
+
const txId = await spvVaultContract.deposit(signer, vault.data, tokenAmounts, {waitForConfirmation: true});
|
|
177
|
+
|
|
178
|
+
this.logger.info("fundVault(): Tokens deposited to vault "+vault.data.getVaultId().toString(10)+", amounts: "+tokenAmounts.map(val => val.toString(10)).join(", ")+", txId: "+txId);
|
|
179
|
+
|
|
180
|
+
return txId;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async withdrawFromVault(vault: SpvVault, tokenAmounts: bigint[], feeRate?: number): Promise<string> {
|
|
184
|
+
tokenAmounts.forEach((rawAmount, index) => {
|
|
185
|
+
if(vault.balances[index]==null) throw new Error("Token not found in the vault");
|
|
186
|
+
if(vault.balances[index].rawAmount<rawAmount) throw new Error("Not enough balance in the vault");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if(!vault.isReady()) throw new Error("Vault not ready, wait for the latest swap to get at least 1 confirmation!");
|
|
190
|
+
|
|
191
|
+
const {signer, spvVaultContract} = this.chains.chains[vault.chainId];
|
|
192
|
+
|
|
193
|
+
const latestUtxo = vault.getLatestUtxo();
|
|
194
|
+
const [txId, voutStr] = latestUtxo.split(":");
|
|
195
|
+
|
|
196
|
+
const opReturnData = spvVaultContract.toOpReturnData(signer.getAddress(), tokenAmounts);
|
|
197
|
+
let opReturnScript: Buffer;
|
|
198
|
+
if(opReturnData.length<76) {
|
|
199
|
+
opReturnScript = Buffer.concat([
|
|
200
|
+
Buffer.from([0x6a, opReturnData.length]),
|
|
201
|
+
opReturnData
|
|
202
|
+
]);
|
|
203
|
+
} else {
|
|
204
|
+
opReturnScript = Buffer.concat([
|
|
205
|
+
Buffer.from([0x6a, 0x4c, opReturnData.length]),
|
|
206
|
+
opReturnData
|
|
207
|
+
]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let psbt = new Transaction({
|
|
211
|
+
allowUnknownOutputs: true
|
|
212
|
+
});
|
|
213
|
+
psbt.addInput({
|
|
214
|
+
txid: txId,
|
|
215
|
+
index: parseInt(voutStr),
|
|
216
|
+
witnessUtxo: {
|
|
217
|
+
amount: BigInt(VAULT_DUST_AMOUNT),
|
|
218
|
+
script: this.bitcoin.toOutputScript(vault.btcAddress)
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
psbt.addOutput({
|
|
222
|
+
amount: BigInt(VAULT_DUST_AMOUNT),
|
|
223
|
+
script: this.bitcoin.toOutputScript(vault.btcAddress)
|
|
224
|
+
});
|
|
225
|
+
psbt.addOutput({
|
|
226
|
+
amount: 0n,
|
|
227
|
+
script: opReturnScript
|
|
228
|
+
});
|
|
229
|
+
|
|
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);
|
|
249
|
+
|
|
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
|
+
}
|
|
263
|
+
|
|
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
|
+
}
|
|
308
|
+
}
|
|
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);
|
|
319
|
+
|
|
320
|
+
try {
|
|
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;
|
|
338
|
+
} catch (e) {
|
|
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;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async checkVaults() {
|
|
347
|
+
const vaults = Object.keys(this.vaultStorage.data).map(key => this.vaultStorage.data[key]);
|
|
348
|
+
|
|
349
|
+
const claimWithdrawals: {vault: SpvVault, withdrawals: SpvWithdrawalTransactionData[]}[] = [];
|
|
350
|
+
|
|
351
|
+
let promises: Promise<void>[] = [];
|
|
352
|
+
|
|
353
|
+
for(let vault of vaults) {
|
|
354
|
+
const chainData = this.chains.chains[vault.chainId];
|
|
355
|
+
if(chainData==null) continue;
|
|
356
|
+
const {signer, spvVaultContract, chainInterface} = chainData;
|
|
357
|
+
if(vault.data.getOwner()!==signer.getAddress()) continue;
|
|
358
|
+
|
|
359
|
+
if(vault.state===SpvVaultState.BTC_INITIATED) {
|
|
360
|
+
//Check if btc tx confirmed
|
|
361
|
+
const txId = vault.initialUtxo.split(":")[0];
|
|
362
|
+
const btcTx = await this.bitcoinRpc.getTransaction(txId);
|
|
363
|
+
if(btcTx.confirmations >= VAULT_INIT_CONFIRMATIONS) {
|
|
364
|
+
//Double-check the state here to prevent race condition
|
|
365
|
+
if(vault.state===SpvVaultState.BTC_INITIATED) {
|
|
366
|
+
vault.state = SpvVaultState.BTC_CONFIRMED;
|
|
367
|
+
await this.saveVault(vault);
|
|
368
|
+
}
|
|
369
|
+
this.logger.info("checkVaults(): Vault ID "+vault.data.getVaultId().toString(10)+" confirmed on bitcoin, opening vault on "+vault.chainId);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if(vault.state===SpvVaultState.BTC_CONFIRMED) {
|
|
374
|
+
//Check if open txs were sent already
|
|
375
|
+
if(vault.scOpenTxs!=null) {
|
|
376
|
+
//Check if confirmed
|
|
377
|
+
let _continue = false;
|
|
378
|
+
for(let txId in vault.scOpenTxs) {
|
|
379
|
+
const tx = vault.scOpenTxs[txId];
|
|
380
|
+
const status = await chainInterface.getTxStatus(tx);
|
|
381
|
+
if(status==="pending") {
|
|
382
|
+
_continue = true;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
if(status==="success") {
|
|
386
|
+
vault.state = SpvVaultState.OPENED;
|
|
387
|
+
await this.saveVault(vault);
|
|
388
|
+
_continue = true;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if(_continue) continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const txs = await spvVaultContract.txsOpen(signer.getAddress(), vault.data);
|
|
396
|
+
let numTx = 0;
|
|
397
|
+
promises.push(
|
|
398
|
+
chainInterface.sendAndConfirm(
|
|
399
|
+
signer, txs, true, undefined, true,
|
|
400
|
+
async (txId: string, rawTx: string) => {
|
|
401
|
+
numTx++;
|
|
402
|
+
if(numTx===txs.length) {
|
|
403
|
+
//Final tx
|
|
404
|
+
vault.scOpenTxs = {[txId]: rawTx};
|
|
405
|
+
await this.saveVault(vault);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
).then(txIds => {
|
|
409
|
+
this.logger.info("checkVaults(): Vault ID "+vault.data.getVaultId().toString(10)+" opened on "+vault.chainId+" txId: "+txIds.join(", "));
|
|
410
|
+
|
|
411
|
+
vault.state = SpvVaultState.OPENED;
|
|
412
|
+
return this.saveVault(vault);
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
if(promises.length>=MAX_PARALLEL_VAULTS_OPENING) {
|
|
416
|
+
await Promise.all(promises);
|
|
417
|
+
promises = [];
|
|
418
|
+
}
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if(vault.state===SpvVaultState.OPENED) {
|
|
423
|
+
let changed = await this.checkVaultReplacedTransactions(vault);
|
|
424
|
+
|
|
425
|
+
//Check if some of the pendingWithdrawals got confirmed
|
|
426
|
+
let latestOwnWithdrawalIndex = -1;
|
|
427
|
+
let latestConfirmedWithdrawalIndex = -1;
|
|
428
|
+
for(let i = vault.pendingWithdrawals.length-1; i>=0; i--) {
|
|
429
|
+
const pendingWithdrawal = vault.pendingWithdrawals[i];
|
|
430
|
+
if(pendingWithdrawal.sending) continue;
|
|
431
|
+
|
|
432
|
+
//Check all the pending withdrawals that were not finalized yet
|
|
433
|
+
const btcTx = await checkTransactionReplaced(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
|
|
434
|
+
if(btcTx==null) {
|
|
435
|
+
//Probable double-spend, remove from pending withdrawals
|
|
436
|
+
if(!vault.doubleSpendPendingWithdrawal(pendingWithdrawal)) {
|
|
437
|
+
this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: "+pendingWithdrawal.btcTx.txid+", but doesn't exist anymore!");
|
|
438
|
+
} else {
|
|
439
|
+
this.logger.info("checkVaults(): Successfully removed withdrawal txId: "+pendingWithdrawal.btcTx.txid+", due to being replaced in the mempool!");
|
|
440
|
+
}
|
|
441
|
+
changed = true;
|
|
442
|
+
} else {
|
|
443
|
+
//Update confirmations count
|
|
444
|
+
if(
|
|
445
|
+
pendingWithdrawal.btcTx.confirmations !== btcTx.confirmations ||
|
|
446
|
+
pendingWithdrawal.btcTx.blockhash !== btcTx.blockhash
|
|
447
|
+
) {
|
|
448
|
+
pendingWithdrawal.btcTx.confirmations = btcTx.confirmations;
|
|
449
|
+
pendingWithdrawal.btcTx.blockhash = btcTx.blockhash;
|
|
450
|
+
changed = true;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
//Check it has enough confirmations
|
|
455
|
+
if(pendingWithdrawal.btcTx.confirmations >= vault.data.getConfirmations()) {
|
|
456
|
+
latestConfirmedWithdrawalIndex = i;
|
|
457
|
+
//Check if the pending withdrawals contain a withdrawal to our own address
|
|
458
|
+
if (pendingWithdrawal.isRecipient(signer.getAddress())) {
|
|
459
|
+
latestOwnWithdrawalIndex = i;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if(changed) {
|
|
464
|
+
await this.saveVault(vault);
|
|
465
|
+
}
|
|
466
|
+
if(this.config.maxUnclaimedWithdrawals!=null && latestConfirmedWithdrawalIndex+1 >= this.config.maxUnclaimedWithdrawals) {
|
|
467
|
+
this.logger.info("checkVaults(): Processing withdrawals by self, because a lot of them are unclaimed!");
|
|
468
|
+
claimWithdrawals.push({vault, withdrawals: vault.pendingWithdrawals.slice(0, latestConfirmedWithdrawalIndex+1)});
|
|
469
|
+
} else if(latestOwnWithdrawalIndex!==-1) {
|
|
470
|
+
claimWithdrawals.push({vault, withdrawals: vault.pendingWithdrawals.slice(0, latestOwnWithdrawalIndex+1)});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
for(let {vault, withdrawals} of claimWithdrawals) {
|
|
476
|
+
if(!await this.claimWithdrawals(vault, withdrawals)) {
|
|
477
|
+
this.logger.error("checkVaults(): Cannot process withdrawals "+withdrawals.map(val => val.btcTx.txid).join(", ")+" for vault: "+vault.data.getVaultId());
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async claimWithdrawals(vault: SpvVault, withdrawal: SpvWithdrawalTransactionData[]): Promise<boolean> {
|
|
484
|
+
const {signer, spvVaultContract} = this.chains.chains[vault.chainId];
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const txId = await spvVaultContract.claim(signer, vault.data, withdrawal.map(tx => {
|
|
488
|
+
return {tx};
|
|
489
|
+
}), undefined, true, {waitForConfirmation: true});
|
|
490
|
+
this.logger.info("claimWithdrawal(): Successfully claimed withdrawals, btcTxIds: "+withdrawal.map(val => val.btcTx.txid).join(", ")+" smartChainTxId: "+txId);
|
|
491
|
+
return true;
|
|
492
|
+
} catch (e) {
|
|
493
|
+
this.logger.error("claimWithdrawal(): Tried to claim but got error: ", e);
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async getVault(chainId: string, owner: string, vaultId: bigint) {
|
|
499
|
+
return this.vaultStorage.data[chainId+"_"+owner+"_"+vaultId.toString(10)];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Returns a ready-to-use vault for a specific request
|
|
504
|
+
*
|
|
505
|
+
* @param chainIdentifier
|
|
506
|
+
* @param totalSats
|
|
507
|
+
* @param token
|
|
508
|
+
* @param amount
|
|
509
|
+
* @param gasToken
|
|
510
|
+
* @param gasTokenAmount
|
|
511
|
+
* @protected
|
|
512
|
+
*/
|
|
513
|
+
async findVaultForSwap(chainIdentifier: string, totalSats: bigint, token: string, amount: bigint, gasToken: string, gasTokenAmount: bigint): Promise<SpvVault | null> {
|
|
514
|
+
const {signer} = this.chains.chains[chainIdentifier];
|
|
515
|
+
|
|
516
|
+
const pluginResponse = await PluginManager.onVaultSelection(
|
|
517
|
+
chainIdentifier, totalSats, {token, amount}, {token: gasToken, amount: gasTokenAmount}
|
|
518
|
+
);
|
|
519
|
+
if(pluginResponse!=null) {
|
|
520
|
+
AmountAssertions.handlePluginErrorResponses(pluginResponse);
|
|
521
|
+
return pluginResponse as SpvVault;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const candidates = Object.keys(this.vaultStorage.data)
|
|
525
|
+
.map(key => this.vaultStorage.data[key])
|
|
526
|
+
.filter(vault =>
|
|
527
|
+
vault.chainId===chainIdentifier && vault.data.getOwner()===signer.getAddress() && vault.isReady()
|
|
528
|
+
)
|
|
529
|
+
.filter(vault => {
|
|
530
|
+
const token0 = vault.balances[0];
|
|
531
|
+
if(token0.token!==token || token0.scaledAmount < amount) return false;
|
|
532
|
+
if(gasToken!=null && gasTokenAmount!==0n) {
|
|
533
|
+
const token1 = vault.balances[1];
|
|
534
|
+
if(token1.token!==gasToken || token1.scaledAmount < gasTokenAmount) return false;
|
|
535
|
+
}
|
|
536
|
+
return true;
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
candidates.sort((a, b) => bigIntSorter(a.balances[0].scaledAmount, b.balances[0].scaledAmount));
|
|
540
|
+
|
|
541
|
+
const result = candidates[0];
|
|
542
|
+
|
|
543
|
+
if(result==null) throw {
|
|
544
|
+
code: 20301,
|
|
545
|
+
msg: "No suitable swap vault found, try again later!"
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
return result;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
saveVault(vault: SpvVault) {
|
|
552
|
+
return this.vaultStorage.saveData(vault.getIdentifier(), vault);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async startVaultsWatchdog() {
|
|
556
|
+
let rerun: () => Promise<void>;
|
|
557
|
+
rerun = async () => {
|
|
558
|
+
await this.checkVaults().catch( e => this.logger.error("startVaultsWatchdog(): Error when periodically checking SPV vaults: ", e));
|
|
559
|
+
setTimeout(rerun, this.config.vaultsCheckInterval);
|
|
560
|
+
};
|
|
561
|
+
await rerun();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async init() {
|
|
565
|
+
const vaults = await this.vaultStorage.loadData(SpvVault);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
568
|
}
|