@atomiqlabs/sdk 8.7.7 → 8.8.4

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