@atomiqlabs/lp-lib 15.0.12 → 15.0.14

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 (163) 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 +40 -40
  5. package/dist/index.js +56 -56
  6. package/dist/info/InfoHandler.d.ts +17 -17
  7. package/dist/info/InfoHandler.js +58 -61
  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 +26 -26
  13. package/dist/prices/BinanceSwapPrice.js +92 -92
  14. package/dist/prices/CoinGeckoSwapPrice.d.ts +30 -30
  15. package/dist/prices/CoinGeckoSwapPrice.js +64 -64
  16. package/dist/prices/ISwapPrice.d.ts +43 -43
  17. package/dist/prices/ISwapPrice.js +55 -55
  18. package/dist/prices/OKXSwapPrice.d.ts +26 -26
  19. package/dist/prices/OKXSwapPrice.js +92 -92
  20. package/dist/storage/IIntermediaryStorage.d.ts +18 -18
  21. package/dist/storage/IIntermediaryStorage.js +2 -2
  22. package/dist/storagemanager/IntermediaryStorageManager.d.ts +18 -18
  23. package/dist/storagemanager/IntermediaryStorageManager.js +104 -104
  24. package/dist/storagemanager/StorageManager.d.ts +12 -12
  25. package/dist/storagemanager/StorageManager.js +57 -57
  26. package/dist/swaps/SwapHandler.d.ts +153 -156
  27. package/dist/swaps/SwapHandler.js +157 -163
  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 -72
  32. package/dist/swaps/assertions/FromBtcAmountAssertions.d.ts +76 -76
  33. package/dist/swaps/assertions/FromBtcAmountAssertions.js +172 -172
  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 +51 -51
  39. package/dist/swaps/escrow/EscrowHandler.js +158 -158
  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 +101 -101
  45. package/dist/swaps/escrow/FromBtcBaseSwapHandler.js +207 -207
  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 +83 -83
  51. package/dist/swaps/escrow/frombtc_abstract/FromBtcAbs.js +318 -318
  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 +107 -107
  55. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.js +673 -675
  56. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.d.ts +32 -32
  57. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.js +88 -88
  58. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.d.ts +171 -171
  59. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.js +718 -718
  60. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.d.ts +28 -28
  61. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.js +64 -64
  62. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.d.ts +177 -177
  63. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.js +863 -863
  64. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.d.ts +24 -24
  65. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.js +58 -58
  66. package/dist/swaps/spv_vault_swap/SpvVault.d.ts +45 -45
  67. package/dist/swaps/spv_vault_swap/SpvVault.js +145 -145
  68. package/dist/swaps/spv_vault_swap/SpvVaultSwap.d.ts +68 -68
  69. package/dist/swaps/spv_vault_swap/SpvVaultSwap.js +158 -158
  70. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.d.ts +68 -68
  71. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +528 -528
  72. package/dist/swaps/spv_vault_swap/SpvVaults.d.ts +68 -68
  73. package/dist/swaps/spv_vault_swap/SpvVaults.js +454 -454
  74. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.d.ts +51 -51
  75. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.js +650 -650
  76. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.d.ts +52 -52
  77. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.js +118 -118
  78. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.d.ts +76 -76
  79. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.js +493 -495
  80. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.d.ts +34 -34
  81. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.js +81 -81
  82. package/dist/utils/BitcoinUtils.d.ts +4 -4
  83. package/dist/utils/BitcoinUtils.js +61 -61
  84. package/dist/utils/Utils.d.ts +29 -29
  85. package/dist/utils/Utils.js +88 -88
  86. package/dist/utils/paramcoders/IParamReader.d.ts +5 -5
  87. package/dist/utils/paramcoders/IParamReader.js +2 -2
  88. package/dist/utils/paramcoders/IParamWriter.d.ts +4 -4
  89. package/dist/utils/paramcoders/IParamWriter.js +2 -2
  90. package/dist/utils/paramcoders/LegacyParamEncoder.d.ts +10 -10
  91. package/dist/utils/paramcoders/LegacyParamEncoder.js +22 -22
  92. package/dist/utils/paramcoders/ParamDecoder.d.ts +25 -25
  93. package/dist/utils/paramcoders/ParamDecoder.js +222 -222
  94. package/dist/utils/paramcoders/ParamEncoder.d.ts +9 -9
  95. package/dist/utils/paramcoders/ParamEncoder.js +22 -22
  96. package/dist/utils/paramcoders/SchemaVerifier.d.ts +21 -21
  97. package/dist/utils/paramcoders/SchemaVerifier.js +84 -84
  98. package/dist/utils/paramcoders/server/ServerParamDecoder.d.ts +8 -8
  99. package/dist/utils/paramcoders/server/ServerParamDecoder.js +105 -105
  100. package/dist/utils/paramcoders/server/ServerParamEncoder.d.ts +11 -11
  101. package/dist/utils/paramcoders/server/ServerParamEncoder.js +65 -65
  102. package/dist/wallets/IBitcoinWallet.d.ts +67 -67
  103. package/dist/wallets/IBitcoinWallet.js +2 -2
  104. package/dist/wallets/ILightningWallet.d.ts +117 -117
  105. package/dist/wallets/ILightningWallet.js +37 -37
  106. package/dist/wallets/ISpvVaultSigner.d.ts +7 -7
  107. package/dist/wallets/ISpvVaultSigner.js +2 -2
  108. package/dist/wallets/ISpvVaultWallet.d.ts +42 -42
  109. package/dist/wallets/ISpvVaultWallet.js +2 -2
  110. package/package.json +36 -36
  111. package/src/fees/IBtcFeeEstimator.ts +6 -6
  112. package/src/index.ts +51 -51
  113. package/src/info/InfoHandler.ts +100 -106
  114. package/src/plugins/IPlugin.ts +174 -174
  115. package/src/plugins/PluginManager.ts +354 -354
  116. package/src/prices/BinanceSwapPrice.ts +113 -113
  117. package/src/prices/CoinGeckoSwapPrice.ts +87 -87
  118. package/src/prices/ISwapPrice.ts +88 -88
  119. package/src/prices/OKXSwapPrice.ts +113 -113
  120. package/src/storage/IIntermediaryStorage.ts +19 -19
  121. package/src/storagemanager/IntermediaryStorageManager.ts +109 -109
  122. package/src/storagemanager/StorageManager.ts +68 -68
  123. package/src/swaps/SwapHandler.ts +272 -280
  124. package/src/swaps/SwapHandlerSwap.ts +141 -141
  125. package/src/swaps/assertions/AmountAssertions.ts +77 -76
  126. package/src/swaps/assertions/FromBtcAmountAssertions.ts +238 -238
  127. package/src/swaps/assertions/LightningAssertions.ts +103 -103
  128. package/src/swaps/assertions/ToBtcAmountAssertions.ts +203 -203
  129. package/src/swaps/escrow/EscrowHandler.ts +179 -179
  130. package/src/swaps/escrow/EscrowHandlerSwap.ts +86 -86
  131. package/src/swaps/escrow/FromBtcBaseSwap.ts +38 -38
  132. package/src/swaps/escrow/FromBtcBaseSwapHandler.ts +283 -283
  133. package/src/swaps/escrow/ToBtcBaseSwap.ts +85 -85
  134. package/src/swaps/escrow/ToBtcBaseSwapHandler.ts +129 -129
  135. package/src/swaps/escrow/frombtc_abstract/FromBtcAbs.ts +452 -452
  136. package/src/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.ts +61 -61
  137. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.ts +855 -856
  138. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.ts +137 -137
  139. package/src/swaps/escrow/tobtc_abstract/ToBtcAbs.ts +890 -890
  140. package/src/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.ts +108 -108
  141. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.ts +1112 -1112
  142. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.ts +80 -80
  143. package/src/swaps/spv_vault_swap/SpvVault.ts +178 -178
  144. package/src/swaps/spv_vault_swap/SpvVaultSwap.ts +228 -228
  145. package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +671 -671
  146. package/src/swaps/spv_vault_swap/SpvVaults.ts +526 -526
  147. package/src/swaps/trusted/frombtc_trusted/FromBtcTrusted.ts +747 -747
  148. package/src/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.ts +185 -185
  149. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.ts +591 -592
  150. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.ts +121 -121
  151. package/src/utils/BitcoinUtils.ts +59 -59
  152. package/src/utils/Utils.ts +102 -102
  153. package/src/utils/paramcoders/IParamReader.ts +7 -7
  154. package/src/utils/paramcoders/IParamWriter.ts +8 -8
  155. package/src/utils/paramcoders/LegacyParamEncoder.ts +27 -27
  156. package/src/utils/paramcoders/ParamDecoder.ts +218 -218
  157. package/src/utils/paramcoders/ParamEncoder.ts +29 -29
  158. package/src/utils/paramcoders/SchemaVerifier.ts +96 -96
  159. package/src/utils/paramcoders/server/ServerParamDecoder.ts +115 -115
  160. package/src/utils/paramcoders/server/ServerParamEncoder.ts +75 -75
  161. package/src/wallets/IBitcoinWallet.ts +68 -68
  162. package/src/wallets/ILightningWallet.ts +178 -178
  163. package/src/wallets/ISpvVaultSigner.ts +10 -10
@@ -1,527 +1,527 @@
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} 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 {ChainData} 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
-
23
- export class SpvVaults {
24
-
25
- readonly vaultStorage: IStorageManager<SpvVault>;
26
-
27
- readonly bitcoin: IBitcoinWallet;
28
- readonly vaultSigner: ISpvVaultSigner;
29
- readonly bitcoinRpc: BitcoinRpc<any>;
30
- readonly config: {vaultsCheckInterval: number, maxUnclaimedWithdrawals?: number};
31
- readonly getChain: (chainId: string) => ChainData
32
-
33
- readonly logger = {
34
- debug: (msg: string, ...args: any) => console.debug("SpvVaults: "+msg, ...args),
35
- info: (msg: string, ...args: any) => console.info("SpvVaults: "+msg, ...args),
36
- warn: (msg: string, ...args: any) => console.warn("SpvVaults: "+msg, ...args),
37
- error: (msg: string, ...args: any) => console.error("SpvVaults: "+msg, ...args)
38
- };
39
-
40
- constructor(
41
- vaultStorage: IStorageManager<SpvVault>,
42
- bitcoin: IBitcoinWallet,
43
- vaultSigner: ISpvVaultSigner,
44
- bitcoinRpc: BitcoinRpc<any>,
45
- getChain: (chainId: string) => ChainData,
46
- config: {vaultsCheckInterval: number, maxUnclaimedWithdrawals?: number}
47
- ) {
48
- this.vaultStorage = vaultStorage;
49
- this.bitcoin = bitcoin;
50
- this.vaultSigner = vaultSigner;
51
- this.bitcoinRpc = bitcoinRpc;
52
- this.getChain = getChain;
53
- this.config = config;
54
- }
55
-
56
- async processDepositEvent(vault: SpvVault, event: SpvVaultDepositEvent): Promise<void> {
57
- vault.update(event);
58
- await this.saveVault(vault);
59
- }
60
-
61
- async processOpenEvent(vault: SpvVault, event: SpvVaultOpenEvent): Promise<void> {
62
- if(vault.state===SpvVaultState.BTC_CONFIRMED) {
63
- vault.state = SpvVaultState.OPENED;
64
- }
65
- vault.update(event);
66
- await this.saveVault(vault);
67
- }
68
-
69
- async processCloseEvent(vault: SpvVault, event: SpvVaultCloseEvent): Promise<void> {
70
- if(vault.state===SpvVaultState.OPENED) {
71
- vault.state = SpvVaultState.CLOSED;
72
- }
73
- vault.update(event);
74
- await this.saveVault(vault);
75
- }
76
-
77
- async processClaimEvent(vault: SpvVault, swap: SpvVaultSwap | null, event: SpvVaultClaimEvent): Promise<void> {
78
- //Update vault
79
- const foundPendingWithdrawal = vault.pendingWithdrawals.findIndex(val => val.btcTx.txid===event.btcTxId);
80
- if(foundPendingWithdrawal!==-1) vault.pendingWithdrawals.splice(foundPendingWithdrawal, 1);
81
- vault.update(event);
82
- await this.saveVault(vault);
83
- }
84
-
85
- async createVaults(chainId: string, count: number, token: string, confirmations: number = 2, feeRate?: number): Promise<{vaultsCreated: bigint[], btcTxId: string}> {
86
- const {signer, chainInterface, tokenMultipliers, spvVaultContract} = this.getChain(chainId);
87
-
88
- const signerAddress = signer.getAddress();
89
-
90
- //Check vaultId of the latest saved vault
91
- let latestVaultId: bigint = -1n;
92
- for(let key in this.vaultStorage.data) {
93
- const vault = this.vaultStorage.data[key];
94
- if(vault.chainId!==chainId) continue;
95
- if(vault.data.getOwner()!==signerAddress) continue;
96
- if(vault.data.getVaultId() > latestVaultId) latestVaultId = vault.data.getVaultId();
97
- }
98
-
99
- latestVaultId++;
100
-
101
- const vaultAddreses: {vaultId: bigint, address: string}[] = [];
102
- for(let i=0;i<count;i++) {
103
- const vaultId = latestVaultId + BigInt(i);
104
- const address = await this.vaultSigner.getAddress(chainId, vaultId);
105
- vaultAddreses.push({vaultId, address});
106
- }
107
-
108
- //Construct transaction
109
- const txResult = await this.bitcoin.getSignedMultiTransaction(vaultAddreses.map(val => {
110
- return {address: val.address, amount: VAULT_DUST_AMOUNT}
111
- }), feeRate);
112
-
113
- const nativeToken = chainInterface.getNativeCurrencyAddress();
114
-
115
- const vaults = await Promise.all(vaultAddreses.map(async (val, index) => {
116
- const vaultData = await spvVaultContract.createVaultData(signerAddress, val.vaultId, txResult.txId+":"+index, confirmations, [
117
- {token, multiplier: tokenMultipliers?.[token] ?? 1n},
118
- {token: nativeToken, multiplier: tokenMultipliers?.[nativeToken] ?? 1n}
119
- ]);
120
- return new SpvVault(chainId, vaultData, val.address);
121
- }));
122
-
123
- //Save vaults
124
- if(this.vaultStorage.saveDataArr!=null) {
125
- await this.vaultStorage.saveDataArr(vaults.map(val => {
126
- return {id: val.getIdentifier(), object: val}
127
- }));
128
- } else {
129
- for(let vault of vaults) {
130
- await this.vaultStorage.saveData(vault.getIdentifier(), vault);
131
- }
132
- }
133
-
134
- //Send bitcoin tx
135
- await this.bitcoin.sendRawTransaction(txResult.raw);
136
-
137
- this.logger.info("createVaults(): Funding "+count+" vaults, bitcoin txId: "+txResult.txId);
138
-
139
- return {
140
- vaultsCreated: vaults.map(val => val.data.getVaultId()),
141
- btcTxId: txResult.txId
142
- };
143
- }
144
-
145
- async listVaults(chainId?: string, token?: string) {
146
- return Object.keys(this.vaultStorage.data)
147
- .map(key => this.vaultStorage.data[key])
148
- .filter(val => chainId==null ? true : val.chainId===chainId)
149
- .filter(val => val.data.getOwner()===this.getChain(val.chainId)?.signer?.getAddress())
150
- .filter(val => token==null ? true : val.data.getTokenData()[0].token===token);
151
- }
152
-
153
- async fundVault(vault: SpvVault, tokenAmounts: bigint[]): Promise<string> {
154
- if(vault.state!==SpvVaultState.OPENED) throw new Error("Vault not opened!");
155
-
156
- this.logger.info("fundVault(): Depositing tokens to the vault "+vault.data.getVaultId().toString(10)+", amounts: "+tokenAmounts.map(val => val.toString(10)).join(", "));
157
-
158
- const {signer, spvVaultContract} = this.getChain(vault.chainId);
159
-
160
- const txId = await spvVaultContract.deposit(signer, vault.data, tokenAmounts, {waitForConfirmation: true});
161
-
162
- this.logger.info("fundVault(): Tokens deposited to vault "+vault.data.getVaultId().toString(10)+", amounts: "+tokenAmounts.map(val => val.toString(10)).join(", ")+", txId: "+txId);
163
-
164
- return txId;
165
- }
166
-
167
- async withdrawFromVault(vault: SpvVault, tokenAmounts: bigint[], feeRate?: number): Promise<string> {
168
- tokenAmounts.forEach((rawAmount, index) => {
169
- if(vault.balances[index]==null) throw new Error("Token not found in the vault");
170
- if(vault.balances[index].rawAmount<rawAmount) throw new Error("Not enough balance in the vault");
171
- });
172
-
173
- if(!vault.isReady()) throw new Error("Vault not ready, wait for the latest swap to get at least 1 confirmation!");
174
-
175
- const {signer, spvVaultContract} = this.getChain(vault.chainId);
176
-
177
- const latestUtxo = vault.getLatestUtxo();
178
- const [txId, voutStr] = latestUtxo.split(":");
179
-
180
- const opReturnData = spvVaultContract.toOpReturnData(signer.getAddress(), tokenAmounts);
181
- let opReturnScript: Buffer;
182
- if(opReturnData.length<76) {
183
- opReturnScript = Buffer.concat([
184
- Buffer.from([0x6a, opReturnData.length]),
185
- opReturnData
186
- ]);
187
- } else {
188
- opReturnScript = Buffer.concat([
189
- Buffer.from([0x6a, 0x4c, opReturnData.length]),
190
- opReturnData
191
- ]);
192
- }
193
-
194
- let psbt = new Transaction({
195
- allowUnknownOutputs: true
196
- });
197
- psbt.addInput({
198
- txid: txId,
199
- index: parseInt(voutStr),
200
- witnessUtxo: {
201
- amount: BigInt(VAULT_DUST_AMOUNT),
202
- script: this.bitcoin.toOutputScript(vault.btcAddress)
203
- }
204
- });
205
- psbt.addOutput({
206
- amount: BigInt(VAULT_DUST_AMOUNT),
207
- script: this.bitcoin.toOutputScript(vault.btcAddress)
208
- });
209
- psbt.addOutput({
210
- amount: 0n,
211
- script: opReturnScript
212
- });
213
-
214
- psbt = await this.bitcoin.fundPsbt(psbt, feeRate);
215
- if(psbt.inputsLength<2) throw new Error("PSBT needs at least 2 inputs!");
216
- psbt.updateInput(0, {sequence: 0x80000000});
217
- psbt.updateInput(1, {sequence: 0x80000000});
218
- psbt = await this.vaultSigner.signPsbt(vault.chainId, vault.data.getVaultId(), psbt, [0]);
219
- const res = await this.bitcoin.signPsbt(psbt);
220
-
221
- const parsedTransaction = await this.bitcoinRpc.parseTransaction(res.raw);
222
- const withdrawalData = await spvVaultContract.getWithdrawalData(parsedTransaction);
223
-
224
- if(withdrawalData.getSpentVaultUtxo()!==vault.getLatestUtxo()) {
225
- throw new Error("Latest vault UTXO already spent! Please try again later.");
226
- }
227
- (withdrawalData as any).sending = true;
228
- vault.addWithdrawal(withdrawalData);
229
- await this.saveVault(vault);
230
-
231
- try {
232
- await this.bitcoin.sendRawTransaction(res.raw);
233
- (withdrawalData as any).sending = false;
234
- } catch (e) {
235
- (withdrawalData as any).sending = false;
236
- vault.removeWithdrawal(withdrawalData);
237
- await this.saveVault(vault);
238
- throw e;
239
- }
240
-
241
- return res.txId;
242
- }
243
-
244
- /**
245
- * Call this to check whether some of the previously replaced transactions got re-introduced to the mempool
246
- *
247
- * @param vault
248
- * @param save
249
- */
250
- async checkVaultReplacedTransactions(vault: SpvVault, save?: boolean): Promise<boolean> {
251
- const {spvVaultContract} = this.getChain(vault.chainId);
252
-
253
- const initialVaultWithdrawalCount = vault.data.getWithdrawalCount();
254
-
255
- let latestWithdrawalIndex = initialVaultWithdrawalCount;
256
- const newPendingTxns: SpvWithdrawalTransactionData[] = [];
257
- const reintroducedTxIds: Set<string> = new Set();
258
- for(let [withdrawalIndex, replacedWithdrawalGroup] of vault.replacedWithdrawals) {
259
- if(withdrawalIndex<=latestWithdrawalIndex) continue; //Don't check txns that should already be included
260
-
261
- for(let replacedWithdrawal of replacedWithdrawalGroup) {
262
- if(reintroducedTxIds.has(replacedWithdrawal.getTxId())) continue;
263
- const tx = await this.bitcoinRpc.getTransaction(replacedWithdrawal.getTxId());
264
- if(tx==null) continue;
265
-
266
- //Re-introduce transaction to the pending withdrawals list
267
- if(withdrawalIndex>latestWithdrawalIndex) {
268
- const txChain: SpvWithdrawalTransactionData[] = [replacedWithdrawal];
269
- withdrawalIndex--;
270
- while(withdrawalIndex>latestWithdrawalIndex) {
271
- const tx = await this.bitcoinRpc.getTransaction(txChain[0].getSpentVaultUtxo().split(":")[0]);
272
- if(tx==null) break;
273
- reintroducedTxIds.add(tx.txid);
274
- txChain.unshift(await spvVaultContract.getWithdrawalData(tx));
275
- withdrawalIndex--;
276
- }
277
- if(withdrawalIndex>latestWithdrawalIndex) {
278
- this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Tried to re-introduce previously replaced TX, but one of txns in the chain not found!`);
279
- continue;
280
- }
281
- newPendingTxns.push(...txChain);
282
- latestWithdrawalIndex += txChain.length;
283
- break; //Don't check other txns at the same withdrawal index
284
- } else {
285
- this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Tried to re-introduce previously replaced TX, but vault has already processed such withdrawal!`);
286
- }
287
- }
288
- }
289
-
290
- if(newPendingTxns.length===0) return false;
291
-
292
- if(initialVaultWithdrawalCount!==vault.data.getWithdrawalCount()) {
293
- this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Not saving vault after checking replaced transactions, due to withdrawal count changed!`);
294
- return false;
295
- }
296
-
297
- const backup = vault.pendingWithdrawals.splice(0, newPendingTxns.length);
298
- const txsToAddOnTop = vault.pendingWithdrawals.splice(0, vault.pendingWithdrawals.length);
299
-
300
- try {
301
- newPendingTxns.forEach(val => vault.addWithdrawal(val));
302
- txsToAddOnTop.forEach(val => vault.addWithdrawal(val));
303
- for(let i=0;i<newPendingTxns.length;i++) {
304
- const withdrawalIndex = initialVaultWithdrawalCount+i+1;
305
- const arr = vault.replacedWithdrawals.get(withdrawalIndex);
306
- if(arr==null) continue;
307
- const index = arr.indexOf(newPendingTxns[i]);
308
- if(index===-1) {
309
- this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Cannot remove re-introduced tx ${newPendingTxns[i].getTxId()}, not found in the respective array!`);
310
- continue;
311
- }
312
- arr.splice(index, 1);
313
- if(arr.length===0) vault.replacedWithdrawals.delete(withdrawalIndex);
314
- }
315
- this.logger.info(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Re-introduced back ${newPendingTxns.length} txns that were re-added to the mempool!`);
316
- if(save) await this.saveVault(vault);
317
- return true;
318
- } catch (e) {
319
- this.logger.error(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Failed to update the vault with new pending txns (rolling back): `, e);
320
- //Rollback the pending withdrawals
321
- vault.pendingWithdrawals.push(...backup, ...txsToAddOnTop);
322
- return false;
323
- }
324
- }
325
-
326
- async checkVaults() {
327
- const vaults = Object.keys(this.vaultStorage.data).map(key => this.vaultStorage.data[key]);
328
-
329
- const claimWithdrawals: {vault: SpvVault, withdrawals: SpvWithdrawalTransactionData[]}[] = [];
330
-
331
- for(let vault of vaults) {
332
- const {signer, spvVaultContract, chainInterface} = this.getChain(vault.chainId);
333
- if(vault.data.getOwner()!==signer.getAddress()) continue;
334
-
335
- if(vault.state===SpvVaultState.BTC_INITIATED) {
336
- //Check if btc tx confirmed
337
- const txId = vault.initialUtxo.split(":")[0];
338
- const btcTx = await this.bitcoinRpc.getTransaction(txId);
339
- if(btcTx.confirmations >= VAULT_INIT_CONFIRMATIONS) {
340
- //Double-check the state here to prevent race condition
341
- if(vault.state===SpvVaultState.BTC_INITIATED) {
342
- vault.state = SpvVaultState.BTC_CONFIRMED;
343
- await this.saveVault(vault);
344
- }
345
- this.logger.info("checkVaults(): Vault ID "+vault.data.getVaultId().toString(10)+" confirmed on bitcoin, opening vault on "+vault.chainId);
346
- }
347
- }
348
-
349
- if(vault.state===SpvVaultState.BTC_CONFIRMED) {
350
- //Check if open txs were sent already
351
- if(vault.scOpenTx!=null) {
352
- //Check if confirmed
353
- const status = await chainInterface.getTxStatus(vault.scOpenTx.rawTx);
354
- if(status==="pending") return;
355
- if(status==="success") {
356
- vault.state = SpvVaultState.OPENED;
357
- await this.saveVault(vault);
358
- return;
359
- }
360
- }
361
-
362
- const txs = await spvVaultContract.txsOpen(signer.getAddress(), vault.data);
363
- let numTx = 0;
364
- const txIds = await chainInterface.sendAndConfirm(
365
- signer, txs, true, undefined, false,
366
- async (txId: string, rawTx: string) => {
367
- numTx++;
368
- if(numTx===txs.length) {
369
- //Final tx
370
- vault.scOpenTx = {txId, rawTx};
371
- await this.saveVault(vault);
372
- }
373
- }
374
- );
375
- this.logger.info("checkVaults(): Vault ID "+vault.data.getVaultId().toString(10)+" opened on "+vault.chainId+" txId: "+txIds.join(", "));
376
-
377
- vault.state = SpvVaultState.OPENED;
378
- await this.saveVault(vault);
379
- }
380
-
381
- if(vault.state===SpvVaultState.OPENED) {
382
- let changed = await this.checkVaultReplacedTransactions(vault);
383
-
384
- //Check if some of the pendingWithdrawals got confirmed
385
- let latestOwnWithdrawalIndex = -1;
386
- let latestConfirmedWithdrawalIndex = -1;
387
- for(let i = vault.pendingWithdrawals.length-1; i>=0; i--) {
388
- const pendingWithdrawal = vault.pendingWithdrawals[i];
389
- if(pendingWithdrawal.sending) continue;
390
-
391
- //Check all the pending withdrawals that were not finalized yet
392
- const btcTx = await checkTransactionReplaced(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
393
- if(btcTx==null) {
394
- //Probable double-spend, remove from pending withdrawals
395
- if(!vault.doubleSpendPendingWithdrawal(pendingWithdrawal)) {
396
- this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: "+pendingWithdrawal.btcTx.txid+", but doesn't exist anymore!");
397
- } else {
398
- this.logger.info("checkVaults(): Successfully removed withdrawal txId: "+pendingWithdrawal.btcTx.txid+", due to being replaced in the mempool!");
399
- }
400
- changed = true;
401
- } else {
402
- //Update confirmations count
403
- if(
404
- pendingWithdrawal.btcTx.confirmations !== btcTx.confirmations ||
405
- pendingWithdrawal.btcTx.blockhash !== btcTx.blockhash
406
- ) {
407
- pendingWithdrawal.btcTx.confirmations = btcTx.confirmations;
408
- pendingWithdrawal.btcTx.blockhash = btcTx.blockhash;
409
- changed = true;
410
- }
411
- }
412
-
413
- //Check it has enough confirmations
414
- if(pendingWithdrawal.btcTx.confirmations >= vault.data.getConfirmations()) {
415
- latestConfirmedWithdrawalIndex = i;
416
- //Check if the pending withdrawals contain a withdrawal to our own address
417
- if (pendingWithdrawal.isRecipient(signer.getAddress())) {
418
- latestOwnWithdrawalIndex = i;
419
- }
420
- }
421
- }
422
- if(changed) {
423
- await this.saveVault(vault);
424
- }
425
- if(this.config.maxUnclaimedWithdrawals!=null && latestConfirmedWithdrawalIndex+1 >= this.config.maxUnclaimedWithdrawals) {
426
- this.logger.info("checkVaults(): Processing withdrawals by self, because a lot of them are unclaimed!");
427
- claimWithdrawals.push({vault, withdrawals: vault.pendingWithdrawals.slice(0, latestConfirmedWithdrawalIndex+1)});
428
- } else if(latestOwnWithdrawalIndex!==-1) {
429
- claimWithdrawals.push({vault, withdrawals: vault.pendingWithdrawals.slice(0, latestOwnWithdrawalIndex+1)});
430
- }
431
- }
432
- }
433
-
434
- for(let {vault, withdrawals} of claimWithdrawals) {
435
- if(!await this.claimWithdrawals(vault, withdrawals)) {
436
- this.logger.error("checkVaults(): Cannot process withdrawals "+withdrawals.map(val => val.btcTx.txid).join(", ")+" for vault: "+vault.data.getVaultId());
437
- break;
438
- }
439
- }
440
- }
441
-
442
- async claimWithdrawals(vault: SpvVault, withdrawal: SpvWithdrawalTransactionData[]): Promise<boolean> {
443
- const {signer, spvVaultContract} = this.getChain(vault.chainId);
444
-
445
- try {
446
- const txId = await spvVaultContract.claim(signer, vault.data, withdrawal.map(tx => {
447
- return {tx};
448
- }), undefined, true, {waitForConfirmation: true});
449
- this.logger.info("claimWithdrawal(): Successfully claimed withdrawals, btcTxIds: "+withdrawal.map(val => val.btcTx.txid).join(", ")+" smartChainTxId: "+txId);
450
- return true;
451
- } catch (e) {
452
- this.logger.error("claimWithdrawal(): Tried to claim but got error: ", e);
453
- return false;
454
- }
455
- }
456
-
457
- async getVault(chainId: string, owner: string, vaultId: bigint) {
458
- return this.vaultStorage.data[chainId+"_"+owner+"_"+vaultId.toString(10)];
459
- }
460
-
461
- /**
462
- * Returns a ready-to-use vault for a specific request
463
- *
464
- * @param chainIdentifier
465
- * @param totalSats
466
- * @param token
467
- * @param amount
468
- * @param gasToken
469
- * @param gasTokenAmount
470
- * @protected
471
- */
472
- async findVaultForSwap(chainIdentifier: string, totalSats: bigint, token: string, amount: bigint, gasToken: string, gasTokenAmount: bigint): Promise<SpvVault | null> {
473
- const {signer} = this.getChain(chainIdentifier);
474
-
475
- const pluginResponse = await PluginManager.onVaultSelection(
476
- chainIdentifier, totalSats, {token, amount}, {token: gasToken, amount: gasTokenAmount}
477
- );
478
- if(pluginResponse!=null) {
479
- AmountAssertions.handlePluginErrorResponses(pluginResponse);
480
- return pluginResponse as SpvVault;
481
- }
482
-
483
- const candidates = Object.keys(this.vaultStorage.data)
484
- .map(key => this.vaultStorage.data[key])
485
- .filter(vault =>
486
- vault.chainId===chainIdentifier && vault.data.getOwner()===signer.getAddress() && vault.isReady()
487
- )
488
- .filter(vault => {
489
- const token0 = vault.balances[0];
490
- if(token0.token!==token || token0.scaledAmount < amount) return false;
491
- if(gasToken!=null && gasTokenAmount!==0n) {
492
- const token1 = vault.balances[1];
493
- if(token1.token!==gasToken || token1.scaledAmount < gasTokenAmount) return false;
494
- }
495
- return true;
496
- });
497
-
498
- candidates.sort((a, b) => bigIntSorter(a.balances[0].scaledAmount, b.balances[0].scaledAmount));
499
-
500
- const result = candidates[0];
501
-
502
- if(result==null) throw {
503
- code: 20301,
504
- msg: "No suitable swap vault found, try again later!"
505
- };
506
-
507
- return result;
508
- }
509
-
510
- saveVault(vault: SpvVault) {
511
- return this.vaultStorage.saveData(vault.getIdentifier(), vault);
512
- }
513
-
514
- async startVaultsWatchdog() {
515
- let rerun: () => Promise<void>;
516
- rerun = async () => {
517
- await this.checkVaults().catch( e => console.error(e));
518
- setTimeout(rerun, this.config.vaultsCheckInterval);
519
- };
520
- await rerun();
521
- }
522
-
523
- async init() {
524
- const vaults = await this.vaultStorage.loadData(SpvVault);
525
- }
526
-
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} 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 {ChainData} 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
+
23
+ export class SpvVaults {
24
+
25
+ readonly vaultStorage: IStorageManager<SpvVault>;
26
+
27
+ readonly bitcoin: IBitcoinWallet;
28
+ readonly vaultSigner: ISpvVaultSigner;
29
+ readonly bitcoinRpc: BitcoinRpc<any>;
30
+ readonly config: {vaultsCheckInterval: number, maxUnclaimedWithdrawals?: number};
31
+ readonly getChain: (chainId: string) => ChainData
32
+
33
+ readonly logger = {
34
+ debug: (msg: string, ...args: any) => console.debug("SpvVaults: "+msg, ...args),
35
+ info: (msg: string, ...args: any) => console.info("SpvVaults: "+msg, ...args),
36
+ warn: (msg: string, ...args: any) => console.warn("SpvVaults: "+msg, ...args),
37
+ error: (msg: string, ...args: any) => console.error("SpvVaults: "+msg, ...args)
38
+ };
39
+
40
+ constructor(
41
+ vaultStorage: IStorageManager<SpvVault>,
42
+ bitcoin: IBitcoinWallet,
43
+ vaultSigner: ISpvVaultSigner,
44
+ bitcoinRpc: BitcoinRpc<any>,
45
+ getChain: (chainId: string) => ChainData,
46
+ config: {vaultsCheckInterval: number, maxUnclaimedWithdrawals?: number}
47
+ ) {
48
+ this.vaultStorage = vaultStorage;
49
+ this.bitcoin = bitcoin;
50
+ this.vaultSigner = vaultSigner;
51
+ this.bitcoinRpc = bitcoinRpc;
52
+ this.getChain = getChain;
53
+ this.config = config;
54
+ }
55
+
56
+ async processDepositEvent(vault: SpvVault, event: SpvVaultDepositEvent): Promise<void> {
57
+ vault.update(event);
58
+ await this.saveVault(vault);
59
+ }
60
+
61
+ async processOpenEvent(vault: SpvVault, event: SpvVaultOpenEvent): Promise<void> {
62
+ if(vault.state===SpvVaultState.BTC_CONFIRMED) {
63
+ vault.state = SpvVaultState.OPENED;
64
+ }
65
+ vault.update(event);
66
+ await this.saveVault(vault);
67
+ }
68
+
69
+ async processCloseEvent(vault: SpvVault, event: SpvVaultCloseEvent): Promise<void> {
70
+ if(vault.state===SpvVaultState.OPENED) {
71
+ vault.state = SpvVaultState.CLOSED;
72
+ }
73
+ vault.update(event);
74
+ await this.saveVault(vault);
75
+ }
76
+
77
+ async processClaimEvent(vault: SpvVault, swap: SpvVaultSwap | null, event: SpvVaultClaimEvent): Promise<void> {
78
+ //Update vault
79
+ const foundPendingWithdrawal = vault.pendingWithdrawals.findIndex(val => val.btcTx.txid===event.btcTxId);
80
+ if(foundPendingWithdrawal!==-1) vault.pendingWithdrawals.splice(foundPendingWithdrawal, 1);
81
+ vault.update(event);
82
+ await this.saveVault(vault);
83
+ }
84
+
85
+ async createVaults(chainId: string, count: number, token: string, confirmations: number = 2, feeRate?: number): Promise<{vaultsCreated: bigint[], btcTxId: string}> {
86
+ const {signer, chainInterface, tokenMultipliers, spvVaultContract} = this.getChain(chainId);
87
+
88
+ const signerAddress = signer.getAddress();
89
+
90
+ //Check vaultId of the latest saved vault
91
+ let latestVaultId: bigint = -1n;
92
+ for(let key in this.vaultStorage.data) {
93
+ const vault = this.vaultStorage.data[key];
94
+ if(vault.chainId!==chainId) continue;
95
+ if(vault.data.getOwner()!==signerAddress) continue;
96
+ if(vault.data.getVaultId() > latestVaultId) latestVaultId = vault.data.getVaultId();
97
+ }
98
+
99
+ latestVaultId++;
100
+
101
+ const vaultAddreses: {vaultId: bigint, address: string}[] = [];
102
+ for(let i=0;i<count;i++) {
103
+ const vaultId = latestVaultId + BigInt(i);
104
+ const address = await this.vaultSigner.getAddress(chainId, vaultId);
105
+ vaultAddreses.push({vaultId, address});
106
+ }
107
+
108
+ //Construct transaction
109
+ const txResult = await this.bitcoin.getSignedMultiTransaction(vaultAddreses.map(val => {
110
+ return {address: val.address, amount: VAULT_DUST_AMOUNT}
111
+ }), feeRate);
112
+
113
+ const nativeToken = chainInterface.getNativeCurrencyAddress();
114
+
115
+ const vaults = await Promise.all(vaultAddreses.map(async (val, index) => {
116
+ const vaultData = await spvVaultContract.createVaultData(signerAddress, val.vaultId, txResult.txId+":"+index, confirmations, [
117
+ {token, multiplier: tokenMultipliers?.[token] ?? 1n},
118
+ {token: nativeToken, multiplier: tokenMultipliers?.[nativeToken] ?? 1n}
119
+ ]);
120
+ return new SpvVault(chainId, vaultData, val.address);
121
+ }));
122
+
123
+ //Save vaults
124
+ if(this.vaultStorage.saveDataArr!=null) {
125
+ await this.vaultStorage.saveDataArr(vaults.map(val => {
126
+ return {id: val.getIdentifier(), object: val}
127
+ }));
128
+ } else {
129
+ for(let vault of vaults) {
130
+ await this.vaultStorage.saveData(vault.getIdentifier(), vault);
131
+ }
132
+ }
133
+
134
+ //Send bitcoin tx
135
+ await this.bitcoin.sendRawTransaction(txResult.raw);
136
+
137
+ this.logger.info("createVaults(): Funding "+count+" vaults, bitcoin txId: "+txResult.txId);
138
+
139
+ return {
140
+ vaultsCreated: vaults.map(val => val.data.getVaultId()),
141
+ btcTxId: txResult.txId
142
+ };
143
+ }
144
+
145
+ async listVaults(chainId?: string, token?: string) {
146
+ return Object.keys(this.vaultStorage.data)
147
+ .map(key => this.vaultStorage.data[key])
148
+ .filter(val => chainId==null ? true : val.chainId===chainId)
149
+ .filter(val => val.data.getOwner()===this.getChain(val.chainId)?.signer?.getAddress())
150
+ .filter(val => token==null ? true : val.data.getTokenData()[0].token===token);
151
+ }
152
+
153
+ async fundVault(vault: SpvVault, tokenAmounts: bigint[]): Promise<string> {
154
+ if(vault.state!==SpvVaultState.OPENED) throw new Error("Vault not opened!");
155
+
156
+ this.logger.info("fundVault(): Depositing tokens to the vault "+vault.data.getVaultId().toString(10)+", amounts: "+tokenAmounts.map(val => val.toString(10)).join(", "));
157
+
158
+ const {signer, spvVaultContract} = this.getChain(vault.chainId);
159
+
160
+ const txId = await spvVaultContract.deposit(signer, vault.data, tokenAmounts, {waitForConfirmation: true});
161
+
162
+ this.logger.info("fundVault(): Tokens deposited to vault "+vault.data.getVaultId().toString(10)+", amounts: "+tokenAmounts.map(val => val.toString(10)).join(", ")+", txId: "+txId);
163
+
164
+ return txId;
165
+ }
166
+
167
+ async withdrawFromVault(vault: SpvVault, tokenAmounts: bigint[], feeRate?: number): Promise<string> {
168
+ tokenAmounts.forEach((rawAmount, index) => {
169
+ if(vault.balances[index]==null) throw new Error("Token not found in the vault");
170
+ if(vault.balances[index].rawAmount<rawAmount) throw new Error("Not enough balance in the vault");
171
+ });
172
+
173
+ if(!vault.isReady()) throw new Error("Vault not ready, wait for the latest swap to get at least 1 confirmation!");
174
+
175
+ const {signer, spvVaultContract} = this.getChain(vault.chainId);
176
+
177
+ const latestUtxo = vault.getLatestUtxo();
178
+ const [txId, voutStr] = latestUtxo.split(":");
179
+
180
+ const opReturnData = spvVaultContract.toOpReturnData(signer.getAddress(), tokenAmounts);
181
+ let opReturnScript: Buffer;
182
+ if(opReturnData.length<76) {
183
+ opReturnScript = Buffer.concat([
184
+ Buffer.from([0x6a, opReturnData.length]),
185
+ opReturnData
186
+ ]);
187
+ } else {
188
+ opReturnScript = Buffer.concat([
189
+ Buffer.from([0x6a, 0x4c, opReturnData.length]),
190
+ opReturnData
191
+ ]);
192
+ }
193
+
194
+ let psbt = new Transaction({
195
+ allowUnknownOutputs: true
196
+ });
197
+ psbt.addInput({
198
+ txid: txId,
199
+ index: parseInt(voutStr),
200
+ witnessUtxo: {
201
+ amount: BigInt(VAULT_DUST_AMOUNT),
202
+ script: this.bitcoin.toOutputScript(vault.btcAddress)
203
+ }
204
+ });
205
+ psbt.addOutput({
206
+ amount: BigInt(VAULT_DUST_AMOUNT),
207
+ script: this.bitcoin.toOutputScript(vault.btcAddress)
208
+ });
209
+ psbt.addOutput({
210
+ amount: 0n,
211
+ script: opReturnScript
212
+ });
213
+
214
+ psbt = await this.bitcoin.fundPsbt(psbt, feeRate);
215
+ if(psbt.inputsLength<2) throw new Error("PSBT needs at least 2 inputs!");
216
+ psbt.updateInput(0, {sequence: 0x80000000});
217
+ psbt.updateInput(1, {sequence: 0x80000000});
218
+ psbt = await this.vaultSigner.signPsbt(vault.chainId, vault.data.getVaultId(), psbt, [0]);
219
+ const res = await this.bitcoin.signPsbt(psbt);
220
+
221
+ const parsedTransaction = await this.bitcoinRpc.parseTransaction(res.raw);
222
+ const withdrawalData = await spvVaultContract.getWithdrawalData(parsedTransaction);
223
+
224
+ if(withdrawalData.getSpentVaultUtxo()!==vault.getLatestUtxo()) {
225
+ throw new Error("Latest vault UTXO already spent! Please try again later.");
226
+ }
227
+ (withdrawalData as any).sending = true;
228
+ vault.addWithdrawal(withdrawalData);
229
+ await this.saveVault(vault);
230
+
231
+ try {
232
+ await this.bitcoin.sendRawTransaction(res.raw);
233
+ (withdrawalData as any).sending = false;
234
+ } catch (e) {
235
+ (withdrawalData as any).sending = false;
236
+ vault.removeWithdrawal(withdrawalData);
237
+ await this.saveVault(vault);
238
+ throw e;
239
+ }
240
+
241
+ return res.txId;
242
+ }
243
+
244
+ /**
245
+ * Call this to check whether some of the previously replaced transactions got re-introduced to the mempool
246
+ *
247
+ * @param vault
248
+ * @param save
249
+ */
250
+ async checkVaultReplacedTransactions(vault: SpvVault, save?: boolean): Promise<boolean> {
251
+ const {spvVaultContract} = this.getChain(vault.chainId);
252
+
253
+ const initialVaultWithdrawalCount = vault.data.getWithdrawalCount();
254
+
255
+ let latestWithdrawalIndex = initialVaultWithdrawalCount;
256
+ const newPendingTxns: SpvWithdrawalTransactionData[] = [];
257
+ const reintroducedTxIds: Set<string> = new Set();
258
+ for(let [withdrawalIndex, replacedWithdrawalGroup] of vault.replacedWithdrawals) {
259
+ if(withdrawalIndex<=latestWithdrawalIndex) continue; //Don't check txns that should already be included
260
+
261
+ for(let replacedWithdrawal of replacedWithdrawalGroup) {
262
+ if(reintroducedTxIds.has(replacedWithdrawal.getTxId())) continue;
263
+ const tx = await this.bitcoinRpc.getTransaction(replacedWithdrawal.getTxId());
264
+ if(tx==null) continue;
265
+
266
+ //Re-introduce transaction to the pending withdrawals list
267
+ if(withdrawalIndex>latestWithdrawalIndex) {
268
+ const txChain: SpvWithdrawalTransactionData[] = [replacedWithdrawal];
269
+ withdrawalIndex--;
270
+ while(withdrawalIndex>latestWithdrawalIndex) {
271
+ const tx = await this.bitcoinRpc.getTransaction(txChain[0].getSpentVaultUtxo().split(":")[0]);
272
+ if(tx==null) break;
273
+ reintroducedTxIds.add(tx.txid);
274
+ txChain.unshift(await spvVaultContract.getWithdrawalData(tx));
275
+ withdrawalIndex--;
276
+ }
277
+ if(withdrawalIndex>latestWithdrawalIndex) {
278
+ this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Tried to re-introduce previously replaced TX, but one of txns in the chain not found!`);
279
+ continue;
280
+ }
281
+ newPendingTxns.push(...txChain);
282
+ latestWithdrawalIndex += txChain.length;
283
+ break; //Don't check other txns at the same withdrawal index
284
+ } else {
285
+ this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Tried to re-introduce previously replaced TX, but vault has already processed such withdrawal!`);
286
+ }
287
+ }
288
+ }
289
+
290
+ if(newPendingTxns.length===0) return false;
291
+
292
+ if(initialVaultWithdrawalCount!==vault.data.getWithdrawalCount()) {
293
+ this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Not saving vault after checking replaced transactions, due to withdrawal count changed!`);
294
+ return false;
295
+ }
296
+
297
+ const backup = vault.pendingWithdrawals.splice(0, newPendingTxns.length);
298
+ const txsToAddOnTop = vault.pendingWithdrawals.splice(0, vault.pendingWithdrawals.length);
299
+
300
+ try {
301
+ newPendingTxns.forEach(val => vault.addWithdrawal(val));
302
+ txsToAddOnTop.forEach(val => vault.addWithdrawal(val));
303
+ for(let i=0;i<newPendingTxns.length;i++) {
304
+ const withdrawalIndex = initialVaultWithdrawalCount+i+1;
305
+ const arr = vault.replacedWithdrawals.get(withdrawalIndex);
306
+ if(arr==null) continue;
307
+ const index = arr.indexOf(newPendingTxns[i]);
308
+ if(index===-1) {
309
+ this.logger.warn(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Cannot remove re-introduced tx ${newPendingTxns[i].getTxId()}, not found in the respective array!`);
310
+ continue;
311
+ }
312
+ arr.splice(index, 1);
313
+ if(arr.length===0) vault.replacedWithdrawals.delete(withdrawalIndex);
314
+ }
315
+ this.logger.info(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Re-introduced back ${newPendingTxns.length} txns that were re-added to the mempool!`);
316
+ if(save) await this.saveVault(vault);
317
+ return true;
318
+ } catch (e) {
319
+ this.logger.error(`checkVaultReplacedTransactions(${vault.getIdentifier()}): Failed to update the vault with new pending txns (rolling back): `, e);
320
+ //Rollback the pending withdrawals
321
+ vault.pendingWithdrawals.push(...backup, ...txsToAddOnTop);
322
+ return false;
323
+ }
324
+ }
325
+
326
+ async checkVaults() {
327
+ const vaults = Object.keys(this.vaultStorage.data).map(key => this.vaultStorage.data[key]);
328
+
329
+ const claimWithdrawals: {vault: SpvVault, withdrawals: SpvWithdrawalTransactionData[]}[] = [];
330
+
331
+ for(let vault of vaults) {
332
+ const {signer, spvVaultContract, chainInterface} = this.getChain(vault.chainId);
333
+ if(vault.data.getOwner()!==signer.getAddress()) continue;
334
+
335
+ if(vault.state===SpvVaultState.BTC_INITIATED) {
336
+ //Check if btc tx confirmed
337
+ const txId = vault.initialUtxo.split(":")[0];
338
+ const btcTx = await this.bitcoinRpc.getTransaction(txId);
339
+ if(btcTx.confirmations >= VAULT_INIT_CONFIRMATIONS) {
340
+ //Double-check the state here to prevent race condition
341
+ if(vault.state===SpvVaultState.BTC_INITIATED) {
342
+ vault.state = SpvVaultState.BTC_CONFIRMED;
343
+ await this.saveVault(vault);
344
+ }
345
+ this.logger.info("checkVaults(): Vault ID "+vault.data.getVaultId().toString(10)+" confirmed on bitcoin, opening vault on "+vault.chainId);
346
+ }
347
+ }
348
+
349
+ if(vault.state===SpvVaultState.BTC_CONFIRMED) {
350
+ //Check if open txs were sent already
351
+ if(vault.scOpenTx!=null) {
352
+ //Check if confirmed
353
+ const status = await chainInterface.getTxStatus(vault.scOpenTx.rawTx);
354
+ if(status==="pending") return;
355
+ if(status==="success") {
356
+ vault.state = SpvVaultState.OPENED;
357
+ await this.saveVault(vault);
358
+ return;
359
+ }
360
+ }
361
+
362
+ const txs = await spvVaultContract.txsOpen(signer.getAddress(), vault.data);
363
+ let numTx = 0;
364
+ const txIds = await chainInterface.sendAndConfirm(
365
+ signer, txs, true, undefined, false,
366
+ async (txId: string, rawTx: string) => {
367
+ numTx++;
368
+ if(numTx===txs.length) {
369
+ //Final tx
370
+ vault.scOpenTx = {txId, rawTx};
371
+ await this.saveVault(vault);
372
+ }
373
+ }
374
+ );
375
+ this.logger.info("checkVaults(): Vault ID "+vault.data.getVaultId().toString(10)+" opened on "+vault.chainId+" txId: "+txIds.join(", "));
376
+
377
+ vault.state = SpvVaultState.OPENED;
378
+ await this.saveVault(vault);
379
+ }
380
+
381
+ if(vault.state===SpvVaultState.OPENED) {
382
+ let changed = await this.checkVaultReplacedTransactions(vault);
383
+
384
+ //Check if some of the pendingWithdrawals got confirmed
385
+ let latestOwnWithdrawalIndex = -1;
386
+ let latestConfirmedWithdrawalIndex = -1;
387
+ for(let i = vault.pendingWithdrawals.length-1; i>=0; i--) {
388
+ const pendingWithdrawal = vault.pendingWithdrawals[i];
389
+ if(pendingWithdrawal.sending) continue;
390
+
391
+ //Check all the pending withdrawals that were not finalized yet
392
+ const btcTx = await checkTransactionReplaced(pendingWithdrawal.btcTx.txid, pendingWithdrawal.btcTx.raw, this.bitcoinRpc);
393
+ if(btcTx==null) {
394
+ //Probable double-spend, remove from pending withdrawals
395
+ if(!vault.doubleSpendPendingWithdrawal(pendingWithdrawal)) {
396
+ this.logger.warn("checkVaults(): Tried to remove pending withdrawal txId: "+pendingWithdrawal.btcTx.txid+", but doesn't exist anymore!");
397
+ } else {
398
+ this.logger.info("checkVaults(): Successfully removed withdrawal txId: "+pendingWithdrawal.btcTx.txid+", due to being replaced in the mempool!");
399
+ }
400
+ changed = true;
401
+ } else {
402
+ //Update confirmations count
403
+ if(
404
+ pendingWithdrawal.btcTx.confirmations !== btcTx.confirmations ||
405
+ pendingWithdrawal.btcTx.blockhash !== btcTx.blockhash
406
+ ) {
407
+ pendingWithdrawal.btcTx.confirmations = btcTx.confirmations;
408
+ pendingWithdrawal.btcTx.blockhash = btcTx.blockhash;
409
+ changed = true;
410
+ }
411
+ }
412
+
413
+ //Check it has enough confirmations
414
+ if(pendingWithdrawal.btcTx.confirmations >= vault.data.getConfirmations()) {
415
+ latestConfirmedWithdrawalIndex = i;
416
+ //Check if the pending withdrawals contain a withdrawal to our own address
417
+ if (pendingWithdrawal.isRecipient(signer.getAddress())) {
418
+ latestOwnWithdrawalIndex = i;
419
+ }
420
+ }
421
+ }
422
+ if(changed) {
423
+ await this.saveVault(vault);
424
+ }
425
+ if(this.config.maxUnclaimedWithdrawals!=null && latestConfirmedWithdrawalIndex+1 >= this.config.maxUnclaimedWithdrawals) {
426
+ this.logger.info("checkVaults(): Processing withdrawals by self, because a lot of them are unclaimed!");
427
+ claimWithdrawals.push({vault, withdrawals: vault.pendingWithdrawals.slice(0, latestConfirmedWithdrawalIndex+1)});
428
+ } else if(latestOwnWithdrawalIndex!==-1) {
429
+ claimWithdrawals.push({vault, withdrawals: vault.pendingWithdrawals.slice(0, latestOwnWithdrawalIndex+1)});
430
+ }
431
+ }
432
+ }
433
+
434
+ for(let {vault, withdrawals} of claimWithdrawals) {
435
+ if(!await this.claimWithdrawals(vault, withdrawals)) {
436
+ this.logger.error("checkVaults(): Cannot process withdrawals "+withdrawals.map(val => val.btcTx.txid).join(", ")+" for vault: "+vault.data.getVaultId());
437
+ break;
438
+ }
439
+ }
440
+ }
441
+
442
+ async claimWithdrawals(vault: SpvVault, withdrawal: SpvWithdrawalTransactionData[]): Promise<boolean> {
443
+ const {signer, spvVaultContract} = this.getChain(vault.chainId);
444
+
445
+ try {
446
+ const txId = await spvVaultContract.claim(signer, vault.data, withdrawal.map(tx => {
447
+ return {tx};
448
+ }), undefined, true, {waitForConfirmation: true});
449
+ this.logger.info("claimWithdrawal(): Successfully claimed withdrawals, btcTxIds: "+withdrawal.map(val => val.btcTx.txid).join(", ")+" smartChainTxId: "+txId);
450
+ return true;
451
+ } catch (e) {
452
+ this.logger.error("claimWithdrawal(): Tried to claim but got error: ", e);
453
+ return false;
454
+ }
455
+ }
456
+
457
+ async getVault(chainId: string, owner: string, vaultId: bigint) {
458
+ return this.vaultStorage.data[chainId+"_"+owner+"_"+vaultId.toString(10)];
459
+ }
460
+
461
+ /**
462
+ * Returns a ready-to-use vault for a specific request
463
+ *
464
+ * @param chainIdentifier
465
+ * @param totalSats
466
+ * @param token
467
+ * @param amount
468
+ * @param gasToken
469
+ * @param gasTokenAmount
470
+ * @protected
471
+ */
472
+ async findVaultForSwap(chainIdentifier: string, totalSats: bigint, token: string, amount: bigint, gasToken: string, gasTokenAmount: bigint): Promise<SpvVault | null> {
473
+ const {signer} = this.getChain(chainIdentifier);
474
+
475
+ const pluginResponse = await PluginManager.onVaultSelection(
476
+ chainIdentifier, totalSats, {token, amount}, {token: gasToken, amount: gasTokenAmount}
477
+ );
478
+ if(pluginResponse!=null) {
479
+ AmountAssertions.handlePluginErrorResponses(pluginResponse);
480
+ return pluginResponse as SpvVault;
481
+ }
482
+
483
+ const candidates = Object.keys(this.vaultStorage.data)
484
+ .map(key => this.vaultStorage.data[key])
485
+ .filter(vault =>
486
+ vault.chainId===chainIdentifier && vault.data.getOwner()===signer.getAddress() && vault.isReady()
487
+ )
488
+ .filter(vault => {
489
+ const token0 = vault.balances[0];
490
+ if(token0.token!==token || token0.scaledAmount < amount) return false;
491
+ if(gasToken!=null && gasTokenAmount!==0n) {
492
+ const token1 = vault.balances[1];
493
+ if(token1.token!==gasToken || token1.scaledAmount < gasTokenAmount) return false;
494
+ }
495
+ return true;
496
+ });
497
+
498
+ candidates.sort((a, b) => bigIntSorter(a.balances[0].scaledAmount, b.balances[0].scaledAmount));
499
+
500
+ const result = candidates[0];
501
+
502
+ if(result==null) throw {
503
+ code: 20301,
504
+ msg: "No suitable swap vault found, try again later!"
505
+ };
506
+
507
+ return result;
508
+ }
509
+
510
+ saveVault(vault: SpvVault) {
511
+ return this.vaultStorage.saveData(vault.getIdentifier(), vault);
512
+ }
513
+
514
+ async startVaultsWatchdog() {
515
+ let rerun: () => Promise<void>;
516
+ rerun = async () => {
517
+ await this.checkVaults().catch( e => console.error(e));
518
+ setTimeout(rerun, this.config.vaultsCheckInterval);
519
+ };
520
+ await rerun();
521
+ }
522
+
523
+ async init() {
524
+ const vaults = await this.vaultStorage.loadData(SpvVault);
525
+ }
526
+
527
527
  }