@atomiqlabs/lp-lib 17.5.1 → 17.5.2

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 (170) 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 +43 -43
  5. package/dist/index.js +59 -59
  6. package/dist/info/InfoHandler.d.ts +17 -17
  7. package/dist/info/InfoHandler.js +61 -61
  8. package/dist/plugins/IPlugin.d.ts +158 -158
  9. package/dist/plugins/IPlugin.js +35 -35
  10. package/dist/plugins/PluginManager.d.ts +115 -115
  11. package/dist/plugins/PluginManager.js +304 -304
  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 +178 -178
  27. package/dist/swaps/SwapHandler.js +225 -225
  28. package/dist/swaps/SwapHandlerSwap.d.ts +80 -80
  29. package/dist/swaps/SwapHandlerSwap.js +78 -78
  30. package/dist/swaps/assertions/AmountAssertions.d.ts +37 -37
  31. package/dist/swaps/assertions/AmountAssertions.js +83 -83
  32. package/dist/swaps/assertions/FromBtcAmountAssertions.d.ts +76 -76
  33. package/dist/swaps/assertions/FromBtcAmountAssertions.js +189 -189
  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 +154 -154
  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 +329 -329
  52. package/dist/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.d.ts +22 -22
  53. package/dist/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.js +53 -53
  54. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.d.ts +115 -115
  55. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.js +719 -719
  56. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.d.ts +34 -34
  57. package/dist/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.js +94 -94
  58. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.d.ts +119 -119
  59. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.js +730 -730
  60. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.d.ts +56 -56
  61. package/dist/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.js +123 -123
  62. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.d.ts +170 -170
  63. package/dist/swaps/escrow/tobtc_abstract/ToBtcAbs.js +769 -769
  64. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.d.ts +29 -29
  65. package/dist/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.js +67 -67
  66. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.d.ts +178 -178
  67. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.js +912 -912
  68. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.d.ts +25 -25
  69. package/dist/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.js +61 -61
  70. package/dist/swaps/spv_vault_swap/SpvVault.d.ts +45 -45
  71. package/dist/swaps/spv_vault_swap/SpvVault.js +148 -148
  72. package/dist/swaps/spv_vault_swap/SpvVaultSwap.d.ts +71 -71
  73. package/dist/swaps/spv_vault_swap/SpvVaultSwap.js +165 -165
  74. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.d.ts +72 -72
  75. package/dist/swaps/spv_vault_swap/SpvVaultSwapHandler.js +710 -710
  76. package/dist/swaps/spv_vault_swap/SpvVaults.d.ts +70 -70
  77. package/dist/swaps/spv_vault_swap/SpvVaults.js +554 -554
  78. package/dist/swaps/spv_vault_swap/StickyAddress.d.ts +6 -6
  79. package/dist/swaps/spv_vault_swap/StickyAddress.js +19 -19
  80. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.d.ts +52 -52
  81. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrusted.js +677 -677
  82. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.d.ts +53 -53
  83. package/dist/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.js +121 -121
  84. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.d.ts +77 -77
  85. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.js +518 -518
  86. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.d.ts +35 -35
  87. package/dist/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.js +84 -84
  88. package/dist/utils/BitcoinUtils.d.ts +4 -4
  89. package/dist/utils/BitcoinUtils.js +61 -61
  90. package/dist/utils/Utils.d.ts +32 -32
  91. package/dist/utils/Utils.js +130 -130
  92. package/dist/utils/paramcoders/IParamReader.d.ts +5 -5
  93. package/dist/utils/paramcoders/IParamReader.js +2 -2
  94. package/dist/utils/paramcoders/IParamWriter.d.ts +4 -4
  95. package/dist/utils/paramcoders/IParamWriter.js +2 -2
  96. package/dist/utils/paramcoders/LegacyParamEncoder.d.ts +10 -10
  97. package/dist/utils/paramcoders/LegacyParamEncoder.js +22 -22
  98. package/dist/utils/paramcoders/ParamDecoder.d.ts +25 -25
  99. package/dist/utils/paramcoders/ParamDecoder.js +222 -222
  100. package/dist/utils/paramcoders/ParamEncoder.d.ts +9 -9
  101. package/dist/utils/paramcoders/ParamEncoder.js +22 -22
  102. package/dist/utils/paramcoders/SchemaVerifier.d.ts +21 -21
  103. package/dist/utils/paramcoders/SchemaVerifier.js +84 -84
  104. package/dist/utils/paramcoders/server/ServerParamDecoder.d.ts +8 -8
  105. package/dist/utils/paramcoders/server/ServerParamDecoder.js +107 -107
  106. package/dist/utils/paramcoders/server/ServerParamEncoder.d.ts +11 -11
  107. package/dist/utils/paramcoders/server/ServerParamEncoder.js +65 -65
  108. package/dist/wallets/IBitcoinWallet.d.ts +155 -155
  109. package/dist/wallets/IBitcoinWallet.js +100 -100
  110. package/dist/wallets/ILightningWallet.d.ts +137 -137
  111. package/dist/wallets/ILightningWallet.js +37 -37
  112. package/dist/wallets/ISpvVaultSigner.d.ts +7 -7
  113. package/dist/wallets/ISpvVaultSigner.js +2 -2
  114. package/package.json +37 -37
  115. package/src/fees/IBtcFeeEstimator.ts +6 -6
  116. package/src/index.ts +54 -54
  117. package/src/info/InfoHandler.ts +106 -106
  118. package/src/plugins/IPlugin.ts +196 -196
  119. package/src/plugins/PluginManager.ts +389 -389
  120. package/src/prices/BinanceSwapPrice.ts +101 -101
  121. package/src/prices/CoinGeckoSwapPrice.ts +75 -75
  122. package/src/prices/ISwapPrice.ts +88 -88
  123. package/src/prices/OKXSwapPrice.ts +101 -101
  124. package/src/storage/IIntermediaryStorage.ts +19 -19
  125. package/src/storagemanager/IntermediaryStorageManager.ts +118 -118
  126. package/src/storagemanager/StorageManager.ts +78 -78
  127. package/src/swaps/SwapHandler.ts +337 -337
  128. package/src/swaps/SwapHandlerSwap.ts +143 -143
  129. package/src/swaps/assertions/AmountAssertions.ts +97 -97
  130. package/src/swaps/assertions/FromBtcAmountAssertions.ts +257 -257
  131. package/src/swaps/assertions/LightningAssertions.ts +103 -103
  132. package/src/swaps/assertions/ToBtcAmountAssertions.ts +209 -209
  133. package/src/swaps/escrow/EscrowHandler.ts +172 -172
  134. package/src/swaps/escrow/EscrowHandlerSwap.ts +86 -86
  135. package/src/swaps/escrow/FromBtcBaseSwap.ts +38 -38
  136. package/src/swaps/escrow/FromBtcBaseSwapHandler.ts +286 -286
  137. package/src/swaps/escrow/ToBtcBaseSwap.ts +85 -85
  138. package/src/swaps/escrow/ToBtcBaseSwapHandler.ts +129 -129
  139. package/src/swaps/escrow/frombtc_abstract/FromBtcAbs.ts +468 -468
  140. package/src/swaps/escrow/frombtc_abstract/FromBtcSwapAbs.ts +65 -65
  141. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnAbs.ts +900 -900
  142. package/src/swaps/escrow/frombtcln_abstract/FromBtcLnSwapAbs.ts +145 -145
  143. package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAuto.ts +892 -892
  144. package/src/swaps/escrow/frombtcln_autoinit/FromBtcLnAutoSwap.ts +200 -200
  145. package/src/swaps/escrow/tobtc_abstract/ToBtcAbs.ts +947 -947
  146. package/src/swaps/escrow/tobtc_abstract/ToBtcSwapAbs.ts +112 -112
  147. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnAbs.ts +1167 -1167
  148. package/src/swaps/escrow/tobtcln_abstract/ToBtcLnSwapAbs.ts +84 -84
  149. package/src/swaps/spv_vault_swap/SpvVault.ts +182 -182
  150. package/src/swaps/spv_vault_swap/SpvVaultSwap.ts +239 -239
  151. package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +897 -897
  152. package/src/swaps/spv_vault_swap/SpvVaults.ts +640 -640
  153. package/src/swaps/spv_vault_swap/StickyAddress.ts +20 -20
  154. package/src/swaps/trusted/frombtc_trusted/FromBtcTrusted.ts +782 -782
  155. package/src/swaps/trusted/frombtc_trusted/FromBtcTrustedSwap.ts +189 -189
  156. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrusted.ts +622 -622
  157. package/src/swaps/trusted/frombtcln_trusted/FromBtcLnTrustedSwap.ts +125 -125
  158. package/src/utils/BitcoinUtils.ts +59 -59
  159. package/src/utils/Utils.ts +152 -152
  160. package/src/utils/paramcoders/IParamReader.ts +7 -7
  161. package/src/utils/paramcoders/IParamWriter.ts +8 -8
  162. package/src/utils/paramcoders/LegacyParamEncoder.ts +27 -27
  163. package/src/utils/paramcoders/ParamDecoder.ts +218 -218
  164. package/src/utils/paramcoders/ParamEncoder.ts +29 -29
  165. package/src/utils/paramcoders/SchemaVerifier.ts +96 -96
  166. package/src/utils/paramcoders/server/ServerParamDecoder.ts +118 -118
  167. package/src/utils/paramcoders/server/ServerParamEncoder.ts +75 -75
  168. package/src/wallets/IBitcoinWallet.ts +246 -246
  169. package/src/wallets/ILightningWallet.ts +201 -201
  170. package/src/wallets/ISpvVaultSigner.ts +10 -10
@@ -1,897 +1,897 @@
1
- import {MultichainData, SwapBaseConfig, SwapHandler, SwapHandlerType} from "../SwapHandler";
2
- import {Express, Request, Response} from "express";
3
- import {IBitcoinWallet} from "../../wallets/IBitcoinWallet";
4
- import {
5
- BitcoinRpc,
6
- BtcBlock,
7
- ChainEvent,
8
- IStorageManager,
9
- SpvVaultClaimEvent,
10
- SpvVaultCloseEvent,
11
- SpvVaultDepositEvent,
12
- SpvVaultEvent,
13
- SpvVaultOpenEvent,
14
- SpvWithdrawalTransactionData,
15
- SwapData
16
- } from "@atomiqlabs/base";
17
- import {IIntermediaryStorage} from "../../storage/IIntermediaryStorage";
18
- import {ISwapPrice} from "../../prices/ISwapPrice";
19
- import {SpvVaultSwap, SpvVaultSwapState} from "./SpvVaultSwap";
20
- import {ISpvVaultSigner} from "../../wallets/ISpvVaultSigner";
21
- import {PluginManager} from "../../plugins/PluginManager";
22
- import {SpvVault} from "./SpvVault";
23
- import {serverParamDecoder} from "../../utils/paramcoders/server/ServerParamDecoder";
24
- import {
25
- expressHandlerWrapper,
26
- getAbortController,
27
- HEX_REGEX,
28
- isDefinedRuntimeError,
29
- parsePsbt
30
- } from "../../utils/Utils";
31
- import {IParamReader} from "../../utils/paramcoders/IParamReader";
32
- import {ServerParamEncoder} from "../../utils/paramcoders/server/ServerParamEncoder";
33
- import {FieldTypeEnum} from "../../utils/paramcoders/SchemaVerifier";
34
- import {FromBtcAmountAssertions} from "../assertions/FromBtcAmountAssertions";
35
- import {randomBytes} from "crypto";
36
- import {Transaction} from "@scure/btc-signer";
37
- import {SpvVaults, VAULT_DUST_AMOUNT} from "./SpvVaults";
38
- import {isLegacyInput} from "../../utils/BitcoinUtils";
39
- import {AmountAssertions} from "../assertions/AmountAssertions";
40
- import {isQuoteThrow} from "../../plugins/IPlugin";
41
- import {StickyAddress} from "./StickyAddress";
42
-
43
- export type SpvVaultSwapHandlerConfig = SwapBaseConfig & {
44
- vaultsCheckInterval: number,
45
- gasTokenMax: {[chainId: string]: bigint},
46
- maxUnclaimedWithdrawals?: number
47
- };
48
-
49
- export type SpvVaultSwapRequestType = {
50
- address: string,
51
- amount: bigint,
52
- token: string,
53
- gasAmount: bigint,
54
- gasToken: string,
55
- exactOut?: boolean,
56
- callerFeeRate: bigint,
57
- frontingFeeRate: bigint
58
- };
59
-
60
- export type SpvVaultPostQuote = {
61
- quoteId: string,
62
- psbtHex: string
63
- }
64
-
65
- const TX_MAX_VSIZE = 16*1024;
66
-
67
- type AmountAdjustUtxo = {
68
- value: number,
69
- vSize: number,
70
- cpfp?: {
71
- effectiveVSize: number,
72
- effectiveFeeRate: number
73
- }
74
- }
75
-
76
- function parseAmountAdjustUtxos(amountAdjustUtxos: any): AmountAdjustUtxo[] {
77
- if(!Array.isArray(amountAdjustUtxos)) return null;
78
- if(amountAdjustUtxos.length > 250) return null;
79
- const validArray = amountAdjustUtxos.every(value =>
80
- value!=null && typeof(value)==="object" && typeof(value.value)==="number" && typeof(value.vSize)==="number" &&
81
- (value.cpfp==null || (typeof(value.cpfp)==="object" && typeof(value.cpfp.effectiveVSize)==="number" && typeof(value.cpfp.effectiveFeeRate)==="number"))
82
- );
83
- if(!validArray) return null;
84
- return amountAdjustUtxos;
85
- }
86
-
87
- export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapState> {
88
- readonly type = SwapHandlerType.FROM_BTC_SPV;
89
- readonly inflightSwapStates = new Set([SpvVaultSwapState.SIGNED, SpvVaultSwapState.SENT, SpvVaultSwapState.BTC_CONFIRMED]);
90
-
91
- readonly bitcoin: IBitcoinWallet;
92
- readonly bitcoinRpc: BitcoinRpc<BtcBlock>;
93
- readonly vaultSigner: ISpvVaultSigner;
94
-
95
- readonly btcTxIdIndex: Map<string, SpvVaultSwap> = new Map();
96
-
97
- readonly AmountAssertions: FromBtcAmountAssertions;
98
- readonly Vaults: SpvVaults;
99
-
100
- config: SpvVaultSwapHandlerConfig;
101
-
102
- readonly stickyAddresses?: IStorageManager<StickyAddress>;
103
-
104
- constructor(
105
- storageDirectory: IIntermediaryStorage<SpvVaultSwap>,
106
- vaultStorage: IStorageManager<SpvVault>,
107
- path: string,
108
- chainsData: MultichainData,
109
- swapPricing: ISwapPrice,
110
- bitcoin: IBitcoinWallet,
111
- bitcoinRpc: BitcoinRpc<BtcBlock>,
112
- spvVaultSigner: ISpvVaultSigner,
113
- config: SpvVaultSwapHandlerConfig,
114
- stickyAddresses?: IStorageManager<StickyAddress>
115
- ) {
116
- super(storageDirectory, path, chainsData, swapPricing);
117
- this.bitcoinRpc = bitcoinRpc;
118
- this.bitcoin = bitcoin;
119
- this.vaultSigner = spvVaultSigner;
120
- this.config = config;
121
- this.AmountAssertions = new FromBtcAmountAssertions(config, swapPricing);
122
- this.Vaults = new SpvVaults(vaultStorage, bitcoin, spvVaultSigner, bitcoinRpc, this.chains, config);
123
- this.stickyAddresses = stickyAddresses;
124
-
125
- for(let chain in this.allowedTokens) {
126
- //Remove chains that don't support spv vault swaps
127
- const {spvVaultContract} = this.getChain(chain);
128
- if(spvVaultContract==null) this.allowedTokens[chain].clear();
129
- }
130
- }
131
-
132
- private async getStickyAddress(chainId: string, address: string): Promise<string | undefined> {
133
- if(this.stickyAddresses==null) throw new Error("Sticky addresses are not supported!");
134
-
135
- const {chainInterface} = this.getChain(chainId);
136
- const normalizedAddress = chainInterface.normalizeAddress(address);
137
-
138
- const addressIdentifier = chainId+"-"+normalizedAddress;
139
-
140
- const result = this.stickyAddresses.data[addressIdentifier];
141
- if(result==null) return;
142
-
143
- const btcAddress = result.address;
144
-
145
- if(this.bitcoin.isOwnedAddress!=null) {
146
- if(!(await this.bitcoin.isOwnedAddress(btcAddress))) {
147
- this.logger.warn(`getStickyAddress(): Failed to get sticky address, address ${btcAddress} is not controlled by our bitcoin wallet!`);
148
- return;
149
- }
150
- }
151
-
152
- return btcAddress;
153
- }
154
-
155
- async addStickyAddress(chainId: string, address: string, btcAddress: string) {
156
- if(this.stickyAddresses==null) throw new Error("Sticky addresses are not supported!");
157
-
158
- const {chainInterface} = this.getChain(chainId);
159
- const normalizedAddress = chainInterface.normalizeAddress(address);
160
-
161
- if(this.bitcoin.isOwnedAddress!=null) {
162
- if(!(await this.bitcoin.isOwnedAddress(btcAddress))) {
163
- this.logger.warn(`addStickyAddress(): Failed to create sticky address, address ${btcAddress} is not controlled by our bitcoin wallet!`);
164
- return;
165
- }
166
- }
167
-
168
- const addressIdentifier = chainId+"-"+normalizedAddress;
169
-
170
- if(this.stickyAddresses.data[addressIdentifier]!=null) {
171
- this.logger.warn(`addStickyAddress(): Failed to create sticky address, address sticky address already exists for ${addressIdentifier}!`);
172
- return;
173
- }
174
- await this.stickyAddresses.saveData(addressIdentifier, new StickyAddress(btcAddress));
175
- }
176
-
177
- protected async processClaimEvent(swap: SpvVaultSwap | null, event: SpvVaultClaimEvent): Promise<void> {
178
- if(swap==null) return;
179
- //Update swap
180
- swap.txIds.claim = event.meta?.txId;
181
- await this.removeSwapData(swap, SpvVaultSwapState.CLAIMED);
182
- if(swap.saveStickyAddress) try {
183
- await this.addStickyAddress(swap.chainIdentifier, swap.recipient, swap.btcAddress);
184
- } catch (e) {
185
- this.logger.error(`processClaimEvent(): Failed to create the sticky address for swap ${swap.getIdentifier()}`);
186
- }
187
- }
188
-
189
- /**
190
- * Chain event processor
191
- *
192
- * @param chainIdentifier
193
- * @param eventData
194
- */
195
- protected async processEvent(chainIdentifier: string, eventData: ChainEvent<SwapData>[]): Promise<boolean> {
196
- for(let event of eventData) {
197
- if(!(event instanceof SpvVaultEvent)) continue;
198
-
199
- const vault = await this.Vaults.getVault(chainIdentifier, event.owner, event.vaultId);
200
- if(vault==null) continue;
201
-
202
- if(event instanceof SpvVaultOpenEvent) {
203
- await this.Vaults.processOpenEvent(vault, event);
204
- } else if(event instanceof SpvVaultCloseEvent) {
205
- await this.Vaults.processCloseEvent(vault, event);
206
- } else if(event instanceof SpvVaultClaimEvent) {
207
- const swap = this.btcTxIdIndex.get(event.btcTxId);
208
-
209
- if(swap!=null) {
210
- swap.txIds.claim = (event as any).meta?.txId;
211
- if(swap.metadata!=null) swap.metadata.times.claimTxReceived = Date.now();
212
- }
213
-
214
- await this.Vaults.processClaimEvent(vault, swap, event);
215
- await this.processClaimEvent(swap, event);
216
- } else if(event instanceof SpvVaultDepositEvent) {
217
- await this.Vaults.processDepositEvent(vault, event);
218
- }
219
- }
220
-
221
- return true;
222
- }
223
-
224
- /**
225
- * Initializes chain events subscription
226
- */
227
- protected subscribeToEvents() {
228
- for(let key in this.chains.chains) {
229
- this.chains.chains[key].chainEvents.registerListener((events: ChainEvent<SwapData>[]) => this.processEvent(key, events));
230
- }
231
- this.logger.info("SC: Events: subscribed to smartchain events");
232
- }
233
-
234
- async startWatchdog() {
235
- await super.startWatchdog();
236
- await this.Vaults.startVaultsWatchdog();
237
- }
238
-
239
- async init(): Promise<void> {
240
- await this.storageManager.loadData(SpvVaultSwap);
241
- for(let {obj: swap, hash, sequence} of await this.storageManager.query([])) {
242
- if(swap.btcTxId!=null) this.btcTxIdIndex.set(swap.btcTxId, swap);
243
- }
244
- await this.Vaults.init();
245
- if(this.stickyAddresses!=null) {
246
- await this.stickyAddresses.init();
247
- await this.stickyAddresses.loadData(StickyAddress);
248
- }
249
- this.subscribeToEvents();
250
- await PluginManager.serviceInitialize(this);
251
- }
252
-
253
- protected async processPastSwap(swap: SpvVaultSwap): Promise<void> {
254
- if(swap.state===SpvVaultSwapState.CREATED) {
255
- if(swap.expiry < Date.now()/1000) {
256
- await this.removeSwapData(swap, SpvVaultSwapState.EXPIRED);
257
- if(!swap.hasStickyAddress) await this.bitcoin.addUnusedAddress(swap.btcAddress);
258
- }
259
- }
260
-
261
- if(swap.state===SpvVaultSwapState.SIGNED) {
262
- if(swap.sending) return;
263
- const vault = await this.Vaults.getVault(swap.chainIdentifier, swap.vaultOwner, swap.vaultId);
264
- const foundWithdrawal = vault.pendingWithdrawals.find(val => val.btcTx.txid === swap.btcTxId);
265
- let tx = foundWithdrawal?.btcTx;
266
- if(tx==null) tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
267
-
268
- if(tx==null) {
269
- await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
270
- return;
271
- } else if(tx.confirmations===0) {
272
- await swap.setState(SpvVaultSwapState.SENT)
273
- await this.saveSwapData(swap);
274
- return;
275
- } else {
276
- await swap.setState(SpvVaultSwapState.BTC_CONFIRMED)
277
- await this.saveSwapData(swap);
278
- }
279
- }
280
-
281
- if(swap.state===SpvVaultSwapState.SENT || swap.state===SpvVaultSwapState.BTC_CONFIRMED) {
282
- //Check if confirmed or double-spent
283
- if(swap.sending) return;
284
- const vault = await this.Vaults.getVault(swap.chainIdentifier, swap.vaultOwner, swap.vaultId);
285
- const foundWithdrawal = vault.pendingWithdrawals.find(val => val.btcTx.txid === swap.btcTxId);
286
- let tx = foundWithdrawal?.btcTx;
287
- if(tx==null) tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
288
-
289
- if(tx==null) {
290
- await this.removeSwapData(swap, SpvVaultSwapState.DOUBLE_SPENT);
291
- return;
292
- } else if(tx.confirmations > 0) {
293
- if(swap.state!==SpvVaultSwapState.BTC_CONFIRMED) {
294
- await swap.setState(SpvVaultSwapState.BTC_CONFIRMED)
295
- await this.saveSwapData(swap);
296
- }
297
- }
298
- }
299
- }
300
-
301
- protected async processPastSwaps(): Promise<void> {
302
- const swaps = await this.storageManager.query([
303
- {
304
- key: "state",
305
- value: [
306
- SpvVaultSwapState.CREATED, //Check if expired
307
- SpvVaultSwapState.SIGNED, //Check if sent
308
- SpvVaultSwapState.SENT //Check if confirmed or double-spent
309
- ]
310
- }
311
- ]);
312
-
313
- for(let {obj: swap} of swaps) {
314
- await this.processPastSwap(swap);
315
- }
316
- }
317
-
318
- protected getPricePrefetches(chainIdentifier: string, token: string, gasToken: string, abortController: AbortController) {
319
- const pricePrefetchPromise: Promise<bigint> = this.swapPricing.preFetchPrice(token, chainIdentifier).catch(e => {
320
- this.logger.error("getPricePrefetches(): pricePrefetchPromise error: ", e);
321
- abortController.abort(e);
322
- return null;
323
- });
324
- const gasTokenPricePrefetchPromise: Promise<bigint> = token===gasToken ?
325
- pricePrefetchPromise :
326
- this.swapPricing.preFetchPrice(gasToken, chainIdentifier).catch(e => {
327
- this.logger.error("getPricePrefetches(): gasTokenPricePrefetchPromise error: ", e);
328
- abortController.abort(e);
329
- return null;
330
- });
331
- return {pricePrefetchPromise, gasTokenPricePrefetchPromise};
332
- }
333
-
334
- startRestServer(restServer: Express): void {
335
- restServer.use(this.path+"/getQuote", serverParamDecoder(10*1000));
336
- restServer.post(this.path+"/getQuote", expressHandlerWrapper(async (req: Request & {paramReader: IParamReader}, res: Response & {responseStream: ServerParamEncoder}) => {
337
- const metadata: {
338
- request: any,
339
- times: {[key: string]: number},
340
- } = {request: {}, times: {}};
341
-
342
- const chainIdentifier = req.query.chain as string;
343
- const {signer, chainInterface, spvVaultContract} = this.getChain(chainIdentifier);
344
-
345
- metadata.times.requestReceived = Date.now();
346
-
347
- /**
348
- * address: string smart chain address of the recipient
349
- * token: string Desired token to use
350
- * gasToken: string
351
- */
352
- const preFetchParsedBody = await req.paramReader.getParams({
353
- address: (val: string) => val!=null &&
354
- typeof(val)==="string" &&
355
- chainInterface.isValidAddress(val, true) ? val : null,
356
- token: (val: string) => val!=null &&
357
- typeof(val)==="string" &&
358
- this.isTokenSupported(chainIdentifier, val) ? val : null,
359
- gasToken: (val: string) => val!=null &&
360
- typeof(val)==="string" &&
361
- chainInterface.isValidToken(val) ? val : null
362
- });
363
- if(preFetchParsedBody==null) throw {
364
- code: 20100,
365
- msg: "Invalid request body"
366
- };
367
-
368
- const stickyAddressObject = req.paramReader.getExistingParamsOrNull({
369
- stickyAddress: FieldTypeEnum.BooleanOptional
370
- });
371
- const useStickyAddress = stickyAddressObject?.stickyAddress;
372
-
373
- //Create abortController for parallel prefetches
374
- const responseStream = res.responseStream;
375
- const abortController = getAbortController(responseStream);
376
-
377
- //Pre-fetch data
378
- const {
379
- pricePrefetchPromise,
380
- gasTokenPricePrefetchPromise
381
- } = this.getPricePrefetches(chainIdentifier, preFetchParsedBody.token, preFetchParsedBody.gasToken, abortController);
382
- const nativeBalancePrefetch = this.prefetchNativeBalanceIfNeeded(chainIdentifier, abortController);
383
- const btcFeeRatePrefetch: Promise<number> = this.bitcoin.getFeeRate().catch(e => {
384
- abortController.abort(e);
385
- return null;
386
- });
387
-
388
- //Listener that re-adds the returned bitcoin address to the unused address list if request fails or closes
389
- let abortAddUnusedAddressListener: () => void;
390
-
391
- const bitcoinAddressPrefetch: Promise<{address: string, isStickyAddress: boolean} | null> = (async () => {
392
- if(useStickyAddress) {
393
- const result = await this.getStickyAddress(chainIdentifier, preFetchParsedBody.address);
394
- if(result!=null) return {address: result, isStickyAddress: true};
395
- }
396
-
397
- const value = await this.bitcoin.getAddress();
398
-
399
- //Already aborted
400
- if(abortController.signal.aborted) {
401
- this.bitcoin.addUnusedAddress(value);
402
- return null;
403
- }
404
- //Not aborted yet, add an event listener to re-add the address to the unused list
405
- abortController.signal.addEventListener("abort", abortAddUnusedAddressListener = () => {
406
- this.bitcoin.addUnusedAddress(value);
407
- });
408
- return {address: value, isStickyAddress: false};
409
- })().catch(e => {
410
- abortController.abort(e);
411
- return null;
412
- });
413
-
414
- /**
415
- * amount: string amount (in sats)
416
- * gasAmount: string Desired amount in gas token to also get
417
- * exactOut: boolean Whether the swap should be an exact out instead of exact in swap
418
- * callerFeeRate: string Caller/watchtower fee (in output token) to assign to the swap
419
- * frontingFeeRate: string Fronting fee (in output token) to assign to the swap
420
- */
421
- const actualParsedBody = await req.paramReader.getParams({
422
- amount: FieldTypeEnum.BigInt,
423
- gasAmount: FieldTypeEnum.BigInt,
424
- exactOut: FieldTypeEnum.BooleanOptional,
425
- callerFeeRate: FieldTypeEnum.BigInt,
426
- frontingFeeRate: FieldTypeEnum.BigInt,
427
- });
428
- abortController.signal.throwIfAborted();
429
- if(actualParsedBody==null) throw {
430
- code: 20100,
431
- msg: "Invalid request body"
432
- };
433
-
434
- const inputAmountAdjustments = req.paramReader.getExistingParamsOrNull({
435
- amountUtxos: FieldTypeEnum.AnyOptional,
436
- amountFeeRate: FieldTypeEnum.NumberOptional
437
- });
438
- if(inputAmountAdjustments==null) throw {
439
- code: 20100,
440
- msg: "Invalid request body"
441
- };
442
-
443
- const clientInputUtxos: AmountAdjustUtxo[] | null = inputAmountAdjustments?.amountUtxos!=null
444
- ? parseAmountAdjustUtxos(inputAmountAdjustments.amountUtxos)
445
- : null;
446
- if(inputAmountAdjustments?.amountUtxos!=null && clientInputUtxos==null) throw {
447
- code: 20100,
448
- msg: "Invalid request body (amountUtxos)"
449
- };
450
-
451
- const parsedBody: SpvVaultSwapRequestType = {...preFetchParsedBody, ...actualParsedBody};
452
- metadata.request = parsedBody;
453
-
454
- if(parsedBody.gasToken!==chainInterface.getNativeCurrencyAddress()) throw {
455
- code: 20190,
456
- msg: "Unsupported gas token"
457
- };
458
-
459
- if(parsedBody.callerFeeRate < 0n || parsedBody.callerFeeRate >= 2n**20n) throw {
460
- code: 20191,
461
- msg: "Invalid caller fee rate"
462
- };
463
- if(parsedBody.frontingFeeRate < 0n || parsedBody.frontingFeeRate >= 2n**20n) throw {
464
- code: 20192,
465
- msg: "Invalid fronting fee rate"
466
- };
467
-
468
- const requestedAmount = {
469
- input: !parsedBody.exactOut,
470
- amount: parsedBody.exactOut ?
471
- (parsedBody.amount * (100_000n + parsedBody.callerFeeRate + parsedBody.frontingFeeRate) / 100_000n) :
472
- parsedBody.amount,
473
- token: parsedBody.token
474
- };
475
- if(clientInputUtxos!=null) {
476
- if(parsedBody.exactOut) throw {
477
- code: 20193,
478
- msg: "amountAdjustUtxos cannot be specified for exactOut swaps!"
479
- };
480
-
481
- let btcFeeRate = await btcFeeRatePrefetch;
482
- if(inputAmountAdjustments.amountFeeRate!=null && inputAmountAdjustments.amountFeeRate>btcFeeRate)
483
- btcFeeRate = inputAmountAdjustments.amountFeeRate;
484
-
485
- let feeAccumulator: number = 0;
486
- let valueAccumulator: number = 0;
487
- for(let utxo of clientInputUtxos) {
488
- const cpfpAdditionalFee: number = utxo.cpfp==null ? 0 : Math.ceil(utxo.cpfp.effectiveVSize * Math.max(0, btcFeeRate - utxo.cpfp.effectiveFeeRate));
489
- const spendFee: number = utxo.vSize * btcFeeRate;
490
- const totalFee: number = cpfpAdditionalFee + spendFee;
491
- if(totalFee > utxo.value) continue; //Skip detrimental UTXO
492
- feeAccumulator += totalFee;
493
- valueAccumulator += utxo.value;
494
- }
495
-
496
- let baseTxVSize: number = 10.5; // 4b version, 1b inputs, 1b outputs, 4b locktime, 0.5vB witness flag + witness elements count
497
- //vault input and output
498
- baseTxVSize += 32 + 4 + 1 + 4; //Input base
499
- baseTxVSize += this.vaultSigner.getAddressType()==="p2tr" ? (1+1+65)/4 : (1+1+72+1+33)/4;
500
- baseTxVSize += 8 + 1; //Output base
501
- baseTxVSize += this.vaultSigner.getAddressType()==="p2tr" ? 34 : 22;
502
- //opreturn output
503
- baseTxVSize += 8 + 1; //Output base
504
- const opReturnDataSize = spvVaultContract.toOpReturnData(parsedBody.address, parsedBody.gasAmount > 0 ? [0xffffffffffffffffn, 0xffffffffffffffffn] : [0xffffffffffffffffn]).length;
505
- baseTxVSize += (opReturnDataSize <= 0x4b ? 2 : 3 /*Needs an OP_PUSHDATA1 opcode*/) + opReturnDataSize;
506
- //LP output
507
- baseTxVSize += 8 + 1; //Output base
508
- baseTxVSize += this.bitcoin.getAddressType()==="p2tr" ? 34 : this.bitcoin.getAddressType()==="p2wpkh" ? 22 : 23;
509
-
510
- const baseTxFee = Math.ceil(baseTxVSize) * btcFeeRate;
511
- feeAccumulator += baseTxFee;
512
-
513
- const amount = Math.floor(valueAccumulator - Math.ceil(feeAccumulator));
514
- requestedAmount.amount = BigInt(amount);
515
- }
516
-
517
- const gasTokenAmount = {
518
- input: false,
519
- amount: parsedBody.gasAmount * (100_000n + parsedBody.callerFeeRate + parsedBody.frontingFeeRate) / 100_000n,
520
- token: parsedBody.gasToken
521
- } as const;
522
- const request = {
523
- chainIdentifier,
524
- raw: req,
525
- parsed: parsedBody,
526
- metadata
527
- };
528
- const useToken = parsedBody.token;
529
- const gasToken = parsedBody.gasToken;
530
-
531
- this.checkTooManyInflightSwaps();
532
-
533
- //Check request params
534
- const fees = await this.AmountAssertions.preCheckFromBtcAmounts(this.type, request, requestedAmount, gasTokenAmount);
535
- metadata.times.requestChecked = Date.now();
536
-
537
- await this.checkNativeBalance(chainIdentifier, nativeBalancePrefetch, abortController.signal);
538
-
539
- //Check valid amount specified (min/max)
540
- let {
541
- amountBD,
542
- swapFee,
543
- swapFeeInToken,
544
- totalInToken,
545
- amountBDgas,
546
- gasSwapFee,
547
- gasSwapFeeInToken,
548
- totalInGasToken
549
- } = await this.AmountAssertions.checkFromBtcAmount(
550
- this.type,
551
- request,
552
- {...requestedAmount, pricePrefetch: pricePrefetchPromise},
553
- fees,
554
- abortController.signal,
555
- {...gasTokenAmount, pricePrefetch: gasTokenPricePrefetchPromise}
556
- );
557
- metadata.times.priceCalculated = Date.now();
558
-
559
- const totalBtcOutput = amountBD + amountBDgas;
560
-
561
- //Check if we have enough funds to honor the request
562
- let vault: SpvVault;
563
- do {
564
- vault = await this.Vaults.findVaultForSwap(chainIdentifier, totalBtcOutput, useToken, totalInToken, gasToken, totalInGasToken);
565
- } while (await this.Vaults.checkVaultReplacedTransactions(vault, true));
566
- abortController.signal.throwIfAborted();
567
- metadata.times.vaultPicked = Date.now();
568
-
569
- //Create swap receive bitcoin address
570
- const btcFeeRate = await btcFeeRatePrefetch;
571
- const btcAddressObject = await bitcoinAddressPrefetch;
572
- abortController.signal.throwIfAborted();
573
- metadata.times.addressCreated = Date.now();
574
-
575
- const receiveAddress = btcAddressObject.address;
576
- const hasStickyAddress = btcAddressObject.isStickyAddress;
577
-
578
- //Adjust the amounts based on passed fees
579
- if(parsedBody.exactOut) {
580
- totalInToken = parsedBody.amount;
581
- } else {
582
- totalInToken = (totalInToken * 100_000n / (100_000n + parsedBody.callerFeeRate + parsedBody.frontingFeeRate));
583
- }
584
- totalInGasToken = (totalInGasToken * 100_000n / (100_000n + parsedBody.callerFeeRate + parsedBody.frontingFeeRate));
585
-
586
- //Calculate raw amounts
587
- const [rawTokenAmount, rawGasTokenAmount] = vault.toRawAmounts([totalInToken, totalInGasToken]);
588
- [totalInToken, totalInGasToken] = vault.fromRawAmounts([rawTokenAmount, rawGasTokenAmount]);
589
-
590
- const expiry = Math.floor(Date.now() / 1000) + this.getInitAuthorizationTimeout(chainIdentifier);
591
-
592
- //Get PSBT data
593
- const callerFeeShare = parsedBody.callerFeeRate;
594
- const frontingFeeShare = parsedBody.frontingFeeRate;
595
- const executionFeeShare = 0n;
596
- const utxo = vault.getLatestUtxo();
597
-
598
- const quoteId = randomBytes(32).toString("hex");
599
- const swap = new SpvVaultSwap(
600
- chainIdentifier, quoteId, expiry,
601
- vault, utxo,
602
- receiveAddress, btcFeeRate, parsedBody.address, totalBtcOutput, totalInToken, totalInGasToken,
603
- swapFee, swapFeeInToken, gasSwapFee, gasSwapFeeInToken,
604
- callerFeeShare, frontingFeeShare, executionFeeShare,
605
- useToken, gasToken
606
- );
607
- swap.metadata = metadata;
608
- swap.saveStickyAddress = useStickyAddress && !hasStickyAddress
609
- swap.hasStickyAddress = hasStickyAddress;
610
-
611
- //We can remove the listener to add unused address now, as we are about to save the swap
612
- abortController.signal.removeEventListener("abort", abortAddUnusedAddressListener);
613
- await PluginManager.swapCreate(swap);
614
- await this.saveSwapData(swap);
615
-
616
- this.swapLogger.info(swap, "REST: /getQuote: Created swap address: "+receiveAddress+" amount: "+totalBtcOutput.toString(10));
617
-
618
- await responseStream.writeParamsAndEnd({
619
- code: 20000,
620
- msg: "Success",
621
- data: {
622
- quoteId,
623
- expiry,
624
-
625
- address: signer.getAddress(),
626
- vaultId: vault.data.getVaultId().toString(10),
627
-
628
- vaultBtcAddress: vault.btcAddress,
629
- btcAddress: receiveAddress,
630
- btcUtxo: utxo,
631
- btcFeeRate,
632
-
633
- btcAmount: totalBtcOutput.toString(10),
634
- btcAmountSwap: amountBD.toString(10),
635
- btcAmountGas: amountBDgas.toString(10),
636
-
637
- total: totalInToken.toString(10),
638
- totalGas: totalInGasToken.toString(10),
639
-
640
- totalFeeBtc: (swapFee + gasSwapFee).toString(10),
641
-
642
- swapFeeBtc: swapFee.toString(10),
643
- swapFee: swapFeeInToken.toString(10),
644
-
645
- gasSwapFeeBtc: gasSwapFee.toString(10),
646
- gasSwapFee: gasSwapFeeInToken.toString(10),
647
-
648
- callerFeeShare: callerFeeShare.toString(10),
649
- frontingFeeShare: frontingFeeShare.toString(10),
650
- executionFeeShare: executionFeeShare.toString(10),
651
-
652
- usedUtxoInputCalculation: clientInputUtxos!=null
653
- }
654
- });
655
- }));
656
-
657
- restServer.use(this.path+"/postQuote", serverParamDecoder(10*1000));
658
- restServer.post(this.path+"/postQuote", expressHandlerWrapper(async (req: Request & {paramReader: IParamReader}, res: Response & {responseStream: ServerParamEncoder}) => {
659
- let requestReceived = Date.now();
660
-
661
- const parsedBody: SpvVaultPostQuote = await req.paramReader.getParams({
662
- quoteId: FieldTypeEnum.String,
663
- psbtHex: (val: string) => val!=null &&
664
- typeof(val)==="string" &&
665
- HEX_REGEX.test(val) ? val : null
666
- });
667
-
668
- const swap = await this.storageManager.getData(parsedBody.quoteId, 0n);
669
- if(swap==null || swap.state!==SpvVaultSwapState.CREATED || swap.expiry < Date.now()/1000) throw {
670
- code: 20505,
671
- msg: "Invalid quote ID, not found or expired!"
672
- };
673
-
674
- const metadata: {
675
- times: {[key: string]: number},
676
- error?: any
677
- } = swap.metadata;
678
- metadata.times ??= {};
679
- metadata.times.requestReceived = requestReceived;
680
-
681
- this.checkTooManyInflightSwaps();
682
-
683
- const vault = await this.Vaults.getVault(swap.chainIdentifier, swap.vaultOwner, swap.vaultId);
684
- if(vault==null || !vault.isReady()) {
685
- throw {
686
- code: 20506,
687
- msg: "Used vault not found!"
688
- };
689
- }
690
-
691
- //Try parse psbt
692
- let transaction: Transaction;
693
- try {
694
- transaction = Transaction.fromPSBT(Buffer.from(parsedBody.psbtHex, "hex"));
695
- } catch (e) {
696
- this.swapLogger.error(swap, "REST: /postQuote: failed to parse provided PSBT: ", e);
697
- throw {
698
- code: 20507,
699
- msg: "Error parsing PSBT, hex format required!"
700
- };
701
- }
702
-
703
- for(let i=1;i<transaction.inputsLength;i++) { //Skip first vault input
704
- const txIn = transaction.getInput(i);
705
- if (isLegacyInput(txIn)) throw {
706
- code: 20514,
707
- msg: "Legacy (pre-segwit) inputs in tx are not allowed!"
708
- };
709
- }
710
-
711
- //Check the posted quote with the plugins
712
- AmountAssertions.handlePluginErrorResponses(await PluginManager.onHandlePostedFromBtcQuote(
713
- this.type,
714
- {chainIdentifier: swap.chainIdentifier, raw: req, parsed: parsedBody, metadata},
715
- swap
716
- ));
717
-
718
- //Check correct psbt
719
- for(let i=1;i<transaction.inputsLength;i++) { //Skip first vault input
720
- const txIn = transaction.getInput(i);
721
- //Check UTXOs exist and are unspent
722
- if(await this.bitcoinRpc.isSpent(Buffer.from(txIn.txid).toString("hex")+":"+txIn.index.toString(10))) throw {
723
- code: 20515,
724
- msg: "Spent UTXO in inputs!"
725
- };
726
- }
727
-
728
- const {spvVaultContract} = this.getChain(swap.chainIdentifier);
729
-
730
- let data: SpvWithdrawalTransactionData;
731
- try {
732
- data = await spvVaultContract.getWithdrawalData(parsePsbt(transaction));
733
- } catch (e) {
734
- this.swapLogger.error(swap, "REST: /postQuote: failed to parse PSBT to withdrawal tx data: ", e);
735
- throw {
736
- code: 20508,
737
- msg: "PSBT transaction cannot be parsed!"
738
- };
739
- }
740
-
741
- if(
742
- !data.isRecipient(swap.recipient) ||
743
- data.callerFeeRate!==swap.callerFeeShare ||
744
- data.frontingFeeRate!==swap.frontingFeeShare ||
745
- data.executionFeeRate!==swap.executionFeeShare ||
746
- data.rawAmounts[0]!==swap.rawAmountToken ||
747
- data.rawAmounts[1]!==swap.rawAmountGasToken ||
748
- data.getExecutionData()!=null ||
749
- data.getSpentVaultUtxo()!==swap.vaultUtxo ||
750
- data.btcTx.outs[0].value!==VAULT_DUST_AMOUNT ||
751
- !Buffer.from(data.btcTx.outs[0].scriptPubKey.hex, "hex").equals(this.bitcoin.toOutputScript(swap.vaultAddress)) ||
752
- BigInt(data.btcTx.outs[2].value)!==swap.amountBtc ||
753
- !Buffer.from(data.btcTx.outs[2].scriptPubKey.hex, "hex").equals(this.bitcoin.toOutputScript(swap.btcAddress)) ||
754
- (data.btcTx.locktime > 0 && data.btcTx.locktime < 500_000_000) ||
755
- data.btcTx.locktime > Math.floor(Date.now()/1000) - 1_000_000
756
- ) {
757
- this.swapLogger.error(swap, "REST: /postQuote: Invalid psbt data submitted, raw psbt hex: ", parsedBody.psbtHex);
758
- throw {
759
- code: 20509,
760
- msg: "Invalid PSBT provided!"
761
- };
762
- }
763
-
764
- if(swap.vaultUtxo!==vault.getLatestUtxo()) {
765
- throw {
766
- code: 20510,
767
- msg: "Vault UTXO already spent, please get another quote and try again!"
768
- };
769
- }
770
-
771
- //Create abortController for parallel prefetches
772
- const responseStream = res.responseStream;
773
-
774
- const signedTx = await this.vaultSigner.signPsbt(swap.chainIdentifier, swap.vaultId, transaction, [0]);
775
- if(!signedTx.isFinal) throw {
776
- code: 20513,
777
- msg: "One or more PSBT inputs not finalized!"
778
- };
779
-
780
- const effectiveFeeRate = await this.bitcoinRpc.getEffectiveFeeRate(parsePsbt(signedTx));
781
- if(effectiveFeeRate.feeRate < 1 || Math.round(effectiveFeeRate.feeRate) < swap.btcFeeRate) throw {
782
- code: 20511,
783
- msg: "Bitcoin transaction fee too low, expected minimum: "+swap.btcFeeRate+" adjusted effective fee rate: "+effectiveFeeRate.feeRate
784
- }
785
-
786
- const txVsize = signedTx.vsize;
787
- if(txVsize>TX_MAX_VSIZE) throw {
788
- code: 20516,
789
- msg: "Bitcoin transaction size too large, maximum: "+TX_MAX_VSIZE+" actual: "+txVsize
790
- };
791
-
792
- const pluginCheckResult = await PluginManager.onHandlePreFromBtcExecute(
793
- SwapHandlerType.FROM_BTC_SPV,
794
- swap
795
- );
796
- if(isQuoteThrow(pluginCheckResult)) {
797
- const error = {
798
- code: 29999,
799
- msg: pluginCheckResult.message
800
- }
801
- if(swap.metadata!=null) swap.metadata.postQuoteError = error;
802
- await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
803
- throw error;
804
- }
805
-
806
- await this.Vaults.checkVaultReplacedTransactions(vault, true);
807
- if(swap.vaultUtxo!==vault.getLatestUtxo()) {
808
- throw {
809
- code: 20510,
810
- msg: "Vault UTXO already spent, please get another quote and try again!"
811
- };
812
- }
813
-
814
- try {
815
- const btcRawTx = Buffer.from(signedTx.toBytes(true, true)).toString("hex");
816
-
817
- //Double-check the state to prevent race condition
818
- if(swap.state!==SpvVaultSwapState.CREATED) {
819
- throw {
820
- code: 20505,
821
- msg: "Invalid quote ID, not found or expired!"
822
- };
823
- }
824
-
825
- //Double check in-flight swap count
826
- this.checkTooManyInflightSwaps();
827
-
828
- swap.btcTxId = signedTx.id;
829
- swap.state = SpvVaultSwapState.SIGNED;
830
- swap.sending = true;
831
- await this.saveSwapData(swap);
832
-
833
- data.btcTx.raw = btcRawTx;
834
- (data as any).sending = true;
835
- vault.addWithdrawal(data);
836
- await this.Vaults.saveVault(vault);
837
-
838
- this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: "+swap.btcTxId);
839
-
840
- try {
841
- await this.bitcoin.sendRawTransaction(btcRawTx);
842
- await swap.setState(SpvVaultSwapState.SENT);
843
- (data as any).sending = false;
844
- swap.sending = false;
845
- } catch (e) {
846
- this.swapLogger.error(swap, "REST: /postQuote: Failed to send BTC transaction: ", e);
847
- throw {
848
- code: 20512,
849
- msg: "Error broadcasting bitcoin transaction!"
850
- };
851
- }
852
- } catch (e) {
853
- (data as any).sending = false;
854
- swap.sending = false;
855
- vault.removeWithdrawal(data);
856
- await this.Vaults.saveVault(vault);
857
-
858
- if(isDefinedRuntimeError(e) && swap.metadata!=null) swap.metadata.postQuoteError = e;
859
- await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
860
-
861
- throw e;
862
- }
863
-
864
- await responseStream.writeParamsAndEnd({
865
- code: 20000,
866
- msg: "Success",
867
- data: {
868
- txId: swap.btcTxId
869
- }
870
- });
871
- }));
872
- }
873
-
874
- getInfoData(): any {
875
- const mappedDict = {};
876
- for(let chainId in this.config.gasTokenMax) {
877
- mappedDict[chainId] = {
878
- gasToken: this.getChain(chainId).chainInterface.getNativeCurrencyAddress(),
879
- max: this.config.gasTokenMax[chainId].toString(10)
880
- };
881
- }
882
- return {
883
- gasTokens: mappedDict
884
- };
885
- }
886
-
887
- protected async saveSwapData(swap: SpvVaultSwap): Promise<void> {
888
- if(swap.btcTxId!=null) this.btcTxIdIndex.set(swap.btcTxId, swap);
889
- return super.saveSwapData(swap);
890
- }
891
-
892
- protected async removeSwapData(swap: SpvVaultSwap, ultimateState?: SpvVaultSwapState): Promise<void> {
893
- if(swap.btcTxId!=null) this.btcTxIdIndex.delete(swap.btcTxId);
894
- return super.removeSwapData(swap, ultimateState);
895
- }
896
-
897
- }
1
+ import {MultichainData, SwapBaseConfig, SwapHandler, SwapHandlerType} from "../SwapHandler";
2
+ import {Express, Request, Response} from "express";
3
+ import {IBitcoinWallet} from "../../wallets/IBitcoinWallet";
4
+ import {
5
+ BitcoinRpc,
6
+ BtcBlock,
7
+ ChainEvent,
8
+ IStorageManager,
9
+ SpvVaultClaimEvent,
10
+ SpvVaultCloseEvent,
11
+ SpvVaultDepositEvent,
12
+ SpvVaultEvent,
13
+ SpvVaultOpenEvent,
14
+ SpvWithdrawalTransactionData,
15
+ SwapData
16
+ } from "@atomiqlabs/base";
17
+ import {IIntermediaryStorage} from "../../storage/IIntermediaryStorage";
18
+ import {ISwapPrice} from "../../prices/ISwapPrice";
19
+ import {SpvVaultSwap, SpvVaultSwapState} from "./SpvVaultSwap";
20
+ import {ISpvVaultSigner} from "../../wallets/ISpvVaultSigner";
21
+ import {PluginManager} from "../../plugins/PluginManager";
22
+ import {SpvVault} from "./SpvVault";
23
+ import {serverParamDecoder} from "../../utils/paramcoders/server/ServerParamDecoder";
24
+ import {
25
+ expressHandlerWrapper,
26
+ getAbortController,
27
+ HEX_REGEX,
28
+ isDefinedRuntimeError,
29
+ parsePsbt
30
+ } from "../../utils/Utils";
31
+ import {IParamReader} from "../../utils/paramcoders/IParamReader";
32
+ import {ServerParamEncoder} from "../../utils/paramcoders/server/ServerParamEncoder";
33
+ import {FieldTypeEnum} from "../../utils/paramcoders/SchemaVerifier";
34
+ import {FromBtcAmountAssertions} from "../assertions/FromBtcAmountAssertions";
35
+ import {randomBytes} from "crypto";
36
+ import {Transaction} from "@scure/btc-signer";
37
+ import {SpvVaults, VAULT_DUST_AMOUNT} from "./SpvVaults";
38
+ import {isLegacyInput} from "../../utils/BitcoinUtils";
39
+ import {AmountAssertions} from "../assertions/AmountAssertions";
40
+ import {isQuoteThrow} from "../../plugins/IPlugin";
41
+ import {StickyAddress} from "./StickyAddress";
42
+
43
+ export type SpvVaultSwapHandlerConfig = SwapBaseConfig & {
44
+ vaultsCheckInterval: number,
45
+ gasTokenMax: {[chainId: string]: bigint},
46
+ maxUnclaimedWithdrawals?: number
47
+ };
48
+
49
+ export type SpvVaultSwapRequestType = {
50
+ address: string,
51
+ amount: bigint,
52
+ token: string,
53
+ gasAmount: bigint,
54
+ gasToken: string,
55
+ exactOut?: boolean,
56
+ callerFeeRate: bigint,
57
+ frontingFeeRate: bigint
58
+ };
59
+
60
+ export type SpvVaultPostQuote = {
61
+ quoteId: string,
62
+ psbtHex: string
63
+ }
64
+
65
+ const TX_MAX_VSIZE = 16*1024;
66
+
67
+ type AmountAdjustUtxo = {
68
+ value: number,
69
+ vSize: number,
70
+ cpfp?: {
71
+ effectiveVSize: number,
72
+ effectiveFeeRate: number
73
+ }
74
+ }
75
+
76
+ function parseAmountAdjustUtxos(amountAdjustUtxos: any): AmountAdjustUtxo[] {
77
+ if(!Array.isArray(amountAdjustUtxos)) return null;
78
+ if(amountAdjustUtxos.length > 250) return null;
79
+ const validArray = amountAdjustUtxos.every(value =>
80
+ value!=null && typeof(value)==="object" && typeof(value.value)==="number" && typeof(value.vSize)==="number" &&
81
+ (value.cpfp==null || (typeof(value.cpfp)==="object" && typeof(value.cpfp.effectiveVSize)==="number" && typeof(value.cpfp.effectiveFeeRate)==="number"))
82
+ );
83
+ if(!validArray) return null;
84
+ return amountAdjustUtxos;
85
+ }
86
+
87
+ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapState> {
88
+ readonly type = SwapHandlerType.FROM_BTC_SPV;
89
+ readonly inflightSwapStates = new Set([SpvVaultSwapState.SIGNED, SpvVaultSwapState.SENT, SpvVaultSwapState.BTC_CONFIRMED]);
90
+
91
+ readonly bitcoin: IBitcoinWallet;
92
+ readonly bitcoinRpc: BitcoinRpc<BtcBlock>;
93
+ readonly vaultSigner: ISpvVaultSigner;
94
+
95
+ readonly btcTxIdIndex: Map<string, SpvVaultSwap> = new Map();
96
+
97
+ readonly AmountAssertions: FromBtcAmountAssertions;
98
+ readonly Vaults: SpvVaults;
99
+
100
+ config: SpvVaultSwapHandlerConfig;
101
+
102
+ readonly stickyAddresses?: IStorageManager<StickyAddress>;
103
+
104
+ constructor(
105
+ storageDirectory: IIntermediaryStorage<SpvVaultSwap>,
106
+ vaultStorage: IStorageManager<SpvVault>,
107
+ path: string,
108
+ chainsData: MultichainData,
109
+ swapPricing: ISwapPrice,
110
+ bitcoin: IBitcoinWallet,
111
+ bitcoinRpc: BitcoinRpc<BtcBlock>,
112
+ spvVaultSigner: ISpvVaultSigner,
113
+ config: SpvVaultSwapHandlerConfig,
114
+ stickyAddresses?: IStorageManager<StickyAddress>
115
+ ) {
116
+ super(storageDirectory, path, chainsData, swapPricing);
117
+ this.bitcoinRpc = bitcoinRpc;
118
+ this.bitcoin = bitcoin;
119
+ this.vaultSigner = spvVaultSigner;
120
+ this.config = config;
121
+ this.AmountAssertions = new FromBtcAmountAssertions(config, swapPricing);
122
+ this.Vaults = new SpvVaults(vaultStorage, bitcoin, spvVaultSigner, bitcoinRpc, this.chains, config);
123
+ this.stickyAddresses = stickyAddresses;
124
+
125
+ for(let chain in this.allowedTokens) {
126
+ //Remove chains that don't support spv vault swaps
127
+ const {spvVaultContract} = this.getChain(chain);
128
+ if(spvVaultContract==null) this.allowedTokens[chain].clear();
129
+ }
130
+ }
131
+
132
+ private async getStickyAddress(chainId: string, address: string): Promise<string | undefined> {
133
+ if(this.stickyAddresses==null) throw new Error("Sticky addresses are not supported!");
134
+
135
+ const {chainInterface} = this.getChain(chainId);
136
+ const normalizedAddress = chainInterface.normalizeAddress(address);
137
+
138
+ const addressIdentifier = chainId+"-"+normalizedAddress;
139
+
140
+ const result = this.stickyAddresses.data[addressIdentifier];
141
+ if(result==null) return;
142
+
143
+ const btcAddress = result.address;
144
+
145
+ if(this.bitcoin.isOwnedAddress!=null) {
146
+ if(!(await this.bitcoin.isOwnedAddress(btcAddress))) {
147
+ this.logger.warn(`getStickyAddress(): Failed to get sticky address, address ${btcAddress} is not controlled by our bitcoin wallet!`);
148
+ return;
149
+ }
150
+ }
151
+
152
+ return btcAddress;
153
+ }
154
+
155
+ async addStickyAddress(chainId: string, address: string, btcAddress: string) {
156
+ if(this.stickyAddresses==null) throw new Error("Sticky addresses are not supported!");
157
+
158
+ const {chainInterface} = this.getChain(chainId);
159
+ const normalizedAddress = chainInterface.normalizeAddress(address);
160
+
161
+ if(this.bitcoin.isOwnedAddress!=null) {
162
+ if(!(await this.bitcoin.isOwnedAddress(btcAddress))) {
163
+ this.logger.warn(`addStickyAddress(): Failed to create sticky address, address ${btcAddress} is not controlled by our bitcoin wallet!`);
164
+ return;
165
+ }
166
+ }
167
+
168
+ const addressIdentifier = chainId+"-"+normalizedAddress;
169
+
170
+ if(this.stickyAddresses.data[addressIdentifier]!=null) {
171
+ this.logger.warn(`addStickyAddress(): Failed to create sticky address, address sticky address already exists for ${addressIdentifier}!`);
172
+ return;
173
+ }
174
+ await this.stickyAddresses.saveData(addressIdentifier, new StickyAddress(btcAddress));
175
+ }
176
+
177
+ protected async processClaimEvent(swap: SpvVaultSwap | null, event: SpvVaultClaimEvent): Promise<void> {
178
+ if(swap==null) return;
179
+ //Update swap
180
+ swap.txIds.claim = event.meta?.txId;
181
+ await this.removeSwapData(swap, SpvVaultSwapState.CLAIMED);
182
+ if(swap.saveStickyAddress) try {
183
+ await this.addStickyAddress(swap.chainIdentifier, swap.recipient, swap.btcAddress);
184
+ } catch (e) {
185
+ this.logger.error(`processClaimEvent(): Failed to create the sticky address for swap ${swap.getIdentifier()}`);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Chain event processor
191
+ *
192
+ * @param chainIdentifier
193
+ * @param eventData
194
+ */
195
+ protected async processEvent(chainIdentifier: string, eventData: ChainEvent<SwapData>[]): Promise<boolean> {
196
+ for(let event of eventData) {
197
+ if(!(event instanceof SpvVaultEvent)) continue;
198
+
199
+ const vault = await this.Vaults.getVault(chainIdentifier, event.owner, event.vaultId);
200
+ if(vault==null) continue;
201
+
202
+ if(event instanceof SpvVaultOpenEvent) {
203
+ await this.Vaults.processOpenEvent(vault, event);
204
+ } else if(event instanceof SpvVaultCloseEvent) {
205
+ await this.Vaults.processCloseEvent(vault, event);
206
+ } else if(event instanceof SpvVaultClaimEvent) {
207
+ const swap = this.btcTxIdIndex.get(event.btcTxId);
208
+
209
+ if(swap!=null) {
210
+ swap.txIds.claim = (event as any).meta?.txId;
211
+ if(swap.metadata!=null) swap.metadata.times.claimTxReceived = Date.now();
212
+ }
213
+
214
+ await this.Vaults.processClaimEvent(vault, swap, event);
215
+ await this.processClaimEvent(swap, event);
216
+ } else if(event instanceof SpvVaultDepositEvent) {
217
+ await this.Vaults.processDepositEvent(vault, event);
218
+ }
219
+ }
220
+
221
+ return true;
222
+ }
223
+
224
+ /**
225
+ * Initializes chain events subscription
226
+ */
227
+ protected subscribeToEvents() {
228
+ for(let key in this.chains.chains) {
229
+ this.chains.chains[key].chainEvents.registerListener((events: ChainEvent<SwapData>[]) => this.processEvent(key, events));
230
+ }
231
+ this.logger.info("SC: Events: subscribed to smartchain events");
232
+ }
233
+
234
+ async startWatchdog() {
235
+ await super.startWatchdog();
236
+ await this.Vaults.startVaultsWatchdog();
237
+ }
238
+
239
+ async init(): Promise<void> {
240
+ await this.storageManager.loadData(SpvVaultSwap);
241
+ for(let {obj: swap, hash, sequence} of await this.storageManager.query([])) {
242
+ if(swap.btcTxId!=null) this.btcTxIdIndex.set(swap.btcTxId, swap);
243
+ }
244
+ await this.Vaults.init();
245
+ if(this.stickyAddresses!=null) {
246
+ await this.stickyAddresses.init();
247
+ await this.stickyAddresses.loadData(StickyAddress);
248
+ }
249
+ this.subscribeToEvents();
250
+ await PluginManager.serviceInitialize(this);
251
+ }
252
+
253
+ protected async processPastSwap(swap: SpvVaultSwap): Promise<void> {
254
+ if(swap.state===SpvVaultSwapState.CREATED) {
255
+ if(swap.expiry < Date.now()/1000) {
256
+ await this.removeSwapData(swap, SpvVaultSwapState.EXPIRED);
257
+ if(!swap.hasStickyAddress) await this.bitcoin.addUnusedAddress(swap.btcAddress);
258
+ }
259
+ }
260
+
261
+ if(swap.state===SpvVaultSwapState.SIGNED) {
262
+ if(swap.sending) return;
263
+ const vault = await this.Vaults.getVault(swap.chainIdentifier, swap.vaultOwner, swap.vaultId);
264
+ const foundWithdrawal = vault.pendingWithdrawals.find(val => val.btcTx.txid === swap.btcTxId);
265
+ let tx = foundWithdrawal?.btcTx;
266
+ if(tx==null) tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
267
+
268
+ if(tx==null) {
269
+ await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
270
+ return;
271
+ } else if(tx.confirmations===0) {
272
+ await swap.setState(SpvVaultSwapState.SENT)
273
+ await this.saveSwapData(swap);
274
+ return;
275
+ } else {
276
+ await swap.setState(SpvVaultSwapState.BTC_CONFIRMED)
277
+ await this.saveSwapData(swap);
278
+ }
279
+ }
280
+
281
+ if(swap.state===SpvVaultSwapState.SENT || swap.state===SpvVaultSwapState.BTC_CONFIRMED) {
282
+ //Check if confirmed or double-spent
283
+ if(swap.sending) return;
284
+ const vault = await this.Vaults.getVault(swap.chainIdentifier, swap.vaultOwner, swap.vaultId);
285
+ const foundWithdrawal = vault.pendingWithdrawals.find(val => val.btcTx.txid === swap.btcTxId);
286
+ let tx = foundWithdrawal?.btcTx;
287
+ if(tx==null) tx = await this.bitcoinRpc.getTransaction(swap.btcTxId);
288
+
289
+ if(tx==null) {
290
+ await this.removeSwapData(swap, SpvVaultSwapState.DOUBLE_SPENT);
291
+ return;
292
+ } else if(tx.confirmations > 0) {
293
+ if(swap.state!==SpvVaultSwapState.BTC_CONFIRMED) {
294
+ await swap.setState(SpvVaultSwapState.BTC_CONFIRMED)
295
+ await this.saveSwapData(swap);
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ protected async processPastSwaps(): Promise<void> {
302
+ const swaps = await this.storageManager.query([
303
+ {
304
+ key: "state",
305
+ value: [
306
+ SpvVaultSwapState.CREATED, //Check if expired
307
+ SpvVaultSwapState.SIGNED, //Check if sent
308
+ SpvVaultSwapState.SENT //Check if confirmed or double-spent
309
+ ]
310
+ }
311
+ ]);
312
+
313
+ for(let {obj: swap} of swaps) {
314
+ await this.processPastSwap(swap);
315
+ }
316
+ }
317
+
318
+ protected getPricePrefetches(chainIdentifier: string, token: string, gasToken: string, abortController: AbortController) {
319
+ const pricePrefetchPromise: Promise<bigint> = this.swapPricing.preFetchPrice(token, chainIdentifier).catch(e => {
320
+ this.logger.error("getPricePrefetches(): pricePrefetchPromise error: ", e);
321
+ abortController.abort(e);
322
+ return null;
323
+ });
324
+ const gasTokenPricePrefetchPromise: Promise<bigint> = token===gasToken ?
325
+ pricePrefetchPromise :
326
+ this.swapPricing.preFetchPrice(gasToken, chainIdentifier).catch(e => {
327
+ this.logger.error("getPricePrefetches(): gasTokenPricePrefetchPromise error: ", e);
328
+ abortController.abort(e);
329
+ return null;
330
+ });
331
+ return {pricePrefetchPromise, gasTokenPricePrefetchPromise};
332
+ }
333
+
334
+ startRestServer(restServer: Express): void {
335
+ restServer.use(this.path+"/getQuote", serverParamDecoder(10*1000));
336
+ restServer.post(this.path+"/getQuote", expressHandlerWrapper(async (req: Request & {paramReader: IParamReader}, res: Response & {responseStream: ServerParamEncoder}) => {
337
+ const metadata: {
338
+ request: any,
339
+ times: {[key: string]: number},
340
+ } = {request: {}, times: {}};
341
+
342
+ const chainIdentifier = req.query.chain as string;
343
+ const {signer, chainInterface, spvVaultContract} = this.getChain(chainIdentifier);
344
+
345
+ metadata.times.requestReceived = Date.now();
346
+
347
+ /**
348
+ * address: string smart chain address of the recipient
349
+ * token: string Desired token to use
350
+ * gasToken: string
351
+ */
352
+ const preFetchParsedBody = await req.paramReader.getParams({
353
+ address: (val: string) => val!=null &&
354
+ typeof(val)==="string" &&
355
+ chainInterface.isValidAddress(val, true) ? val : null,
356
+ token: (val: string) => val!=null &&
357
+ typeof(val)==="string" &&
358
+ this.isTokenSupported(chainIdentifier, val) ? val : null,
359
+ gasToken: (val: string) => val!=null &&
360
+ typeof(val)==="string" &&
361
+ chainInterface.isValidToken(val) ? val : null
362
+ });
363
+ if(preFetchParsedBody==null) throw {
364
+ code: 20100,
365
+ msg: "Invalid request body"
366
+ };
367
+
368
+ const stickyAddressObject = req.paramReader.getExistingParamsOrNull({
369
+ stickyAddress: FieldTypeEnum.BooleanOptional
370
+ });
371
+ const useStickyAddress = stickyAddressObject?.stickyAddress;
372
+
373
+ //Create abortController for parallel prefetches
374
+ const responseStream = res.responseStream;
375
+ const abortController = getAbortController(responseStream);
376
+
377
+ //Pre-fetch data
378
+ const {
379
+ pricePrefetchPromise,
380
+ gasTokenPricePrefetchPromise
381
+ } = this.getPricePrefetches(chainIdentifier, preFetchParsedBody.token, preFetchParsedBody.gasToken, abortController);
382
+ const nativeBalancePrefetch = this.prefetchNativeBalanceIfNeeded(chainIdentifier, abortController);
383
+ const btcFeeRatePrefetch: Promise<number> = this.bitcoin.getFeeRate().catch(e => {
384
+ abortController.abort(e);
385
+ return null;
386
+ });
387
+
388
+ //Listener that re-adds the returned bitcoin address to the unused address list if request fails or closes
389
+ let abortAddUnusedAddressListener: () => void;
390
+
391
+ const bitcoinAddressPrefetch: Promise<{address: string, isStickyAddress: boolean} | null> = (async () => {
392
+ if(useStickyAddress) {
393
+ const result = await this.getStickyAddress(chainIdentifier, preFetchParsedBody.address);
394
+ if(result!=null) return {address: result, isStickyAddress: true};
395
+ }
396
+
397
+ const value = await this.bitcoin.getAddress();
398
+
399
+ //Already aborted
400
+ if(abortController.signal.aborted) {
401
+ this.bitcoin.addUnusedAddress(value);
402
+ return null;
403
+ }
404
+ //Not aborted yet, add an event listener to re-add the address to the unused list
405
+ abortController.signal.addEventListener("abort", abortAddUnusedAddressListener = () => {
406
+ this.bitcoin.addUnusedAddress(value);
407
+ });
408
+ return {address: value, isStickyAddress: false};
409
+ })().catch(e => {
410
+ abortController.abort(e);
411
+ return null;
412
+ });
413
+
414
+ /**
415
+ * amount: string amount (in sats)
416
+ * gasAmount: string Desired amount in gas token to also get
417
+ * exactOut: boolean Whether the swap should be an exact out instead of exact in swap
418
+ * callerFeeRate: string Caller/watchtower fee (in output token) to assign to the swap
419
+ * frontingFeeRate: string Fronting fee (in output token) to assign to the swap
420
+ */
421
+ const actualParsedBody = await req.paramReader.getParams({
422
+ amount: FieldTypeEnum.BigInt,
423
+ gasAmount: FieldTypeEnum.BigInt,
424
+ exactOut: FieldTypeEnum.BooleanOptional,
425
+ callerFeeRate: FieldTypeEnum.BigInt,
426
+ frontingFeeRate: FieldTypeEnum.BigInt,
427
+ });
428
+ abortController.signal.throwIfAborted();
429
+ if(actualParsedBody==null) throw {
430
+ code: 20100,
431
+ msg: "Invalid request body"
432
+ };
433
+
434
+ const inputAmountAdjustments = req.paramReader.getExistingParamsOrNull({
435
+ amountUtxos: FieldTypeEnum.AnyOptional,
436
+ amountFeeRate: FieldTypeEnum.NumberOptional
437
+ });
438
+ if(inputAmountAdjustments==null) throw {
439
+ code: 20100,
440
+ msg: "Invalid request body"
441
+ };
442
+
443
+ const clientInputUtxos: AmountAdjustUtxo[] | null = inputAmountAdjustments?.amountUtxos!=null
444
+ ? parseAmountAdjustUtxos(inputAmountAdjustments.amountUtxos)
445
+ : null;
446
+ if(inputAmountAdjustments?.amountUtxos!=null && clientInputUtxos==null) throw {
447
+ code: 20100,
448
+ msg: "Invalid request body (amountUtxos)"
449
+ };
450
+
451
+ const parsedBody: SpvVaultSwapRequestType = {...preFetchParsedBody, ...actualParsedBody};
452
+ metadata.request = parsedBody;
453
+
454
+ if(parsedBody.gasToken!==chainInterface.getNativeCurrencyAddress()) throw {
455
+ code: 20190,
456
+ msg: "Unsupported gas token"
457
+ };
458
+
459
+ if(parsedBody.callerFeeRate < 0n || parsedBody.callerFeeRate >= 2n**20n) throw {
460
+ code: 20191,
461
+ msg: "Invalid caller fee rate"
462
+ };
463
+ if(parsedBody.frontingFeeRate < 0n || parsedBody.frontingFeeRate >= 2n**20n) throw {
464
+ code: 20192,
465
+ msg: "Invalid fronting fee rate"
466
+ };
467
+
468
+ const requestedAmount = {
469
+ input: !parsedBody.exactOut,
470
+ amount: parsedBody.exactOut ?
471
+ (parsedBody.amount * (100_000n + parsedBody.callerFeeRate + parsedBody.frontingFeeRate) / 100_000n) :
472
+ parsedBody.amount,
473
+ token: parsedBody.token
474
+ };
475
+ if(clientInputUtxos!=null) {
476
+ if(parsedBody.exactOut) throw {
477
+ code: 20193,
478
+ msg: "amountAdjustUtxos cannot be specified for exactOut swaps!"
479
+ };
480
+
481
+ let btcFeeRate = await btcFeeRatePrefetch;
482
+ if(inputAmountAdjustments.amountFeeRate!=null && inputAmountAdjustments.amountFeeRate>btcFeeRate)
483
+ btcFeeRate = inputAmountAdjustments.amountFeeRate;
484
+
485
+ let feeAccumulator: number = 0;
486
+ let valueAccumulator: number = 0;
487
+ for(let utxo of clientInputUtxos) {
488
+ const cpfpAdditionalFee: number = utxo.cpfp==null ? 0 : Math.ceil(utxo.cpfp.effectiveVSize * Math.max(0, btcFeeRate - utxo.cpfp.effectiveFeeRate));
489
+ const spendFee: number = utxo.vSize * btcFeeRate;
490
+ const totalFee: number = cpfpAdditionalFee + spendFee;
491
+ if(totalFee > utxo.value) continue; //Skip detrimental UTXO
492
+ feeAccumulator += totalFee;
493
+ valueAccumulator += utxo.value;
494
+ }
495
+
496
+ let baseTxVSize: number = 10.5; // 4b version, 1b inputs, 1b outputs, 4b locktime, 0.5vB witness flag + witness elements count
497
+ //vault input and output
498
+ baseTxVSize += 32 + 4 + 1 + 4; //Input base
499
+ baseTxVSize += this.vaultSigner.getAddressType()==="p2tr" ? (1+1+65)/4 : (1+1+72+1+33)/4;
500
+ baseTxVSize += 8 + 1; //Output base
501
+ baseTxVSize += this.vaultSigner.getAddressType()==="p2tr" ? 34 : 22;
502
+ //opreturn output
503
+ baseTxVSize += 8 + 1; //Output base
504
+ const opReturnDataSize = spvVaultContract.toOpReturnData(parsedBody.address, parsedBody.gasAmount > 0 ? [0xffffffffffffffffn, 0xffffffffffffffffn] : [0xffffffffffffffffn]).length;
505
+ baseTxVSize += (opReturnDataSize <= 0x4b ? 2 : 3 /*Needs an OP_PUSHDATA1 opcode*/) + opReturnDataSize;
506
+ //LP output
507
+ baseTxVSize += 8 + 1; //Output base
508
+ baseTxVSize += this.bitcoin.getAddressType()==="p2tr" ? 34 : this.bitcoin.getAddressType()==="p2wpkh" ? 22 : 23;
509
+
510
+ const baseTxFee = Math.ceil(baseTxVSize) * btcFeeRate;
511
+ feeAccumulator += baseTxFee;
512
+
513
+ const amount = Math.floor(valueAccumulator - Math.ceil(feeAccumulator));
514
+ requestedAmount.amount = BigInt(amount);
515
+ }
516
+
517
+ const gasTokenAmount = {
518
+ input: false,
519
+ amount: parsedBody.gasAmount * (100_000n + parsedBody.callerFeeRate + parsedBody.frontingFeeRate) / 100_000n,
520
+ token: parsedBody.gasToken
521
+ } as const;
522
+ const request = {
523
+ chainIdentifier,
524
+ raw: req,
525
+ parsed: parsedBody,
526
+ metadata
527
+ };
528
+ const useToken = parsedBody.token;
529
+ const gasToken = parsedBody.gasToken;
530
+
531
+ this.checkTooManyInflightSwaps();
532
+
533
+ //Check request params
534
+ const fees = await this.AmountAssertions.preCheckFromBtcAmounts(this.type, request, requestedAmount, gasTokenAmount);
535
+ metadata.times.requestChecked = Date.now();
536
+
537
+ await this.checkNativeBalance(chainIdentifier, nativeBalancePrefetch, abortController.signal);
538
+
539
+ //Check valid amount specified (min/max)
540
+ let {
541
+ amountBD,
542
+ swapFee,
543
+ swapFeeInToken,
544
+ totalInToken,
545
+ amountBDgas,
546
+ gasSwapFee,
547
+ gasSwapFeeInToken,
548
+ totalInGasToken
549
+ } = await this.AmountAssertions.checkFromBtcAmount(
550
+ this.type,
551
+ request,
552
+ {...requestedAmount, pricePrefetch: pricePrefetchPromise},
553
+ fees,
554
+ abortController.signal,
555
+ {...gasTokenAmount, pricePrefetch: gasTokenPricePrefetchPromise}
556
+ );
557
+ metadata.times.priceCalculated = Date.now();
558
+
559
+ const totalBtcOutput = amountBD + amountBDgas;
560
+
561
+ //Check if we have enough funds to honor the request
562
+ let vault: SpvVault;
563
+ do {
564
+ vault = await this.Vaults.findVaultForSwap(chainIdentifier, totalBtcOutput, useToken, totalInToken, gasToken, totalInGasToken);
565
+ } while (await this.Vaults.checkVaultReplacedTransactions(vault, true));
566
+ abortController.signal.throwIfAborted();
567
+ metadata.times.vaultPicked = Date.now();
568
+
569
+ //Create swap receive bitcoin address
570
+ const btcFeeRate = await btcFeeRatePrefetch;
571
+ const btcAddressObject = await bitcoinAddressPrefetch;
572
+ abortController.signal.throwIfAborted();
573
+ metadata.times.addressCreated = Date.now();
574
+
575
+ const receiveAddress = btcAddressObject.address;
576
+ const hasStickyAddress = btcAddressObject.isStickyAddress;
577
+
578
+ //Adjust the amounts based on passed fees
579
+ if(parsedBody.exactOut) {
580
+ totalInToken = parsedBody.amount;
581
+ } else {
582
+ totalInToken = (totalInToken * 100_000n / (100_000n + parsedBody.callerFeeRate + parsedBody.frontingFeeRate));
583
+ }
584
+ totalInGasToken = (totalInGasToken * 100_000n / (100_000n + parsedBody.callerFeeRate + parsedBody.frontingFeeRate));
585
+
586
+ //Calculate raw amounts
587
+ const [rawTokenAmount, rawGasTokenAmount] = vault.toRawAmounts([totalInToken, totalInGasToken]);
588
+ [totalInToken, totalInGasToken] = vault.fromRawAmounts([rawTokenAmount, rawGasTokenAmount]);
589
+
590
+ const expiry = Math.floor(Date.now() / 1000) + this.getInitAuthorizationTimeout(chainIdentifier);
591
+
592
+ //Get PSBT data
593
+ const callerFeeShare = parsedBody.callerFeeRate;
594
+ const frontingFeeShare = parsedBody.frontingFeeRate;
595
+ const executionFeeShare = 0n;
596
+ const utxo = vault.getLatestUtxo();
597
+
598
+ const quoteId = randomBytes(32).toString("hex");
599
+ const swap = new SpvVaultSwap(
600
+ chainIdentifier, quoteId, expiry,
601
+ vault, utxo,
602
+ receiveAddress, btcFeeRate, parsedBody.address, totalBtcOutput, totalInToken, totalInGasToken,
603
+ swapFee, swapFeeInToken, gasSwapFee, gasSwapFeeInToken,
604
+ callerFeeShare, frontingFeeShare, executionFeeShare,
605
+ useToken, gasToken
606
+ );
607
+ swap.metadata = metadata;
608
+ swap.saveStickyAddress = useStickyAddress && !hasStickyAddress
609
+ swap.hasStickyAddress = hasStickyAddress;
610
+
611
+ //We can remove the listener to add unused address now, as we are about to save the swap
612
+ abortController.signal.removeEventListener("abort", abortAddUnusedAddressListener);
613
+ await PluginManager.swapCreate(swap);
614
+ await this.saveSwapData(swap);
615
+
616
+ this.swapLogger.info(swap, "REST: /getQuote: Created swap address: "+receiveAddress+" amount: "+totalBtcOutput.toString(10));
617
+
618
+ await responseStream.writeParamsAndEnd({
619
+ code: 20000,
620
+ msg: "Success",
621
+ data: {
622
+ quoteId,
623
+ expiry,
624
+
625
+ address: signer.getAddress(),
626
+ vaultId: vault.data.getVaultId().toString(10),
627
+
628
+ vaultBtcAddress: vault.btcAddress,
629
+ btcAddress: receiveAddress,
630
+ btcUtxo: utxo,
631
+ btcFeeRate,
632
+
633
+ btcAmount: totalBtcOutput.toString(10),
634
+ btcAmountSwap: amountBD.toString(10),
635
+ btcAmountGas: amountBDgas.toString(10),
636
+
637
+ total: totalInToken.toString(10),
638
+ totalGas: totalInGasToken.toString(10),
639
+
640
+ totalFeeBtc: (swapFee + gasSwapFee).toString(10),
641
+
642
+ swapFeeBtc: swapFee.toString(10),
643
+ swapFee: swapFeeInToken.toString(10),
644
+
645
+ gasSwapFeeBtc: gasSwapFee.toString(10),
646
+ gasSwapFee: gasSwapFeeInToken.toString(10),
647
+
648
+ callerFeeShare: callerFeeShare.toString(10),
649
+ frontingFeeShare: frontingFeeShare.toString(10),
650
+ executionFeeShare: executionFeeShare.toString(10),
651
+
652
+ usedUtxoInputCalculation: clientInputUtxos!=null
653
+ }
654
+ });
655
+ }));
656
+
657
+ restServer.use(this.path+"/postQuote", serverParamDecoder(10*1000));
658
+ restServer.post(this.path+"/postQuote", expressHandlerWrapper(async (req: Request & {paramReader: IParamReader}, res: Response & {responseStream: ServerParamEncoder}) => {
659
+ let requestReceived = Date.now();
660
+
661
+ const parsedBody: SpvVaultPostQuote = await req.paramReader.getParams({
662
+ quoteId: FieldTypeEnum.String,
663
+ psbtHex: (val: string) => val!=null &&
664
+ typeof(val)==="string" &&
665
+ HEX_REGEX.test(val) ? val : null
666
+ });
667
+
668
+ const swap = await this.storageManager.getData(parsedBody.quoteId, 0n);
669
+ if(swap==null || swap.state!==SpvVaultSwapState.CREATED || swap.expiry < Date.now()/1000) throw {
670
+ code: 20505,
671
+ msg: "Invalid quote ID, not found or expired!"
672
+ };
673
+
674
+ const metadata: {
675
+ times: {[key: string]: number},
676
+ error?: any
677
+ } = swap.metadata;
678
+ metadata.times ??= {};
679
+ metadata.times.requestReceived = requestReceived;
680
+
681
+ this.checkTooManyInflightSwaps();
682
+
683
+ const vault = await this.Vaults.getVault(swap.chainIdentifier, swap.vaultOwner, swap.vaultId);
684
+ if(vault==null || !vault.isReady()) {
685
+ throw {
686
+ code: 20506,
687
+ msg: "Used vault not found!"
688
+ };
689
+ }
690
+
691
+ //Try parse psbt
692
+ let transaction: Transaction;
693
+ try {
694
+ transaction = Transaction.fromPSBT(Buffer.from(parsedBody.psbtHex, "hex"));
695
+ } catch (e) {
696
+ this.swapLogger.error(swap, "REST: /postQuote: failed to parse provided PSBT: ", e);
697
+ throw {
698
+ code: 20507,
699
+ msg: "Error parsing PSBT, hex format required!"
700
+ };
701
+ }
702
+
703
+ for(let i=1;i<transaction.inputsLength;i++) { //Skip first vault input
704
+ const txIn = transaction.getInput(i);
705
+ if (isLegacyInput(txIn)) throw {
706
+ code: 20514,
707
+ msg: "Legacy (pre-segwit) inputs in tx are not allowed!"
708
+ };
709
+ }
710
+
711
+ //Check the posted quote with the plugins
712
+ AmountAssertions.handlePluginErrorResponses(await PluginManager.onHandlePostedFromBtcQuote(
713
+ this.type,
714
+ {chainIdentifier: swap.chainIdentifier, raw: req, parsed: parsedBody, metadata},
715
+ swap
716
+ ));
717
+
718
+ //Check correct psbt
719
+ for(let i=1;i<transaction.inputsLength;i++) { //Skip first vault input
720
+ const txIn = transaction.getInput(i);
721
+ //Check UTXOs exist and are unspent
722
+ if(await this.bitcoinRpc.isSpent(Buffer.from(txIn.txid).toString("hex")+":"+txIn.index.toString(10))) throw {
723
+ code: 20515,
724
+ msg: "Spent UTXO in inputs!"
725
+ };
726
+ }
727
+
728
+ const {spvVaultContract} = this.getChain(swap.chainIdentifier);
729
+
730
+ let data: SpvWithdrawalTransactionData;
731
+ try {
732
+ data = await spvVaultContract.getWithdrawalData(parsePsbt(transaction));
733
+ } catch (e) {
734
+ this.swapLogger.error(swap, "REST: /postQuote: failed to parse PSBT to withdrawal tx data: ", e);
735
+ throw {
736
+ code: 20508,
737
+ msg: "PSBT transaction cannot be parsed!"
738
+ };
739
+ }
740
+
741
+ if(
742
+ !data.isRecipient(swap.recipient) ||
743
+ data.callerFeeRate!==swap.callerFeeShare ||
744
+ data.frontingFeeRate!==swap.frontingFeeShare ||
745
+ data.executionFeeRate!==swap.executionFeeShare ||
746
+ data.rawAmounts[0]!==swap.rawAmountToken ||
747
+ data.rawAmounts[1]!==swap.rawAmountGasToken ||
748
+ data.getExecutionData()!=null ||
749
+ data.getSpentVaultUtxo()!==swap.vaultUtxo ||
750
+ data.btcTx.outs[0].value!==VAULT_DUST_AMOUNT ||
751
+ !Buffer.from(data.btcTx.outs[0].scriptPubKey.hex, "hex").equals(this.bitcoin.toOutputScript(swap.vaultAddress)) ||
752
+ BigInt(data.btcTx.outs[2].value)!==swap.amountBtc ||
753
+ !Buffer.from(data.btcTx.outs[2].scriptPubKey.hex, "hex").equals(this.bitcoin.toOutputScript(swap.btcAddress)) ||
754
+ (data.btcTx.locktime > 0 && data.btcTx.locktime < 500_000_000) ||
755
+ data.btcTx.locktime > Math.floor(Date.now()/1000) - 1_000_000
756
+ ) {
757
+ this.swapLogger.error(swap, "REST: /postQuote: Invalid psbt data submitted, raw psbt hex: ", parsedBody.psbtHex);
758
+ throw {
759
+ code: 20509,
760
+ msg: "Invalid PSBT provided!"
761
+ };
762
+ }
763
+
764
+ if(swap.vaultUtxo!==vault.getLatestUtxo()) {
765
+ throw {
766
+ code: 20510,
767
+ msg: "Vault UTXO already spent, please get another quote and try again!"
768
+ };
769
+ }
770
+
771
+ //Create abortController for parallel prefetches
772
+ const responseStream = res.responseStream;
773
+
774
+ const signedTx = await this.vaultSigner.signPsbt(swap.chainIdentifier, swap.vaultId, transaction, [0]);
775
+ if(!signedTx.isFinal) throw {
776
+ code: 20513,
777
+ msg: "One or more PSBT inputs not finalized!"
778
+ };
779
+
780
+ const effectiveFeeRate = await this.bitcoinRpc.getEffectiveFeeRate(parsePsbt(signedTx));
781
+ if(effectiveFeeRate.feeRate < 1 || Math.round(effectiveFeeRate.feeRate) < swap.btcFeeRate) throw {
782
+ code: 20511,
783
+ msg: "Bitcoin transaction fee too low, expected minimum: "+swap.btcFeeRate+" adjusted effective fee rate: "+effectiveFeeRate.feeRate
784
+ }
785
+
786
+ const txVsize = signedTx.vsize;
787
+ if(txVsize>TX_MAX_VSIZE) throw {
788
+ code: 20516,
789
+ msg: "Bitcoin transaction size too large, maximum: "+TX_MAX_VSIZE+" actual: "+txVsize
790
+ };
791
+
792
+ const pluginCheckResult = await PluginManager.onHandlePreFromBtcExecute(
793
+ SwapHandlerType.FROM_BTC_SPV,
794
+ swap
795
+ );
796
+ if(isQuoteThrow(pluginCheckResult)) {
797
+ const error = {
798
+ code: 29999,
799
+ msg: pluginCheckResult.message
800
+ }
801
+ if(swap.metadata!=null) swap.metadata.postQuoteError = error;
802
+ await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
803
+ throw error;
804
+ }
805
+
806
+ await this.Vaults.checkVaultReplacedTransactions(vault, true);
807
+ if(swap.vaultUtxo!==vault.getLatestUtxo()) {
808
+ throw {
809
+ code: 20510,
810
+ msg: "Vault UTXO already spent, please get another quote and try again!"
811
+ };
812
+ }
813
+
814
+ try {
815
+ const btcRawTx = Buffer.from(signedTx.toBytes(true, true)).toString("hex");
816
+
817
+ //Double-check the state to prevent race condition
818
+ if(swap.state!==SpvVaultSwapState.CREATED) {
819
+ throw {
820
+ code: 20505,
821
+ msg: "Invalid quote ID, not found or expired!"
822
+ };
823
+ }
824
+
825
+ //Double check in-flight swap count
826
+ this.checkTooManyInflightSwaps();
827
+
828
+ swap.btcTxId = signedTx.id;
829
+ swap.state = SpvVaultSwapState.SIGNED;
830
+ swap.sending = true;
831
+ await this.saveSwapData(swap);
832
+
833
+ data.btcTx.raw = btcRawTx;
834
+ (data as any).sending = true;
835
+ vault.addWithdrawal(data);
836
+ await this.Vaults.saveVault(vault);
837
+
838
+ this.swapLogger.info(swap, "REST: /postQuote: BTC transaction signed, txId: "+swap.btcTxId);
839
+
840
+ try {
841
+ await this.bitcoin.sendRawTransaction(btcRawTx);
842
+ await swap.setState(SpvVaultSwapState.SENT);
843
+ (data as any).sending = false;
844
+ swap.sending = false;
845
+ } catch (e) {
846
+ this.swapLogger.error(swap, "REST: /postQuote: Failed to send BTC transaction: ", e);
847
+ throw {
848
+ code: 20512,
849
+ msg: "Error broadcasting bitcoin transaction!"
850
+ };
851
+ }
852
+ } catch (e) {
853
+ (data as any).sending = false;
854
+ swap.sending = false;
855
+ vault.removeWithdrawal(data);
856
+ await this.Vaults.saveVault(vault);
857
+
858
+ if(isDefinedRuntimeError(e) && swap.metadata!=null) swap.metadata.postQuoteError = e;
859
+ await this.removeSwapData(swap, SpvVaultSwapState.FAILED);
860
+
861
+ throw e;
862
+ }
863
+
864
+ await responseStream.writeParamsAndEnd({
865
+ code: 20000,
866
+ msg: "Success",
867
+ data: {
868
+ txId: swap.btcTxId
869
+ }
870
+ });
871
+ }));
872
+ }
873
+
874
+ getInfoData(): any {
875
+ const mappedDict = {};
876
+ for(let chainId in this.config.gasTokenMax) {
877
+ mappedDict[chainId] = {
878
+ gasToken: this.getChain(chainId).chainInterface.getNativeCurrencyAddress(),
879
+ max: this.config.gasTokenMax[chainId].toString(10)
880
+ };
881
+ }
882
+ return {
883
+ gasTokens: mappedDict
884
+ };
885
+ }
886
+
887
+ protected async saveSwapData(swap: SpvVaultSwap): Promise<void> {
888
+ if(swap.btcTxId!=null) this.btcTxIdIndex.set(swap.btcTxId, swap);
889
+ return super.saveSwapData(swap);
890
+ }
891
+
892
+ protected async removeSwapData(swap: SpvVaultSwap, ultimateState?: SpvVaultSwapState): Promise<void> {
893
+ if(swap.btcTxId!=null) this.btcTxIdIndex.delete(swap.btcTxId);
894
+ return super.removeSwapData(swap, ultimateState);
895
+ }
896
+
897
+ }