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