@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.
Files changed (167) hide show
  1. package/LICENSE +201 -201
  2. package/dist/fees/IBtcFeeEstimator.d.ts +3 -3
  3. package/dist/fees/IBtcFeeEstimator.js +2 -2
  4. package/dist/index.d.ts +42 -42
  5. package/dist/index.js +58 -58
  6. package/dist/info/InfoHandler.d.ts +17 -17
  7. package/dist/info/InfoHandler.js +60 -60
  8. package/dist/plugins/IPlugin.d.ts +144 -144
  9. package/dist/plugins/IPlugin.js +34 -34
  10. package/dist/plugins/PluginManager.d.ts +113 -113
  11. package/dist/plugins/PluginManager.js +274 -274
  12. package/dist/prices/BinanceSwapPrice.d.ts +29 -29
  13. package/dist/prices/BinanceSwapPrice.js +79 -79
  14. package/dist/prices/CoinGeckoSwapPrice.d.ts +33 -33
  15. package/dist/prices/CoinGeckoSwapPrice.js +51 -51
  16. package/dist/prices/ISwapPrice.d.ts +43 -43
  17. package/dist/prices/ISwapPrice.js +55 -55
  18. package/dist/prices/OKXSwapPrice.d.ts +29 -29
  19. package/dist/prices/OKXSwapPrice.js +79 -79
  20. package/dist/storage/IIntermediaryStorage.d.ts +18 -18
  21. package/dist/storage/IIntermediaryStorage.js +2 -2
  22. package/dist/storagemanager/IntermediaryStorageManager.d.ts +19 -19
  23. package/dist/storagemanager/IntermediaryStorageManager.js +111 -111
  24. package/dist/storagemanager/StorageManager.d.ts +13 -13
  25. package/dist/storagemanager/StorageManager.js +64 -64
  26. package/dist/swaps/SwapHandler.d.ts +171 -171
  27. package/dist/swaps/SwapHandler.js +217 -217
  28. package/dist/swaps/SwapHandlerSwap.d.ts +79 -79
  29. package/dist/swaps/SwapHandlerSwap.js +78 -78
  30. package/dist/swaps/assertions/AmountAssertions.d.ts +28 -28
  31. package/dist/swaps/assertions/AmountAssertions.js +74 -74
  32. package/dist/swaps/assertions/FromBtcAmountAssertions.d.ts +76 -76
  33. package/dist/swaps/assertions/FromBtcAmountAssertions.js +185 -185
  34. package/dist/swaps/assertions/LightningAssertions.d.ts +44 -44
  35. package/dist/swaps/assertions/LightningAssertions.js +86 -86
  36. package/dist/swaps/assertions/ToBtcAmountAssertions.d.ts +53 -53
  37. package/dist/swaps/assertions/ToBtcAmountAssertions.js +150 -150
  38. package/dist/swaps/escrow/EscrowHandler.d.ts +50 -50
  39. package/dist/swaps/escrow/EscrowHandler.js +151 -151
  40. package/dist/swaps/escrow/EscrowHandlerSwap.d.ts +35 -35
  41. package/dist/swaps/escrow/EscrowHandlerSwap.js +69 -69
  42. package/dist/swaps/escrow/FromBtcBaseSwap.d.ts +14 -14
  43. package/dist/swaps/escrow/FromBtcBaseSwap.js +32 -32
  44. package/dist/swaps/escrow/FromBtcBaseSwapHandler.d.ts +102 -102
  45. package/dist/swaps/escrow/FromBtcBaseSwapHandler.js +210 -210
  46. package/dist/swaps/escrow/ToBtcBaseSwap.d.ts +36 -36
  47. package/dist/swaps/escrow/ToBtcBaseSwap.js +67 -67
  48. package/dist/swaps/escrow/ToBtcBaseSwapHandler.d.ts +53 -53
  49. package/dist/swaps/escrow/ToBtcBaseSwapHandler.js +81 -81
  50. package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.d.ts +84 -84
  51. package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.js +322 -322
  52. package/dist/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.d.ts +21 -21
  53. package/dist/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.js +50 -50
  54. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.d.ts +108 -108
  55. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.js +695 -695
  56. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.d.ts +33 -33
  57. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.js +91 -91
  58. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.d.ts +112 -112
  59. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.js +708 -708
  60. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.d.ts +55 -55
  61. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.js +120 -120
  62. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.d.ts +170 -170
  63. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.js +745 -745
  64. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.d.ts +28 -28
  65. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.js +64 -64
  66. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.d.ts +178 -178
  67. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.js +900 -899
  68. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.d.ts +24 -24
  69. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.js +58 -58
  70. package/dist/swaps/spv_vault_swap/SpvVault.d.ts +44 -44
  71. package/dist/swaps/spv_vault_swap/SpvVault.js +145 -145
  72. package/dist/swaps/spv_vault_swap/SpvVaultSwap.d.ts +68 -68
  73. package/dist/swaps/spv_vault_swap/SpvVaultSwap.js +158 -158
  74. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.d.ts +68 -68
  75. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +561 -561
  76. package/dist/swaps/spv_vault_swap/SpvVaults.d.ts +63 -63
  77. package/dist/swaps/spv_vault_swap/SpvVaults.js +491 -491
  78. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.d.ts +52 -52
  79. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.js +662 -662
  80. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.d.ts +52 -52
  81. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.js +118 -118
  82. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.d.ts +77 -77
  83. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.js +504 -504
  84. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.d.ts +34 -34
  85. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.js +81 -81
  86. package/dist/utils/BitcoinUtils.d.ts +4 -4
  87. package/dist/utils/BitcoinUtils.js +61 -61
  88. package/dist/utils/Utils.d.ts +32 -32
  89. package/dist/utils/Utils.js +129 -129
  90. package/dist/utils/paramcoders/IParamReader.d.ts +5 -5
  91. package/dist/utils/paramcoders/IParamReader.js +2 -2
  92. package/dist/utils/paramcoders/IParamWriter.d.ts +4 -4
  93. package/dist/utils/paramcoders/IParamWriter.js +2 -2
  94. package/dist/utils/paramcoders/LegacyParamEncoder.d.ts +10 -10
  95. package/dist/utils/paramcoders/LegacyParamEncoder.js +22 -22
  96. package/dist/utils/paramcoders/ParamDecoder.d.ts +25 -25
  97. package/dist/utils/paramcoders/ParamDecoder.js +222 -222
  98. package/dist/utils/paramcoders/ParamEncoder.d.ts +9 -9
  99. package/dist/utils/paramcoders/ParamEncoder.js +22 -22
  100. package/dist/utils/paramcoders/SchemaVerifier.d.ts +21 -21
  101. package/dist/utils/paramcoders/SchemaVerifier.js +84 -84
  102. package/dist/utils/paramcoders/server/ServerParamDecoder.d.ts +8 -8
  103. package/dist/utils/paramcoders/server/ServerParamDecoder.js +107 -107
  104. package/dist/utils/paramcoders/server/ServerParamEncoder.d.ts +11 -11
  105. package/dist/utils/paramcoders/server/ServerParamEncoder.js +65 -65
  106. package/dist/wallets/IBitcoinWallet.d.ts +149 -149
  107. package/dist/wallets/IBitcoinWallet.js +97 -97
  108. package/dist/wallets/ILightningWallet.d.ts +136 -136
  109. package/dist/wallets/ILightningWallet.js +37 -37
  110. package/dist/wallets/ISpvVaultSigner.d.ts +7 -7
  111. package/dist/wallets/ISpvVaultSigner.js +2 -2
  112. package/package.json +36 -36
  113. package/src/fees/IBtcFeeEstimator.ts +6 -6
  114. package/src/index.ts +53 -53
  115. package/src/info/InfoHandler.ts +103 -103
  116. package/src/plugins/IPlugin.ts +174 -174
  117. package/src/plugins/PluginManager.ts +354 -354
  118. package/src/prices/BinanceSwapPrice.ts +101 -101
  119. package/src/prices/CoinGeckoSwapPrice.ts +75 -75
  120. package/src/prices/ISwapPrice.ts +88 -88
  121. package/src/prices/OKXSwapPrice.ts +101 -101
  122. package/src/storage/IIntermediaryStorage.ts +19 -19
  123. package/src/storagemanager/IntermediaryStorageManager.ts +118 -118
  124. package/src/storagemanager/StorageManager.ts +78 -78
  125. package/src/swaps/SwapHandler.ts +323 -323
  126. package/src/swaps/SwapHandlerSwap.ts +141 -141
  127. package/src/swaps/assertions/AmountAssertions.ts +77 -77
  128. package/src/swaps/assertions/FromBtcAmountAssertions.ts +251 -251
  129. package/src/swaps/assertions/LightningAssertions.ts +103 -103
  130. package/src/swaps/assertions/ToBtcAmountAssertions.ts +203 -203
  131. package/src/swaps/escrow/EscrowHandler.ts +172 -172
  132. package/src/swaps/escrow/EscrowHandlerSwap.ts +86 -86
  133. package/src/swaps/escrow/FromBtcBaseSwap.ts +38 -38
  134. package/src/swaps/escrow/FromBtcBaseSwapHandler.ts +286 -286
  135. package/src/swaps/escrow/ToBtcBaseSwap.ts +85 -85
  136. package/src/swaps/escrow/ToBtcBaseSwapHandler.ts +129 -129
  137. package/src/swaps/escrow/frombtc_abstract/FromBtcAbs.ts +457 -457
  138. package/src/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.ts +61 -61
  139. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.ts +873 -873
  140. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.ts +141 -141
  141. package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.ts +866 -866
  142. package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.ts +196 -196
  143. package/src/swaps/escrow/tobtc_abstract/ToBtcAbs.ts +920 -920
  144. package/src/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.ts +108 -108
  145. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.ts +1150 -1149
  146. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.ts +80 -80
  147. package/src/swaps/spv_vault_swap/SpvVault.ts +178 -178
  148. package/src/swaps/spv_vault_swap/SpvVaultSwap.ts +228 -228
  149. package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +718 -718
  150. package/src/swaps/spv_vault_swap/SpvVaults.ts +567 -567
  151. package/src/swaps/trusted/frombtc_trusted/FromBtcTrusted.ts +762 -762
  152. package/src/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.ts +185 -185
  153. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.ts +603 -603
  154. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.ts +121 -121
  155. package/src/utils/BitcoinUtils.ts +59 -59
  156. package/src/utils/Utils.ts +150 -150
  157. package/src/utils/paramcoders/IParamReader.ts +7 -7
  158. package/src/utils/paramcoders/IParamWriter.ts +8 -8
  159. package/src/utils/paramcoders/LegacyParamEncoder.ts +27 -27
  160. package/src/utils/paramcoders/ParamDecoder.ts +218 -218
  161. package/src/utils/paramcoders/ParamEncoder.ts +29 -29
  162. package/src/utils/paramcoders/SchemaVerifier.ts +96 -96
  163. package/src/utils/paramcoders/server/ServerParamDecoder.ts +118 -118
  164. package/src/utils/paramcoders/server/ServerParamEncoder.ts +75 -75
  165. package/src/wallets/IBitcoinWallet.ts +237 -237
  166. package/src/wallets/ILightningWallet.ts +200 -200
  167. 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
  }