@atomiqlabs/sdk 8.9.1 → 8.9.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 (366) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +1760 -1760
  3. package/api/index.d.ts +1 -1
  4. package/api/index.js +3 -3
  5. package/dist/ApiList.d.ts +37 -37
  6. package/dist/ApiList.js +30 -30
  7. package/dist/SmartChainAssets.d.ts +181 -181
  8. package/dist/SmartChainAssets.js +181 -181
  9. package/dist/api/ApiEndpoints.d.ts +393 -393
  10. package/dist/api/ApiEndpoints.js +2 -2
  11. package/dist/api/ApiParser.d.ts +10 -10
  12. package/dist/api/ApiParser.js +134 -134
  13. package/dist/api/ApiTypes.d.ts +157 -157
  14. package/dist/api/ApiTypes.js +75 -75
  15. package/dist/api/SerializedAction.d.ts +40 -40
  16. package/dist/api/SerializedAction.js +59 -59
  17. package/dist/api/SwapperApi.d.ts +50 -50
  18. package/dist/api/SwapperApi.js +431 -431
  19. package/dist/api/index.d.ts +5 -5
  20. package/dist/api/index.js +24 -24
  21. package/dist/bitcoin/coinselect2/accumulative.d.ts +7 -7
  22. package/dist/bitcoin/coinselect2/accumulative.js +52 -52
  23. package/dist/bitcoin/coinselect2/blackjack.d.ts +7 -7
  24. package/dist/bitcoin/coinselect2/blackjack.js +38 -38
  25. package/dist/bitcoin/coinselect2/index.d.ts +20 -20
  26. package/dist/bitcoin/coinselect2/index.js +69 -69
  27. package/dist/bitcoin/coinselect2/utils.d.ts +82 -82
  28. package/dist/bitcoin/coinselect2/utils.js +158 -158
  29. package/dist/bitcoin/wallet/BitcoinWallet.d.ts +113 -113
  30. package/dist/bitcoin/wallet/BitcoinWallet.js +335 -335
  31. package/dist/bitcoin/wallet/IBitcoinWallet.d.ts +116 -116
  32. package/dist/bitcoin/wallet/IBitcoinWallet.js +21 -21
  33. package/dist/bitcoin/wallet/SingleAddressBitcoinWallet.d.ts +106 -106
  34. package/dist/bitcoin/wallet/SingleAddressBitcoinWallet.js +196 -196
  35. package/dist/enums/FeeType.d.ts +15 -15
  36. package/dist/enums/FeeType.js +19 -19
  37. package/dist/enums/SwapAmountType.d.ts +15 -15
  38. package/dist/enums/SwapAmountType.js +19 -19
  39. package/dist/enums/SwapDirection.d.ts +15 -15
  40. package/dist/enums/SwapDirection.js +19 -19
  41. package/dist/enums/SwapSide.d.ts +15 -15
  42. package/dist/enums/SwapSide.js +19 -19
  43. package/dist/enums/SwapType.d.ts +75 -75
  44. package/dist/enums/SwapType.js +79 -79
  45. package/dist/errors/IntermediaryError.d.ts +13 -13
  46. package/dist/errors/IntermediaryError.js +27 -27
  47. package/dist/errors/RequestError.d.ts +32 -32
  48. package/dist/errors/RequestError.js +54 -54
  49. package/dist/errors/UserError.d.ts +8 -8
  50. package/dist/errors/UserError.js +16 -16
  51. package/dist/events/UnifiedSwapEventListener.d.ts +24 -24
  52. package/dist/events/UnifiedSwapEventListener.js +138 -138
  53. package/dist/http/HttpUtils.d.ts +29 -29
  54. package/dist/http/HttpUtils.js +97 -97
  55. package/dist/http/paramcoders/IParamReader.d.ts +8 -8
  56. package/dist/http/paramcoders/IParamReader.js +2 -2
  57. package/dist/http/paramcoders/ParamDecoder.d.ts +44 -44
  58. package/dist/http/paramcoders/ParamDecoder.js +137 -137
  59. package/dist/http/paramcoders/ParamEncoder.d.ts +20 -20
  60. package/dist/http/paramcoders/ParamEncoder.js +36 -36
  61. package/dist/http/paramcoders/SchemaVerifier.d.ts +26 -26
  62. package/dist/http/paramcoders/SchemaVerifier.js +145 -145
  63. package/dist/http/paramcoders/client/ResponseParamDecoder.d.ts +11 -11
  64. package/dist/http/paramcoders/client/ResponseParamDecoder.js +57 -57
  65. package/dist/http/paramcoders/client/StreamParamEncoder.d.ts +13 -13
  66. package/dist/http/paramcoders/client/StreamParamEncoder.js +26 -26
  67. package/dist/http/paramcoders/client/StreamingFetchPromise.d.ts +17 -17
  68. package/dist/http/paramcoders/client/StreamingFetchPromise.js +175 -175
  69. package/dist/index.d.ts +86 -86
  70. package/dist/index.js +159 -159
  71. package/dist/intermediaries/Intermediary.d.ts +178 -178
  72. package/dist/intermediaries/Intermediary.js +166 -166
  73. package/dist/intermediaries/IntermediaryDiscovery.d.ts +216 -216
  74. package/dist/intermediaries/IntermediaryDiscovery.js +424 -424
  75. package/dist/intermediaries/apis/IntermediaryAPI.d.ts +607 -607
  76. package/dist/intermediaries/apis/IntermediaryAPI.js +764 -764
  77. package/dist/intermediaries/apis/TrustedIntermediaryAPI.d.ts +155 -155
  78. package/dist/intermediaries/apis/TrustedIntermediaryAPI.js +137 -137
  79. package/dist/intermediaries/auth/SignedKeyBasedAuth.d.ts +14 -14
  80. package/dist/intermediaries/auth/SignedKeyBasedAuth.js +68 -68
  81. package/dist/lnurl/LNURL.d.ts +102 -102
  82. package/dist/lnurl/LNURL.js +321 -321
  83. package/dist/prices/RedundantSwapPrice.d.ts +110 -110
  84. package/dist/prices/RedundantSwapPrice.js +222 -222
  85. package/dist/prices/SingleSwapPrice.d.ts +34 -34
  86. package/dist/prices/SingleSwapPrice.js +44 -44
  87. package/dist/prices/SwapPriceWithChain.d.ts +107 -107
  88. package/dist/prices/SwapPriceWithChain.js +128 -128
  89. package/dist/prices/abstract/ICachedSwapPrice.d.ts +28 -28
  90. package/dist/prices/abstract/ICachedSwapPrice.js +62 -62
  91. package/dist/prices/abstract/IPriceProvider.d.ts +81 -81
  92. package/dist/prices/abstract/IPriceProvider.js +74 -74
  93. package/dist/prices/abstract/ISwapPrice.d.ts +168 -168
  94. package/dist/prices/abstract/ISwapPrice.js +279 -279
  95. package/dist/prices/providers/BinancePriceProvider.d.ts +23 -23
  96. package/dist/prices/providers/BinancePriceProvider.js +30 -30
  97. package/dist/prices/providers/CoinGeckoPriceProvider.d.ts +23 -23
  98. package/dist/prices/providers/CoinGeckoPriceProvider.js +29 -29
  99. package/dist/prices/providers/CoinPaprikaPriceProvider.d.ts +25 -25
  100. package/dist/prices/providers/CoinPaprikaPriceProvider.js +29 -29
  101. package/dist/prices/providers/CustomPriceProvider.d.ts +24 -24
  102. package/dist/prices/providers/CustomPriceProvider.js +35 -35
  103. package/dist/prices/providers/KrakenPriceProvider.d.ts +38 -38
  104. package/dist/prices/providers/KrakenPriceProvider.js +45 -45
  105. package/dist/prices/providers/OKXPriceProvider.d.ts +34 -34
  106. package/dist/prices/providers/OKXPriceProvider.js +29 -29
  107. package/dist/prices/providers/abstract/ExchangePriceProvider.d.ts +17 -17
  108. package/dist/prices/providers/abstract/ExchangePriceProvider.js +21 -21
  109. package/dist/prices/providers/abstract/HttpPriceProvider.d.ts +7 -7
  110. package/dist/prices/providers/abstract/HttpPriceProvider.js +12 -12
  111. package/dist/storage/IUnifiedStorage.d.ts +127 -127
  112. package/dist/storage/IUnifiedStorage.js +2 -2
  113. package/dist/storage/UnifiedSwapStorage.d.ts +120 -120
  114. package/dist/storage/UnifiedSwapStorage.js +154 -154
  115. package/dist/storage-browser/IndexedDBUnifiedStorage.d.ts +63 -63
  116. package/dist/storage-browser/IndexedDBUnifiedStorage.js +298 -298
  117. package/dist/storage-browser/LocalStorageManager.d.ts +49 -49
  118. package/dist/storage-browser/LocalStorageManager.js +93 -93
  119. package/dist/swapper/Swapper.d.ts +765 -770
  120. package/dist/swapper/Swapper.js +1749 -1758
  121. package/dist/swapper/SwapperFactory.d.ts +135 -135
  122. package/dist/swapper/SwapperFactory.js +162 -162
  123. package/dist/swapper/SwapperUtils.d.ts +222 -222
  124. package/dist/swapper/SwapperUtils.js +519 -519
  125. package/dist/swapper/SwapperWithChain.d.ts +404 -404
  126. package/dist/swapper/SwapperWithChain.js +469 -469
  127. package/dist/swapper/SwapperWithSigner.d.ts +322 -322
  128. package/dist/swapper/SwapperWithSigner.js +318 -318
  129. package/dist/swaps/IAddressSwap.d.ts +22 -22
  130. package/dist/swaps/IAddressSwap.js +14 -14
  131. package/dist/swaps/IBTCWalletSwap.d.ts +73 -73
  132. package/dist/swaps/IBTCWalletSwap.js +18 -18
  133. package/dist/swaps/IClaimableSwap.d.ts +49 -49
  134. package/dist/swaps/IClaimableSwap.js +15 -15
  135. package/dist/swaps/IClaimableSwapWrapper.d.ts +15 -15
  136. package/dist/swaps/IClaimableSwapWrapper.js +2 -2
  137. package/dist/swaps/IRefundableSwap.d.ts +43 -43
  138. package/dist/swaps/IRefundableSwap.js +14 -14
  139. package/dist/swaps/ISwap.d.ts +453 -453
  140. package/dist/swaps/ISwap.js +371 -371
  141. package/dist/swaps/ISwapWithGasDrop.d.ts +21 -21
  142. package/dist/swaps/ISwapWithGasDrop.js +12 -12
  143. package/dist/swaps/ISwapWrapper.d.ts +295 -295
  144. package/dist/swaps/ISwapWrapper.js +373 -373
  145. package/dist/swaps/escrow_swaps/IEscrowSelfInitSwap.d.ts +98 -98
  146. package/dist/swaps/escrow_swaps/IEscrowSelfInitSwap.js +126 -126
  147. package/dist/swaps/escrow_swaps/IEscrowSwap.d.ts +139 -139
  148. package/dist/swaps/escrow_swaps/IEscrowSwap.js +172 -172
  149. package/dist/swaps/escrow_swaps/IEscrowSwapWrapper.d.ts +129 -129
  150. package/dist/swaps/escrow_swaps/IEscrowSwapWrapper.js +167 -167
  151. package/dist/swaps/escrow_swaps/frombtc/IFromBTCLNWrapper.d.ts +107 -107
  152. package/dist/swaps/escrow_swaps/frombtc/IFromBTCLNWrapper.js +130 -130
  153. package/dist/swaps/escrow_swaps/frombtc/IFromBTCSelfInitSwap.d.ts +162 -162
  154. package/dist/swaps/escrow_swaps/frombtc/IFromBTCSelfInitSwap.js +190 -190
  155. package/dist/swaps/escrow_swaps/frombtc/IFromBTCWrapper.d.ts +64 -64
  156. package/dist/swaps/escrow_swaps/frombtc/IFromBTCWrapper.js +82 -82
  157. package/dist/swaps/escrow_swaps/frombtc/ln/FromBTCLNSwap.d.ts +547 -547
  158. package/dist/swaps/escrow_swaps/frombtc/ln/FromBTCLNSwap.js +1419 -1419
  159. package/dist/swaps/escrow_swaps/frombtc/ln/FromBTCLNWrapper.d.ts +192 -192
  160. package/dist/swaps/escrow_swaps/frombtc/ln/FromBTCLNWrapper.js +432 -432
  161. package/dist/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap.d.ts +650 -650
  162. package/dist/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap.js +1577 -1577
  163. package/dist/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoWrapper.d.ts +237 -237
  164. package/dist/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoWrapper.js +525 -525
  165. package/dist/swaps/escrow_swaps/frombtc/onchain/FromBTCSwap.d.ts +491 -491
  166. package/dist/swaps/escrow_swaps/frombtc/onchain/FromBTCSwap.js +1463 -1463
  167. package/dist/swaps/escrow_swaps/frombtc/onchain/FromBTCWrapper.d.ts +204 -204
  168. package/dist/swaps/escrow_swaps/frombtc/onchain/FromBTCWrapper.js +406 -406
  169. package/dist/swaps/escrow_swaps/tobtc/IToBTCSwap.d.ts +446 -446
  170. package/dist/swaps/escrow_swaps/tobtc/IToBTCSwap.js +1097 -1097
  171. package/dist/swaps/escrow_swaps/tobtc/IToBTCWrapper.d.ts +68 -68
  172. package/dist/swaps/escrow_swaps/tobtc/IToBTCWrapper.js +117 -117
  173. package/dist/swaps/escrow_swaps/tobtc/ln/ToBTCLNSwap.d.ts +127 -127
  174. package/dist/swaps/escrow_swaps/tobtc/ln/ToBTCLNSwap.js +256 -256
  175. package/dist/swaps/escrow_swaps/tobtc/ln/ToBTCLNWrapper.d.ts +252 -252
  176. package/dist/swaps/escrow_swaps/tobtc/ln/ToBTCLNWrapper.js +535 -535
  177. package/dist/swaps/escrow_swaps/tobtc/onchain/ToBTCSwap.d.ts +73 -73
  178. package/dist/swaps/escrow_swaps/tobtc/onchain/ToBTCSwap.js +155 -155
  179. package/dist/swaps/escrow_swaps/tobtc/onchain/ToBTCWrapper.d.ts +134 -134
  180. package/dist/swaps/escrow_swaps/tobtc/onchain/ToBTCWrapper.js +286 -286
  181. package/dist/swaps/spv_swaps/SpvFromBTCSwap.d.ts +694 -694
  182. package/dist/swaps/spv_swaps/SpvFromBTCSwap.js +1687 -1687
  183. package/dist/swaps/spv_swaps/SpvFromBTCWrapper.d.ts +259 -259
  184. package/dist/swaps/spv_swaps/SpvFromBTCWrapper.js +947 -947
  185. package/dist/swaps/trusted/ln/LnForGasSwap.d.ts +302 -302
  186. package/dist/swaps/trusted/ln/LnForGasSwap.js +625 -625
  187. package/dist/swaps/trusted/ln/LnForGasWrapper.d.ts +40 -40
  188. package/dist/swaps/trusted/ln/LnForGasWrapper.js +82 -82
  189. package/dist/swaps/trusted/onchain/OnchainForGasSwap.d.ts +343 -343
  190. package/dist/swaps/trusted/onchain/OnchainForGasSwap.js +698 -698
  191. package/dist/swaps/trusted/onchain/OnchainForGasWrapper.d.ts +71 -71
  192. package/dist/swaps/trusted/onchain/OnchainForGasWrapper.js +93 -93
  193. package/dist/types/AmountData.d.ts +10 -10
  194. package/dist/types/AmountData.js +2 -2
  195. package/dist/types/CustomPriceFunction.d.ts +11 -11
  196. package/dist/types/CustomPriceFunction.js +2 -2
  197. package/dist/types/PriceInfoType.d.ts +28 -28
  198. package/dist/types/PriceInfoType.js +57 -57
  199. package/dist/types/SwapExecutionAction.d.ts +195 -195
  200. package/dist/types/SwapExecutionAction.js +106 -106
  201. package/dist/types/SwapExecutionStep.d.ts +144 -144
  202. package/dist/types/SwapExecutionStep.js +87 -87
  203. package/dist/types/SwapStateInfo.d.ts +5 -5
  204. package/dist/types/SwapStateInfo.js +2 -2
  205. package/dist/types/SwapWithSigner.d.ts +17 -17
  206. package/dist/types/SwapWithSigner.js +43 -43
  207. package/dist/types/Token.d.ts +99 -99
  208. package/dist/types/Token.js +76 -76
  209. package/dist/types/TokenAmount.d.ts +75 -75
  210. package/dist/types/TokenAmount.js +85 -85
  211. package/dist/types/fees/Fee.d.ts +50 -50
  212. package/dist/types/fees/Fee.js +2 -2
  213. package/dist/types/fees/FeeBreakdown.d.ts +11 -11
  214. package/dist/types/fees/FeeBreakdown.js +2 -2
  215. package/dist/types/fees/PercentagePPM.d.ts +17 -17
  216. package/dist/types/fees/PercentagePPM.js +18 -18
  217. package/dist/types/lnurl/LNURLPay.d.ts +61 -61
  218. package/dist/types/lnurl/LNURLPay.js +31 -31
  219. package/dist/types/lnurl/LNURLWithdraw.d.ts +48 -48
  220. package/dist/types/lnurl/LNURLWithdraw.js +27 -27
  221. package/dist/types/wallets/LightningInvoiceCreateService.d.ts +24 -24
  222. package/dist/types/wallets/LightningInvoiceCreateService.js +15 -15
  223. package/dist/types/wallets/MinimalBitcoinWalletInterface.d.ts +23 -23
  224. package/dist/types/wallets/MinimalBitcoinWalletInterface.js +2 -2
  225. package/dist/types/wallets/MinimalLightningNetworkWalletInterface.d.ts +9 -9
  226. package/dist/types/wallets/MinimalLightningNetworkWalletInterface.js +2 -2
  227. package/dist/utils/AutomaticClockDriftCorrection.d.ts +1 -1
  228. package/dist/utils/AutomaticClockDriftCorrection.js +70 -70
  229. package/dist/utils/BitcoinUtils.d.ts +18 -18
  230. package/dist/utils/BitcoinUtils.js +174 -174
  231. package/dist/utils/BitcoinWalletUtils.d.ts +7 -7
  232. package/dist/utils/BitcoinWalletUtils.js +14 -14
  233. package/dist/utils/Logger.d.ts +7 -7
  234. package/dist/utils/Logger.js +12 -12
  235. package/dist/utils/RetryUtils.d.ts +22 -22
  236. package/dist/utils/RetryUtils.js +67 -67
  237. package/dist/utils/SwapUtils.d.ts +88 -88
  238. package/dist/utils/SwapUtils.js +72 -72
  239. package/dist/utils/TimeoutUtils.d.ts +17 -17
  240. package/dist/utils/TimeoutUtils.js +55 -55
  241. package/dist/utils/TokenUtils.d.ts +19 -19
  242. package/dist/utils/TokenUtils.js +37 -37
  243. package/dist/utils/TypeUtils.d.ts +7 -7
  244. package/dist/utils/TypeUtils.js +2 -2
  245. package/dist/utils/Utils.d.ts +69 -69
  246. package/dist/utils/Utils.js +214 -214
  247. package/package.json +46 -46
  248. package/src/SmartChainAssets.ts +186 -186
  249. package/src/api/ApiEndpoints.ts +427 -427
  250. package/src/api/ApiParser.ts +138 -138
  251. package/src/api/ApiTypes.ts +229 -229
  252. package/src/api/SerializedAction.ts +97 -97
  253. package/src/api/SwapperApi.ts +545 -545
  254. package/src/api/index.ts +5 -5
  255. package/src/bitcoin/coinselect2/accumulative.ts +69 -69
  256. package/src/bitcoin/coinselect2/blackjack.ts +50 -50
  257. package/src/bitcoin/coinselect2/index.ts +93 -93
  258. package/src/bitcoin/coinselect2/utils.ts +236 -236
  259. package/src/bitcoin/wallet/BitcoinWallet.ts +439 -439
  260. package/src/bitcoin/wallet/IBitcoinWallet.ts +140 -140
  261. package/src/bitcoin/wallet/SingleAddressBitcoinWallet.ts +225 -225
  262. package/src/enums/FeeType.ts +15 -15
  263. package/src/enums/SwapAmountType.ts +16 -16
  264. package/src/enums/SwapDirection.ts +15 -15
  265. package/src/enums/SwapSide.ts +16 -16
  266. package/src/enums/SwapType.ts +75 -75
  267. package/src/errors/IntermediaryError.ts +28 -28
  268. package/src/errors/RequestError.ts +64 -64
  269. package/src/errors/UserError.ts +15 -15
  270. package/src/events/UnifiedSwapEventListener.ts +181 -181
  271. package/src/http/HttpUtils.ts +97 -97
  272. package/src/http/paramcoders/IParamReader.ts +9 -9
  273. package/src/http/paramcoders/ParamDecoder.ts +145 -145
  274. package/src/http/paramcoders/ParamEncoder.ts +40 -40
  275. package/src/http/paramcoders/SchemaVerifier.ts +153 -153
  276. package/src/http/paramcoders/client/ResponseParamDecoder.ts +57 -57
  277. package/src/http/paramcoders/client/StreamParamEncoder.ts +28 -28
  278. package/src/http/paramcoders/client/StreamingFetchPromise.ts +194 -194
  279. package/src/index.ts +141 -141
  280. package/src/intermediaries/Intermediary.ts +280 -280
  281. package/src/intermediaries/IntermediaryDiscovery.ts +548 -548
  282. package/src/intermediaries/apis/IntermediaryAPI.ts +1247 -1247
  283. package/src/intermediaries/auth/SignedKeyBasedAuth.ts +69 -69
  284. package/src/lnurl/LNURL.ts +402 -402
  285. package/src/prices/RedundantSwapPrice.ts +264 -264
  286. package/src/prices/SingleSwapPrice.ts +50 -50
  287. package/src/prices/SwapPriceWithChain.ts +194 -194
  288. package/src/prices/abstract/ICachedSwapPrice.ts +85 -85
  289. package/src/prices/abstract/IPriceProvider.ts +127 -127
  290. package/src/prices/abstract/ISwapPrice.ts +390 -390
  291. package/src/prices/providers/BinancePriceProvider.ts +48 -48
  292. package/src/prices/providers/CoinGeckoPriceProvider.ts +46 -46
  293. package/src/prices/providers/CoinPaprikaPriceProvider.ts +49 -49
  294. package/src/prices/providers/CustomPriceProvider.ts +40 -40
  295. package/src/prices/providers/KrakenPriceProvider.ts +83 -83
  296. package/src/prices/providers/OKXPriceProvider.ts +59 -59
  297. package/src/prices/providers/abstract/ExchangePriceProvider.ts +31 -31
  298. package/src/prices/providers/abstract/HttpPriceProvider.ts +14 -14
  299. package/src/storage/IUnifiedStorage.ts +136 -136
  300. package/src/storage/UnifiedSwapStorage.ts +175 -175
  301. package/src/storage-browser/IndexedDBUnifiedStorage.ts +350 -350
  302. package/src/storage-browser/LocalStorageManager.ts +106 -106
  303. package/src/swapper/Swapper.ts +2557 -2570
  304. package/src/swapper/SwapperFactory.ts +307 -307
  305. package/src/swapper/SwapperUtils.ts +610 -610
  306. package/src/swapper/SwapperWithChain.ts +707 -707
  307. package/src/swapper/SwapperWithSigner.ts +511 -511
  308. package/src/swaps/IAddressSwap.ts +30 -30
  309. package/src/swaps/IBTCWalletSwap.ts +92 -92
  310. package/src/swaps/IClaimableSwap.ts +65 -65
  311. package/src/swaps/IClaimableSwapWrapper.ts +17 -17
  312. package/src/swaps/IRefundableSwap.ts +58 -58
  313. package/src/swaps/ISwap.ts +775 -775
  314. package/src/swaps/ISwapWithGasDrop.ts +25 -25
  315. package/src/swaps/ISwapWrapper.ts +564 -564
  316. package/src/swaps/escrow_swaps/IEscrowSelfInitSwap.ts +217 -217
  317. package/src/swaps/escrow_swaps/IEscrowSwap.ts +271 -271
  318. package/src/swaps/escrow_swaps/IEscrowSwapWrapper.ts +284 -284
  319. package/src/swaps/escrow_swaps/frombtc/IFromBTCLNWrapper.ts +172 -172
  320. package/src/swaps/escrow_swaps/frombtc/IFromBTCSelfInitSwap.ts +300 -300
  321. package/src/swaps/escrow_swaps/frombtc/IFromBTCWrapper.ts +107 -107
  322. package/src/swaps/escrow_swaps/frombtc/ln/FromBTCLNSwap.ts +1670 -1671
  323. package/src/swaps/escrow_swaps/frombtc/ln/FromBTCLNWrapper.ts +603 -603
  324. package/src/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap.ts +1883 -1883
  325. package/src/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoWrapper.ts +752 -752
  326. package/src/swaps/escrow_swaps/frombtc/onchain/FromBTCSwap.ts +1753 -1753
  327. package/src/swaps/escrow_swaps/frombtc/onchain/FromBTCWrapper.ts +612 -612
  328. package/src/swaps/escrow_swaps/tobtc/IToBTCSwap.ts +1327 -1327
  329. package/src/swaps/escrow_swaps/tobtc/IToBTCWrapper.ts +138 -138
  330. package/src/swaps/escrow_swaps/tobtc/ln/ToBTCLNSwap.ts +304 -304
  331. package/src/swaps/escrow_swaps/tobtc/ln/ToBTCLNWrapper.ts +787 -787
  332. package/src/swaps/escrow_swaps/tobtc/onchain/ToBTCSwap.ts +206 -206
  333. package/src/swaps/escrow_swaps/tobtc/onchain/ToBTCWrapper.ts +403 -403
  334. package/src/swaps/spv_swaps/SpvFromBTCSwap.ts +2148 -2148
  335. package/src/swaps/spv_swaps/SpvFromBTCWrapper.ts +1238 -1238
  336. package/src/swaps/trusted/ln/LnForGasSwap.ts +753 -753
  337. package/src/swaps/trusted/ln/LnForGasWrapper.ts +90 -90
  338. package/src/swaps/trusted/onchain/OnchainForGasSwap.ts +843 -843
  339. package/src/swaps/trusted/onchain/OnchainForGasWrapper.ts +133 -133
  340. package/src/types/AmountData.ts +9 -9
  341. package/src/types/CustomPriceFunction.ts +11 -11
  342. package/src/types/PriceInfoType.ts +66 -66
  343. package/src/types/SwapExecutionAction.ts +323 -323
  344. package/src/types/SwapExecutionStep.ts +224 -224
  345. package/src/types/SwapStateInfo.ts +6 -6
  346. package/src/types/SwapWithSigner.ts +61 -61
  347. package/src/types/Token.ts +163 -163
  348. package/src/types/TokenAmount.ts +167 -167
  349. package/src/types/fees/Fee.ts +56 -56
  350. package/src/types/fees/FeeBreakdown.ts +11 -11
  351. package/src/types/fees/PercentagePPM.ts +26 -26
  352. package/src/types/lnurl/LNURLPay.ts +79 -79
  353. package/src/types/lnurl/LNURLWithdraw.ts +61 -61
  354. package/src/types/wallets/LightningInvoiceCreateService.ts +30 -30
  355. package/src/types/wallets/MinimalBitcoinWalletInterface.ts +21 -21
  356. package/src/types/wallets/MinimalLightningNetworkWalletInterface.ts +9 -9
  357. package/src/utils/AutomaticClockDriftCorrection.ts +71 -71
  358. package/src/utils/BitcoinUtils.ts +164 -164
  359. package/src/utils/BitcoinWalletUtils.ts +15 -15
  360. package/src/utils/Logger.ts +14 -14
  361. package/src/utils/RetryUtils.ts +78 -78
  362. package/src/utils/SwapUtils.ts +99 -99
  363. package/src/utils/TimeoutUtils.ts +49 -49
  364. package/src/utils/TokenUtils.ts +33 -33
  365. package/src/utils/TypeUtils.ts +8 -8
  366. package/src/utils/Utils.ts +221 -221
@@ -1,1238 +1,1238 @@
1
- import {ISwapWrapper, ISwapWrapperOptions, SwapTypeDefinition, WrapperCtorTokens} from "../ISwapWrapper";
2
- import {
3
- BitcoinRpcWithAddressIndex, BtcBlock,
4
- BtcRelay,
5
- ChainEvent,
6
- ChainType,
7
- RelaySynchronizer,
8
- SpvVaultClaimEvent,
9
- SpvVaultCloseEvent, SpvVaultData,
10
- SpvVaultFrontEvent,
11
- SpvVaultTokenBalance,
12
- SpvWithdrawalClaimedState,
13
- SpvWithdrawalFrontedState,
14
- SpvWithdrawalStateType
15
- } from "@atomiqlabs/base";
16
- import {SpvFromBTCSwap, SpvFromBTCSwapInit, SpvFromBTCSwapState} from "./SpvFromBTCSwap";
17
- import {BTC_NETWORK, TEST_NETWORK} from "@scure/btc-signer/utils";
18
- import {SwapType} from "../../enums/SwapType";
19
- import {UnifiedSwapStorage} from "../../storage/UnifiedSwapStorage";
20
- import {UnifiedSwapEventListener} from "../../events/UnifiedSwapEventListener";
21
- import {ISwapPrice} from "../../prices/abstract/ISwapPrice";
22
- import {EventEmitter} from "events";
23
- import {Intermediary} from "../../intermediaries/Intermediary";
24
- import {extendAbortController, mapArrayToObject, randomBytes, throwIfUndefined} from "../../utils/Utils";
25
- import {
26
- fromOutputScript,
27
- getDummyOutputScript,
28
- toCoinselectAddressType,
29
- toOutputScript
30
- } from "../../utils/BitcoinUtils";
31
- import {IntermediaryAPI, SpvFromBTCPrepareResponseType} from "../../intermediaries/apis/IntermediaryAPI";
32
- import {OutOfBoundsError, RequestError} from "../../errors/RequestError";
33
- import {IntermediaryError} from "../../errors/IntermediaryError";
34
- import {CoinselectAddressTypes} from "../../bitcoin/coinselect2";
35
- import {OutScript, Transaction} from "@scure/btc-signer";
36
- import {ISwap} from "../ISwap";
37
- import {IClaimableSwapWrapper} from "../IClaimableSwapWrapper";
38
- import {AmountData} from "../../types/AmountData";
39
- import {tryWithRetries} from "../../utils/RetryUtils";
40
- import {AllOptional} from "../../utils/TypeUtils";
41
- import {UserError} from "../../errors/UserError";
42
- import {BitcoinWalletUtxo, BitcoinWalletUtxoBase, IBitcoinWallet} from "../../bitcoin/wallet/IBitcoinWallet";
43
- import {utils} from "../../bitcoin/coinselect2/utils";
44
- import {BitcoinWallet} from "../../bitcoin/wallet/BitcoinWallet";
45
-
46
- export type SpvFromBTCOptions = {
47
- /**
48
- * Optional additional native token to receive as an output of the swap (e.g. STRK on Starknet or cBTC on Citrea).
49
- *
50
- * When passed as a `bigint` it is specified in base units of the token and in `string` it is the human readable
51
- * decimal format.
52
- */
53
- gasAmount?: bigint | string,
54
- /**
55
- * The LP enforces a minimum bitcoin fee rate in sats/vB for the swap transaction. With this config you can optionally
56
- * limit how high of a minimum fee rate would you accept.
57
- *
58
- * By default the maximum allowed fee rate is calculated dynamically based on current bitcoin fee rate as:
59
- *
60
- * `maxAllowedBitcoinFeeRate` = 10 + `currentBitcoinFeeRate` * 1.5
61
- */
62
- maxAllowedBitcoinFeeRate?: number,
63
- /**
64
- * A flag to attach 0 watchtower fee to the swap, this would make the settlement unattractive for the watchtowers
65
- * and therefore automatic settlement for such swaps will not be possible, you will have to settle manually
66
- * with {@link FromBTCLNSwap.claim} or {@link FromBTCLNSwap.txsClaim} functions.
67
- */
68
- unsafeZeroWatchtowerFee?: boolean,
69
- /**
70
- * A safety factor to use when estimating the watchtower fee to attach to the swap (this has to cover the gas fee
71
- * of watchtowers settling the swap). A higher multiple here would mean that a swap is more attractive for
72
- * watchtowers to settle automatically.
73
- *
74
- * Uses a `1.25` multiple by default (i.e. the current network fee is multiplied by 1.25 and then used to estimate
75
- * the settlement gas fee cost)
76
- */
77
- feeSafetyFactor?: number,
78
- /**
79
- * Instruct the LP to create a "sticky address" for your destination wallet address. After the first successful
80
- * swap with that LP, the used bitcoin address will be permanently linked to your destination wallet address. So
81
- * all subsequent swaps to the same address will yield the same LP deposit bitcoin address. Useful for corporate
82
- * whitelist-only wallets
83
- */
84
- stickyAddress?: boolean,
85
- /**
86
- * A bitcoin wallet UTXOs to fully use as an input for this swap, use this option along with passing `amount` as
87
- * `undefined` when you want to swap the full BTC balance of the wallet in a single swap
88
- */
89
- sourceWalletUtxos?: BitcoinWalletUtxoBase[] | Promise<BitcoinWalletUtxoBase[]>,
90
- /**
91
- * Bitcoin fee rate to use when deriving `maxAllowedBitcoinFeeRate` and when calculating the input amount based
92
- * on the `sourceWalletUtxos`
93
- */
94
- bitcoinFeeRate?: Promise<number> | number,
95
-
96
- /**
97
- * @deprecated Use `maxAllowedBitcoinFeeRate` instead!
98
- */
99
- maxAllowedNetworkFeeRate?: number,
100
- };
101
-
102
- export type SpvFromBTCWrapperOptions = ISwapWrapperOptions & {
103
- maxConfirmations: number,
104
- bitcoinNetwork: BTC_NETWORK,
105
- bitcoinBlocktime: number,
106
- maxTransactionsDelta: number, //Maximum accepted difference in state between SC state and bitcoin state, in terms of by how many transactions are they differing
107
- maxRawAmountAdjustmentDifferencePPM: number,
108
- maxBtcFeeMultiplier: number,
109
- maxBtcFeeOffset: number
110
- };
111
-
112
- export type SpvFromBTCTypeDefinition<T extends ChainType> = SwapTypeDefinition<T, SpvFromBTCWrapper<T>, SpvFromBTCSwap<T>>;
113
-
114
- export const REQUIRED_SPV_SWAP_VAULT_ADDRESS_TYPE: CoinselectAddressTypes = "p2tr";
115
- export const REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE: CoinselectAddressTypes = "p2wpkh";
116
-
117
- /**
118
- * New spv vault (UTXO-controlled vault) based swaps for Bitcoin -> Smart chain swaps not requiring
119
- * any initiation on the destination chain, and with the added possibility for the user to receive
120
- * a native token on the destination chain as part of the swap (a "gas drop" feature).
121
- *
122
- * @category Swaps/Bitcoin → Smart chain
123
- */
124
- export class SpvFromBTCWrapper<
125
- T extends ChainType
126
- > extends ISwapWrapper<T, SpvFromBTCTypeDefinition<T>, SpvFromBTCWrapperOptions> implements IClaimableSwapWrapper<SpvFromBTCSwap<T>> {
127
- public readonly TYPE: SwapType.SPV_VAULT_FROM_BTC = SwapType.SPV_VAULT_FROM_BTC;
128
- /**
129
- * @internal
130
- */
131
- readonly _claimableSwapStates = [SpvFromBTCSwapState.BTC_TX_CONFIRMED];
132
- /**
133
- * @internal
134
- */
135
- readonly _swapDeserializer = SpvFromBTCSwap;
136
-
137
-
138
- /**
139
- * @internal
140
- */
141
- protected readonly btcRelay: (version?: string) => BtcRelay<any, T["TX"], any> = (version?: string) => {
142
- const _version = version ?? "v1";
143
- const data = this.versionedContracts[_version];
144
- if(data==null) throw new Error(`Invalid contract version ${_version} requested`);
145
- return data.btcRelay;
146
- };
147
- /**
148
- * @internal
149
- */
150
- protected readonly tickSwapState: Array<SpvFromBTCSwap<T>["_state"]> = [
151
- SpvFromBTCSwapState.CREATED,
152
- SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED,
153
- SpvFromBTCSwapState.SIGNED,
154
- SpvFromBTCSwapState.POSTED,
155
- SpvFromBTCSwapState.BROADCASTED
156
- ];
157
-
158
-
159
- /**
160
- * @internal
161
- */
162
- readonly _synchronizer: (version?: string) => RelaySynchronizer<any, T["TX"], any> = (version?: string) => {
163
- const _version = version ?? "v1";
164
- const data = this.versionedSynchronizer[_version];
165
- if(data==null) throw new Error(`Invalid contract version ${_version} requested`);
166
- return data.synchronizer;
167
- };
168
- /**
169
- * @internal
170
- */
171
- readonly _contract: (version?: string) => T["SpvVaultContract"] = (version?: string) => {
172
- const _version = version ?? "v1";
173
- const data = this.versionedContracts[_version];
174
- if(data==null) throw new Error(`Invalid contract version ${_version} requested`);
175
- return data.spvVaultContract;
176
- };
177
- /**
178
- * @internal
179
- */
180
- readonly _btcRpc: BitcoinRpcWithAddressIndex<BtcBlock>;
181
- /**
182
- * @internal
183
- */
184
- readonly _spvWithdrawalDataDeserializer: (version?: string) => (new (data: any) => T["SpvVaultWithdrawalData"]) = (version?: string) => {
185
- const _version = version ?? "v1";
186
- const data = this.versionedContracts[_version];
187
- if(data==null) throw new Error(`Invalid contract version ${_version} requested`);
188
- return data.spvVaultWithdrawalDataConstructor;
189
- };
190
-
191
- /**
192
- * @internal
193
- */
194
- readonly _pendingSwapStates: Array<SpvFromBTCSwap<T>["_state"]> = [
195
- SpvFromBTCSwapState.CREATED,
196
- SpvFromBTCSwapState.SIGNED,
197
- SpvFromBTCSwapState.POSTED,
198
- SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED,
199
- SpvFromBTCSwapState.BROADCASTED,
200
- SpvFromBTCSwapState.DECLINED,
201
- SpvFromBTCSwapState.BTC_TX_CONFIRMED
202
- ];
203
-
204
- private readonly versionedContracts: {
205
- [version: string]: {
206
- btcRelay: BtcRelay<any, T["TX"], any>,
207
- spvVaultContract: T["SpvVaultContract"],
208
- spvVaultWithdrawalDataConstructor: new (data: any) => T["SpvVaultWithdrawalData"]
209
- }
210
- } = {};
211
-
212
- private readonly versionedSynchronizer: {
213
- [version: string]: {
214
- synchronizer: RelaySynchronizer<any, T["TX"], any>
215
- }
216
- } = {};
217
-
218
- /**
219
- * @param chainIdentifier
220
- * @param unifiedStorage Storage interface for the current environment
221
- * @param unifiedChainEvents On-chain event listener
222
- * @param chain
223
- * @param prices Pricing to use
224
- * @param tokens
225
- * @param versionedContracts
226
- * @param versionedSynchronizer
227
- * @param btcRpc Bitcoin RPC which also supports getting transactions by txoHash
228
- * @param lpApi
229
- * @param options
230
- * @param events Instance to use for emitting events
231
- */
232
- constructor(
233
- chainIdentifier: string,
234
- unifiedStorage: UnifiedSwapStorage<T>,
235
- unifiedChainEvents: UnifiedSwapEventListener<T>,
236
- chain: T["ChainInterface"],
237
- prices: ISwapPrice,
238
- tokens: WrapperCtorTokens,
239
- versionedContracts: {
240
- [version: string]: {
241
- btcRelay: BtcRelay<any, T["TX"], any>,
242
- spvVaultContract: T["SpvVaultContract"],
243
- spvVaultWithdrawalDataConstructor: new (data: any) => T["SpvVaultWithdrawalData"]
244
- }
245
- },
246
- versionedSynchronizer: {
247
- [version: string]: {
248
- synchronizer: RelaySynchronizer<any, T["TX"], any>
249
- }
250
- },
251
- btcRpc: BitcoinRpcWithAddressIndex<any>,
252
- lpApi: IntermediaryAPI,
253
- options?: AllOptional<SpvFromBTCWrapperOptions>,
254
- events?: EventEmitter<{swapState: [ISwap]}>
255
- ) {
256
- super(
257
- chainIdentifier, unifiedStorage, unifiedChainEvents, chain, prices, tokens, lpApi,
258
- {
259
- ...options,
260
- bitcoinNetwork: options?.bitcoinNetwork ?? TEST_NETWORK,
261
- maxConfirmations: options?.maxConfirmations ?? 6,
262
- bitcoinBlocktime: options?.bitcoinBlocktime ?? 10*60,
263
- maxTransactionsDelta: options?.maxTransactionsDelta ?? 3,
264
- maxRawAmountAdjustmentDifferencePPM: options?.maxRawAmountAdjustmentDifferencePPM ?? 100,
265
- maxBtcFeeOffset: options?.maxBtcFeeOffset ?? 10,
266
- maxBtcFeeMultiplier: options?.maxBtcFeeMultiplier ?? 1.5
267
- },
268
- events
269
- );
270
- this.versionedContracts = versionedContracts;
271
- this.versionedSynchronizer = versionedSynchronizer;
272
- this._btcRpc = btcRpc;
273
- }
274
-
275
- private async processEventFront(event: SpvVaultFrontEvent, swap: SpvFromBTCSwap<T>): Promise<boolean> {
276
- if(
277
- swap._state===SpvFromBTCSwapState.SIGNED || swap._state===SpvFromBTCSwapState.POSTED ||
278
- swap._state===SpvFromBTCSwapState.BROADCASTED || swap._state===SpvFromBTCSwapState.DECLINED ||
279
- swap._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED || swap._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED
280
- ) {
281
- swap._state = SpvFromBTCSwapState.FRONTED;
282
- await swap._setBitcoinTxId(event.btcTxId).catch(e => {
283
- this.logger.warn("processEventFront(): Failed to set bitcoin txId: ", e);
284
- });
285
- return true;
286
- }
287
- return false;
288
- }
289
-
290
- private async processEventClaim(event: SpvVaultClaimEvent, swap: SpvFromBTCSwap<T>): Promise<boolean> {
291
- if(
292
- swap._state===SpvFromBTCSwapState.SIGNED || swap._state===SpvFromBTCSwapState.POSTED ||
293
- swap._state===SpvFromBTCSwapState.BROADCASTED || swap._state===SpvFromBTCSwapState.DECLINED ||
294
- swap._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED || swap._state===SpvFromBTCSwapState.FRONTED ||
295
- swap._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED
296
- ) {
297
- swap._state = SpvFromBTCSwapState.CLAIMED;
298
- await swap._setBitcoinTxId(event.btcTxId).catch(e => {
299
- this.logger.warn("processEventClaim(): Failed to set bitcoin txId: ", e);
300
- });
301
- return true;
302
- }
303
- return false;
304
- }
305
-
306
- private processEventClose(event: SpvVaultCloseEvent, swap: SpvFromBTCSwap<T>): Promise<boolean> {
307
- if(
308
- swap._state===SpvFromBTCSwapState.SIGNED || swap._state===SpvFromBTCSwapState.POSTED ||
309
- swap._state===SpvFromBTCSwapState.BROADCASTED || swap._state===SpvFromBTCSwapState.DECLINED ||
310
- swap._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED || swap._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED
311
- ) {
312
- swap._state = SpvFromBTCSwapState.CLOSED;
313
- return Promise.resolve(true);
314
- }
315
- return Promise.resolve(false);
316
- }
317
-
318
- /**
319
- * @inheritDoc
320
- * @internal
321
- */
322
- protected async processEvent(event: ChainEvent<T["Data"]>, swap: SpvFromBTCSwap<T>): Promise<void> {
323
- if(swap==null) return;
324
-
325
- let swapChanged: boolean = false;
326
- if(event instanceof SpvVaultFrontEvent) {
327
- swapChanged = await this.processEventFront(event, swap);
328
- if(event.meta?.txId!=null && swap._frontTxId!==event.meta.txId) {
329
- swap._frontTxId = event.meta.txId;
330
- swapChanged ||= true;
331
- }
332
- }
333
- if(event instanceof SpvVaultClaimEvent) {
334
- swapChanged = await this.processEventClaim(event, swap);
335
- if(event.meta?.txId!=null && swap._claimTxId!==event.meta.txId) {
336
- swap._claimTxId = event.meta.txId;
337
- swapChanged ||= true;
338
- }
339
- }
340
- if(event instanceof SpvVaultCloseEvent) {
341
- swapChanged = await this.processEventClose(event, swap);
342
- }
343
-
344
- this.logger.info("processEvents(): "+event.constructor.name+" processed for "+swap.getId()+" swap: ", swap);
345
-
346
- if(swapChanged) {
347
- await swap._saveAndEmit();
348
- }
349
- }
350
-
351
- /**
352
- * Pre-fetches latest finalized block height of the smart chain
353
- *
354
- * @param abortController
355
- * @private
356
- */
357
- private async preFetchFinalizedBlockHeight(abortController: AbortController): Promise<number | undefined> {
358
- try {
359
- const block = await this._chain.getFinalizedBlock();
360
- return block.height;
361
- } catch (e) {
362
- abortController.abort(e);
363
- }
364
- }
365
-
366
- /**
367
- * Pre-fetches caller (watchtower) bounty data for the swap. Doesn't throw, instead returns null and aborts the
368
- * provided abortController
369
- *
370
- * @param amountData
371
- * @param options Options as passed to the swap creation function
372
- * @param abortController
373
- * @param contractVersion
374
- * @private
375
- */
376
- private async preFetchCallerFeeInNativeToken(
377
- amountData: {amount?: bigint},
378
- options: {
379
- unsafeZeroWatchtowerFee: boolean,
380
- feeSafetyFactor: number
381
- },
382
- abortController: AbortController,
383
- contractVersion: string
384
- ): Promise<bigint | undefined> {
385
- if(options.unsafeZeroWatchtowerFee) return 0n;
386
- if(amountData.amount===0n) return 0n;
387
-
388
- try {
389
- const [
390
- feePerBlock,
391
- btcRelayData,
392
- currentBtcBlock,
393
- claimFeeRate
394
- ] = await Promise.all([
395
- this.btcRelay(contractVersion).getFeePerBlock(),
396
- this.btcRelay(contractVersion).getTipData(),
397
- this._btcRpc.getTipHeight(),
398
- this._contract(contractVersion).getClaimFee(this._chain.randomAddress())
399
- ]);
400
-
401
- if(btcRelayData==null) throw new Error("Btc relay doesn't seem to be initialized!");
402
-
403
- const currentBtcRelayBlock = btcRelayData.blockheight;
404
- const blockDelta = Math.max(currentBtcBlock-currentBtcRelayBlock+this._options.maxConfirmations, 0);
405
-
406
- const totalFeeInNativeToken = (
407
- (BigInt(blockDelta) * feePerBlock) +
408
- (claimFeeRate * BigInt(this._options.maxTransactionsDelta))
409
- ) * BigInt(Math.floor(options.feeSafetyFactor*1000000)) / 1_000_000n;
410
-
411
- return totalFeeInNativeToken;
412
- } catch (e) {
413
- abortController.abort(e);
414
- }
415
- }
416
-
417
- /**
418
- * Pre-fetches caller (watchtower) bounty data for the swap. Doesn't throw, instead returns null and aborts the
419
- * provided abortController
420
- *
421
- * @param amountPrefetch
422
- * @param totalFeeInNativeTokenPrefetch
423
- * @param amountData
424
- * @param options Options as passed to the swap creation function
425
- * @param pricePrefetch
426
- * @param nativeTokenPricePrefetch
427
- * @param abortSignal
428
- * @private
429
- */
430
- private async computeCallerFeeShare(
431
- amountPrefetch: Promise<bigint | undefined>,
432
- totalFeeInNativeTokenPrefetch: Promise<bigint | undefined>,
433
- amountData: {exactIn: boolean, token: string},
434
- options: {unsafeZeroWatchtowerFee: boolean},
435
- pricePrefetch: Promise<bigint | undefined>,
436
- nativeTokenPricePrefetch: Promise<bigint | undefined> | undefined,
437
- abortSignal?: AbortSignal
438
- ): Promise<bigint> {
439
- if(options.unsafeZeroWatchtowerFee) return 0n;
440
-
441
- const amount = await throwIfUndefined(amountPrefetch, "Cannot get swap amount!");
442
- if(amount===0n) return 0n;
443
-
444
- const totalFeeInNativeToken = await throwIfUndefined(totalFeeInNativeTokenPrefetch, "Cannot get total fee in native token!");
445
- const nativeTokenPrice = await nativeTokenPricePrefetch;
446
-
447
- let payoutAmount: bigint;
448
- if(amountData.exactIn) {
449
- //Convert input amount in BTC to
450
- const amountInNativeToken = await this._prices.getFromBtcSwapAmount(this.chainIdentifier, amount, this._chain.getNativeCurrencyAddress(), abortSignal, nativeTokenPrice);
451
- payoutAmount = amountInNativeToken - totalFeeInNativeToken;
452
- } else {
453
- if(amountData.token===this._chain.getNativeCurrencyAddress()) {
454
- //Both amounts in same currency
455
- payoutAmount = amount;
456
- } else {
457
- //Need to convert both to native currency
458
- const btcAmount = await this._prices.getToBtcSwapAmount(this.chainIdentifier, amount, amountData.token, abortSignal, await pricePrefetch);
459
- payoutAmount = await this._prices.getFromBtcSwapAmount(this.chainIdentifier, btcAmount, this._chain.getNativeCurrencyAddress(), abortSignal, nativeTokenPrice);
460
- }
461
- }
462
-
463
- this.logger.debug("computeCallerFeeShare(): Caller fee in native token: "+totalFeeInNativeToken.toString(10)+" total payout in native token: "+payoutAmount.toString(10));
464
-
465
- const callerFeeShare = ((totalFeeInNativeToken * 100_000n) + payoutAmount - 1n) / payoutAmount; //Make sure to round up here
466
- if(callerFeeShare < 0n) return 0n;
467
- if(callerFeeShare >= 2n**20n) return 2n**20n - 1n;
468
- return callerFeeShare;
469
- }
470
-
471
- /**
472
- * Verifies response returned from intermediary
473
- *
474
- * @param resp Response as returned by the intermediary
475
- * @param amountData
476
- * @param lp Intermediary
477
- * @param options Options as passed to the swap creation function
478
- * @param callerFeeShare
479
- * @param maxBitcoinFeeRatePromise Maximum accepted fee rate from the LPs
480
- * @param bitcoinFeeRatePromise
481
- * @param abortSignal
482
- * @private
483
- * @throws {IntermediaryError} in case the response is invalid
484
- */
485
- private async verifyReturnedData(
486
- resp: SpvFromBTCPrepareResponseType,
487
- amountData: AmountData,
488
- lp: Intermediary,
489
- options: {
490
- gasAmount: bigint,
491
- sourceWalletUtxos?: Promise<BitcoinWalletUtxoBase[]>
492
- },
493
- callerFeeShare: bigint,
494
- maxBitcoinFeeRatePromise: Promise<number | undefined>,
495
- bitcoinFeeRatePromise: Promise<number | undefined> | undefined,
496
- abortSignal: AbortSignal
497
- ): Promise<{
498
- vault: T["SpvVaultData"],
499
- vaultUtxoValue: number
500
- }> {
501
- const btcFeeRate = await throwIfUndefined(maxBitcoinFeeRatePromise, "Bitcoin fee rate promise failed!");
502
- abortSignal.throwIfAborted();
503
- if(btcFeeRate!=null && resp.btcFeeRate > btcFeeRate) throw new IntermediaryError(`Required bitcoin fee rate returned from the LP is too high! Maximum accepted: ${btcFeeRate} sats/vB, required by LP: ${resp.btcFeeRate} sats/vB`);
504
-
505
- const lpVersion = lp.getContractVersion(this.chainIdentifier);
506
-
507
- //Vault related
508
- let vaultScript: Uint8Array;
509
- let vaultAddressType: CoinselectAddressTypes;
510
- let btcAddressScript: Uint8Array;
511
- let btcAddressType: CoinselectAddressTypes;
512
- //Ensure valid btc addresses returned
513
- try {
514
- vaultScript = toOutputScript(this._options.bitcoinNetwork, resp.vaultBtcAddress);
515
- vaultAddressType = toCoinselectAddressType(vaultScript);
516
- btcAddressScript = toOutputScript(this._options.bitcoinNetwork, resp.btcAddress);
517
- btcAddressType = toCoinselectAddressType(btcAddressScript);
518
- } catch (e) {
519
- throw new IntermediaryError("Invalid btc address data returned", e);
520
- }
521
- const decodedUtxo = resp.btcUtxo.split(":");
522
- if(
523
- resp.address!==lp.getAddress(this.chainIdentifier) || //Ensure the LP is indeed the vault owner
524
- resp.vaultId < 0n || //Ensure vaultId is not negative
525
- vaultScript==null || //Make sure vault script is parsable and of known type
526
- btcAddressScript==null || //Make sure btc address script is parsable and of known type
527
- btcAddressType!==REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE || //Constrain the btc address script type
528
- vaultAddressType!==REQUIRED_SPV_SWAP_VAULT_ADDRESS_TYPE || //Constrain the vault script type
529
- decodedUtxo.length!==2 || decodedUtxo[0].length!==64 || isNaN(parseInt(decodedUtxo[1])) || //Check valid UTXO
530
- resp.btcFeeRate < 1 || resp.btcFeeRate > 10000 //Sanity check on the returned BTC fee rate
531
- ) throw new IntermediaryError("Invalid vault data returned!");
532
-
533
- //Amounts sanity
534
- if(resp.btcAmountSwap + resp.btcAmountGas !==resp.btcAmount) throw new Error("Btc amount mismatch");
535
- if(resp.swapFeeBtc + resp.gasSwapFeeBtc !==resp.totalFeeBtc) throw new Error("Btc fee mismatch");
536
-
537
- //TODO: For now ensure fees are at 0
538
- if(
539
- resp.callerFeeShare!==callerFeeShare ||
540
- resp.frontingFeeShare!==0n ||
541
- resp.executionFeeShare!==0n
542
- ) throw new IntermediaryError("Invalid caller/fronting/execution fee returned");
543
-
544
- //Check expiry
545
- const timeNowSeconds = Math.floor(Date.now()/1000);
546
- if(resp.expiry < timeNowSeconds) throw new IntermediaryError(`Quote already expired, expiry: ${resp.expiry}, systemTime: ${timeNowSeconds}, clockAdjusted: ${(Date as any)._now!=null}`);
547
-
548
- let utxo = resp.btcUtxo.toLowerCase();
549
- const [txId, voutStr] = utxo.split(":");
550
-
551
- const abortController = extendAbortController(abortSignal);
552
- let [vault, {vaultUtxoValue, btcTx}] = await Promise.all([
553
- (async() => {
554
- //Fetch vault data
555
- let vault: T["SpvVaultData"] | null;
556
- try {
557
- vault = await this._contract(lpVersion).getVaultData(resp.address, resp.vaultId);
558
- } catch (e) {
559
- this.logger.error("Error getting spv vault (owner: "+resp.address+" vaultId: "+resp.vaultId.toString(10)+"): ", e);
560
- throw new IntermediaryError("Spv swap vault not found", e);
561
- }
562
- abortController.signal.throwIfAborted();
563
-
564
- //Make sure vault is opened
565
- if(vault==null || !vault.isOpened()) throw new IntermediaryError("Returned spv swap vault is not opened!");
566
- //Make sure the vault doesn't require insane amount of confirmations
567
- if(vault.getConfirmations()>this._options.maxConfirmations) throw new IntermediaryError("SPV swap vault needs too many confirmations: "+vault.getConfirmations());
568
- const tokenData = vault.getTokenData();
569
-
570
- //Amounts - make sure the amounts match
571
- if(amountData.exactIn) {
572
- if(!resp.usedUtxoInputCalculation) {
573
- //Legacy calculation
574
- if(resp.btcAmount !== amountData.amount) throw new IntermediaryError("Invalid amount returned");
575
- } else {
576
- //Implies the raw UTXOs were passed for amount derivation
577
- //Verify the derivation was done correctly
578
- if(options.sourceWalletUtxos==null) throw new IntermediaryError("Invalid usedUtxoInputCalcuation return value");
579
- if(bitcoinFeeRatePromise==null) throw new Error("bitcoinFeeRatePromise must be passed for UTXO-based input amount calculation checks");
580
- const walletUtxos = await options.sourceWalletUtxos;
581
- const bitcoinFeeRate = await throwIfUndefined(bitcoinFeeRatePromise, "Failed to fetch bitcoin fee rate!");
582
- const {balance} = BitcoinWallet.getSpendableBalance(
583
- walletUtxos, Math.max(resp.btcFeeRate, bitcoinFeeRate),
584
- this.getDummySwapPsbt(options.gasAmount!==0n), REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE
585
- );
586
- if(resp.btcAmount !== balance) throw new IntermediaryError(`Invalid amount returned, expected: ${balance.toString(10)}, got: ${resp.btcAmount.toString(10)}`);
587
- }
588
- } else {
589
- //Check the difference between amount adjusted due to scaling to raw amount
590
- const adjustedAmount = amountData.amount / tokenData[0].multiplier * tokenData[0].multiplier;
591
- const adjustmentPPM = (amountData.amount - adjustedAmount)*1_000_000n / amountData.amount;
592
- if(adjustmentPPM > this._options.maxRawAmountAdjustmentDifferencePPM)
593
- throw new IntermediaryError("Invalid amount0 multiplier used, rawAmount diff too high");
594
- if(resp.total !== adjustedAmount) throw new IntermediaryError("Invalid total returned");
595
- }
596
- if(options.gasAmount===0n) {
597
- if(resp.totalGas !== 0n) throw new IntermediaryError("Invalid gas total returned");
598
- } else {
599
- //Check the difference between amount adjusted due to scaling to raw amount
600
- const adjustedGasAmount = options.gasAmount / tokenData[0].multiplier * tokenData[0].multiplier;
601
- const adjustmentPPM = (options.gasAmount - adjustedGasAmount)*1_000_000n / options.gasAmount;
602
- if(adjustmentPPM > this._options.maxRawAmountAdjustmentDifferencePPM)
603
- throw new IntermediaryError("Invalid amount1 multiplier used, rawAmount diff too high");
604
- if(resp.totalGas !== adjustedGasAmount) throw new IntermediaryError("Invalid gas total returned");
605
- }
606
-
607
- return vault;
608
- })(),
609
- (async() => {
610
- //Require the vault UTXO to have at least 1 confirmation
611
- let btcTx = await this._btcRpc.getTransaction(txId);
612
- if(btcTx==null) throw new IntermediaryError("Invalid UTXO, doesn't exist (txId)");
613
- abortController.signal.throwIfAborted();
614
- if(btcTx.confirmations==null || btcTx.confirmations<1) throw new IntermediaryError("SPV vault UTXO not confirmed");
615
- const vout = parseInt(voutStr);
616
- if(btcTx.outs[vout]==null) throw new IntermediaryError("Invalid UTXO, doesn't exist");
617
- const vaultUtxoValue = btcTx.outs[vout].value;
618
- return {btcTx, vaultUtxoValue};
619
- })(),
620
- (async() => {
621
- //Require vault UTXO is unspent
622
- if(await this._btcRpc.isSpent(utxo)) throw new IntermediaryError("Returned spv vault UTXO is already spent", null, true);
623
- abortController.signal.throwIfAborted();
624
- })()
625
- ]).catch(e => {
626
- abortController.abort(e);
627
- throw e;
628
- });
629
-
630
- this.logger.debug("verifyReturnedData(): Vault UTXO: "+vault.getUtxo()+" current utxo: "+utxo);
631
-
632
- //Trace returned utxo back to what's saved on-chain
633
- let pendingWithdrawals: T["SpvVaultWithdrawalData"][] = [];
634
- while(vault.getUtxo()!==utxo) {
635
- const [txId, voutStr] = utxo.split(":");
636
- //Such that 1st tx isn't fetched twice
637
- if(btcTx.txid!==txId) {
638
- const _btcTx = await this._btcRpc.getTransaction(txId);
639
- if(_btcTx==null) throw new IntermediaryError("Invalid ancestor transaction (not found)");
640
- btcTx = _btcTx;
641
- }
642
- const withdrawalData = await this._contract(lpVersion).getWithdrawalData(btcTx);
643
- abortSignal.throwIfAborted();
644
- pendingWithdrawals.unshift(withdrawalData);
645
- utxo = pendingWithdrawals[0].getSpentVaultUtxo();
646
- this.logger.debug("verifyReturnedData(): Vault UTXO: "+vault.getUtxo()+" current utxo: "+utxo);
647
- if(pendingWithdrawals.length>=this._options.maxTransactionsDelta)
648
- throw new IntermediaryError("BTC <> SC state difference too deep, maximum: "+this._options.maxTransactionsDelta);
649
- }
650
-
651
- //Verify that the vault has enough balance after processing all pending withdrawals
652
- let vaultBalances: {balances: SpvVaultTokenBalance[]};
653
- try {
654
- vaultBalances = vault.calculateStateAfter(pendingWithdrawals);
655
- } catch (e) {
656
- this.logger.error("Error calculating spv vault balance (owner: "+resp.address+" vaultId: "+resp.vaultId.toString(10)+"): ", e);
657
- throw new IntermediaryError("Spv swap vault balance prediction failed", e);
658
- }
659
- if(vaultBalances.balances[0].scaledAmount < resp.total)
660
- throw new IntermediaryError("SPV swap vault, insufficient balance, required: "+resp.total.toString(10)+
661
- " has: "+vaultBalances.balances[0].scaledAmount.toString(10));
662
- if(vaultBalances.balances[1].scaledAmount < resp.totalGas)
663
- throw new IntermediaryError("SPV swap vault, insufficient balance, required: "+resp.totalGas.toString(10)+
664
- " has: "+vaultBalances.balances[1].scaledAmount.toString(10));
665
-
666
- //Also verify that all the withdrawal txns are valid, this is an extra sanity check
667
- try {
668
- for(let withdrawal of pendingWithdrawals) {
669
- await this._contract(lpVersion).checkWithdrawalTx(withdrawal);
670
- }
671
- } catch (e) {
672
- this.logger.error("Error calculating spv vault balance (owner: "+resp.address+" vaultId: "+resp.vaultId.toString(10)+"): ", e);
673
- throw new IntermediaryError("Spv swap vault balance prediction failed", e);
674
- }
675
- abortSignal.throwIfAborted();
676
-
677
- return {
678
- vault,
679
- vaultUtxoValue
680
- };
681
- }
682
-
683
- private async amountPrefetch(
684
- amountData: {token: string, exactIn: boolean, amount?: bigint},
685
- bitcoinFeeRatePromise: Promise<number | undefined>,
686
- walletUtxosPromise: Promise<BitcoinWalletUtxoBase[]> | undefined,
687
- includeGas: boolean,
688
- abortController: AbortController
689
- ): Promise<bigint | undefined> {
690
- if(amountData.amount!=null) return amountData.amount;
691
- try {
692
- const bitcoinFeeRate = await throwIfUndefined(bitcoinFeeRatePromise, "Cannot fetch Bitcoin fee rate!");
693
- if(walletUtxosPromise==null) throw new UserError("Cannot use empty amount without passing UTXOs!");
694
- const walletUtxos = await walletUtxosPromise;
695
- if(walletUtxos.length===0)
696
- throw new UserError("Wallet doesn't have any BTC balance");
697
- const spendableBalance = await BitcoinWallet.getSpendableBalance(
698
- walletUtxos, bitcoinFeeRate,
699
- this.getDummySwapPsbt(includeGas), REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE
700
- );
701
- return spendableBalance.balance;
702
- } catch (e) {
703
- abortController.abort(e);
704
- }
705
- }
706
-
707
- private bitcoinFeeRatePrefetch(
708
- options: {
709
- maxAllowedBitcoinFeeRate: number,
710
- sourceWalletUtxos?: Promise<BitcoinWalletUtxoBase[]>,
711
- bitcoinFeeRate?: Promise<number>
712
- },
713
- abortController: AbortController
714
- ) {
715
- let bitcoinFeeRatePromise: Promise<number | undefined> | undefined;
716
- if(options?.sourceWalletUtxos!=null) {
717
- if(options.bitcoinFeeRate!=null) {
718
- bitcoinFeeRatePromise = options.bitcoinFeeRate.then(value => {
719
- if(options.maxAllowedBitcoinFeeRate!=Infinity && options.maxAllowedBitcoinFeeRate<value)
720
- throw new Error("Passed `maxAllowedBitcoinFeeRate` cannot be lower than `bitcoinFeeRate`");
721
- return value;
722
- });
723
- } else {
724
- bitcoinFeeRatePromise = this._btcRpc.getFeeRate().then(value => {
725
- if(options.maxAllowedBitcoinFeeRate!=Infinity && value > options.maxAllowedBitcoinFeeRate) return options.maxAllowedBitcoinFeeRate;
726
- return value;
727
- });
728
- }
729
- bitcoinFeeRatePromise = bitcoinFeeRatePromise.catch(e => {
730
- abortController.abort(e);
731
- return undefined;
732
- });
733
- }
734
- const maxBitcoinFeeRatePromise: Promise<number | undefined> = options.maxAllowedBitcoinFeeRate!=Infinity
735
- ? Promise.resolve(options.maxAllowedBitcoinFeeRate)
736
- : throwIfUndefined(bitcoinFeeRatePromise ?? options.bitcoinFeeRate ?? this._btcRpc.getFeeRate())
737
- .then(x => this._options.maxBtcFeeOffset + (x*this._options.maxBtcFeeMultiplier))
738
- .catch(e => {
739
- abortController.abort(e);
740
- return undefined;
741
- });
742
-
743
- return {
744
- bitcoinFeeRatePromise,
745
- maxBitcoinFeeRatePromise
746
- }
747
- }
748
-
749
- /**
750
- * Returns a newly created Bitcoin -> Smart chain swap using the SPV vault (UTXO-controlled vault) swap protocol,
751
- * with the passed amount. Also allows specifying additional "gas drop" native token that the receipient receives
752
- * on the destination chain in the `options` argument.
753
- *
754
- * @param recipient Recipient address on the destination smart chain
755
- * @param amountData Amount, token and exact input/output data for to swap
756
- * @param lps An array of intermediaries (LPs) to get the quotes from
757
- * @param options Optional additional quote options
758
- * @param additionalParams Optional additional parameters sent to the LP when creating the swap
759
- * @param abortSignal Abort signal
760
- */
761
- public create(
762
- recipient: string,
763
- amountData: { amount?: bigint, token: string, exactIn: boolean },
764
- lps: Intermediary[],
765
- options?: SpvFromBTCOptions,
766
- additionalParams?: Record<string, any>,
767
- abortSignal?: AbortSignal
768
- ): {
769
- quote: Promise<SpvFromBTCSwap<T>>,
770
- intermediary: Intermediary
771
- }[] {
772
- const _options = {
773
- gasAmount: this.parseGasAmount(options?.gasAmount),
774
- unsafeZeroWatchtowerFee: options?.unsafeZeroWatchtowerFee ?? false,
775
- feeSafetyFactor: options?.feeSafetyFactor ?? 1.25,
776
- maxAllowedBitcoinFeeRate: options?.maxAllowedBitcoinFeeRate ?? options?.maxAllowedNetworkFeeRate ?? Infinity,
777
- sourceWalletUtxos: options?.sourceWalletUtxos==undefined
778
- ? undefined
779
- : options?.sourceWalletUtxos instanceof Promise ? options.sourceWalletUtxos : Promise.resolve(options.sourceWalletUtxos),
780
- bitcoinFeeRate: options?.bitcoinFeeRate==undefined
781
- ? undefined
782
- : options?.bitcoinFeeRate instanceof Promise ? options.bitcoinFeeRate : Promise.resolve(options.bitcoinFeeRate),
783
- };
784
-
785
- if(
786
- _options.gasAmount!==0n &&
787
- (
788
- this._chain.shouldGetNativeTokenDrop!=null
789
- ? !this._chain.shouldGetNativeTokenDrop(amountData.token)
790
- : amountData.token===this._chain.getNativeCurrencyAddress()
791
- )
792
- ) throw new UserError("Cannot specify `gasAmount` for swaps to a native token!");
793
-
794
- if(amountData.amount==null && options?.sourceWalletUtxos==null)
795
- throw new UserError("Source wallet UTXOs need to be passed when amount is null!");
796
- if(amountData.amount==null && !amountData.exactIn)
797
- throw new UserError("Amount can be null only for exactIn swaps!");
798
- if(amountData.amount!=null && options?.sourceWalletUtxos!=null)
799
- throw new UserError("Source wallet UTXOs cannot be passed while specifying an input amount!");
800
-
801
- const lpVersions = Intermediary.getContractVersionsForLps(this.chainIdentifier, lps);
802
-
803
- const _abortController = extendAbortController(abortSignal);
804
- const pricePrefetchPromise: Promise<bigint | undefined> = this.preFetchPrice(amountData, _abortController.signal);
805
- const usdPricePrefetchPromise: Promise<number | undefined> = this.preFetchUsdPrice(_abortController.signal);
806
- const finalizedBlockHeightPrefetchPromise: Promise<number | undefined> = this.preFetchFinalizedBlockHeight(_abortController);
807
- const nativeTokenAddress = this._chain.getNativeCurrencyAddress();
808
- const gasTokenPricePrefetchPromise: Promise<bigint | undefined> | undefined = _options.gasAmount===0n ?
809
- undefined :
810
- this.preFetchPrice({token: nativeTokenAddress}, _abortController.signal);
811
- const callerFeePrefetchPromise = mapArrayToObject(lpVersions, (contractVersion: string) => {
812
- return this.preFetchCallerFeeInNativeToken(amountData, _options, _abortController, contractVersion);
813
- });
814
- const {maxBitcoinFeeRatePromise, bitcoinFeeRatePromise} = this.bitcoinFeeRatePrefetch(_options, _abortController);
815
- const amountPromise = this.amountPrefetch(
816
- amountData, maxBitcoinFeeRatePromise, _options.sourceWalletUtxos, _options.gasAmount!==0n, _abortController
817
- );
818
-
819
- return lps.map(lp => {
820
- return {
821
- intermediary: lp,
822
- quote: tryWithRetries(async () => {
823
- if(lp.services[SwapType.SPV_VAULT_FROM_BTC]==null) throw new Error("LP service for processing spv vault swaps not found!");
824
- const version = lp.getContractVersion(this.chainIdentifier);
825
-
826
- const abortController = extendAbortController(_abortController.signal);
827
- const callerFeeRatePromise = this.computeCallerFeeShare(
828
- amountPromise,
829
- callerFeePrefetchPromise[version],
830
- amountData,
831
- _options,
832
- pricePrefetchPromise,
833
- gasTokenPricePrefetchPromise,
834
- abortController.signal
835
- );
836
-
837
- try {
838
- const resp = await tryWithRetries(async(retryCount: number) => {
839
- return await this._lpApi.prepareSpvFromBTC(
840
- this.chainIdentifier, lp.url,
841
- {
842
- address: recipient,
843
- amount: throwIfUndefined(amountPromise, "Failed to compute swap amount"),
844
- token: amountData.token.toString(),
845
- exactOut: !amountData.exactIn,
846
- gasToken: nativeTokenAddress,
847
- gasAmount: _options.gasAmount,
848
- callerFeeRate: throwIfUndefined(callerFeeRatePromise, "Caller fee prefetch failed!"),
849
- frontingFeeRate: 0n,
850
- stickyAddress: options?.stickyAddress,
851
- amountUtxos: _options.sourceWalletUtxos!=null
852
- ? _options.sourceWalletUtxos.then(utxos => {
853
- if(utxos.length===0) return undefined;
854
- return utxos.map(utxo => ({
855
- value: utxo.value,
856
- vSize: utils.inputBytes({type: utxo.type}),
857
- cpfp: utxo.cpfp==null ? undefined : {effectiveVSize: utxo.cpfp?.txVsize, effectiveFeeRate: utxo.cpfp?.txEffectiveFeeRate}
858
- }));
859
- })
860
- : undefined,
861
- amountFeeRate: bitcoinFeeRatePromise,
862
- additionalParams
863
- },
864
- this._options.postRequestTimeout, abortController.signal, retryCount>0 ? false : undefined
865
- );
866
- }, undefined, e => e instanceof RequestError, abortController.signal);
867
-
868
- this.logger.debug("create("+lp.url+"): LP response: ", resp)
869
-
870
- const callerFeeShare = await callerFeeRatePromise;
871
- const amount = await throwIfUndefined(amountPromise);
872
-
873
- const [
874
- pricingInfo,
875
- gasPricingInfo,
876
- {vault, vaultUtxoValue}
877
- ] = await Promise.all([
878
- this.verifyReturnedPrice(
879
- lp.services[SwapType.SPV_VAULT_FROM_BTC],
880
- false, resp.btcAmountSwap,
881
- resp.total * (100_000n + callerFeeShare) / 100_000n,
882
- amountData.token, {swapFeeBtc: resp.swapFeeBtc}, pricePrefetchPromise, usdPricePrefetchPromise, abortController.signal
883
- ),
884
- _options.gasAmount===0n ? Promise.resolve(undefined) : this.verifyReturnedPrice(
885
- {...lp.services[SwapType.SPV_VAULT_FROM_BTC], swapBaseFee: 0}, //Base fee should be charged only on the amount, not on gas
886
- false, resp.btcAmountGas,
887
- resp.totalGas * (100_000n + callerFeeShare) / 100_000n,
888
- nativeTokenAddress, {swapFeeBtc: resp.gasSwapFeeBtc}, gasTokenPricePrefetchPromise, usdPricePrefetchPromise, abortController.signal
889
- ),
890
- this.verifyReturnedData(
891
- resp,
892
- {...amountData, amount},
893
- lp, _options, callerFeeShare, maxBitcoinFeeRatePromise, bitcoinFeeRatePromise, abortController.signal
894
- )
895
- ]);
896
-
897
- let minimumBtcFeeRate: number = resp.btcFeeRate;
898
- if(bitcoinFeeRatePromise!=null) minimumBtcFeeRate = Math.max(minimumBtcFeeRate, await throwIfUndefined(bitcoinFeeRatePromise));
899
-
900
- const swapInit: SpvFromBTCSwapInit = {
901
- pricingInfo,
902
- url: lp.url,
903
- expiry: resp.expiry * 1000,
904
- swapFee: resp.swapFee,
905
- swapFeeBtc: resp.swapFeeBtc,
906
- exactIn: amountData.exactIn ?? true,
907
-
908
- quoteId: resp.quoteId,
909
-
910
- recipient,
911
-
912
- vaultOwner: resp.address,
913
- vaultId: resp.vaultId,
914
- vaultRequiredConfirmations: vault.getConfirmations(),
915
- vaultTokenMultipliers: vault.getTokenData().map(val => val.multiplier),
916
- vaultBtcAddress: resp.vaultBtcAddress,
917
- vaultUtxo: resp.btcUtxo,
918
- vaultUtxoValue: BigInt(vaultUtxoValue),
919
-
920
- btcDestinationAddress: resp.btcAddress,
921
- btcAmount: resp.btcAmount,
922
- btcAmountSwap: resp.btcAmountSwap,
923
- btcAmountGas: resp.btcAmountGas,
924
- minimumBtcFeeRate,
925
-
926
- outputTotalSwap: resp.total,
927
- outputSwapToken: amountData.token,
928
- outputTotalGas: resp.totalGas,
929
- outputGasToken: nativeTokenAddress,
930
- gasSwapFeeBtc: resp.gasSwapFeeBtc,
931
- gasSwapFee: resp.gasSwapFee,
932
- gasPricingInfo,
933
-
934
- callerFeeShare: resp.callerFeeShare,
935
- frontingFeeShare: resp.frontingFeeShare,
936
- executionFeeShare: resp.executionFeeShare,
937
-
938
- genesisSmartChainBlockHeight: await throwIfUndefined(
939
- finalizedBlockHeightPrefetchPromise,
940
- "Network finalized blockheight pre-fetch failed!"
941
- ),
942
- contractVersion: version
943
- };
944
- const quote = new SpvFromBTCSwap<T>(this, swapInit);
945
- return quote;
946
- } catch (e) {
947
- if(e instanceof OutOfBoundsError) {
948
- const amountResult = await amountPromise.catch(() => undefined);
949
- if(_options.sourceWalletUtxos!=null && amountResult!=null && amountResult<=0n) {
950
- e = new UserError("Wallet doesn't have enough BTC balance to cover transaction fees");
951
- }
952
- }
953
- abortController.abort(e);
954
- throw e;
955
- }
956
- }, undefined, err => !(err instanceof IntermediaryError && err.recoverable), _abortController.signal)
957
- }
958
- });
959
- }
960
-
961
- /**
962
- * Recovers an SPV vault (UTXO-controlled vault) based swap from smart chain on-chain data
963
- *
964
- * @param state State of the spv vault withdrawal recovered from on-chain data
965
- * @param vault SPV vault processing the swap
966
- * @param lp Intermediary (LP) used as a counterparty for the swap
967
- */
968
- public async recoverFromState(state: SpvWithdrawalClaimedState | SpvWithdrawalFrontedState, contractVersion: string, vault?: SpvVaultData | null, lp?: Intermediary): Promise<SpvFromBTCSwap<T> | null> {
969
- //Get the vault
970
- vault ??= await this._contract(contractVersion).getVaultData(state.owner, state.vaultId);
971
- if(vault==null) return null;
972
- if(state.btcTxId==null) return null;
973
- const btcTx = await this._btcRpc.getTransaction(state.btcTxId);
974
- if(btcTx==null) return null;
975
- const withdrawalData = await this._contract(contractVersion).getWithdrawalData(btcTx)
976
- .catch(e => {
977
- this.logger.warn(`Error parsing withdrawal data for tx ${btcTx.txid}: `, e);
978
- return null;
979
- });
980
- if(withdrawalData==null) return null;
981
-
982
- const vaultTokens = vault.getTokenData();
983
- const withdrawalDataOutputs = withdrawalData.getTotalOutput();
984
-
985
- const txBlock = await state.getTxBlock?.();
986
-
987
- const swapInit: SpvFromBTCSwapInit = {
988
- pricingInfo: {
989
- isValid: true,
990
- satsBaseFee: 0n,
991
- swapPriceUSatPerToken: 100_000_000_000_000n,
992
- realPriceUSatPerToken: 100_000_000_000_000n,
993
- differencePPM: 0n,
994
- feePPM: 0n,
995
- },
996
- url: lp?.url,
997
- expiry: 0,
998
- swapFee: 0n,
999
- swapFeeBtc: 0n,
1000
- exactIn: true,
1001
-
1002
- //Use bitcoin tx id as quote id, even though this is not strictly correct as this
1003
- // is an off-chain identifier presented by the LP that cannot be recovered from on-chain
1004
- // data
1005
- quoteId: btcTx.txid,
1006
-
1007
- recipient: state.recipient,
1008
-
1009
- vaultOwner: state.owner,
1010
- vaultId: state.vaultId,
1011
- vaultRequiredConfirmations: vault.getConfirmations(),
1012
- vaultTokenMultipliers: vault.getTokenData().map(val => val.multiplier),
1013
- vaultBtcAddress: fromOutputScript(this._options.bitcoinNetwork, withdrawalData.getNewVaultScript().toString("hex")),
1014
- vaultUtxo: withdrawalData.getSpentVaultUtxo(),
1015
- vaultUtxoValue: BigInt(withdrawalData.getNewVaultBtcAmount()),
1016
-
1017
- btcDestinationAddress: fromOutputScript(this._options.bitcoinNetwork, btcTx.outs[2].scriptPubKey.hex),
1018
- btcAmount: BigInt(btcTx.outs[2].value),
1019
- btcAmountSwap: BigInt(btcTx.outs[2].value),
1020
- btcAmountGas: 0n,
1021
- minimumBtcFeeRate: 0,
1022
-
1023
- outputTotalSwap: withdrawalDataOutputs[0] * vaultTokens[0].multiplier,
1024
- outputSwapToken: vaultTokens[0].token,
1025
- outputTotalGas: withdrawalDataOutputs[1] * vaultTokens[1].multiplier,
1026
- outputGasToken: vaultTokens[1].token,
1027
- gasSwapFeeBtc: 0n,
1028
- gasSwapFee: 0n,
1029
- gasPricingInfo: {
1030
- isValid: true,
1031
- satsBaseFee: 0n,
1032
- swapPriceUSatPerToken: 100_000_000_000_000n,
1033
- realPriceUSatPerToken: 100_000_000_000_000n,
1034
- differencePPM: 0n,
1035
- feePPM: 0n,
1036
- },
1037
-
1038
- callerFeeShare: withdrawalData.callerFeeRate,
1039
- frontingFeeShare: withdrawalData.frontingFeeRate,
1040
- executionFeeShare: withdrawalData.executionFeeRate,
1041
-
1042
- genesisSmartChainBlockHeight: txBlock?.blockHeight ?? 0,
1043
-
1044
- contractVersion
1045
- };
1046
- const quote = new SpvFromBTCSwap<T>(this, swapInit);
1047
- quote._data = withdrawalData;
1048
- if(txBlock!=null) {
1049
- quote.createdAt = txBlock.blockTime*1000;
1050
- } else if(btcTx.blockhash==null) {
1051
- quote.createdAt = Date.now();
1052
- } else {
1053
- const blockHeader = await this._btcRpc.getBlockHeader(btcTx.blockhash);
1054
- quote.createdAt = blockHeader==null ? Date.now() : blockHeader.getTimestamp()*1000;
1055
- }
1056
- quote._setInitiated();
1057
- if(btcTx.inputAddresses!=null) quote._senderAddress = btcTx.inputAddresses[1];
1058
- if(state.type===SpvWithdrawalStateType.FRONTED) {
1059
- quote._frontTxId = state.txId;
1060
- quote._state = SpvFromBTCSwapState.FRONTED;
1061
- } else {
1062
- quote._claimTxId = state.txId;
1063
- quote._state = SpvFromBTCSwapState.CLAIMED;
1064
- }
1065
- await quote._save();
1066
- return quote;
1067
- }
1068
-
1069
- /**
1070
- * Returns a random dummy PSBT that can be used for fee estimation, the last output (the LP output) is omitted
1071
- * to allow for coinselection algorithm to determine maximum sendable amount there
1072
- *
1073
- * @param includeGasToken Whether to return the PSBT also with the gas token amount (increases the vSize by 8)
1074
- */
1075
- public getDummySwapPsbt(includeGasToken = false): Transaction {
1076
- //Construct dummy swap psbt
1077
- const psbt = new Transaction({
1078
- allowUnknownInputs: true,
1079
- allowLegacyWitnessUtxo: true,
1080
- allowUnknownOutputs: true
1081
- });
1082
-
1083
- const randomVaultOutScript = getDummyOutputScript(REQUIRED_SPV_SWAP_VAULT_ADDRESS_TYPE);
1084
-
1085
- psbt.addInput({
1086
- txid: randomBytes(32),
1087
- index: 0,
1088
- witnessUtxo: {
1089
- script: randomVaultOutScript,
1090
- amount: 600n
1091
- }
1092
- });
1093
-
1094
- psbt.addOutput({
1095
- script: randomVaultOutScript,
1096
- amount: 600n
1097
- });
1098
-
1099
- let longestOpReturnData: Buffer | undefined = undefined;
1100
- for(let contractVersion in this.versionedContracts) {
1101
- if(this.versionedContracts[contractVersion].spvVaultContract==null) continue;
1102
- const opReturnData = this._contract(contractVersion).toOpReturnData(
1103
- this._chain.randomAddress(),
1104
- includeGasToken ? [0xFFFFFFFFFFFFFFFFn, 0xFFFFFFFFFFFFFFFFn] : [0xFFFFFFFFFFFFFFFFn]
1105
- );
1106
- if(longestOpReturnData==null || longestOpReturnData.length < opReturnData.length) longestOpReturnData = opReturnData;
1107
- }
1108
- if(longestOpReturnData==null) throw new Error(`No contract version supporting the Spv Vault BTC -> ${this.chainIdentifier} swaps found!`);
1109
-
1110
- psbt.addOutput({
1111
- script: Buffer.concat([
1112
- longestOpReturnData.length <= 75 ? Buffer.from([0x6a, longestOpReturnData.length]) : Buffer.from([0x6a, 0x4c, longestOpReturnData.length]),
1113
- longestOpReturnData
1114
- ]),
1115
- amount: 0n
1116
- });
1117
-
1118
- return psbt;
1119
- }
1120
-
1121
- /**
1122
- * @inheritDoc
1123
- * @internal
1124
- */
1125
- protected async _checkPastSwaps(pastSwaps: SpvFromBTCSwap<T>[]): Promise<{
1126
- changedSwaps: SpvFromBTCSwap<T>[];
1127
- removeSwaps: SpvFromBTCSwap<T>[]
1128
- }> {
1129
- const changedSwaps: Set<SpvFromBTCSwap<T>> = new Set();
1130
- const removeSwaps: SpvFromBTCSwap<T>[] = [];
1131
-
1132
- const broadcastedOrConfirmedSwaps: {[version: string]: (SpvFromBTCSwap<T> & {_data: T["SpvVaultWithdrawalData"]})[]} = {};
1133
-
1134
- for(let pastSwap of pastSwaps) {
1135
- let changed: boolean = false;
1136
-
1137
- if(
1138
- pastSwap._state===SpvFromBTCSwapState.SIGNED ||
1139
- pastSwap._state===SpvFromBTCSwapState.POSTED ||
1140
- pastSwap._state===SpvFromBTCSwapState.BROADCASTED ||
1141
- pastSwap._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED ||
1142
- pastSwap._state===SpvFromBTCSwapState.DECLINED ||
1143
- pastSwap._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED
1144
- ) {
1145
- //Check BTC transaction
1146
- if(await pastSwap._syncStateFromBitcoin(false)) changed ||= true;
1147
- }
1148
-
1149
- if(
1150
- pastSwap._state===SpvFromBTCSwapState.CREATED ||
1151
- pastSwap._state===SpvFromBTCSwapState.SIGNED ||
1152
- pastSwap._state===SpvFromBTCSwapState.POSTED
1153
- ) {
1154
- if(await pastSwap._verifyQuoteDefinitelyExpired()) {
1155
- if(pastSwap._state===SpvFromBTCSwapState.CREATED) {
1156
- pastSwap._state = SpvFromBTCSwapState.QUOTE_EXPIRED;
1157
- } else {
1158
- pastSwap._state = SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED;
1159
- }
1160
- changed ||= true;
1161
- }
1162
- }
1163
-
1164
- if(pastSwap.isQuoteExpired()) {
1165
- removeSwaps.push(pastSwap);
1166
- continue;
1167
- }
1168
- if(changed) changedSwaps.add(pastSwap);
1169
-
1170
- if(pastSwap._state===SpvFromBTCSwapState.BROADCASTED || pastSwap._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED) {
1171
- if(pastSwap._data!=null) (broadcastedOrConfirmedSwaps[pastSwap._contractVersion ?? "v1"] ??= []).push(pastSwap as (SpvFromBTCSwap<T> & {_data: T["SpvVaultWithdrawalData"]}));
1172
- }
1173
- }
1174
-
1175
- for(let contractVersion in broadcastedOrConfirmedSwaps) {
1176
- if(this.versionedContracts[contractVersion]==null) {
1177
- this.logger.warn(`_checkPastSwaps(): No contract was found for ${this.chainIdentifier} version ${contractVersion}! Skipping these swaps!`);
1178
- continue;
1179
- }
1180
-
1181
- const _broadcastedOrConfirmedSwaps = broadcastedOrConfirmedSwaps[contractVersion];
1182
-
1183
- const checkWithdrawalStateSwaps: (SpvFromBTCSwap<T> & {_data: T["SpvVaultWithdrawalData"]})[] = [];
1184
- const _fronts = await this._contract(contractVersion).getFronterAddresses(_broadcastedOrConfirmedSwaps.map(val => ({
1185
- ...val.getSpvVaultData(),
1186
- withdrawal: val._data!
1187
- })));
1188
- const _vaultUtxos = await this._contract(contractVersion).getVaultLatestUtxos(_broadcastedOrConfirmedSwaps.map(val => val.getSpvVaultData()));
1189
- for(const pastSwap of _broadcastedOrConfirmedSwaps) {
1190
- const fronterAddress = _fronts[pastSwap._data.getTxId()];
1191
- const vault = pastSwap.getSpvVaultData();
1192
- const latestVaultUtxo = _vaultUtxos[vault.owner]?.[vault.vaultId.toString(10)];
1193
- if(fronterAddress===undefined) this.logger.warn(`_checkPastSwaps(): No fronter address returned for ${pastSwap._data.getTxId()}`);
1194
- if(latestVaultUtxo===undefined) this.logger.warn(`_checkPastSwaps(): No last vault utxo returned for ${pastSwap._data.getTxId()}`);
1195
- if(await pastSwap._shouldCheckWithdrawalState(fronterAddress, latestVaultUtxo)) checkWithdrawalStateSwaps.push(pastSwap);
1196
- }
1197
-
1198
- const withdrawalStates = await this._contract(contractVersion).getWithdrawalStates(
1199
- checkWithdrawalStateSwaps.map(val => ({
1200
- withdrawal: val._data,
1201
- scStartBlockheight: val._genesisSmartChainBlockHeight
1202
- }))
1203
- );
1204
- for(const pastSwap of checkWithdrawalStateSwaps) {
1205
- const status = withdrawalStates[pastSwap._data.getTxId()];
1206
- if(status==null) {
1207
- this.logger.warn(`_checkPastSwaps(): No withdrawal state returned for ${pastSwap._data.getTxId()}`);
1208
- continue;
1209
- }
1210
- this.logger.debug("syncStateFromChain(): status of "+pastSwap._data.btcTx.txid, status?.type);
1211
- let changed = false;
1212
- switch(status.type) {
1213
- case SpvWithdrawalStateType.FRONTED:
1214
- pastSwap._frontTxId = status.txId;
1215
- pastSwap._state = SpvFromBTCSwapState.FRONTED;
1216
- changed ||= true;
1217
- break;
1218
- case SpvWithdrawalStateType.CLAIMED:
1219
- pastSwap._claimTxId = status.txId;
1220
- pastSwap._state = SpvFromBTCSwapState.CLAIMED;
1221
- changed ||= true;
1222
- break;
1223
- case SpvWithdrawalStateType.CLOSED:
1224
- pastSwap._state = SpvFromBTCSwapState.CLOSED;
1225
- changed ||= true;
1226
- break;
1227
- }
1228
- if(changed) changedSwaps.add(pastSwap);
1229
- }
1230
- }
1231
-
1232
- return {
1233
- changedSwaps: Array.from(changedSwaps),
1234
- removeSwaps
1235
- };
1236
- }
1237
-
1238
- }
1
+ import {ISwapWrapper, ISwapWrapperOptions, SwapTypeDefinition, WrapperCtorTokens} from "../ISwapWrapper";
2
+ import {
3
+ BitcoinRpcWithAddressIndex, BtcBlock,
4
+ BtcRelay,
5
+ ChainEvent,
6
+ ChainType,
7
+ RelaySynchronizer,
8
+ SpvVaultClaimEvent,
9
+ SpvVaultCloseEvent, SpvVaultData,
10
+ SpvVaultFrontEvent,
11
+ SpvVaultTokenBalance,
12
+ SpvWithdrawalClaimedState,
13
+ SpvWithdrawalFrontedState,
14
+ SpvWithdrawalStateType
15
+ } from "@atomiqlabs/base";
16
+ import {SpvFromBTCSwap, SpvFromBTCSwapInit, SpvFromBTCSwapState} from "./SpvFromBTCSwap";
17
+ import {BTC_NETWORK, TEST_NETWORK} from "@scure/btc-signer/utils";
18
+ import {SwapType} from "../../enums/SwapType";
19
+ import {UnifiedSwapStorage} from "../../storage/UnifiedSwapStorage";
20
+ import {UnifiedSwapEventListener} from "../../events/UnifiedSwapEventListener";
21
+ import {ISwapPrice} from "../../prices/abstract/ISwapPrice";
22
+ import {EventEmitter} from "events";
23
+ import {Intermediary} from "../../intermediaries/Intermediary";
24
+ import {extendAbortController, mapArrayToObject, randomBytes, throwIfUndefined} from "../../utils/Utils";
25
+ import {
26
+ fromOutputScript,
27
+ getDummyOutputScript,
28
+ toCoinselectAddressType,
29
+ toOutputScript
30
+ } from "../../utils/BitcoinUtils";
31
+ import {IntermediaryAPI, SpvFromBTCPrepareResponseType} from "../../intermediaries/apis/IntermediaryAPI";
32
+ import {OutOfBoundsError, RequestError} from "../../errors/RequestError";
33
+ import {IntermediaryError} from "../../errors/IntermediaryError";
34
+ import {CoinselectAddressTypes} from "../../bitcoin/coinselect2";
35
+ import {OutScript, Transaction} from "@scure/btc-signer";
36
+ import {ISwap} from "../ISwap";
37
+ import {IClaimableSwapWrapper} from "../IClaimableSwapWrapper";
38
+ import {AmountData} from "../../types/AmountData";
39
+ import {tryWithRetries} from "../../utils/RetryUtils";
40
+ import {AllOptional} from "../../utils/TypeUtils";
41
+ import {UserError} from "../../errors/UserError";
42
+ import {BitcoinWalletUtxo, BitcoinWalletUtxoBase, IBitcoinWallet} from "../../bitcoin/wallet/IBitcoinWallet";
43
+ import {utils} from "../../bitcoin/coinselect2/utils";
44
+ import {BitcoinWallet} from "../../bitcoin/wallet/BitcoinWallet";
45
+
46
+ export type SpvFromBTCOptions = {
47
+ /**
48
+ * Optional additional native token to receive as an output of the swap (e.g. STRK on Starknet or cBTC on Citrea).
49
+ *
50
+ * When passed as a `bigint` it is specified in base units of the token and in `string` it is the human readable
51
+ * decimal format.
52
+ */
53
+ gasAmount?: bigint | string,
54
+ /**
55
+ * The LP enforces a minimum bitcoin fee rate in sats/vB for the swap transaction. With this config you can optionally
56
+ * limit how high of a minimum fee rate would you accept.
57
+ *
58
+ * By default the maximum allowed fee rate is calculated dynamically based on current bitcoin fee rate as:
59
+ *
60
+ * `maxAllowedBitcoinFeeRate` = 10 + `currentBitcoinFeeRate` * 1.5
61
+ */
62
+ maxAllowedBitcoinFeeRate?: number,
63
+ /**
64
+ * A flag to attach 0 watchtower fee to the swap, this would make the settlement unattractive for the watchtowers
65
+ * and therefore automatic settlement for such swaps will not be possible, you will have to settle manually
66
+ * with {@link FromBTCLNSwap.claim} or {@link FromBTCLNSwap.txsClaim} functions.
67
+ */
68
+ unsafeZeroWatchtowerFee?: boolean,
69
+ /**
70
+ * A safety factor to use when estimating the watchtower fee to attach to the swap (this has to cover the gas fee
71
+ * of watchtowers settling the swap). A higher multiple here would mean that a swap is more attractive for
72
+ * watchtowers to settle automatically.
73
+ *
74
+ * Uses a `1.25` multiple by default (i.e. the current network fee is multiplied by 1.25 and then used to estimate
75
+ * the settlement gas fee cost)
76
+ */
77
+ feeSafetyFactor?: number,
78
+ /**
79
+ * Instruct the LP to create a "sticky address" for your destination wallet address. After the first successful
80
+ * swap with that LP, the used bitcoin address will be permanently linked to your destination wallet address. So
81
+ * all subsequent swaps to the same address will yield the same LP deposit bitcoin address. Useful for corporate
82
+ * whitelist-only wallets
83
+ */
84
+ stickyAddress?: boolean,
85
+ /**
86
+ * A bitcoin wallet UTXOs to fully use as an input for this swap, use this option along with passing `amount` as
87
+ * `undefined` when you want to swap the full BTC balance of the wallet in a single swap
88
+ */
89
+ sourceWalletUtxos?: BitcoinWalletUtxoBase[] | Promise<BitcoinWalletUtxoBase[]>,
90
+ /**
91
+ * Bitcoin fee rate to use when deriving `maxAllowedBitcoinFeeRate` and when calculating the input amount based
92
+ * on the `sourceWalletUtxos`
93
+ */
94
+ bitcoinFeeRate?: Promise<number> | number,
95
+
96
+ /**
97
+ * @deprecated Use `maxAllowedBitcoinFeeRate` instead!
98
+ */
99
+ maxAllowedNetworkFeeRate?: number,
100
+ };
101
+
102
+ export type SpvFromBTCWrapperOptions = ISwapWrapperOptions & {
103
+ maxConfirmations: number,
104
+ bitcoinNetwork: BTC_NETWORK,
105
+ bitcoinBlocktime: number,
106
+ maxTransactionsDelta: number, //Maximum accepted difference in state between SC state and bitcoin state, in terms of by how many transactions are they differing
107
+ maxRawAmountAdjustmentDifferencePPM: number,
108
+ maxBtcFeeMultiplier: number,
109
+ maxBtcFeeOffset: number
110
+ };
111
+
112
+ export type SpvFromBTCTypeDefinition<T extends ChainType> = SwapTypeDefinition<T, SpvFromBTCWrapper<T>, SpvFromBTCSwap<T>>;
113
+
114
+ export const REQUIRED_SPV_SWAP_VAULT_ADDRESS_TYPE: CoinselectAddressTypes = "p2tr";
115
+ export const REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE: CoinselectAddressTypes = "p2wpkh";
116
+
117
+ /**
118
+ * New spv vault (UTXO-controlled vault) based swaps for Bitcoin -> Smart chain swaps not requiring
119
+ * any initiation on the destination chain, and with the added possibility for the user to receive
120
+ * a native token on the destination chain as part of the swap (a "gas drop" feature).
121
+ *
122
+ * @category Swaps/Bitcoin → Smart chain
123
+ */
124
+ export class SpvFromBTCWrapper<
125
+ T extends ChainType
126
+ > extends ISwapWrapper<T, SpvFromBTCTypeDefinition<T>, SpvFromBTCWrapperOptions> implements IClaimableSwapWrapper<SpvFromBTCSwap<T>> {
127
+ public readonly TYPE: SwapType.SPV_VAULT_FROM_BTC = SwapType.SPV_VAULT_FROM_BTC;
128
+ /**
129
+ * @internal
130
+ */
131
+ readonly _claimableSwapStates = [SpvFromBTCSwapState.BTC_TX_CONFIRMED];
132
+ /**
133
+ * @internal
134
+ */
135
+ readonly _swapDeserializer = SpvFromBTCSwap;
136
+
137
+
138
+ /**
139
+ * @internal
140
+ */
141
+ protected readonly btcRelay: (version?: string) => BtcRelay<any, T["TX"], any> = (version?: string) => {
142
+ const _version = version ?? "v1";
143
+ const data = this.versionedContracts[_version];
144
+ if(data==null) throw new Error(`Invalid contract version ${_version} requested`);
145
+ return data.btcRelay;
146
+ };
147
+ /**
148
+ * @internal
149
+ */
150
+ protected readonly tickSwapState: Array<SpvFromBTCSwap<T>["_state"]> = [
151
+ SpvFromBTCSwapState.CREATED,
152
+ SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED,
153
+ SpvFromBTCSwapState.SIGNED,
154
+ SpvFromBTCSwapState.POSTED,
155
+ SpvFromBTCSwapState.BROADCASTED
156
+ ];
157
+
158
+
159
+ /**
160
+ * @internal
161
+ */
162
+ readonly _synchronizer: (version?: string) => RelaySynchronizer<any, T["TX"], any> = (version?: string) => {
163
+ const _version = version ?? "v1";
164
+ const data = this.versionedSynchronizer[_version];
165
+ if(data==null) throw new Error(`Invalid contract version ${_version} requested`);
166
+ return data.synchronizer;
167
+ };
168
+ /**
169
+ * @internal
170
+ */
171
+ readonly _contract: (version?: string) => T["SpvVaultContract"] = (version?: string) => {
172
+ const _version = version ?? "v1";
173
+ const data = this.versionedContracts[_version];
174
+ if(data==null) throw new Error(`Invalid contract version ${_version} requested`);
175
+ return data.spvVaultContract;
176
+ };
177
+ /**
178
+ * @internal
179
+ */
180
+ readonly _btcRpc: BitcoinRpcWithAddressIndex<BtcBlock>;
181
+ /**
182
+ * @internal
183
+ */
184
+ readonly _spvWithdrawalDataDeserializer: (version?: string) => (new (data: any) => T["SpvVaultWithdrawalData"]) = (version?: string) => {
185
+ const _version = version ?? "v1";
186
+ const data = this.versionedContracts[_version];
187
+ if(data==null) throw new Error(`Invalid contract version ${_version} requested`);
188
+ return data.spvVaultWithdrawalDataConstructor;
189
+ };
190
+
191
+ /**
192
+ * @internal
193
+ */
194
+ readonly _pendingSwapStates: Array<SpvFromBTCSwap<T>["_state"]> = [
195
+ SpvFromBTCSwapState.CREATED,
196
+ SpvFromBTCSwapState.SIGNED,
197
+ SpvFromBTCSwapState.POSTED,
198
+ SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED,
199
+ SpvFromBTCSwapState.BROADCASTED,
200
+ SpvFromBTCSwapState.DECLINED,
201
+ SpvFromBTCSwapState.BTC_TX_CONFIRMED
202
+ ];
203
+
204
+ private readonly versionedContracts: {
205
+ [version: string]: {
206
+ btcRelay: BtcRelay<any, T["TX"], any>,
207
+ spvVaultContract: T["SpvVaultContract"],
208
+ spvVaultWithdrawalDataConstructor: new (data: any) => T["SpvVaultWithdrawalData"]
209
+ }
210
+ } = {};
211
+
212
+ private readonly versionedSynchronizer: {
213
+ [version: string]: {
214
+ synchronizer: RelaySynchronizer<any, T["TX"], any>
215
+ }
216
+ } = {};
217
+
218
+ /**
219
+ * @param chainIdentifier
220
+ * @param unifiedStorage Storage interface for the current environment
221
+ * @param unifiedChainEvents On-chain event listener
222
+ * @param chain
223
+ * @param prices Pricing to use
224
+ * @param tokens
225
+ * @param versionedContracts
226
+ * @param versionedSynchronizer
227
+ * @param btcRpc Bitcoin RPC which also supports getting transactions by txoHash
228
+ * @param lpApi
229
+ * @param options
230
+ * @param events Instance to use for emitting events
231
+ */
232
+ constructor(
233
+ chainIdentifier: string,
234
+ unifiedStorage: UnifiedSwapStorage<T>,
235
+ unifiedChainEvents: UnifiedSwapEventListener<T>,
236
+ chain: T["ChainInterface"],
237
+ prices: ISwapPrice,
238
+ tokens: WrapperCtorTokens,
239
+ versionedContracts: {
240
+ [version: string]: {
241
+ btcRelay: BtcRelay<any, T["TX"], any>,
242
+ spvVaultContract: T["SpvVaultContract"],
243
+ spvVaultWithdrawalDataConstructor: new (data: any) => T["SpvVaultWithdrawalData"]
244
+ }
245
+ },
246
+ versionedSynchronizer: {
247
+ [version: string]: {
248
+ synchronizer: RelaySynchronizer<any, T["TX"], any>
249
+ }
250
+ },
251
+ btcRpc: BitcoinRpcWithAddressIndex<any>,
252
+ lpApi: IntermediaryAPI,
253
+ options?: AllOptional<SpvFromBTCWrapperOptions>,
254
+ events?: EventEmitter<{swapState: [ISwap]}>
255
+ ) {
256
+ super(
257
+ chainIdentifier, unifiedStorage, unifiedChainEvents, chain, prices, tokens, lpApi,
258
+ {
259
+ ...options,
260
+ bitcoinNetwork: options?.bitcoinNetwork ?? TEST_NETWORK,
261
+ maxConfirmations: options?.maxConfirmations ?? 6,
262
+ bitcoinBlocktime: options?.bitcoinBlocktime ?? 10*60,
263
+ maxTransactionsDelta: options?.maxTransactionsDelta ?? 3,
264
+ maxRawAmountAdjustmentDifferencePPM: options?.maxRawAmountAdjustmentDifferencePPM ?? 100,
265
+ maxBtcFeeOffset: options?.maxBtcFeeOffset ?? 10,
266
+ maxBtcFeeMultiplier: options?.maxBtcFeeMultiplier ?? 1.5
267
+ },
268
+ events
269
+ );
270
+ this.versionedContracts = versionedContracts;
271
+ this.versionedSynchronizer = versionedSynchronizer;
272
+ this._btcRpc = btcRpc;
273
+ }
274
+
275
+ private async processEventFront(event: SpvVaultFrontEvent, swap: SpvFromBTCSwap<T>): Promise<boolean> {
276
+ if(
277
+ swap._state===SpvFromBTCSwapState.SIGNED || swap._state===SpvFromBTCSwapState.POSTED ||
278
+ swap._state===SpvFromBTCSwapState.BROADCASTED || swap._state===SpvFromBTCSwapState.DECLINED ||
279
+ swap._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED || swap._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED
280
+ ) {
281
+ swap._state = SpvFromBTCSwapState.FRONTED;
282
+ await swap._setBitcoinTxId(event.btcTxId).catch(e => {
283
+ this.logger.warn("processEventFront(): Failed to set bitcoin txId: ", e);
284
+ });
285
+ return true;
286
+ }
287
+ return false;
288
+ }
289
+
290
+ private async processEventClaim(event: SpvVaultClaimEvent, swap: SpvFromBTCSwap<T>): Promise<boolean> {
291
+ if(
292
+ swap._state===SpvFromBTCSwapState.SIGNED || swap._state===SpvFromBTCSwapState.POSTED ||
293
+ swap._state===SpvFromBTCSwapState.BROADCASTED || swap._state===SpvFromBTCSwapState.DECLINED ||
294
+ swap._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED || swap._state===SpvFromBTCSwapState.FRONTED ||
295
+ swap._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED
296
+ ) {
297
+ swap._state = SpvFromBTCSwapState.CLAIMED;
298
+ await swap._setBitcoinTxId(event.btcTxId).catch(e => {
299
+ this.logger.warn("processEventClaim(): Failed to set bitcoin txId: ", e);
300
+ });
301
+ return true;
302
+ }
303
+ return false;
304
+ }
305
+
306
+ private processEventClose(event: SpvVaultCloseEvent, swap: SpvFromBTCSwap<T>): Promise<boolean> {
307
+ if(
308
+ swap._state===SpvFromBTCSwapState.SIGNED || swap._state===SpvFromBTCSwapState.POSTED ||
309
+ swap._state===SpvFromBTCSwapState.BROADCASTED || swap._state===SpvFromBTCSwapState.DECLINED ||
310
+ swap._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED || swap._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED
311
+ ) {
312
+ swap._state = SpvFromBTCSwapState.CLOSED;
313
+ return Promise.resolve(true);
314
+ }
315
+ return Promise.resolve(false);
316
+ }
317
+
318
+ /**
319
+ * @inheritDoc
320
+ * @internal
321
+ */
322
+ protected async processEvent(event: ChainEvent<T["Data"]>, swap: SpvFromBTCSwap<T>): Promise<void> {
323
+ if(swap==null) return;
324
+
325
+ let swapChanged: boolean = false;
326
+ if(event instanceof SpvVaultFrontEvent) {
327
+ swapChanged = await this.processEventFront(event, swap);
328
+ if(event.meta?.txId!=null && swap._frontTxId!==event.meta.txId) {
329
+ swap._frontTxId = event.meta.txId;
330
+ swapChanged ||= true;
331
+ }
332
+ }
333
+ if(event instanceof SpvVaultClaimEvent) {
334
+ swapChanged = await this.processEventClaim(event, swap);
335
+ if(event.meta?.txId!=null && swap._claimTxId!==event.meta.txId) {
336
+ swap._claimTxId = event.meta.txId;
337
+ swapChanged ||= true;
338
+ }
339
+ }
340
+ if(event instanceof SpvVaultCloseEvent) {
341
+ swapChanged = await this.processEventClose(event, swap);
342
+ }
343
+
344
+ this.logger.info("processEvents(): "+event.constructor.name+" processed for "+swap.getId()+" swap: ", swap);
345
+
346
+ if(swapChanged) {
347
+ await swap._saveAndEmit();
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Pre-fetches latest finalized block height of the smart chain
353
+ *
354
+ * @param abortController
355
+ * @private
356
+ */
357
+ private async preFetchFinalizedBlockHeight(abortController: AbortController): Promise<number | undefined> {
358
+ try {
359
+ const block = await this._chain.getFinalizedBlock();
360
+ return block.height;
361
+ } catch (e) {
362
+ abortController.abort(e);
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Pre-fetches caller (watchtower) bounty data for the swap. Doesn't throw, instead returns null and aborts the
368
+ * provided abortController
369
+ *
370
+ * @param amountData
371
+ * @param options Options as passed to the swap creation function
372
+ * @param abortController
373
+ * @param contractVersion
374
+ * @private
375
+ */
376
+ private async preFetchCallerFeeInNativeToken(
377
+ amountData: {amount?: bigint},
378
+ options: {
379
+ unsafeZeroWatchtowerFee: boolean,
380
+ feeSafetyFactor: number
381
+ },
382
+ abortController: AbortController,
383
+ contractVersion: string
384
+ ): Promise<bigint | undefined> {
385
+ if(options.unsafeZeroWatchtowerFee) return 0n;
386
+ if(amountData.amount===0n) return 0n;
387
+
388
+ try {
389
+ const [
390
+ feePerBlock,
391
+ btcRelayData,
392
+ currentBtcBlock,
393
+ claimFeeRate
394
+ ] = await Promise.all([
395
+ this.btcRelay(contractVersion).getFeePerBlock(),
396
+ this.btcRelay(contractVersion).getTipData(),
397
+ this._btcRpc.getTipHeight(),
398
+ this._contract(contractVersion).getClaimFee(this._chain.randomAddress())
399
+ ]);
400
+
401
+ if(btcRelayData==null) throw new Error("Btc relay doesn't seem to be initialized!");
402
+
403
+ const currentBtcRelayBlock = btcRelayData.blockheight;
404
+ const blockDelta = Math.max(currentBtcBlock-currentBtcRelayBlock+this._options.maxConfirmations, 0);
405
+
406
+ const totalFeeInNativeToken = (
407
+ (BigInt(blockDelta) * feePerBlock) +
408
+ (claimFeeRate * BigInt(this._options.maxTransactionsDelta))
409
+ ) * BigInt(Math.floor(options.feeSafetyFactor*1000000)) / 1_000_000n;
410
+
411
+ return totalFeeInNativeToken;
412
+ } catch (e) {
413
+ abortController.abort(e);
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Pre-fetches caller (watchtower) bounty data for the swap. Doesn't throw, instead returns null and aborts the
419
+ * provided abortController
420
+ *
421
+ * @param amountPrefetch
422
+ * @param totalFeeInNativeTokenPrefetch
423
+ * @param amountData
424
+ * @param options Options as passed to the swap creation function
425
+ * @param pricePrefetch
426
+ * @param nativeTokenPricePrefetch
427
+ * @param abortSignal
428
+ * @private
429
+ */
430
+ private async computeCallerFeeShare(
431
+ amountPrefetch: Promise<bigint | undefined>,
432
+ totalFeeInNativeTokenPrefetch: Promise<bigint | undefined>,
433
+ amountData: {exactIn: boolean, token: string},
434
+ options: {unsafeZeroWatchtowerFee: boolean},
435
+ pricePrefetch: Promise<bigint | undefined>,
436
+ nativeTokenPricePrefetch: Promise<bigint | undefined> | undefined,
437
+ abortSignal?: AbortSignal
438
+ ): Promise<bigint> {
439
+ if(options.unsafeZeroWatchtowerFee) return 0n;
440
+
441
+ const amount = await throwIfUndefined(amountPrefetch, "Cannot get swap amount!");
442
+ if(amount===0n) return 0n;
443
+
444
+ const totalFeeInNativeToken = await throwIfUndefined(totalFeeInNativeTokenPrefetch, "Cannot get total fee in native token!");
445
+ const nativeTokenPrice = await nativeTokenPricePrefetch;
446
+
447
+ let payoutAmount: bigint;
448
+ if(amountData.exactIn) {
449
+ //Convert input amount in BTC to
450
+ const amountInNativeToken = await this._prices.getFromBtcSwapAmount(this.chainIdentifier, amount, this._chain.getNativeCurrencyAddress(), abortSignal, nativeTokenPrice);
451
+ payoutAmount = amountInNativeToken - totalFeeInNativeToken;
452
+ } else {
453
+ if(amountData.token===this._chain.getNativeCurrencyAddress()) {
454
+ //Both amounts in same currency
455
+ payoutAmount = amount;
456
+ } else {
457
+ //Need to convert both to native currency
458
+ const btcAmount = await this._prices.getToBtcSwapAmount(this.chainIdentifier, amount, amountData.token, abortSignal, await pricePrefetch);
459
+ payoutAmount = await this._prices.getFromBtcSwapAmount(this.chainIdentifier, btcAmount, this._chain.getNativeCurrencyAddress(), abortSignal, nativeTokenPrice);
460
+ }
461
+ }
462
+
463
+ this.logger.debug("computeCallerFeeShare(): Caller fee in native token: "+totalFeeInNativeToken.toString(10)+" total payout in native token: "+payoutAmount.toString(10));
464
+
465
+ const callerFeeShare = ((totalFeeInNativeToken * 100_000n) + payoutAmount - 1n) / payoutAmount; //Make sure to round up here
466
+ if(callerFeeShare < 0n) return 0n;
467
+ if(callerFeeShare >= 2n**20n) return 2n**20n - 1n;
468
+ return callerFeeShare;
469
+ }
470
+
471
+ /**
472
+ * Verifies response returned from intermediary
473
+ *
474
+ * @param resp Response as returned by the intermediary
475
+ * @param amountData
476
+ * @param lp Intermediary
477
+ * @param options Options as passed to the swap creation function
478
+ * @param callerFeeShare
479
+ * @param maxBitcoinFeeRatePromise Maximum accepted fee rate from the LPs
480
+ * @param bitcoinFeeRatePromise
481
+ * @param abortSignal
482
+ * @private
483
+ * @throws {IntermediaryError} in case the response is invalid
484
+ */
485
+ private async verifyReturnedData(
486
+ resp: SpvFromBTCPrepareResponseType,
487
+ amountData: AmountData,
488
+ lp: Intermediary,
489
+ options: {
490
+ gasAmount: bigint,
491
+ sourceWalletUtxos?: Promise<BitcoinWalletUtxoBase[]>
492
+ },
493
+ callerFeeShare: bigint,
494
+ maxBitcoinFeeRatePromise: Promise<number | undefined>,
495
+ bitcoinFeeRatePromise: Promise<number | undefined> | undefined,
496
+ abortSignal: AbortSignal
497
+ ): Promise<{
498
+ vault: T["SpvVaultData"],
499
+ vaultUtxoValue: number
500
+ }> {
501
+ const btcFeeRate = await throwIfUndefined(maxBitcoinFeeRatePromise, "Bitcoin fee rate promise failed!");
502
+ abortSignal.throwIfAborted();
503
+ if(btcFeeRate!=null && resp.btcFeeRate > btcFeeRate) throw new IntermediaryError(`Required bitcoin fee rate returned from the LP is too high! Maximum accepted: ${btcFeeRate} sats/vB, required by LP: ${resp.btcFeeRate} sats/vB`);
504
+
505
+ const lpVersion = lp.getContractVersion(this.chainIdentifier);
506
+
507
+ //Vault related
508
+ let vaultScript: Uint8Array;
509
+ let vaultAddressType: CoinselectAddressTypes;
510
+ let btcAddressScript: Uint8Array;
511
+ let btcAddressType: CoinselectAddressTypes;
512
+ //Ensure valid btc addresses returned
513
+ try {
514
+ vaultScript = toOutputScript(this._options.bitcoinNetwork, resp.vaultBtcAddress);
515
+ vaultAddressType = toCoinselectAddressType(vaultScript);
516
+ btcAddressScript = toOutputScript(this._options.bitcoinNetwork, resp.btcAddress);
517
+ btcAddressType = toCoinselectAddressType(btcAddressScript);
518
+ } catch (e) {
519
+ throw new IntermediaryError("Invalid btc address data returned", e);
520
+ }
521
+ const decodedUtxo = resp.btcUtxo.split(":");
522
+ if(
523
+ resp.address!==lp.getAddress(this.chainIdentifier) || //Ensure the LP is indeed the vault owner
524
+ resp.vaultId < 0n || //Ensure vaultId is not negative
525
+ vaultScript==null || //Make sure vault script is parsable and of known type
526
+ btcAddressScript==null || //Make sure btc address script is parsable and of known type
527
+ btcAddressType!==REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE || //Constrain the btc address script type
528
+ vaultAddressType!==REQUIRED_SPV_SWAP_VAULT_ADDRESS_TYPE || //Constrain the vault script type
529
+ decodedUtxo.length!==2 || decodedUtxo[0].length!==64 || isNaN(parseInt(decodedUtxo[1])) || //Check valid UTXO
530
+ resp.btcFeeRate < 1 || resp.btcFeeRate > 10000 //Sanity check on the returned BTC fee rate
531
+ ) throw new IntermediaryError("Invalid vault data returned!");
532
+
533
+ //Amounts sanity
534
+ if(resp.btcAmountSwap + resp.btcAmountGas !==resp.btcAmount) throw new Error("Btc amount mismatch");
535
+ if(resp.swapFeeBtc + resp.gasSwapFeeBtc !==resp.totalFeeBtc) throw new Error("Btc fee mismatch");
536
+
537
+ //TODO: For now ensure fees are at 0
538
+ if(
539
+ resp.callerFeeShare!==callerFeeShare ||
540
+ resp.frontingFeeShare!==0n ||
541
+ resp.executionFeeShare!==0n
542
+ ) throw new IntermediaryError("Invalid caller/fronting/execution fee returned");
543
+
544
+ //Check expiry
545
+ const timeNowSeconds = Math.floor(Date.now()/1000);
546
+ if(resp.expiry < timeNowSeconds) throw new IntermediaryError(`Quote already expired, expiry: ${resp.expiry}, systemTime: ${timeNowSeconds}, clockAdjusted: ${(Date as any)._now!=null}`);
547
+
548
+ let utxo = resp.btcUtxo.toLowerCase();
549
+ const [txId, voutStr] = utxo.split(":");
550
+
551
+ const abortController = extendAbortController(abortSignal);
552
+ let [vault, {vaultUtxoValue, btcTx}] = await Promise.all([
553
+ (async() => {
554
+ //Fetch vault data
555
+ let vault: T["SpvVaultData"] | null;
556
+ try {
557
+ vault = await this._contract(lpVersion).getVaultData(resp.address, resp.vaultId);
558
+ } catch (e) {
559
+ this.logger.error("Error getting spv vault (owner: "+resp.address+" vaultId: "+resp.vaultId.toString(10)+"): ", e);
560
+ throw new IntermediaryError("Spv swap vault not found", e);
561
+ }
562
+ abortController.signal.throwIfAborted();
563
+
564
+ //Make sure vault is opened
565
+ if(vault==null || !vault.isOpened()) throw new IntermediaryError("Returned spv swap vault is not opened!");
566
+ //Make sure the vault doesn't require insane amount of confirmations
567
+ if(vault.getConfirmations()>this._options.maxConfirmations) throw new IntermediaryError("SPV swap vault needs too many confirmations: "+vault.getConfirmations());
568
+ const tokenData = vault.getTokenData();
569
+
570
+ //Amounts - make sure the amounts match
571
+ if(amountData.exactIn) {
572
+ if(!resp.usedUtxoInputCalculation) {
573
+ //Legacy calculation
574
+ if(resp.btcAmount !== amountData.amount) throw new IntermediaryError("Invalid amount returned");
575
+ } else {
576
+ //Implies the raw UTXOs were passed for amount derivation
577
+ //Verify the derivation was done correctly
578
+ if(options.sourceWalletUtxos==null) throw new IntermediaryError("Invalid usedUtxoInputCalcuation return value");
579
+ if(bitcoinFeeRatePromise==null) throw new Error("bitcoinFeeRatePromise must be passed for UTXO-based input amount calculation checks");
580
+ const walletUtxos = await options.sourceWalletUtxos;
581
+ const bitcoinFeeRate = await throwIfUndefined(bitcoinFeeRatePromise, "Failed to fetch bitcoin fee rate!");
582
+ const {balance} = BitcoinWallet.getSpendableBalance(
583
+ walletUtxos, Math.max(resp.btcFeeRate, bitcoinFeeRate),
584
+ this.getDummySwapPsbt(options.gasAmount!==0n), REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE
585
+ );
586
+ if(resp.btcAmount !== balance) throw new IntermediaryError(`Invalid amount returned, expected: ${balance.toString(10)}, got: ${resp.btcAmount.toString(10)}`);
587
+ }
588
+ } else {
589
+ //Check the difference between amount adjusted due to scaling to raw amount
590
+ const adjustedAmount = amountData.amount / tokenData[0].multiplier * tokenData[0].multiplier;
591
+ const adjustmentPPM = (amountData.amount - adjustedAmount)*1_000_000n / amountData.amount;
592
+ if(adjustmentPPM > this._options.maxRawAmountAdjustmentDifferencePPM)
593
+ throw new IntermediaryError("Invalid amount0 multiplier used, rawAmount diff too high");
594
+ if(resp.total !== adjustedAmount) throw new IntermediaryError("Invalid total returned");
595
+ }
596
+ if(options.gasAmount===0n) {
597
+ if(resp.totalGas !== 0n) throw new IntermediaryError("Invalid gas total returned");
598
+ } else {
599
+ //Check the difference between amount adjusted due to scaling to raw amount
600
+ const adjustedGasAmount = options.gasAmount / tokenData[0].multiplier * tokenData[0].multiplier;
601
+ const adjustmentPPM = (options.gasAmount - adjustedGasAmount)*1_000_000n / options.gasAmount;
602
+ if(adjustmentPPM > this._options.maxRawAmountAdjustmentDifferencePPM)
603
+ throw new IntermediaryError("Invalid amount1 multiplier used, rawAmount diff too high");
604
+ if(resp.totalGas !== adjustedGasAmount) throw new IntermediaryError("Invalid gas total returned");
605
+ }
606
+
607
+ return vault;
608
+ })(),
609
+ (async() => {
610
+ //Require the vault UTXO to have at least 1 confirmation
611
+ let btcTx = await this._btcRpc.getTransaction(txId);
612
+ if(btcTx==null) throw new IntermediaryError("Invalid UTXO, doesn't exist (txId)");
613
+ abortController.signal.throwIfAborted();
614
+ if(btcTx.confirmations==null || btcTx.confirmations<1) throw new IntermediaryError("SPV vault UTXO not confirmed");
615
+ const vout = parseInt(voutStr);
616
+ if(btcTx.outs[vout]==null) throw new IntermediaryError("Invalid UTXO, doesn't exist");
617
+ const vaultUtxoValue = btcTx.outs[vout].value;
618
+ return {btcTx, vaultUtxoValue};
619
+ })(),
620
+ (async() => {
621
+ //Require vault UTXO is unspent
622
+ if(await this._btcRpc.isSpent(utxo)) throw new IntermediaryError("Returned spv vault UTXO is already spent", null, true);
623
+ abortController.signal.throwIfAborted();
624
+ })()
625
+ ]).catch(e => {
626
+ abortController.abort(e);
627
+ throw e;
628
+ });
629
+
630
+ this.logger.debug("verifyReturnedData(): Vault UTXO: "+vault.getUtxo()+" current utxo: "+utxo);
631
+
632
+ //Trace returned utxo back to what's saved on-chain
633
+ let pendingWithdrawals: T["SpvVaultWithdrawalData"][] = [];
634
+ while(vault.getUtxo()!==utxo) {
635
+ const [txId, voutStr] = utxo.split(":");
636
+ //Such that 1st tx isn't fetched twice
637
+ if(btcTx.txid!==txId) {
638
+ const _btcTx = await this._btcRpc.getTransaction(txId);
639
+ if(_btcTx==null) throw new IntermediaryError("Invalid ancestor transaction (not found)");
640
+ btcTx = _btcTx;
641
+ }
642
+ const withdrawalData = await this._contract(lpVersion).getWithdrawalData(btcTx);
643
+ abortSignal.throwIfAborted();
644
+ pendingWithdrawals.unshift(withdrawalData);
645
+ utxo = pendingWithdrawals[0].getSpentVaultUtxo();
646
+ this.logger.debug("verifyReturnedData(): Vault UTXO: "+vault.getUtxo()+" current utxo: "+utxo);
647
+ if(pendingWithdrawals.length>=this._options.maxTransactionsDelta)
648
+ throw new IntermediaryError("BTC <> SC state difference too deep, maximum: "+this._options.maxTransactionsDelta);
649
+ }
650
+
651
+ //Verify that the vault has enough balance after processing all pending withdrawals
652
+ let vaultBalances: {balances: SpvVaultTokenBalance[]};
653
+ try {
654
+ vaultBalances = vault.calculateStateAfter(pendingWithdrawals);
655
+ } catch (e) {
656
+ this.logger.error("Error calculating spv vault balance (owner: "+resp.address+" vaultId: "+resp.vaultId.toString(10)+"): ", e);
657
+ throw new IntermediaryError("Spv swap vault balance prediction failed", e);
658
+ }
659
+ if(vaultBalances.balances[0].scaledAmount < resp.total)
660
+ throw new IntermediaryError("SPV swap vault, insufficient balance, required: "+resp.total.toString(10)+
661
+ " has: "+vaultBalances.balances[0].scaledAmount.toString(10));
662
+ if(vaultBalances.balances[1].scaledAmount < resp.totalGas)
663
+ throw new IntermediaryError("SPV swap vault, insufficient balance, required: "+resp.totalGas.toString(10)+
664
+ " has: "+vaultBalances.balances[1].scaledAmount.toString(10));
665
+
666
+ //Also verify that all the withdrawal txns are valid, this is an extra sanity check
667
+ try {
668
+ for(let withdrawal of pendingWithdrawals) {
669
+ await this._contract(lpVersion).checkWithdrawalTx(withdrawal);
670
+ }
671
+ } catch (e) {
672
+ this.logger.error("Error calculating spv vault balance (owner: "+resp.address+" vaultId: "+resp.vaultId.toString(10)+"): ", e);
673
+ throw new IntermediaryError("Spv swap vault balance prediction failed", e);
674
+ }
675
+ abortSignal.throwIfAborted();
676
+
677
+ return {
678
+ vault,
679
+ vaultUtxoValue
680
+ };
681
+ }
682
+
683
+ private async amountPrefetch(
684
+ amountData: {token: string, exactIn: boolean, amount?: bigint},
685
+ bitcoinFeeRatePromise: Promise<number | undefined>,
686
+ walletUtxosPromise: Promise<BitcoinWalletUtxoBase[]> | undefined,
687
+ includeGas: boolean,
688
+ abortController: AbortController
689
+ ): Promise<bigint | undefined> {
690
+ if(amountData.amount!=null) return amountData.amount;
691
+ try {
692
+ const bitcoinFeeRate = await throwIfUndefined(bitcoinFeeRatePromise, "Cannot fetch Bitcoin fee rate!");
693
+ if(walletUtxosPromise==null) throw new UserError("Cannot use empty amount without passing UTXOs!");
694
+ const walletUtxos = await walletUtxosPromise;
695
+ if(walletUtxos.length===0)
696
+ throw new UserError("Wallet doesn't have any BTC balance");
697
+ const spendableBalance = await BitcoinWallet.getSpendableBalance(
698
+ walletUtxos, bitcoinFeeRate,
699
+ this.getDummySwapPsbt(includeGas), REQUIRED_SPV_SWAP_LP_ADDRESS_TYPE
700
+ );
701
+ return spendableBalance.balance;
702
+ } catch (e) {
703
+ abortController.abort(e);
704
+ }
705
+ }
706
+
707
+ private bitcoinFeeRatePrefetch(
708
+ options: {
709
+ maxAllowedBitcoinFeeRate: number,
710
+ sourceWalletUtxos?: Promise<BitcoinWalletUtxoBase[]>,
711
+ bitcoinFeeRate?: Promise<number>
712
+ },
713
+ abortController: AbortController
714
+ ) {
715
+ let bitcoinFeeRatePromise: Promise<number | undefined> | undefined;
716
+ if(options?.sourceWalletUtxos!=null) {
717
+ if(options.bitcoinFeeRate!=null) {
718
+ bitcoinFeeRatePromise = options.bitcoinFeeRate.then(value => {
719
+ if(options.maxAllowedBitcoinFeeRate!=Infinity && options.maxAllowedBitcoinFeeRate<value)
720
+ throw new Error("Passed `maxAllowedBitcoinFeeRate` cannot be lower than `bitcoinFeeRate`");
721
+ return value;
722
+ });
723
+ } else {
724
+ bitcoinFeeRatePromise = this._btcRpc.getFeeRate().then(value => {
725
+ if(options.maxAllowedBitcoinFeeRate!=Infinity && value > options.maxAllowedBitcoinFeeRate) return options.maxAllowedBitcoinFeeRate;
726
+ return value;
727
+ });
728
+ }
729
+ bitcoinFeeRatePromise = bitcoinFeeRatePromise.catch(e => {
730
+ abortController.abort(e);
731
+ return undefined;
732
+ });
733
+ }
734
+ const maxBitcoinFeeRatePromise: Promise<number | undefined> = options.maxAllowedBitcoinFeeRate!=Infinity
735
+ ? Promise.resolve(options.maxAllowedBitcoinFeeRate)
736
+ : throwIfUndefined(bitcoinFeeRatePromise ?? options.bitcoinFeeRate ?? this._btcRpc.getFeeRate())
737
+ .then(x => this._options.maxBtcFeeOffset + (x*this._options.maxBtcFeeMultiplier))
738
+ .catch(e => {
739
+ abortController.abort(e);
740
+ return undefined;
741
+ });
742
+
743
+ return {
744
+ bitcoinFeeRatePromise,
745
+ maxBitcoinFeeRatePromise
746
+ }
747
+ }
748
+
749
+ /**
750
+ * Returns a newly created Bitcoin -> Smart chain swap using the SPV vault (UTXO-controlled vault) swap protocol,
751
+ * with the passed amount. Also allows specifying additional "gas drop" native token that the receipient receives
752
+ * on the destination chain in the `options` argument.
753
+ *
754
+ * @param recipient Recipient address on the destination smart chain
755
+ * @param amountData Amount, token and exact input/output data for to swap
756
+ * @param lps An array of intermediaries (LPs) to get the quotes from
757
+ * @param options Optional additional quote options
758
+ * @param additionalParams Optional additional parameters sent to the LP when creating the swap
759
+ * @param abortSignal Abort signal
760
+ */
761
+ public create(
762
+ recipient: string,
763
+ amountData: { amount?: bigint, token: string, exactIn: boolean },
764
+ lps: Intermediary[],
765
+ options?: SpvFromBTCOptions,
766
+ additionalParams?: Record<string, any>,
767
+ abortSignal?: AbortSignal
768
+ ): {
769
+ quote: Promise<SpvFromBTCSwap<T>>,
770
+ intermediary: Intermediary
771
+ }[] {
772
+ const _options = {
773
+ gasAmount: this.parseGasAmount(options?.gasAmount),
774
+ unsafeZeroWatchtowerFee: options?.unsafeZeroWatchtowerFee ?? false,
775
+ feeSafetyFactor: options?.feeSafetyFactor ?? 1.25,
776
+ maxAllowedBitcoinFeeRate: options?.maxAllowedBitcoinFeeRate ?? options?.maxAllowedNetworkFeeRate ?? Infinity,
777
+ sourceWalletUtxos: options?.sourceWalletUtxos==undefined
778
+ ? undefined
779
+ : options?.sourceWalletUtxos instanceof Promise ? options.sourceWalletUtxos : Promise.resolve(options.sourceWalletUtxos),
780
+ bitcoinFeeRate: options?.bitcoinFeeRate==undefined
781
+ ? undefined
782
+ : options?.bitcoinFeeRate instanceof Promise ? options.bitcoinFeeRate : Promise.resolve(options.bitcoinFeeRate),
783
+ };
784
+
785
+ if(
786
+ _options.gasAmount!==0n &&
787
+ (
788
+ this._chain.shouldGetNativeTokenDrop!=null
789
+ ? !this._chain.shouldGetNativeTokenDrop(amountData.token)
790
+ : amountData.token===this._chain.getNativeCurrencyAddress()
791
+ )
792
+ ) throw new UserError("Cannot specify `gasAmount` for swaps to a native token!");
793
+
794
+ if(amountData.amount==null && options?.sourceWalletUtxos==null)
795
+ throw new UserError("Source wallet UTXOs need to be passed when amount is null!");
796
+ if(amountData.amount==null && !amountData.exactIn)
797
+ throw new UserError("Amount can be null only for exactIn swaps!");
798
+ if(amountData.amount!=null && options?.sourceWalletUtxos!=null)
799
+ throw new UserError("Source wallet UTXOs cannot be passed while specifying an input amount!");
800
+
801
+ const lpVersions = Intermediary.getContractVersionsForLps(this.chainIdentifier, lps);
802
+
803
+ const _abortController = extendAbortController(abortSignal);
804
+ const pricePrefetchPromise: Promise<bigint | undefined> = this.preFetchPrice(amountData, _abortController.signal);
805
+ const usdPricePrefetchPromise: Promise<number | undefined> = this.preFetchUsdPrice(_abortController.signal);
806
+ const finalizedBlockHeightPrefetchPromise: Promise<number | undefined> = this.preFetchFinalizedBlockHeight(_abortController);
807
+ const nativeTokenAddress = this._chain.getNativeCurrencyAddress();
808
+ const gasTokenPricePrefetchPromise: Promise<bigint | undefined> | undefined = _options.gasAmount===0n ?
809
+ undefined :
810
+ this.preFetchPrice({token: nativeTokenAddress}, _abortController.signal);
811
+ const callerFeePrefetchPromise = mapArrayToObject(lpVersions, (contractVersion: string) => {
812
+ return this.preFetchCallerFeeInNativeToken(amountData, _options, _abortController, contractVersion);
813
+ });
814
+ const {maxBitcoinFeeRatePromise, bitcoinFeeRatePromise} = this.bitcoinFeeRatePrefetch(_options, _abortController);
815
+ const amountPromise = this.amountPrefetch(
816
+ amountData, maxBitcoinFeeRatePromise, _options.sourceWalletUtxos, _options.gasAmount!==0n, _abortController
817
+ );
818
+
819
+ return lps.map(lp => {
820
+ return {
821
+ intermediary: lp,
822
+ quote: tryWithRetries(async () => {
823
+ if(lp.services[SwapType.SPV_VAULT_FROM_BTC]==null) throw new Error("LP service for processing spv vault swaps not found!");
824
+ const version = lp.getContractVersion(this.chainIdentifier);
825
+
826
+ const abortController = extendAbortController(_abortController.signal);
827
+ const callerFeeRatePromise = this.computeCallerFeeShare(
828
+ amountPromise,
829
+ callerFeePrefetchPromise[version],
830
+ amountData,
831
+ _options,
832
+ pricePrefetchPromise,
833
+ gasTokenPricePrefetchPromise,
834
+ abortController.signal
835
+ );
836
+
837
+ try {
838
+ const resp = await tryWithRetries(async(retryCount: number) => {
839
+ return await this._lpApi.prepareSpvFromBTC(
840
+ this.chainIdentifier, lp.url,
841
+ {
842
+ address: recipient,
843
+ amount: throwIfUndefined(amountPromise, "Failed to compute swap amount"),
844
+ token: amountData.token.toString(),
845
+ exactOut: !amountData.exactIn,
846
+ gasToken: nativeTokenAddress,
847
+ gasAmount: _options.gasAmount,
848
+ callerFeeRate: throwIfUndefined(callerFeeRatePromise, "Caller fee prefetch failed!"),
849
+ frontingFeeRate: 0n,
850
+ stickyAddress: options?.stickyAddress,
851
+ amountUtxos: _options.sourceWalletUtxos!=null
852
+ ? _options.sourceWalletUtxos.then(utxos => {
853
+ if(utxos.length===0) return undefined;
854
+ return utxos.map(utxo => ({
855
+ value: utxo.value,
856
+ vSize: utils.inputBytes({type: utxo.type}),
857
+ cpfp: utxo.cpfp==null ? undefined : {effectiveVSize: utxo.cpfp?.txVsize, effectiveFeeRate: utxo.cpfp?.txEffectiveFeeRate}
858
+ }));
859
+ })
860
+ : undefined,
861
+ amountFeeRate: bitcoinFeeRatePromise,
862
+ additionalParams
863
+ },
864
+ this._options.postRequestTimeout, abortController.signal, retryCount>0 ? false : undefined
865
+ );
866
+ }, undefined, e => e instanceof RequestError, abortController.signal);
867
+
868
+ this.logger.debug("create("+lp.url+"): LP response: ", resp)
869
+
870
+ const callerFeeShare = await callerFeeRatePromise;
871
+ const amount = await throwIfUndefined(amountPromise);
872
+
873
+ const [
874
+ pricingInfo,
875
+ gasPricingInfo,
876
+ {vault, vaultUtxoValue}
877
+ ] = await Promise.all([
878
+ this.verifyReturnedPrice(
879
+ lp.services[SwapType.SPV_VAULT_FROM_BTC],
880
+ false, resp.btcAmountSwap,
881
+ resp.total * (100_000n + callerFeeShare) / 100_000n,
882
+ amountData.token, {swapFeeBtc: resp.swapFeeBtc}, pricePrefetchPromise, usdPricePrefetchPromise, abortController.signal
883
+ ),
884
+ _options.gasAmount===0n ? Promise.resolve(undefined) : this.verifyReturnedPrice(
885
+ {...lp.services[SwapType.SPV_VAULT_FROM_BTC], swapBaseFee: 0}, //Base fee should be charged only on the amount, not on gas
886
+ false, resp.btcAmountGas,
887
+ resp.totalGas * (100_000n + callerFeeShare) / 100_000n,
888
+ nativeTokenAddress, {swapFeeBtc: resp.gasSwapFeeBtc}, gasTokenPricePrefetchPromise, usdPricePrefetchPromise, abortController.signal
889
+ ),
890
+ this.verifyReturnedData(
891
+ resp,
892
+ {...amountData, amount},
893
+ lp, _options, callerFeeShare, maxBitcoinFeeRatePromise, bitcoinFeeRatePromise, abortController.signal
894
+ )
895
+ ]);
896
+
897
+ let minimumBtcFeeRate: number = resp.btcFeeRate;
898
+ if(bitcoinFeeRatePromise!=null) minimumBtcFeeRate = Math.max(minimumBtcFeeRate, await throwIfUndefined(bitcoinFeeRatePromise));
899
+
900
+ const swapInit: SpvFromBTCSwapInit = {
901
+ pricingInfo,
902
+ url: lp.url,
903
+ expiry: resp.expiry * 1000,
904
+ swapFee: resp.swapFee,
905
+ swapFeeBtc: resp.swapFeeBtc,
906
+ exactIn: amountData.exactIn ?? true,
907
+
908
+ quoteId: resp.quoteId,
909
+
910
+ recipient,
911
+
912
+ vaultOwner: resp.address,
913
+ vaultId: resp.vaultId,
914
+ vaultRequiredConfirmations: vault.getConfirmations(),
915
+ vaultTokenMultipliers: vault.getTokenData().map(val => val.multiplier),
916
+ vaultBtcAddress: resp.vaultBtcAddress,
917
+ vaultUtxo: resp.btcUtxo,
918
+ vaultUtxoValue: BigInt(vaultUtxoValue),
919
+
920
+ btcDestinationAddress: resp.btcAddress,
921
+ btcAmount: resp.btcAmount,
922
+ btcAmountSwap: resp.btcAmountSwap,
923
+ btcAmountGas: resp.btcAmountGas,
924
+ minimumBtcFeeRate,
925
+
926
+ outputTotalSwap: resp.total,
927
+ outputSwapToken: amountData.token,
928
+ outputTotalGas: resp.totalGas,
929
+ outputGasToken: nativeTokenAddress,
930
+ gasSwapFeeBtc: resp.gasSwapFeeBtc,
931
+ gasSwapFee: resp.gasSwapFee,
932
+ gasPricingInfo,
933
+
934
+ callerFeeShare: resp.callerFeeShare,
935
+ frontingFeeShare: resp.frontingFeeShare,
936
+ executionFeeShare: resp.executionFeeShare,
937
+
938
+ genesisSmartChainBlockHeight: await throwIfUndefined(
939
+ finalizedBlockHeightPrefetchPromise,
940
+ "Network finalized blockheight pre-fetch failed!"
941
+ ),
942
+ contractVersion: version
943
+ };
944
+ const quote = new SpvFromBTCSwap<T>(this, swapInit);
945
+ return quote;
946
+ } catch (e) {
947
+ if(e instanceof OutOfBoundsError) {
948
+ const amountResult = await amountPromise.catch(() => undefined);
949
+ if(_options.sourceWalletUtxos!=null && amountResult!=null && amountResult<=0n) {
950
+ e = new UserError("Wallet doesn't have enough BTC balance to cover transaction fees");
951
+ }
952
+ }
953
+ abortController.abort(e);
954
+ throw e;
955
+ }
956
+ }, undefined, err => !(err instanceof IntermediaryError && err.recoverable), _abortController.signal)
957
+ }
958
+ });
959
+ }
960
+
961
+ /**
962
+ * Recovers an SPV vault (UTXO-controlled vault) based swap from smart chain on-chain data
963
+ *
964
+ * @param state State of the spv vault withdrawal recovered from on-chain data
965
+ * @param vault SPV vault processing the swap
966
+ * @param lp Intermediary (LP) used as a counterparty for the swap
967
+ */
968
+ public async recoverFromState(state: SpvWithdrawalClaimedState | SpvWithdrawalFrontedState, contractVersion: string, vault?: SpvVaultData | null, lp?: Intermediary): Promise<SpvFromBTCSwap<T> | null> {
969
+ //Get the vault
970
+ vault ??= await this._contract(contractVersion).getVaultData(state.owner, state.vaultId);
971
+ if(vault==null) return null;
972
+ if(state.btcTxId==null) return null;
973
+ const btcTx = await this._btcRpc.getTransaction(state.btcTxId);
974
+ if(btcTx==null) return null;
975
+ const withdrawalData = await this._contract(contractVersion).getWithdrawalData(btcTx)
976
+ .catch(e => {
977
+ this.logger.warn(`Error parsing withdrawal data for tx ${btcTx.txid}: `, e);
978
+ return null;
979
+ });
980
+ if(withdrawalData==null) return null;
981
+
982
+ const vaultTokens = vault.getTokenData();
983
+ const withdrawalDataOutputs = withdrawalData.getTotalOutput();
984
+
985
+ const txBlock = await state.getTxBlock?.();
986
+
987
+ const swapInit: SpvFromBTCSwapInit = {
988
+ pricingInfo: {
989
+ isValid: true,
990
+ satsBaseFee: 0n,
991
+ swapPriceUSatPerToken: 100_000_000_000_000n,
992
+ realPriceUSatPerToken: 100_000_000_000_000n,
993
+ differencePPM: 0n,
994
+ feePPM: 0n,
995
+ },
996
+ url: lp?.url,
997
+ expiry: 0,
998
+ swapFee: 0n,
999
+ swapFeeBtc: 0n,
1000
+ exactIn: true,
1001
+
1002
+ //Use bitcoin tx id as quote id, even though this is not strictly correct as this
1003
+ // is an off-chain identifier presented by the LP that cannot be recovered from on-chain
1004
+ // data
1005
+ quoteId: btcTx.txid,
1006
+
1007
+ recipient: state.recipient,
1008
+
1009
+ vaultOwner: state.owner,
1010
+ vaultId: state.vaultId,
1011
+ vaultRequiredConfirmations: vault.getConfirmations(),
1012
+ vaultTokenMultipliers: vault.getTokenData().map(val => val.multiplier),
1013
+ vaultBtcAddress: fromOutputScript(this._options.bitcoinNetwork, withdrawalData.getNewVaultScript().toString("hex")),
1014
+ vaultUtxo: withdrawalData.getSpentVaultUtxo(),
1015
+ vaultUtxoValue: BigInt(withdrawalData.getNewVaultBtcAmount()),
1016
+
1017
+ btcDestinationAddress: fromOutputScript(this._options.bitcoinNetwork, btcTx.outs[2].scriptPubKey.hex),
1018
+ btcAmount: BigInt(btcTx.outs[2].value),
1019
+ btcAmountSwap: BigInt(btcTx.outs[2].value),
1020
+ btcAmountGas: 0n,
1021
+ minimumBtcFeeRate: 0,
1022
+
1023
+ outputTotalSwap: withdrawalDataOutputs[0] * vaultTokens[0].multiplier,
1024
+ outputSwapToken: vaultTokens[0].token,
1025
+ outputTotalGas: withdrawalDataOutputs[1] * vaultTokens[1].multiplier,
1026
+ outputGasToken: vaultTokens[1].token,
1027
+ gasSwapFeeBtc: 0n,
1028
+ gasSwapFee: 0n,
1029
+ gasPricingInfo: {
1030
+ isValid: true,
1031
+ satsBaseFee: 0n,
1032
+ swapPriceUSatPerToken: 100_000_000_000_000n,
1033
+ realPriceUSatPerToken: 100_000_000_000_000n,
1034
+ differencePPM: 0n,
1035
+ feePPM: 0n,
1036
+ },
1037
+
1038
+ callerFeeShare: withdrawalData.callerFeeRate,
1039
+ frontingFeeShare: withdrawalData.frontingFeeRate,
1040
+ executionFeeShare: withdrawalData.executionFeeRate,
1041
+
1042
+ genesisSmartChainBlockHeight: txBlock?.blockHeight ?? 0,
1043
+
1044
+ contractVersion
1045
+ };
1046
+ const quote = new SpvFromBTCSwap<T>(this, swapInit);
1047
+ quote._data = withdrawalData;
1048
+ if(txBlock!=null) {
1049
+ quote.createdAt = txBlock.blockTime*1000;
1050
+ } else if(btcTx.blockhash==null) {
1051
+ quote.createdAt = Date.now();
1052
+ } else {
1053
+ const blockHeader = await this._btcRpc.getBlockHeader(btcTx.blockhash);
1054
+ quote.createdAt = blockHeader==null ? Date.now() : blockHeader.getTimestamp()*1000;
1055
+ }
1056
+ quote._setInitiated();
1057
+ if(btcTx.inputAddresses!=null) quote._senderAddress = btcTx.inputAddresses[1];
1058
+ if(state.type===SpvWithdrawalStateType.FRONTED) {
1059
+ quote._frontTxId = state.txId;
1060
+ quote._state = SpvFromBTCSwapState.FRONTED;
1061
+ } else {
1062
+ quote._claimTxId = state.txId;
1063
+ quote._state = SpvFromBTCSwapState.CLAIMED;
1064
+ }
1065
+ await quote._save();
1066
+ return quote;
1067
+ }
1068
+
1069
+ /**
1070
+ * Returns a random dummy PSBT that can be used for fee estimation, the last output (the LP output) is omitted
1071
+ * to allow for coinselection algorithm to determine maximum sendable amount there
1072
+ *
1073
+ * @param includeGasToken Whether to return the PSBT also with the gas token amount (increases the vSize by 8)
1074
+ */
1075
+ public getDummySwapPsbt(includeGasToken = false): Transaction {
1076
+ //Construct dummy swap psbt
1077
+ const psbt = new Transaction({
1078
+ allowUnknownInputs: true,
1079
+ allowLegacyWitnessUtxo: true,
1080
+ allowUnknownOutputs: true
1081
+ });
1082
+
1083
+ const randomVaultOutScript = getDummyOutputScript(REQUIRED_SPV_SWAP_VAULT_ADDRESS_TYPE);
1084
+
1085
+ psbt.addInput({
1086
+ txid: randomBytes(32),
1087
+ index: 0,
1088
+ witnessUtxo: {
1089
+ script: randomVaultOutScript,
1090
+ amount: 600n
1091
+ }
1092
+ });
1093
+
1094
+ psbt.addOutput({
1095
+ script: randomVaultOutScript,
1096
+ amount: 600n
1097
+ });
1098
+
1099
+ let longestOpReturnData: Buffer | undefined = undefined;
1100
+ for(let contractVersion in this.versionedContracts) {
1101
+ if(this.versionedContracts[contractVersion].spvVaultContract==null) continue;
1102
+ const opReturnData = this._contract(contractVersion).toOpReturnData(
1103
+ this._chain.randomAddress(),
1104
+ includeGasToken ? [0xFFFFFFFFFFFFFFFFn, 0xFFFFFFFFFFFFFFFFn] : [0xFFFFFFFFFFFFFFFFn]
1105
+ );
1106
+ if(longestOpReturnData==null || longestOpReturnData.length < opReturnData.length) longestOpReturnData = opReturnData;
1107
+ }
1108
+ if(longestOpReturnData==null) throw new Error(`No contract version supporting the Spv Vault BTC -> ${this.chainIdentifier} swaps found!`);
1109
+
1110
+ psbt.addOutput({
1111
+ script: Buffer.concat([
1112
+ longestOpReturnData.length <= 75 ? Buffer.from([0x6a, longestOpReturnData.length]) : Buffer.from([0x6a, 0x4c, longestOpReturnData.length]),
1113
+ longestOpReturnData
1114
+ ]),
1115
+ amount: 0n
1116
+ });
1117
+
1118
+ return psbt;
1119
+ }
1120
+
1121
+ /**
1122
+ * @inheritDoc
1123
+ * @internal
1124
+ */
1125
+ protected async _checkPastSwaps(pastSwaps: SpvFromBTCSwap<T>[]): Promise<{
1126
+ changedSwaps: SpvFromBTCSwap<T>[];
1127
+ removeSwaps: SpvFromBTCSwap<T>[]
1128
+ }> {
1129
+ const changedSwaps: Set<SpvFromBTCSwap<T>> = new Set();
1130
+ const removeSwaps: SpvFromBTCSwap<T>[] = [];
1131
+
1132
+ const broadcastedOrConfirmedSwaps: {[version: string]: (SpvFromBTCSwap<T> & {_data: T["SpvVaultWithdrawalData"]})[]} = {};
1133
+
1134
+ for(let pastSwap of pastSwaps) {
1135
+ let changed: boolean = false;
1136
+
1137
+ if(
1138
+ pastSwap._state===SpvFromBTCSwapState.SIGNED ||
1139
+ pastSwap._state===SpvFromBTCSwapState.POSTED ||
1140
+ pastSwap._state===SpvFromBTCSwapState.BROADCASTED ||
1141
+ pastSwap._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED ||
1142
+ pastSwap._state===SpvFromBTCSwapState.DECLINED ||
1143
+ pastSwap._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED
1144
+ ) {
1145
+ //Check BTC transaction
1146
+ if(await pastSwap._syncStateFromBitcoin(false)) changed ||= true;
1147
+ }
1148
+
1149
+ if(
1150
+ pastSwap._state===SpvFromBTCSwapState.CREATED ||
1151
+ pastSwap._state===SpvFromBTCSwapState.SIGNED ||
1152
+ pastSwap._state===SpvFromBTCSwapState.POSTED
1153
+ ) {
1154
+ if(await pastSwap._verifyQuoteDefinitelyExpired()) {
1155
+ if(pastSwap._state===SpvFromBTCSwapState.CREATED) {
1156
+ pastSwap._state = SpvFromBTCSwapState.QUOTE_EXPIRED;
1157
+ } else {
1158
+ pastSwap._state = SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED;
1159
+ }
1160
+ changed ||= true;
1161
+ }
1162
+ }
1163
+
1164
+ if(pastSwap.isQuoteExpired()) {
1165
+ removeSwaps.push(pastSwap);
1166
+ continue;
1167
+ }
1168
+ if(changed) changedSwaps.add(pastSwap);
1169
+
1170
+ if(pastSwap._state===SpvFromBTCSwapState.BROADCASTED || pastSwap._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED) {
1171
+ if(pastSwap._data!=null) (broadcastedOrConfirmedSwaps[pastSwap._contractVersion ?? "v1"] ??= []).push(pastSwap as (SpvFromBTCSwap<T> & {_data: T["SpvVaultWithdrawalData"]}));
1172
+ }
1173
+ }
1174
+
1175
+ for(let contractVersion in broadcastedOrConfirmedSwaps) {
1176
+ if(this.versionedContracts[contractVersion]==null) {
1177
+ this.logger.warn(`_checkPastSwaps(): No contract was found for ${this.chainIdentifier} version ${contractVersion}! Skipping these swaps!`);
1178
+ continue;
1179
+ }
1180
+
1181
+ const _broadcastedOrConfirmedSwaps = broadcastedOrConfirmedSwaps[contractVersion];
1182
+
1183
+ const checkWithdrawalStateSwaps: (SpvFromBTCSwap<T> & {_data: T["SpvVaultWithdrawalData"]})[] = [];
1184
+ const _fronts = await this._contract(contractVersion).getFronterAddresses(_broadcastedOrConfirmedSwaps.map(val => ({
1185
+ ...val.getSpvVaultData(),
1186
+ withdrawal: val._data!
1187
+ })));
1188
+ const _vaultUtxos = await this._contract(contractVersion).getVaultLatestUtxos(_broadcastedOrConfirmedSwaps.map(val => val.getSpvVaultData()));
1189
+ for(const pastSwap of _broadcastedOrConfirmedSwaps) {
1190
+ const fronterAddress = _fronts[pastSwap._data.getTxId()];
1191
+ const vault = pastSwap.getSpvVaultData();
1192
+ const latestVaultUtxo = _vaultUtxos[vault.owner]?.[vault.vaultId.toString(10)];
1193
+ if(fronterAddress===undefined) this.logger.warn(`_checkPastSwaps(): No fronter address returned for ${pastSwap._data.getTxId()}`);
1194
+ if(latestVaultUtxo===undefined) this.logger.warn(`_checkPastSwaps(): No last vault utxo returned for ${pastSwap._data.getTxId()}`);
1195
+ if(await pastSwap._shouldCheckWithdrawalState(fronterAddress, latestVaultUtxo)) checkWithdrawalStateSwaps.push(pastSwap);
1196
+ }
1197
+
1198
+ const withdrawalStates = await this._contract(contractVersion).getWithdrawalStates(
1199
+ checkWithdrawalStateSwaps.map(val => ({
1200
+ withdrawal: val._data,
1201
+ scStartBlockheight: val._genesisSmartChainBlockHeight
1202
+ }))
1203
+ );
1204
+ for(const pastSwap of checkWithdrawalStateSwaps) {
1205
+ const status = withdrawalStates[pastSwap._data.getTxId()];
1206
+ if(status==null) {
1207
+ this.logger.warn(`_checkPastSwaps(): No withdrawal state returned for ${pastSwap._data.getTxId()}`);
1208
+ continue;
1209
+ }
1210
+ this.logger.debug("syncStateFromChain(): status of "+pastSwap._data.btcTx.txid, status?.type);
1211
+ let changed = false;
1212
+ switch(status.type) {
1213
+ case SpvWithdrawalStateType.FRONTED:
1214
+ pastSwap._frontTxId = status.txId;
1215
+ pastSwap._state = SpvFromBTCSwapState.FRONTED;
1216
+ changed ||= true;
1217
+ break;
1218
+ case SpvWithdrawalStateType.CLAIMED:
1219
+ pastSwap._claimTxId = status.txId;
1220
+ pastSwap._state = SpvFromBTCSwapState.CLAIMED;
1221
+ changed ||= true;
1222
+ break;
1223
+ case SpvWithdrawalStateType.CLOSED:
1224
+ pastSwap._state = SpvFromBTCSwapState.CLOSED;
1225
+ changed ||= true;
1226
+ break;
1227
+ }
1228
+ if(changed) changedSwaps.add(pastSwap);
1229
+ }
1230
+ }
1231
+
1232
+ return {
1233
+ changedSwaps: Array.from(changedSwaps),
1234
+ removeSwaps
1235
+ };
1236
+ }
1237
+
1238
+ }