From 25ccd9955838e9904e47f114c2abffcc7df7f084 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 16 Jan 2023 10:31:48 +0100 Subject: [PATCH] initial commit --- .dockerignore | 25 + .gitignore | 3 + .gitmodules | 6 + .run/BTCPayServer_ Altcoins-HTTPS.run.xml | 19 + BTCPayServerPlugins.sln | 117 ++++ ConfigBuilder/ConfigBuilder.csproj | 17 + ConfigBuilder/Dockerfile | 18 + ConfigBuilder/Program.cs | 12 + .../AOPPController.cs | 222 ++++++++ .../BTCPayServer.Plugins.AOPP/AOPPPlugin.cs | 42 ++ .../BTCPayServer.Plugins.AOPP/AOPPService.cs | 51 ++ .../BTCPayServer.Plugins.AOPP/AOPPSettings.cs | 7 + .../BTCPayServer.Plugins.AOPP.csproj | 19 + Plugins/BTCPayServer.Plugins.AOPP/Pack.ps1 | 2 + .../Resources/js/aoppComponent.js | 36 ++ .../UpdateAOPPSettingsViewModel.cs | 8 + .../Views/AOPP/UpdateAOPPSettings.cshtml | 23 + .../Views/Shared/AOPP/AOPPNav.cshtml | 18 + .../AOPP/CheckoutContentExtension.cshtml | 40 ++ .../Views/Shared/AOPP/CheckoutEnd.cshtml | 13 + .../Shared/AOPP/CheckoutTabExtension.cshtml | 14 + .../AOPP/StoreIntegrationAOPPOption.cshtml | 57 ++ .../Views/_ViewImports.cshtml | 1 + ...PayServer.Plugins.BitcoinWhitepaper.csproj | 36 ++ .../BitcoinWhitepaperPlugin.cs | 17 + .../bitcoin.pdf | Bin 0 -> 184292 bytes .../BTCPayServer.Plugins.FixedFloat.csproj | 40 ++ .../FixedFloatController.cs | 81 +++ .../FixedFloatPlugin.cs | 48 ++ .../FixedFloatService.cs | 46 ++ .../FixedFloatSettings.cs | 8 + .../BTCPayServer.Plugins.FixedFloat/Pack.ps1 | 2 + .../Resources/assets/ff.png | Bin 0 -> 747 bytes .../Resources/js/fixedFloatComponent.js | 35 ++ .../UpdateFixedFloatSettingsViewModel.cs | 8 + .../UpdateFixedFloatSettings.cshtml | 28 + .../CheckoutContentExtension.cshtml | 19 + .../Shared/FixedFloat/CheckoutEnd.cshtml | 12 + .../CheckoutPaymentExtension.cshtml | 46 ++ .../CheckoutPaymentMethodExtension.cshtml | 16 + .../FixedFloat/CheckoutTabExtension.cshtml | 14 + .../Shared/FixedFloat/FixedFloatNav.cshtml | 16 + .../StoreIntegrationFixedFloatOption.cshtml | 58 ++ .../Views/_ViewImports.cshtml | 2 + .../BTCPayServer.Plugins.FujiOracle.csproj | 18 + .../FujiOracleController.cs | 330 ++++++++++++ .../FujiOraclePlugin.cs | 32 ++ .../FujiOracleService.cs | 37 ++ .../FujiOracleSettings.cs | 11 + .../BTCPayServer.Plugins.FujiOracle/Pack.ps1 | 2 + .../UpdateFujioracleSettings.cshtml | 55 ++ .../Shared/FujiOracle/FujiOracleNav.cshtml | 19 + .../StoreIntegrationFujiOracleOption.cshtml | 58 ++ .../Views/_ViewImports.cshtml | 4 + .../BTCPayServer.Plugins.LSP.csproj | 19 + .../BTCPayServer.Plugins.LSP/LSPController.cs | 301 +++++++++++ Plugins/BTCPayServer.Plugins.LSP/LSPPlugin.cs | 31 ++ .../BTCPayServer.Plugins.LSP/LSPService.cs | 37 ++ Plugins/BTCPayServer.Plugins.LSP/Pack.ps1 | 2 + .../UpdateLSPViewModel.cs | 21 + .../Views/LSP/Connect.cshtml | 73 +++ .../Views/LSP/UpdateLSPSettings.cshtml | 78 +++ .../Views/LSP/View.cshtml | 88 +++ .../Views/Shared/LSP/LSPNav.cshtml | 19 + .../LSP/StoreIntegrationLSPOption.cshtml | 61 +++ .../Views/_ViewImports.cshtml | 4 + .../BTCPayServer.Plugins.LiquidPlus.csproj | 39 ++ .../CustomLiquidAssetsController.cs | 114 ++++ .../Controllers/StoreLiquidController.cs | 264 +++++++++ .../LiquidPlusPlugin.cs | 97 ++++ .../Models/CustomLiquidAssetsSettings.cs | 29 + .../Models/CustomLiquidAssetsViewModel.cs | 7 + .../BTCPayServer.Plugins.LiquidPlus/Pack.ps1 | 2 + .../Services/CustomLiquidAssetsRepository.cs | 60 +++ .../Views/CustomLiquidAssets/Assets.cshtml | 79 +++ .../CustomLiquidAssetsNavExtension.cshtml | 7 + .../Views/Shared/LiquidNav.cshtml | 23 + .../Shared/StoreNavLiquidExtension.cshtml | 15 + .../StoreLiquid/GenerateLiquidScript.cshtml | 65 +++ .../Views/_ViewImports.cshtml | 1 + .../BTCPayServer.Plugins.NFC.csproj | 40 ++ .../BTCPayServer.Plugins.NFC/NFCController.cs | 99 ++++ Plugins/BTCPayServer.Plugins.NFC/NFCPlugin.cs | 31 ++ Plugins/BTCPayServer.Plugins.NFC/Pack.ps1 | 2 + .../Resources/js/lnurlwnfc.js | 80 +++ .../Views/Shared/NFC/CheckoutEnd.cshtml | 14 + .../NFC/LightningCheckoutPostContent.cshtml | 12 + .../Views/_ViewImports.cshtml | 1 + ...TCPayServer.Plugins.RockstarStylist.csproj | 18 + .../Pack.ps1 | 2 + .../RockstarStyleProvider.cs | 37 ++ .../RockstarStylistPlugin.cs | 27 + .../Shared/InvoiceCheckoutThemeOptions.cshtml | 26 + .../Views/_ViewImports.cshtml | 1 + .../BTCPayServer.Plugins.SideShift.csproj | 40 ++ .../BTCPayServer.Plugins.SideShift/Pack.ps1 | 2 + .../Resources/assets/sideshift.svg | 14 + .../Resources/js/sideShiftComponent.js | 38 ++ .../SideShiftController.cs | 82 +++ .../SideShiftPlugin.cs | 48 ++ .../SideShiftService.cs | 49 ++ .../SideShiftSettings.cs | 8 + .../UpdateSideShiftSettingsViewModel.cs | 8 + .../SideShift/CheckoutContentExtension.cshtml | 26 + .../Views/Shared/SideShift/CheckoutEnd.cshtml | 16 + .../SideShift/CheckoutPaymentExtension.cshtml | 69 +++ .../CheckoutPaymentMethodExtension.cshtml | 15 + .../SideShift/CheckoutTabExtension.cshtml | 14 + .../Shared/SideShift/SideShiftNav.cshtml | 16 + .../StoreIntegrationSideShiftOption.cshtml | 59 ++ .../SideShift/UpdateSideShiftSettings.cshtml | 28 + .../Views/_ViewImports.cshtml | 2 + .../BTCPayServer.Plugins.TicketTailor.csproj | 40 ++ .../Pack.ps1 | 2 + .../Resources/assets/tt.png | Bin 0 -> 1572 bytes .../TicketTailorClient.cs | 266 +++++++++ .../TicketTailorController.cs | 503 ++++++++++++++++++ .../TicketTailorPlugin.cs | 33 ++ .../TicketTailorService.cs | 236 ++++++++ .../TicketTailorSettings.cs | 15 + .../UpdateTicketTailorSettingsViewModel.cs | 33 ++ .../StoreIntegrationTicketTailorOption.cshtml | 59 ++ .../TicketTailor/TicketTailorNav.cshtml | 17 + .../Views/TicketTailor/Receipt.cshtml | 133 +++++ .../UpdateTicketTailorSettings.cshtml | 134 +++++ .../Views/TicketTailor/View.cshtml | 163 ++++++ .../Views/_ViewImports.cshtml | 4 + .../BTCPayCoinjoinCoinSelector.cs | 406 ++++++++++++++ .../BTCPayKeyChain.cs | 77 +++ .../BTCPayServer.Plugins.Wabisabi.csproj | 48 ++ .../BTCPayWallet.cs | 474 +++++++++++++++++ .../Coordinator/CoordinatorExtensions.cs | 56 ++ ...MultiPooledSerializerJsonInputFormatter.cs | 49 ++ ...ontextAwareSerializerJsonInputFormatter.cs | 44 ++ .../ControllerBasedJsonInputFormatter.cs | 80 +++ ...rBasedJsonInputFormatterMvcOptionsSetup.cs | 70 +++ ...putFormatterServiceCollectionExtensions.cs | 27 + .../Filters/ExceptionTranslateAttribute.cs | 41 ++ ...ollerBasedJsonSerializerSettingsBuilder.cs | 10 + .../Filters/JsonSerializerPooledPolicy.cs | 16 + .../Filters/LateResponseLoggerFilter.cs | 20 + .../UseWasabiJsonInputFormatterAttribute.cs | 6 + .../WasabiSpecificJsonSerializerFilter.cs | 37 ++ .../Coordinator/WabiSabiController.cs | 159 ++++++ .../WabisabiCoordinatorConfigController.cs | 82 +++ .../Coordinator/WabisabiCoordinatorService.cs | 262 +++++++++ .../WabisabiCoordinatorSettings.cs | 26 + .../Coordinator/WasabiLeechController.cs | 36 ++ .../Extensions.cs | 38 ++ .../LocalisedUTXOLocker.cs | 32 ++ .../NBXInternalDestinationProvider.cs | 179 +++++++ .../BTCPayServer.Plugins.Wabisabi/Pack.ps1 | 2 + .../Smartifier.cs | 187 +++++++ .../StoreIntegrationWabisabiOption.cshtml | 58 ++ .../Shared/Wabisabi/WabisabiDashboard.cshtml | 460 ++++++++++++++++ .../Views/Shared/Wabisabi/WabisabiNav.cshtml | 18 + .../WabisabiServerNavvExtension.cshtml | 8 + .../UpdateWabisabiSettings.cshtml | 54 ++ .../Views/WabisabiStore/Spend.cshtml | 65 +++ .../UpdateWabisabiStoreSettings.cshtml | 322 +++++++++++ .../Views/_ViewImports.cshtml | 4 + .../WabisabiCoordinatorClientInstance.cs | 228 ++++++++ .../WabisabiPlugin.cs | 119 +++++ .../WabisabiService.cs | 73 +++ .../WabisabiStore.Wabisabi.csproj | 41 ++ .../WabisabiStoreController.cs | 375 +++++++++++++ .../WabisabiStoreSettings.cs | 32 ++ .../WalletProvider.cs | 257 +++++++++ .../WasabiCoordinatorStatusFetcher.cs | 46 ++ submodules/btcpayserver | 1 + submodules/walletwasabi | 1 + 171 files changed, 10592 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .run/BTCPayServer_ Altcoins-HTTPS.run.xml create mode 100644 BTCPayServerPlugins.sln create mode 100644 ConfigBuilder/ConfigBuilder.csproj create mode 100644 ConfigBuilder/Dockerfile create mode 100644 ConfigBuilder/Program.cs create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/AOPPController.cs create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/AOPPPlugin.cs create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/AOPPService.cs create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/AOPPSettings.cs create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/BTCPayServer.Plugins.AOPP.csproj create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/Pack.ps1 create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/Resources/js/aoppComponent.js create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/UpdateAOPPSettingsViewModel.cs create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/Views/AOPP/UpdateAOPPSettings.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/AOPPNav.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/CheckoutContentExtension.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/CheckoutEnd.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/CheckoutTabExtension.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/StoreIntegrationAOPPOption.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.AOPP/Views/_ViewImports.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.BitcoinWhitepaper/BTCPayServer.Plugins.BitcoinWhitepaper.csproj create mode 100644 Plugins/BTCPayServer.Plugins.BitcoinWhitepaper/BitcoinWhitepaperPlugin.cs create mode 100644 Plugins/BTCPayServer.Plugins.BitcoinWhitepaper/bitcoin.pdf create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/BTCPayServer.Plugins.FixedFloat.csproj create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatController.cs create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatPlugin.cs create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatService.cs create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatSettings.cs create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/Pack.ps1 create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/Resources/assets/ff.png create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/Resources/js/fixedFloatComponent.js create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/UpdateFixedFloatSettingsViewModel.cs create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/Views/FixedFloat/UpdateFixedFloatSettings.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutContentExtension.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutEnd.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutPaymentExtension.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutPaymentMethodExtension.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutTabExtension.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/FixedFloatNav.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/StoreIntegrationFixedFloatOption.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.FixedFloat/Views/_ViewImports.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.FujiOracle/BTCPayServer.Plugins.FujiOracle.csproj create mode 100644 Plugins/BTCPayServer.Plugins.FujiOracle/FujiOracleController.cs create mode 100644 Plugins/BTCPayServer.Plugins.FujiOracle/FujiOraclePlugin.cs create mode 100644 Plugins/BTCPayServer.Plugins.FujiOracle/FujiOracleService.cs create mode 100644 Plugins/BTCPayServer.Plugins.FujiOracle/FujiOracleSettings.cs create mode 100644 Plugins/BTCPayServer.Plugins.FujiOracle/Pack.ps1 create mode 100644 Plugins/BTCPayServer.Plugins.FujiOracle/Views/FujiOracle/UpdateFujioracleSettings.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.FujiOracle/Views/Shared/FujiOracle/FujiOracleNav.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.FujiOracle/Views/Shared/FujiOracle/StoreIntegrationFujiOracleOption.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.FujiOracle/Views/_ViewImports.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.LSP/BTCPayServer.Plugins.LSP.csproj create mode 100644 Plugins/BTCPayServer.Plugins.LSP/LSPController.cs create mode 100644 Plugins/BTCPayServer.Plugins.LSP/LSPPlugin.cs create mode 100644 Plugins/BTCPayServer.Plugins.LSP/LSPService.cs create mode 100644 Plugins/BTCPayServer.Plugins.LSP/Pack.ps1 create mode 100644 Plugins/BTCPayServer.Plugins.LSP/UpdateLSPViewModel.cs create mode 100644 Plugins/BTCPayServer.Plugins.LSP/Views/LSP/Connect.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.LSP/Views/LSP/UpdateLSPSettings.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.LSP/Views/LSP/View.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.LSP/Views/Shared/LSP/LSPNav.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.LSP/Views/Shared/LSP/StoreIntegrationLSPOption.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.LSP/Views/_ViewImports.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/BTCPayServer.Plugins.LiquidPlus.csproj create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/Controllers/CustomLiquidAssetsController.cs create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/Controllers/StoreLiquidController.cs create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/LiquidPlusPlugin.cs create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/Models/CustomLiquidAssetsSettings.cs create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/Models/CustomLiquidAssetsViewModel.cs create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/Pack.ps1 create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/Services/CustomLiquidAssetsRepository.cs create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/Views/CustomLiquidAssets/Assets.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/Views/Shared/CustomLiquidAssetsNavExtension.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/Views/Shared/LiquidNav.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/Views/Shared/StoreNavLiquidExtension.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/Views/StoreLiquid/GenerateLiquidScript.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.LiquidPlus/Views/_ViewImports.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.NFC/BTCPayServer.Plugins.NFC.csproj create mode 100644 Plugins/BTCPayServer.Plugins.NFC/NFCController.cs create mode 100644 Plugins/BTCPayServer.Plugins.NFC/NFCPlugin.cs create mode 100644 Plugins/BTCPayServer.Plugins.NFC/Pack.ps1 create mode 100644 Plugins/BTCPayServer.Plugins.NFC/Resources/js/lnurlwnfc.js create mode 100644 Plugins/BTCPayServer.Plugins.NFC/Views/Shared/NFC/CheckoutEnd.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.NFC/Views/Shared/NFC/LightningCheckoutPostContent.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.NFC/Views/_ViewImports.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.RockstarStylist/BTCPayServer.Plugins.RockstarStylist.csproj create mode 100644 Plugins/BTCPayServer.Plugins.RockstarStylist/Pack.ps1 create mode 100644 Plugins/BTCPayServer.Plugins.RockstarStylist/RockstarStyleProvider.cs create mode 100644 Plugins/BTCPayServer.Plugins.RockstarStylist/RockstarStylistPlugin.cs create mode 100644 Plugins/BTCPayServer.Plugins.RockstarStylist/Views/Shared/InvoiceCheckoutThemeOptions.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.RockstarStylist/Views/_ViewImports.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/BTCPayServer.Plugins.SideShift.csproj create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/Pack.ps1 create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/Resources/assets/sideshift.svg create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/Resources/js/sideShiftComponent.js create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/SideShiftController.cs create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/SideShiftPlugin.cs create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/SideShiftService.cs create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/SideShiftSettings.cs create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/UpdateSideShiftSettingsViewModel.cs create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutContentExtension.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutEnd.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutPaymentExtension.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutPaymentMethodExtension.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutTabExtension.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/SideShiftNav.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/StoreIntegrationSideShiftOption.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/Views/SideShift/UpdateSideShiftSettings.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.SideShift/Views/_ViewImports.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/BTCPayServer.Plugins.TicketTailor.csproj create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/Pack.ps1 create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/Resources/assets/tt.png create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorClient.cs create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorController.cs create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorPlugin.cs create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorService.cs create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorSettings.cs create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/UpdateTicketTailorSettingsViewModel.cs create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/Views/Shared/TicketTailor/StoreIntegrationTicketTailorOption.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/Views/Shared/TicketTailor/TicketTailorNav.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/Receipt.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/UpdateTicketTailorSettings.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/View.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.TicketTailor/Views/_ViewImports.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayKeyChain.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/CoordinatorExtensions.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/Filters/ContextAwareMultiPooledSerializerJsonInputFormatter.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/Filters/ContextAwareSerializerJsonInputFormatter.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/Filters/ControllerBasedJsonInputFormatter.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/Filters/ControllerBasedJsonInputFormatterMvcOptionsSetup.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/Filters/ControllerBasedJsonInputFormatterServiceCollectionExtensions.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/Filters/ExceptionTranslateAttribute.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/Filters/IControllerBasedJsonSerializerSettingsBuilder.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/Filters/JsonSerializerPooledPolicy.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/Filters/LateResponseLoggerFilter.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/Filters/UseWasabiJsonInputFormatterAttribute.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/Filters/WasabiSpecificJsonSerializerFilter.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/WabiSabiController.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/WabisabiCoordinatorConfigController.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/WabisabiCoordinatorService.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/WabisabiCoordinatorSettings.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Coordinator/WasabiLeechController.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Extensions.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/LocalisedUTXOLocker.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/NBXInternalDestinationProvider.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Pack.ps1 create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Smartifier.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/StoreIntegrationWabisabiOption.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiDashboard.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiNav.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Views/Shared/Wabisabi/WabisabiServerNavvExtension.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiCoordinatorConfig/UpdateWabisabiSettings.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/Spend.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/UpdateWabisabiStoreSettings.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/Views/_ViewImports.cshtml create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiCoordinatorClientInstance.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiPlugin.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiService.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStore.Wabisabi.csproj create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreController.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreSettings.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/WalletProvider.cs create mode 100644 Plugins/BTCPayServer.Plugins.Wabisabi/WasabiCoordinatorStatusFetcher.cs create mode 160000 submodules/btcpayserver create mode 160000 submodules/walletwasabi diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ab637e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +**/bin/**/* +**/obj +.idea diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2425aab --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "submodules/walletwasabi"] + path = submodules/walletwasabi + url = https://github.com/kukks/walletwasabi +[submodule "submodules/btcpayserver"] + path = submodules/btcpayserver + url = https://github.com/btcpayserver/btcpayserver diff --git a/.run/BTCPayServer_ Altcoins-HTTPS.run.xml b/.run/BTCPayServer_ Altcoins-HTTPS.run.xml new file mode 100644 index 0000000..5516a3a --- /dev/null +++ b/.run/BTCPayServer_ Altcoins-HTTPS.run.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/BTCPayServerPlugins.sln b/BTCPayServerPlugins.sln new file mode 100644 index 0000000..9bb8691 --- /dev/null +++ b/BTCPayServerPlugins.sln @@ -0,0 +1,117 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletWasabi", "submodules\walletwasabi\WalletWasabi\WalletWasabi.csproj", "{D1D1116C-38F9-4EA3-AC65-A75FEA82E5C8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer", "submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj", "{B19C9F52-DC47-466D-8B5C-2D202B7B003F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.BitcoinWhitepaper", "Plugins\BTCPayServer.Plugins.BitcoinWhitepaper\BTCPayServer.Plugins.BitcoinWhitepaper.csproj", "{AD9635BB-C70E-4676-BB04-900D51B01666}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Abstractions", "submodules\btcpayserver\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj", "{8F158B88-0FEE-44FF-8552-7C0F17D5C508}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Client", "submodules\btcpayserver\BTCPayServer.Client\BTCPayServer.Client.csproj", "{DF85EFA4-0EF5-4A99-853F-E6F9C88E3F8C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Data", "submodules\btcpayserver\BTCPayServer.Data\BTCPayServer.Data.csproj", "{2C5C4DF9-BA1F-4671-9F24-B22D7C9C3D21}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Rating", "submodules\btcpayserver\BTCPayServer.Rating\BTCPayServer.Rating.csproj", "{D7E7309D-C4F4-496A-B2C8-BC5D3991B9C0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Common", "submodules\btcpayserver\BTCPayServer.Common\BTCPayServer.Common.csproj", "{3F2E0BA0-9EA7-490F-894D-F9703F35B174}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Custodians.FakeCustodian", "submodules\btcpayserver\Plugins\BTCPayServer.Plugins.Custodians.FakeCustodian\BTCPayServer.Plugins.Custodians.FakeCustodian.csproj", "{CCDE6E23-60B7-4B12-95AA-91CB40DBC3BD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BTCPay", "BTCPay", "{9E04ECE9-E304-4FF2-9CBC-83256E6C6962}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigBuilder", "ConfigBuilder\ConfigBuilder.csproj", "{6295533A-F941-40CA-B889-FE6C0432ED53}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.FixedFloat", "Plugins\BTCPayServer.Plugins.FixedFloat\BTCPayServer.Plugins.FixedFloat.csproj", "{58863D86-3C78-4BEC-ACB6-2F82CC141210}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.LiquidPlus", "Plugins\BTCPayServer.Plugins.LiquidPlus\BTCPayServer.Plugins.LiquidPlus.csproj", "{B4E2ED08-4AD3-4648-8BDB-3107200460B9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.NFC", "Plugins\BTCPayServer.Plugins.NFC\BTCPayServer.Plugins.NFC.csproj", "{71885A5E-1B00-4676-9566-D81AAE37406C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.SideShift", "Plugins\BTCPayServer.Plugins.SideShift\BTCPayServer.Plugins.SideShift.csproj", "{5E1BAA06-7828-47BC-89D6-19C2A78EA427}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.TicketTailor", "Plugins\BTCPayServer.Plugins.TicketTailor\BTCPayServer.Plugins.TicketTailor.csproj", "{7AFC20EB-1696-47D7-8E57-822B05DD18F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Wabisabi", "Plugins\BTCPayServer.Plugins.Wabisabi\BTCPayServer.Plugins.Wabisabi.csproj", "{0D438B7D-F996-4BF3-8F54-02CB9DF120D8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D1D1116C-38F9-4EA3-AC65-A75FEA82E5C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1D1116C-38F9-4EA3-AC65-A75FEA82E5C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1D1116C-38F9-4EA3-AC65-A75FEA82E5C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1D1116C-38F9-4EA3-AC65-A75FEA82E5C8}.Release|Any CPU.Build.0 = Release|Any CPU + {B19C9F52-DC47-466D-8B5C-2D202B7B003F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B19C9F52-DC47-466D-8B5C-2D202B7B003F}.Release|Any CPU.Build.0 = Release|Any CPU + {B19C9F52-DC47-466D-8B5C-2D202B7B003F}.Debug|Any CPU.ActiveCfg = Altcoins-Debug|Any CPU + {B19C9F52-DC47-466D-8B5C-2D202B7B003F}.Debug|Any CPU.Build.0 = Altcoins-Debug|Any CPU + {AD9635BB-C70E-4676-BB04-900D51B01666}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD9635BB-C70E-4676-BB04-900D51B01666}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD9635BB-C70E-4676-BB04-900D51B01666}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD9635BB-C70E-4676-BB04-900D51B01666}.Release|Any CPU.Build.0 = Release|Any CPU + {8F158B88-0FEE-44FF-8552-7C0F17D5C508}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F158B88-0FEE-44FF-8552-7C0F17D5C508}.Release|Any CPU.Build.0 = Release|Any CPU + {8F158B88-0FEE-44FF-8552-7C0F17D5C508}.Debug|Any CPU.ActiveCfg = Altcoins-Debug|Any CPU + {8F158B88-0FEE-44FF-8552-7C0F17D5C508}.Debug|Any CPU.Build.0 = Altcoins-Debug|Any CPU + {DF85EFA4-0EF5-4A99-853F-E6F9C88E3F8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF85EFA4-0EF5-4A99-853F-E6F9C88E3F8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF85EFA4-0EF5-4A99-853F-E6F9C88E3F8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF85EFA4-0EF5-4A99-853F-E6F9C88E3F8C}.Release|Any CPU.Build.0 = Release|Any CPU + {2C5C4DF9-BA1F-4671-9F24-B22D7C9C3D21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C5C4DF9-BA1F-4671-9F24-B22D7C9C3D21}.Release|Any CPU.Build.0 = Release|Any CPU + {2C5C4DF9-BA1F-4671-9F24-B22D7C9C3D21}.Debug|Any CPU.ActiveCfg = Altcoins-Debug|Any CPU + {2C5C4DF9-BA1F-4671-9F24-B22D7C9C3D21}.Debug|Any CPU.Build.0 = Altcoins-Debug|Any CPU + {D7E7309D-C4F4-496A-B2C8-BC5D3991B9C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7E7309D-C4F4-496A-B2C8-BC5D3991B9C0}.Release|Any CPU.Build.0 = Release|Any CPU + {D7E7309D-C4F4-496A-B2C8-BC5D3991B9C0}.Debug|Any CPU.ActiveCfg = Altcoins-Release|Any CPU + {D7E7309D-C4F4-496A-B2C8-BC5D3991B9C0}.Debug|Any CPU.Build.0 = Altcoins-Release|Any CPU + {3F2E0BA0-9EA7-490F-894D-F9703F35B174}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F2E0BA0-9EA7-490F-894D-F9703F35B174}.Release|Any CPU.Build.0 = Release|Any CPU + {3F2E0BA0-9EA7-490F-894D-F9703F35B174}.Debug|Any CPU.ActiveCfg = Altcoins-Debug|Any CPU + {3F2E0BA0-9EA7-490F-894D-F9703F35B174}.Debug|Any CPU.Build.0 = Altcoins-Debug|Any CPU + {CCDE6E23-60B7-4B12-95AA-91CB40DBC3BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCDE6E23-60B7-4B12-95AA-91CB40DBC3BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCDE6E23-60B7-4B12-95AA-91CB40DBC3BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCDE6E23-60B7-4B12-95AA-91CB40DBC3BD}.Release|Any CPU.Build.0 = Release|Any CPU + {6295533A-F941-40CA-B889-FE6C0432ED53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6295533A-F941-40CA-B889-FE6C0432ED53}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6295533A-F941-40CA-B889-FE6C0432ED53}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6295533A-F941-40CA-B889-FE6C0432ED53}.Release|Any CPU.Build.0 = Release|Any CPU + {58863D86-3C78-4BEC-ACB6-2F82CC141210}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58863D86-3C78-4BEC-ACB6-2F82CC141210}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58863D86-3C78-4BEC-ACB6-2F82CC141210}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58863D86-3C78-4BEC-ACB6-2F82CC141210}.Release|Any CPU.Build.0 = Release|Any CPU + {B4E2ED08-4AD3-4648-8BDB-3107200460B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4E2ED08-4AD3-4648-8BDB-3107200460B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4E2ED08-4AD3-4648-8BDB-3107200460B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4E2ED08-4AD3-4648-8BDB-3107200460B9}.Release|Any CPU.Build.0 = Release|Any CPU + {71885A5E-1B00-4676-9566-D81AAE37406C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71885A5E-1B00-4676-9566-D81AAE37406C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71885A5E-1B00-4676-9566-D81AAE37406C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71885A5E-1B00-4676-9566-D81AAE37406C}.Release|Any CPU.Build.0 = Release|Any CPU + {5E1BAA06-7828-47BC-89D6-19C2A78EA427}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E1BAA06-7828-47BC-89D6-19C2A78EA427}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E1BAA06-7828-47BC-89D6-19C2A78EA427}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E1BAA06-7828-47BC-89D6-19C2A78EA427}.Release|Any CPU.Build.0 = Release|Any CPU + {7AFC20EB-1696-47D7-8E57-822B05DD18F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AFC20EB-1696-47D7-8E57-822B05DD18F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AFC20EB-1696-47D7-8E57-822B05DD18F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AFC20EB-1696-47D7-8E57-822B05DD18F2}.Release|Any CPU.Build.0 = Release|Any CPU + {0D438B7D-F996-4BF3-8F54-02CB9DF120D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D438B7D-F996-4BF3-8F54-02CB9DF120D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D438B7D-F996-4BF3-8F54-02CB9DF120D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D438B7D-F996-4BF3-8F54-02CB9DF120D8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962} + {DF85EFA4-0EF5-4A99-853F-E6F9C88E3F8C} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962} + {8F158B88-0FEE-44FF-8552-7C0F17D5C508} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962} + {D7E7309D-C4F4-496A-B2C8-BC5D3991B9C0} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962} + {3F2E0BA0-9EA7-490F-894D-F9703F35B174} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962} + {CCDE6E23-60B7-4B12-95AA-91CB40DBC3BD} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962} + {2C5C4DF9-BA1F-4671-9F24-B22D7C9C3D21} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962} + EndGlobalSection +EndGlobal diff --git a/ConfigBuilder/ConfigBuilder.csproj b/ConfigBuilder/ConfigBuilder.csproj new file mode 100644 index 0000000..ec44a9e --- /dev/null +++ b/ConfigBuilder/ConfigBuilder.csproj @@ -0,0 +1,17 @@ + + + + Exe + net6.0 + enable + enable + Linux + + + + + .dockerignore + + + + diff --git a/ConfigBuilder/Dockerfile b/ConfigBuilder/Dockerfile new file mode 100644 index 0000000..0083922 --- /dev/null +++ b/ConfigBuilder/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["ConfigBuilder/ConfigBuilder.csproj", "ConfigBuilder/"] +RUN dotnet restore "ConfigBuilder/ConfigBuilder.csproj" +COPY . . +WORKDIR "/src/ConfigBuilder" +RUN dotnet build "ConfigBuilder.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "ConfigBuilder.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "ConfigBuilder.dll"] diff --git a/ConfigBuilder/Program.cs b/ConfigBuilder/Program.cs new file mode 100644 index 0000000..a884bfc --- /dev/null +++ b/ConfigBuilder/Program.cs @@ -0,0 +1,12 @@ +using System.Text.Json; + +var plugins = Directory.GetDirectories("../../../../Plugins"); +Console.WriteLine(string.Join(',',plugins)); +var p = string.Join(';', plugins.Select(s => $"{Path.GetFullPath(s)}/bin/Debug/net6.0/{Path.GetFileName(s)}.dll" ));; +var fileContents = $"{{ \"DEBUG_PLUGINS\": \"{p}\"}}"; +var content = JsonSerializer.Serialize(new +{ + DEBUG_PLUGINS = p +}); + +await File.WriteAllTextAsync("../../../../submodules/BTCPayServer/BTCPayServer/appsettings.dev.json", content); \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.AOPP/AOPPController.cs b/Plugins/BTCPayServer.Plugins.AOPP/AOPPController.cs new file mode 100644 index 0000000..0e034a9 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/AOPPController.cs @@ -0,0 +1,222 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Common; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using NBitcoin.Crypto; +using NBitcoin.Protocol; +using NBXplorer; +using Newtonsoft.Json; + +namespace BTCPayServer.Plugins.AOPP +{ + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Route("plugins/{storeId}/AOPP")] + public class AOPPController : Controller + { + private readonly BTCPayServerClient _btcPayServerClient; + private readonly AOPPService _AOPPService; + + public AOPPController(BTCPayServerClient btcPayServerClient, AOPPService AOPPService) + { + _btcPayServerClient = btcPayServerClient; + _AOPPService = AOPPService; + } + + [HttpGet("")] + public async Task UpdateAOPPSettings(string storeId) + { + var store = await _btcPayServerClient.GetStore(storeId); + + UpdateAOPPSettingsViewModel vm = new UpdateAOPPSettingsViewModel(); + vm.StoreName = store.Name; + AOPPSettings AOPP = null; + try + { + AOPP = await _AOPPService.GetAOPPForStore(storeId); + } + catch (Exception) + { + // ignored + } + + SetExistingValues(AOPP, vm); + return View(vm); + } + + private void SetExistingValues(AOPPSettings existing, UpdateAOPPSettingsViewModel vm) + { + if (existing == null) + return; + vm.Enabled = existing.Enabled; + } + + [HttpPost("")] + public async Task UpdateAOPPSettings(string storeId, UpdateAOPPSettingsViewModel vm, + string command) + { + if (vm.Enabled) + { + if (!ModelState.IsValid) + { + return View(vm); + } + } + + var AOPPSettings = new AOPPSettings() + { + Enabled = vm.Enabled, + }; + + switch (command) + { + case "save": + await _AOPPService.SetAOPPForStore(storeId, AOPPSettings); + TempData["SuccessMessage"] = "AOPP settings modified"; + return RedirectToAction(nameof(UpdateAOPPSettings), new {storeId}); + + default: + return View(vm); + } + } + + + + internal static String BITCOIN_SIGNED_MESSAGE_HEADER = "Bitcoin Signed Message:\n"; + internal static byte[] BITCOIN_SIGNED_MESSAGE_HEADER_BYTES = Encoding.UTF8.GetBytes(BITCOIN_SIGNED_MESSAGE_HEADER); + + //http://bitcoinj.googlecode.com/git-history/keychain/core/src/main/java/com/google/bitcoin/core/Utils.java + private static byte[] FormatMessageForSigning(byte[] messageBytes) + { + MemoryStream ms = new MemoryStream(); + + ms.WriteByte((byte)BITCOIN_SIGNED_MESSAGE_HEADER_BYTES.Length); + ms.Write(BITCOIN_SIGNED_MESSAGE_HEADER_BYTES, 0, BITCOIN_SIGNED_MESSAGE_HEADER_BYTES.Length); + + VarInt size = new VarInt((ulong)messageBytes.Length); + ms.Write(size.ToBytes(), 0, size.ToBytes().Length); + ms.Write(messageBytes, 0, messageBytes.Length); + return ms.ToArray(); + } + + public class AoppRequest + { + public Uri aopp { get; set; } + } + + [HttpPost] + [Route("{invoiceId}")] + [AllowAnonymous] + public async Task AOPPExecute(string storeId, string invoiceId, + [FromBody] AoppRequest request , + [FromServices] IHttpClientFactory httpClientFactory, + [FromServices] BTCPayNetworkProvider btcPayNetworkProvider, + [FromServices] IExplorerClientProvider explorerClientProvider, + [FromServices] BTCPayServerClient btcPayServerClient, + [FromServices] IBTCPayServerClientFactory btcPayServerClientFactory) + { + try + { + var client = await btcPayServerClientFactory.Create(null, new[] {storeId}); + + var invoice = await client.GetInvoice(storeId, invoiceId); + if (invoice.Status is not InvoiceStatus.New) + { + return NotFound(); + } + + var qs = HttpUtility.ParseQueryString(request.aopp.Query); + var asset = qs.Get("asset"); + var network = btcPayNetworkProvider.GetNetwork(asset); + + + + var invoicePaymentMethods = await client.GetInvoicePaymentMethods(storeId, invoiceId); + + var pm = invoicePaymentMethods.FirstOrDefault(model => + model.PaymentMethod.Equals(asset, StringComparison.InvariantCultureIgnoreCase)); + if (pm is null) + { + return NotFound(); + } + var supported = (await client.GetStoreOnChainPaymentMethods(storeId)) + .FirstOrDefault(settings => settings.CryptoCode.Equals(asset, StringComparison.InvariantCultureIgnoreCase)); +; + var msg = qs.Get("msg"); + var format = qs.Get("format"); + var callback = new Uri(qs.Get("callback")!, UriKind.Absolute); + ScriptType? expectedType = null; + switch (format) + { + case "p2pkh": + expectedType = ScriptType.P2PKH; + break; + case "p2wpkh": + expectedType = ScriptType.P2WPKH; + break; + case "p2sh": + expectedType = ScriptType.P2SH; + break; + case "p2tr": + expectedType = ScriptType.Taproot; + break; + case "any": + break; + } + + var address = BitcoinAddress.Create(pm.Destination, network.NBitcoinNetwork); + if (expectedType is not null && !address.ScriptPubKey + .IsScriptType(expectedType.Value)) + { + return BadRequest(); + } + var derivatonScheme = + network.NBXplorerNetwork.DerivationStrategyFactory.Parse(supported.DerivationScheme); + var explorerClient = explorerClientProvider.GetExplorerClient(network); + var extKeyStr = await explorerClient.GetMetadataAsync( + derivatonScheme, + WellknownMetadataKeys.AccountHDKey); + if (extKeyStr == null) + { + return BadRequest(); + } + + var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork); + + var keyInfo = await explorerClient.GetKeyInformationAsync(derivatonScheme, address.ScriptPubKey); + var privateKey = accountKey.Derive(keyInfo.KeyPath).PrivateKey; + + var messageBytes = Encoding.UTF8.GetBytes(msg); + byte[] data = FormatMessageForSigning(messageBytes); + var hash = Hashes.DoubleSHA256(data); + var sig = Convert.ToBase64String(privateKey.SignCompact(hash, true).Signature); + + var response = new + { + version = 0, + address = pm.Destination, + signature = sig + }; + using var httpClient = httpClientFactory.CreateClient(); + await httpClient.PostAsync(callback, + new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json")); + return Ok(); + } + catch (Exception e) + { + return BadRequest(new {ErrorMessage = e.Message}); + } + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.AOPP/AOPPPlugin.cs b/Plugins/BTCPayServer.Plugins.AOPP/AOPPPlugin.cs new file mode 100644 index 0000000..e62dace --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/AOPPPlugin.cs @@ -0,0 +1,42 @@ +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Plugins.AOPP +{ + public class AOPPPlugin : BaseBTCPayServerPlugin + { + public override string Identifier => "BTCPayServer.Plugins.AOPP"; + public override string Name => "AOPP"; + + + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new() { Identifier = nameof(BTCPayServer), Condition = ">=1.6.0.0" } + }; + + public override string Description => + "Allows you to support the AOPP protocol in invoices to allow customers to bypass stupid KYC rules."; + + public override void Execute(IServiceCollection applicationBuilder) + { + applicationBuilder.AddSingleton(); + applicationBuilder.AddSingleton(new UIExtension("AOPP/StoreIntegrationAOPPOption", + "store-integrations-list")); + applicationBuilder.AddSingleton(new UIExtension("AOPP/CheckoutContentExtension", + "checkout-bitcoin-post-content")); + applicationBuilder.AddSingleton(new UIExtension("AOPP/CheckoutContentExtension", + "checkout-lightning-post-content")); + applicationBuilder.AddSingleton(new UIExtension("AOPP/CheckoutTabExtension", + "checkout-bitcoin-post-tabs")); + applicationBuilder.AddSingleton(new UIExtension("AOPP/CheckoutTabExtension", + "checkout-lightning-post-tabs")); + applicationBuilder.AddSingleton(new UIExtension("AOPP/CheckoutEnd", + "checkout-end")); + applicationBuilder.AddSingleton(new UIExtension("AOPP/AOPPNav", + "store-integrations-nav")); + base.Execute(applicationBuilder); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.AOPP/AOPPService.cs b/Plugins/BTCPayServer.Plugins.AOPP/AOPPService.cs new file mode 100644 index 0000000..87ba10b --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/AOPPService.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using Microsoft.Extensions.Caching.Memory; + +namespace BTCPayServer.Plugins.AOPP +{ + public class AOPPService + { + private readonly ISettingsRepository _settingsRepository; + private readonly IMemoryCache _memoryCache; + private readonly IStoreRepository _storeRepository; + + public AOPPService(ISettingsRepository settingsRepository, IMemoryCache memoryCache, + IStoreRepository storeRepository) + { + _settingsRepository = settingsRepository; + _memoryCache = memoryCache; + _storeRepository = storeRepository; + } + + + public async Task GetAOPPForStore(string storeId) + { + var k = $"{nameof(AOPPSettings)}_{storeId}"; + return await _memoryCache.GetOrCreateAsync(k, async _ => + { + var res = await _storeRepository.GetSettingAsync(storeId, + nameof(AOPPSettings)); + if (res is not null) return res; + res = await _settingsRepository.GetSettingAsync(k); + + if (res is not null) + { + await SetAOPPForStore(storeId, res); + } + + await _settingsRepository.UpdateSetting(null, k); + return res; + }); + } + + public async Task SetAOPPForStore(string storeId, AOPPSettings AOPPSettings) + { + var k = $"{nameof(AOPPSettings)}_{storeId}"; + await _storeRepository.UpdateSetting(storeId, nameof(AOPPSettings), AOPPSettings); + _memoryCache.Set(k, AOPPSettings); + } + + + } +} diff --git a/Plugins/BTCPayServer.Plugins.AOPP/AOPPSettings.cs b/Plugins/BTCPayServer.Plugins.AOPP/AOPPSettings.cs new file mode 100644 index 0000000..02677f0 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/AOPPSettings.cs @@ -0,0 +1,7 @@ +namespace BTCPayServer.Plugins.AOPP +{ + public class AOPPSettings + { + public bool Enabled { get; set; } + } +} diff --git a/Plugins/BTCPayServer.Plugins.AOPP/BTCPayServer.Plugins.AOPP.csproj b/Plugins/BTCPayServer.Plugins.AOPP/BTCPayServer.Plugins.AOPP.csproj new file mode 100644 index 0000000..2b90ff4 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/BTCPayServer.Plugins.AOPP.csproj @@ -0,0 +1,19 @@ + + + net6.0 + true + false + true + 1.0.1 + + + + + + + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.AOPP/Pack.ps1 b/Plugins/BTCPayServer.Plugins.AOPP/Pack.ps1 new file mode 100644 index 0000000..933a81a --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/Pack.ps1 @@ -0,0 +1,2 @@ +dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.AOPP +dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.AOPP BTCPayServer.Plugins.AOPP ../packed diff --git a/Plugins/BTCPayServer.Plugins.AOPP/Resources/js/aoppComponent.js b/Plugins/BTCPayServer.Plugins.AOPP/Resources/js/aoppComponent.js new file mode 100644 index 0000000..49920e9 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/Resources/js/aoppComponent.js @@ -0,0 +1,36 @@ +Vue.component("AOPP", { + props: ["srvModel"], + methods: { + onaoppChange: function(){ + this.aoppAddressInputDirty = true; + this.aoppAddressInputInvalid = false; + }, + onSubmit : function(){ + var self = this; + if (this.aoppAddressInput && this.aoppAddressInput.startsWith("aopp:?")) { + this.aoppAddressFormSubmitting = true; + // Push the email to a server, once the reception is confirmed move on + $.ajax({ + url: "/plugins/"+this.srvModel.storeId+"/AOPP/" +this.srvModel.invoiceId, + type: "POST", + data: JSON.stringify({ aopp: this.aoppAddressInput }), + contentType: "application/json; charset=utf-8" + }) + .done(function () { + }).always(function () { + self.aoppAddressFormSubmitting = false; + }); + } else { + this.aoppAddressInputInvalid = true; + } + } + }, + data: function () { + return { + aoppAddressInput: "", + aoppAddressInputDirty: false, + aoppAddressInputInvalid: false, + aoppAddressFormSubmitting: false + } + } +}); diff --git a/Plugins/BTCPayServer.Plugins.AOPP/UpdateAOPPSettingsViewModel.cs b/Plugins/BTCPayServer.Plugins.AOPP/UpdateAOPPSettingsViewModel.cs new file mode 100644 index 0000000..81bdb75 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/UpdateAOPPSettingsViewModel.cs @@ -0,0 +1,8 @@ +namespace BTCPayServer.Plugins.AOPP +{ + public class UpdateAOPPSettingsViewModel + { + public bool Enabled { get; set; } + public string StoreName { get; set; } + } +} diff --git a/Plugins/BTCPayServer.Plugins.AOPP/Views/AOPP/UpdateAOPPSettings.cshtml b/Plugins/BTCPayServer.Plugins.AOPP/Views/AOPP/UpdateAOPPSettings.cshtml new file mode 100644 index 0000000..4ef5676 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/Views/AOPP/UpdateAOPPSettings.cshtml @@ -0,0 +1,23 @@ +@model BTCPayServer.Plugins.AOPP.UpdateAOPPSettingsViewModel +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewData["NavPartialName"] = "../UIStores/_Nav"; +} + +

@ViewData["PageTitle"]

+ +
+
+
+
+ + +
+ +
+
+
+ +@section PageFootContent { + +} diff --git a/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/AOPPNav.cshtml b/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/AOPPNav.cshtml new file mode 100644 index 0000000..9fb0c6e --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/AOPPNav.cshtml @@ -0,0 +1,18 @@ +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Plugins.AOPP +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null && + nameof(AOPPController).StartsWith(controller.ToString(), StringComparison.InvariantCultureIgnoreCase); +} +@if (!string.IsNullOrEmpty(storeId)) +{ + +} diff --git a/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/CheckoutContentExtension.cshtml b/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/CheckoutContentExtension.cshtml new file mode 100644 index 0000000..681f992 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/CheckoutContentExtension.cshtml @@ -0,0 +1,40 @@ +@using BTCPayServer.Plugins.AOPP +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq +@inject AOPPService AOPPService +@{ + var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value(); + var settings = await AOPPService.GetAOPPForStore(storeId); + if (settings?.Enabled is true) + { +
+ +
+
+ {{$t("AOPP")}} +
+
+ + If you are sending funds from an exchange that requiores that you "verify" the withdrawal access, you can use this tool to bypass this madness. You even earn bonus points if they try to pass that data over to a chain surveillance service, by poisoning their clusters. + + {{$t("Please enter a valid aopp address")}} +
+
+ + + + +
+
+
+
+ } +} diff --git a/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/CheckoutEnd.cshtml b/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/CheckoutEnd.cshtml new file mode 100644 index 0000000..e3666e4 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/CheckoutEnd.cshtml @@ -0,0 +1,13 @@ +@using BTCPayServer.Plugins.AOPP +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq +@inject AOPPService AOPPService +@{ + + var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value(); + var settings = await AOPPService.GetAOPPForStore(storeId); + if (settings?.Enabled is true) + { + + } +} diff --git a/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/CheckoutTabExtension.cshtml b/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/CheckoutTabExtension.cshtml new file mode 100644 index 0000000..4749b25 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/CheckoutTabExtension.cshtml @@ -0,0 +1,14 @@ +@using BTCPayServer.Plugins.AOPP +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq +@inject AOPPService AOPPService +@{ + var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value(); + var settings = await AOPPService.GetAOPPForStore(storeId); + if (settings?.Enabled is true) + { +
+ {{$t("AOPP")}} +
+ } +} diff --git a/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/StoreIntegrationAOPPOption.cshtml b/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/StoreIntegrationAOPPOption.cshtml new file mode 100644 index 0000000..0571ce9 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/Views/Shared/AOPP/StoreIntegrationAOPPOption.cshtml @@ -0,0 +1,57 @@ +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Plugins.AOPP +@using Microsoft.AspNetCore.Mvc.TagHelpers +@inject AOPPService AOPPService +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + AOPPSettings settings = null; + if (!string.IsNullOrEmpty(storeId)) + { + try + { + settings = await AOPPService.GetAOPPForStore(storeId); + } + catch (Exception) + { + } + } +} +@if (!string.IsNullOrEmpty(storeId)) +{ +
  • +
    + + + AOPP + + + Allows your customers to pay with altcoins that are not supported by BTCPay Server. + + + + @if (settings?.Enabled is true) + { + + + Enabled + + | + + Modify + + } + else + { + + + Disabled + + + Setup + + } + +
    +
  • +} diff --git a/Plugins/BTCPayServer.Plugins.AOPP/Views/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.AOPP/Views/_ViewImports.cshtml new file mode 100644 index 0000000..afa82bb --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.AOPP/Views/_ViewImports.cshtml @@ -0,0 +1 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Plugins/BTCPayServer.Plugins.BitcoinWhitepaper/BTCPayServer.Plugins.BitcoinWhitepaper.csproj b/Plugins/BTCPayServer.Plugins.BitcoinWhitepaper/BTCPayServer.Plugins.BitcoinWhitepaper.csproj new file mode 100644 index 0000000..7157900 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.BitcoinWhitepaper/BTCPayServer.Plugins.BitcoinWhitepaper.csproj @@ -0,0 +1,36 @@ + + + net6.0 + 10 + + + + + Bitcoin Whitepaper + This makes the Bitcoin whitepaper available on your BTCPay Server. + Kukks + 1.0.2 + + + + + true + false + true + + + + + + StaticWebAssetsEnabled=false + false + runtime;native;build;buildTransitive;contentFiles + + + + + + + + + \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.BitcoinWhitepaper/BitcoinWhitepaperPlugin.cs b/Plugins/BTCPayServer.Plugins.BitcoinWhitepaper/BitcoinWhitepaperPlugin.cs new file mode 100644 index 0000000..c8e2365 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.BitcoinWhitepaper/BitcoinWhitepaperPlugin.cs @@ -0,0 +1,17 @@ +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; + +namespace BTCPayServer.Plugins.BitcoinWhitepaper +{ + public class BitcoinWhitepaperPlugin: BaseBTCPayServerPlugin + { + public override string Identifier { get; } = "BTCPayServer.Plugins.BitcoinWhitepaper"; + public override string Name { get; } = "Bitcoin Whitepaper"; + public override string Description { get; } = "This makes the Bitcoin whitepaper available on your BTCPay Server."; + + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new() { Identifier = nameof(BTCPayServer), Condition = ">=1.4.0.0" } + }; + } +} diff --git a/Plugins/BTCPayServer.Plugins.BitcoinWhitepaper/bitcoin.pdf b/Plugins/BTCPayServer.Plugins.BitcoinWhitepaper/bitcoin.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1e19b739f6e296dd1b38f71f20bf9152e6d9f6d1 GIT binary patch literal 184292 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%0AuqRR%y(EF;^kYm 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 literal 0 HcmV?d00001 diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/BTCPayServer.Plugins.FixedFloat.csproj b/Plugins/BTCPayServer.Plugins.FixedFloat/BTCPayServer.Plugins.FixedFloat.csproj new file mode 100644 index 0000000..dc94ba4 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/BTCPayServer.Plugins.FixedFloat.csproj @@ -0,0 +1,40 @@ + + + + net6.0 + 10 + + + + + FixedFloat + Allows you to embed a FixedFloat conversion screen to allow customers to pay with altcoins. + Kukks + 1.0.6 + + + + + true + false + true + + + + + + StaticWebAssetsEnabled=false + false + runtime;native;build;buildTransitive;contentFiles + + + + + + + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatController.cs b/Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatController.cs new file mode 100644 index 0000000..5e37eda --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatController.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Client; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Plugins.FixedFloat +{ + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Route("plugins/{storeId}/FixedFloat")] + public class FixedFloatController : Controller + { + private readonly BTCPayServerClient _btcPayServerClient; + private readonly FixedFloatService _FixedFloatService; + + public FixedFloatController(BTCPayServerClient btcPayServerClient, FixedFloatService FixedFloatService) + { + _btcPayServerClient = btcPayServerClient; + _FixedFloatService = FixedFloatService; + } + + [HttpGet("")] + public async Task UpdateFixedFloatSettings(string storeId) + { + var store = await _btcPayServerClient.GetStore(storeId); + + UpdateFixedFloatSettingsViewModel vm = new UpdateFixedFloatSettingsViewModel(); + vm.StoreName = store.Name; + FixedFloatSettings FixedFloat = null; + try + { + FixedFloat = await _FixedFloatService.GetFixedFloatForStore(storeId); + } + catch (Exception) + { + // ignored + } + + SetExistingValues(FixedFloat, vm); + return View(vm); + } + + private void SetExistingValues(FixedFloatSettings existing, UpdateFixedFloatSettingsViewModel vm) + { + if (existing == null) + return; + vm.Enabled = existing.Enabled; + } + + [HttpPost("")] + public async Task UpdateFixedFloatSettings(string storeId, UpdateFixedFloatSettingsViewModel vm, + string command) + { + if (vm.Enabled) + { + if (!ModelState.IsValid) + { + return View(vm); + } + } + + var FixedFloatSettings = new FixedFloatSettings() + { + Enabled = vm.Enabled, + }; + + switch (command) + { + case "save": + await _FixedFloatService.SetFixedFloatForStore(storeId, FixedFloatSettings); + TempData["SuccessMessage"] = "FixedFloat settings modified"; + return RedirectToAction(nameof(UpdateFixedFloatSettings), new {storeId}); + + default: + return View(vm); + } + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatPlugin.cs b/Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatPlugin.cs new file mode 100644 index 0000000..f448995 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatPlugin.cs @@ -0,0 +1,48 @@ +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Plugins.FixedFloat +{ + public class FixedFloatPlugin : BaseBTCPayServerPlugin + { + public override string Identifier => "BTCPayServer.Plugins.FixedFloat"; + public override string Name => "FixedFloat"; + + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new() { Identifier = nameof(BTCPayServer), Condition = ">=1.7.0.0" } + }; + + public override string Description => + "Allows you to embed a FixedFloat conversion screen to allow customers to pay with altcoins."; + + public override void Execute(IServiceCollection applicationBuilder) + { + applicationBuilder.AddSingleton(); + applicationBuilder.AddSingleton(new UIExtension("FixedFloat/FixedFloatNav", + "store-integrations-nav")); + applicationBuilder.AddSingleton(new UIExtension("FixedFloat/StoreIntegrationFixedFloatOption", + "store-integrations-list")); + // Checkout v2 + applicationBuilder.AddSingleton(new UIExtension("FixedFloat/CheckoutPaymentMethodExtension", + "checkout-payment-method")); + applicationBuilder.AddSingleton(new UIExtension("FixedFloat/CheckoutPaymentExtension", + "checkout-payment")); + // Checkout Classic + applicationBuilder.AddSingleton(new UIExtension("FixedFloat/CheckoutContentExtension", + "checkout-bitcoin-post-content")); + applicationBuilder.AddSingleton(new UIExtension("FixedFloat/CheckoutContentExtension", + "checkout-lightning-post-content")); + applicationBuilder.AddSingleton(new UIExtension("FixedFloat/CheckoutTabExtension", + "checkout-bitcoin-post-tabs")); + applicationBuilder.AddSingleton(new UIExtension("FixedFloat/CheckoutTabExtension", + "checkout-lightning-post-tabs")); + applicationBuilder.AddSingleton(new UIExtension("FixedFloat/CheckoutEnd", + "checkout-end")); + base.Execute(applicationBuilder); + } + } + +} diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatService.cs b/Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatService.cs new file mode 100644 index 0000000..9940349 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatService.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using Microsoft.Extensions.Caching.Memory; + +namespace BTCPayServer.Plugins.FixedFloat +{ + public class FixedFloatService + { + private readonly ISettingsRepository _settingsRepository; + private readonly IStoreRepository _storeRepository; + private readonly IMemoryCache _memoryCache; + + public FixedFloatService(ISettingsRepository settingsRepository, IStoreRepository storeRepository, IMemoryCache memoryCache) + { + _settingsRepository = settingsRepository; + _storeRepository = storeRepository; + _memoryCache = memoryCache; + } + public async Task GetFixedFloatForStore(string storeId) + { + var k = $"{nameof(FixedFloatSettings)}_{storeId}"; + return await _memoryCache.GetOrCreateAsync(k, async _ => + { + var res = await _storeRepository.GetSettingAsync(storeId, + nameof(FixedFloatSettings)); + if (res is not null) return res; + res = await _settingsRepository.GetSettingAsync(k); + + if (res is not null) + { + await SetFixedFloatForStore(storeId, res); + } + + await _settingsRepository.UpdateSetting(null, k); + return res; + }); + } + + public async Task SetFixedFloatForStore(string storeId, FixedFloatSettings fixedFloatSettings) + { + var k = $"{nameof(FixedFloatSettings)}_{storeId}"; + await _storeRepository.UpdateSetting(storeId, nameof(FixedFloatSettings), fixedFloatSettings); + _memoryCache.Set(k, fixedFloatSettings); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatSettings.cs b/Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatSettings.cs new file mode 100644 index 0000000..f521771 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/FixedFloatSettings.cs @@ -0,0 +1,8 @@ +namespace BTCPayServer.Plugins.FixedFloat +{ + public class FixedFloatSettings + { + public bool Enabled { get; set; } + public decimal AmountMarkupPercentage { get; set; } = 0; + } +} diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/Pack.ps1 b/Plugins/BTCPayServer.Plugins.FixedFloat/Pack.ps1 new file mode 100644 index 0000000..6454eb8 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/Pack.ps1 @@ -0,0 +1,2 @@ +dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.FixedFloat +dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.FixedFloat BTCPayServer.Plugins.FixedFloat ../packed diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/Resources/assets/ff.png b/Plugins/BTCPayServer.Plugins.FixedFloat/Resources/assets/ff.png new file mode 100644 index 0000000000000000000000000000000000000000..2385931f94f7270a50b7509f872edc02ab59227d GIT binary patch literal 747 zcmVi^i-_5~ACH4HK!y+D@&YYnG68NS@_-2=c%@5h z143pYAjCFc#4QMJVGj2B_MGkPAFALNZ|p9hgJDO<1Cy?yjVl~>$i@Im6DH6auvLvK z8t-e~eMw~ifVJR~mITb9-a4I0Qm#Ks6V6;d!?w%L-yaT6;Bu~i4)!iy`}0hIe_0ro zLp^5zz$ZHJB)awBUT1}w(65~V8h##ZQer~85CmIrKdaR?Xn@Zvh2_vPoEKsMuQX{| zvJ9k4-55!NEcP&W$OVm6S_J+uvQK}USQyGm4Zkt z=&QG5nEs=Gz?aS<$@q_*#-a+~@e3kV!RG_93wW*upuuxApelIPPg*wvs)TP?l-yYv zP&NFt9~%?oy28&?V!}kOGrWZf_w&vIYXx8EPo?#ItIb*&1w3XDG@76?GV%AD zK + +

    @ViewData["Title"]

    + + + +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutContentExtension.cshtml b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutContentExtension.cshtml new file mode 100644 index 0000000..13f46c3 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutContentExtension.cshtml @@ -0,0 +1,19 @@ +@using BTCPayServer.Plugins.FixedFloat +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq +@inject FixedFloatService FixedFloatService +@{ + var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value(); + var settings = await FixedFloatService.GetFixedFloatForStore(storeId); + if (settings?.Enabled is true) + { +
    + + + +
    + } +} diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutEnd.cshtml b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutEnd.cshtml new file mode 100644 index 0000000..1796f6e --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutEnd.cshtml @@ -0,0 +1,12 @@ +@using BTCPayServer.Plugins.FixedFloat +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq +@inject FixedFloatService FixedFloatService +@{ + var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value(); + var settings = await FixedFloatService.GetFixedFloatForStore(storeId); + if (settings?.Enabled is true) + { + + } +} diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutPaymentExtension.cshtml b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutPaymentExtension.cshtml new file mode 100644 index 0000000..2c2e853 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutPaymentExtension.cshtml @@ -0,0 +1,46 @@ +@using BTCPayServer.Plugins.FixedFloat +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq +@inject FixedFloatService FixedFloatService +@{ + var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value(); + var settings = await FixedFloatService.GetFixedFloatForStore(storeId); +} +@if (settings?.Enabled is true) +{ + + +} diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutPaymentMethodExtension.cshtml b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutPaymentMethodExtension.cshtml new file mode 100644 index 0000000..af5b384 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutPaymentMethodExtension.cshtml @@ -0,0 +1,16 @@ +@using BTCPayServer.Plugins.FixedFloat +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq +@inject FixedFloatService FixedFloatService +@{ + const string id = "FixedFloat"; + var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value(); + var settings = await FixedFloatService.GetFixedFloatForStore(storeId); + + if (settings?.Enabled is true) + { +
    + @id + + } +} diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutTabExtension.cshtml b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutTabExtension.cshtml new file mode 100644 index 0000000..bbb3923 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/CheckoutTabExtension.cshtml @@ -0,0 +1,14 @@ +@using BTCPayServer.Plugins.FixedFloat +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq +@inject FixedFloatService FixedFloatService +@{ + var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value(); + var settings = await FixedFloatService.GetFixedFloatForStore(storeId); + if (settings?.Enabled is true) + { +
    + {{$t("Altcoins (FixedFloat)")}} +
    + } +} diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/FixedFloatNav.cshtml b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/FixedFloatNav.cshtml new file mode 100644 index 0000000..02aa4b6 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/FixedFloatNav.cshtml @@ -0,0 +1,16 @@ +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Abstractions.Extensions +@using Microsoft.AspNetCore.Mvc.TagHelpers +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); +} +@if (!string.IsNullOrEmpty(storeId)) +{ + +} diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/StoreIntegrationFixedFloatOption.cshtml b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/StoreIntegrationFixedFloatOption.cshtml new file mode 100644 index 0000000..00d847a --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/Shared/FixedFloat/StoreIntegrationFixedFloatOption.cshtml @@ -0,0 +1,58 @@ +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Plugins.FixedFloat +@using Microsoft.AspNetCore.Routing +@inject FixedFloatService FixedFloatService +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + + FixedFloatSettings settings = null; + if (!string.IsNullOrEmpty(storeId)) + { + try + { + settings = await FixedFloatService.GetFixedFloatForStore(storeId); + } + catch (Exception) + { + } + } +} +@if (!string.IsNullOrEmpty(storeId)) +{ +
  • +
    + + + FixedFloat + + + Allows your customers to pay with altcoins that are not supported by BTCPay Server. + + + + @if (settings?.Enabled is true) + { + + + Enabled + + | + + Modify + + } + else + { + + + Disabled + + + Setup + + } + +
    +
  • +} diff --git a/Plugins/BTCPayServer.Plugins.FixedFloat/Views/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/_ViewImports.cshtml new file mode 100644 index 0000000..52e6837 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FixedFloat/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@addTagHelper *, BTCPayServer.Abstractions +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Plugins/BTCPayServer.Plugins.FujiOracle/BTCPayServer.Plugins.FujiOracle.csproj b/Plugins/BTCPayServer.Plugins.FujiOracle/BTCPayServer.Plugins.FujiOracle.csproj new file mode 100644 index 0000000..24991ee --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FujiOracle/BTCPayServer.Plugins.FujiOracle.csproj @@ -0,0 +1,18 @@ + + + net6.0 + true + false + true + 1.0.0 + + + + + + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.FujiOracle/FujiOracleController.cs b/Plugins/BTCPayServer.Plugins.FujiOracle/FujiOracleController.cs new file mode 100644 index 0000000..25b304f --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FujiOracle/FujiOracleController.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.VisualBasic; +using NBitcoin; +using NBitcoin.Crypto; +using NBitcoin.DataEncoders; +using NBitcoin.Secp256k1; +using Newtonsoft.Json.Linq; +using Key = NBitcoin.Key; + +namespace BTCPayServer.Plugins.FujiOracle +{ + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Route("plugins/{storeId}/FujiOracle")] + public class FujiOracleController : Controller + { + + + + private async Task CreateClient(string storeId) + { + return await _btcPayServerClientFactory.Create(null, new[] {storeId}, new DefaultHttpContext() + { + Request = + { + Scheme = "https", + Host = Request.Host, + Path = Request.Path, + PathBase = Request.PathBase + } + }); + } + + + private readonly IHttpClientFactory _httpClientFactory; + private readonly FujiOracleService _FujiOracleService; + private readonly IBTCPayServerClientFactory _btcPayServerClientFactory; + + public FujiOracleController(IHttpClientFactory httpClientFactory, + FujiOracleService FujiOracleService, + IBTCPayServerClientFactory btcPayServerClientFactory) + { + _httpClientFactory = httpClientFactory; + _FujiOracleService = FujiOracleService; + _btcPayServerClientFactory = btcPayServerClientFactory; + } + + [HttpGet("update")] + public async Task UpdateFujiOracleSettings(string storeId) + { + var + FujiOracle = (await _FujiOracleService.GetFujiOracleForStore(storeId)) ?? new(); + + + return View(FujiOracle); + } + + + + [HttpPost("update")] + public async Task UpdateFujiOracleSettings(string storeId, + FujiOracleSettings vm, + string command) + { + if (command == "generate") + { + ModelState.Clear(); + + if (ECPrivKey.TryCreate(new ReadOnlySpan(RandomNumberGenerator.GetBytes(32)), out var key)) + { + vm.Key = key.ToHex(); + } + return View(vm); + } + + if (command == "add-pair") + { + vm.Pairs ??= new List(); + vm.Pairs.Add(""); + return View(vm); + } + + if (command.StartsWith("remove-pair")) + { + var i = int.Parse(command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + + 1)); + vm.Pairs.RemoveAt(i); + return View(vm); + } + + + var validPairsToQuery = ""; + for (var i = 0; i < vm.Pairs.Count; i++) + { + string vmPair = vm.Pairs[i]; + if (string.IsNullOrWhiteSpace(vmPair)) + { + + ModelState.AddModelError($"{vm.Pairs}[{i}]", + $"Remove invalid"); + continue; + } + + var split = vmPair.Split("_", StringSplitOptions.RemoveEmptyEntries); + if (split.Length != 2) + { + ModelState.AddModelError($"{vm.Pairs}[{i}]", + $"Invalid format, needs to be BTC_USD format"); + continue; + } + + validPairsToQuery += "," + vmPair; + } + + validPairsToQuery = validPairsToQuery.TrimStart(','); + if (!string.IsNullOrEmpty(validPairsToQuery)) + { + try + { + var url = Request.GetAbsoluteUri(Url.Action("GetRates2", + "BitpayRate", new {storeId, currencyPairs = validPairsToQuery})); + var resp = JArray.Parse(await _httpClientFactory.CreateClient().GetStringAsync(url)); + + for (var i = 0; i < vm.Pairs.Count; i++) + { + + string vmPair = vm.Pairs[i]; + if (!resp.Any(token => token["currencyPair"].Value() == vmPair)) + { + ModelState.AddModelError($"{vm.Pairs}[{i}]", + $"You store could not resolve pair {vmPair}"); + } + } + } + catch (Exception e) + { + } + + } + + if (string.IsNullOrEmpty(vm.Key) && vm.Enabled) + { + + ModelState.AddModelError(nameof(vm.Enabled), + $"Cannot enable without a key"); + } + + if (!string.IsNullOrEmpty(vm.Key)) + { + try + { + if (HexEncoder.IsWellFormed(vm.Key)) + { + ECPrivKey.Create(Encoders.Hex.DecodeData(vm.Key)); + } + } + catch (Exception e) + { + + ModelState.AddModelError(nameof(vm.Enabled), + $"Key was invalid"); + } + } + if (!ModelState.IsValid) + { + return View(vm); + } + + + switch (command?.ToLowerInvariant()) + { + case "save": + await _FujiOracleService.SetFujiOracleForStore(storeId, vm); + TempData["SuccessMessage"] = "FujiOracle settings modified"; + return RedirectToAction(nameof(UpdateFujiOracleSettings), new {storeId}); + + default: + return View(vm); + } + } + + [AllowAnonymous] + [HttpGet("")] + public async Task GetOracleInfo(string storeId) + { + var oracle = await _FujiOracleService.GetFujiOracleForStore(storeId); + if (oracle is null || !oracle.Enabled || oracle.Key is null) + { + return NotFound(); + } + + return Ok(new + { + publicKey = new Key(Encoders.Hex.DecodeData(oracle.Key)).PubKey.ToHex(), + availableTickers = oracle.Pairs.ToArray() + }); + } + [AllowAnonymous] + [HttpGet("{pair}")] + public async Task GetOracleAttestation(string storeId, string pair) + { + var oracle = await _FujiOracleService.GetFujiOracleForStore(storeId); + if (oracle is null || !oracle.Enabled || oracle.Key is null || !oracle.Pairs.Contains(pair)) + { + return NotFound(); + } + var url = Request.GetAbsoluteUri(Url.Action("GetRates2", + "BitpayRate", new {storeId, currencyPairs = pair})); + var resp = JArray.Parse(await _httpClientFactory.CreateClient().GetStringAsync(url)).First(); + var ts = DateTimeOffset.Now.ToUnixTimeSeconds(); + var rate =(long)decimal.Truncate( resp["rate"].Value()); + var messageBytes = BitConverter.GetBytes(ts).Concat(BitConverter.GetBytes(rate)).ToArray(); + using var sha256Hash = System.Security.Cryptography.SHA256.Create(); + var messageHash = sha256Hash.ComputeHash(messageBytes); + var key = Extensions.ParseKey(oracle.Key); + var buf = new byte[64]; + key.SignBIP340(messageHash).WriteToSpan(buf); + var sig = buf.ToHex(); + + return Ok(new + { + timestamp = ts.ToString(), + lastPrice = rate.ToString(), + attestation = new { + signature= sig, + message= messageBytes.ToHex(), + messageHash= messageHash.ToHex() + }, + }); + } + } + + public static class Extensions + { + public static ECPrivKey ParseKey(string key) + { + return ECPrivKey.Create(key.DecodHexData()); + } + + public static byte[] DecodHexData(this string encoded) + { + if (encoded == null) + throw new ArgumentNullException(nameof(encoded)); + if (encoded.Length % 2 == 1) + throw new FormatException("Invalid Hex String"); + + var result = new byte[encoded.Length / 2]; + for (int i = 0, j = 0; i < encoded.Length; i += 2, j++) + { + var a = IsDigit(encoded[i]); + var b = IsDigit(encoded[i + 1]); + if (a == -1 || b == -1) + throw new FormatException("Invalid Hex String"); + result[j] = (byte)(((uint)a << 4) | (uint)b); + } + + return result; + } + + public static int IsDigit(this char c) + { + if ('0' <= c && c <= '9') + { + return c - '0'; + } + else if ('a' <= c && c <= 'f') + { + return c - 'a' + 10; + } + else if ('A' <= c && c <= 'F') + { + return c - 'A' + 10; + } + else + { + return -1; + } + } + + public static string ToHex(this byte[] bytes) + { + var builder = new StringBuilder(); + foreach (var t in bytes) + { + builder.Append(t.ToHex()); + } + + return builder.ToString(); + } + + private static string ToHex(this byte b) + { + return b.ToString("x2"); + } + + public static string ToHex(this Span bytes) + { + var builder = new StringBuilder(); + foreach (var t in bytes) + { + builder.Append(t.ToHex()); + } + + return builder.ToString(); + } + + public static string ToHex(this ECPrivKey key) + { + Span output = new(new byte[32]); + key.WriteToSpan(output); + return output.ToHex(); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.FujiOracle/FujiOraclePlugin.cs b/Plugins/BTCPayServer.Plugins.FujiOracle/FujiOraclePlugin.cs new file mode 100644 index 0000000..4fdd2fb --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FujiOracle/FujiOraclePlugin.cs @@ -0,0 +1,32 @@ +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Plugins.FujiOracle +{ + public class FujiOraclePlugin : BaseBTCPayServerPlugin + { + public override string Identifier => "BTCPayServer.Plugins.FujiOracle"; + public override string Name => "Fuji Oracle"; + + + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new() { Identifier = nameof(BTCPayServer), Condition = ">=1.6.0.0" } + }; + + public override string Description => + "Allows you to become an oracle for the fuji.money platform"; + + public override void Execute(IServiceCollection applicationBuilder) + { + applicationBuilder.AddSingleton(); + applicationBuilder.AddSingleton(new UIExtension("FujiOracle/StoreIntegrationFujiOracleOption", + "store-integrations-list")); + applicationBuilder.AddSingleton(new UIExtension("FujiOracle/FujiOracleNav", + "store-integrations-nav")); + base.Execute(applicationBuilder); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.FujiOracle/FujiOracleService.cs b/Plugins/BTCPayServer.Plugins.FujiOracle/FujiOracleService.cs new file mode 100644 index 0000000..c2ef3fe --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FujiOracle/FujiOracleService.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using Microsoft.Extensions.Caching.Memory; + +namespace BTCPayServer.Plugins.FujiOracle +{ + public class FujiOracleService + { + private readonly IMemoryCache _memoryCache; + private readonly IStoreRepository _storeRepository; + + public FujiOracleService(IMemoryCache memoryCache, IStoreRepository storeRepository) + { + _memoryCache = memoryCache; + _storeRepository = storeRepository; + } + + public async Task GetFujiOracleForStore(string storeId) + { + var k = $"{nameof(FujiOracleSettings)}_{storeId}"; + return await _memoryCache.GetOrCreateAsync(k, async _ => + { + var res = await _storeRepository.GetSettingAsync(storeId, + nameof(FujiOracleSettings)); + return res; + }); + } + + public async Task SetFujiOracleForStore(string storeId, FujiOracleSettings FujiOracleSettings) + { + var k = $"{nameof(FujiOracleSettings)}_{storeId}"; + + await _storeRepository.UpdateSetting(storeId, nameof(FujiOracleSettings), FujiOracleSettings); + _memoryCache.Set(k, FujiOracleSettings); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.FujiOracle/FujiOracleSettings.cs b/Plugins/BTCPayServer.Plugins.FujiOracle/FujiOracleSettings.cs new file mode 100644 index 0000000..e883021 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FujiOracle/FujiOracleSettings.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace BTCPayServer.Plugins.FujiOracle +{ + public class FujiOracleSettings + { + public bool Enabled { get; set; } + public string Key { get; set; } + public List Pairs { get; set; } = new(); + } +} diff --git a/Plugins/BTCPayServer.Plugins.FujiOracle/Pack.ps1 b/Plugins/BTCPayServer.Plugins.FujiOracle/Pack.ps1 new file mode 100644 index 0000000..9b6d2a6 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FujiOracle/Pack.ps1 @@ -0,0 +1,2 @@ +dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.FujiOracle +dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.FujiOracle BTCPayServer.Plugins.FujiOracle ../packed diff --git a/Plugins/BTCPayServer.Plugins.FujiOracle/Views/FujiOracle/UpdateFujioracleSettings.cshtml b/Plugins/BTCPayServer.Plugins.FujiOracle/Views/FujiOracle/UpdateFujioracleSettings.cshtml new file mode 100644 index 0000000..d3b51bf --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FujiOracle/Views/FujiOracle/UpdateFujioracleSettings.cshtml @@ -0,0 +1,55 @@ +@using BTCPayServer.Abstractions.Extensions +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using BTCPayServer.Abstractions.Contracts +@model BTCPayServer.Plugins.FujiOracle.FujiOracleSettings +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + Layout = "../Shared/_NavLayout.cshtml"; + ViewData["NavPartialName"] = "../UIStores/_Nav"; + ViewData.SetActivePage("Fuji Oracle", "Update Fuji Oracle Settings", null); + +} + +

    @ViewData["Title"]

    +
    +
    +
    +
    + +
    + + +
    + + +
    + + +
    +
    + + +
    + +
    +
    + Pairs +
    + + @for (var index = 0; index < Model.Pairs.Count; index++) + { +
    + + + +
    + } +
    +
    + +
    +
    +
    +
    + diff --git a/Plugins/BTCPayServer.Plugins.FujiOracle/Views/Shared/FujiOracle/FujiOracleNav.cshtml b/Plugins/BTCPayServer.Plugins.FujiOracle/Views/Shared/FujiOracle/FujiOracleNav.cshtml new file mode 100644 index 0000000..ca944fc --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FujiOracle/Views/Shared/FujiOracle/FujiOracleNav.cshtml @@ -0,0 +1,19 @@ +@inject IScopeProvider ScopeProvider +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Plugins.FujiOracle +@using Microsoft.AspNetCore.Mvc.TagHelpers +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null && + nameof(FujiOracleController).StartsWith(controller?.ToString(), StringComparison.InvariantCultureIgnoreCase); +} +@if (!string.IsNullOrEmpty(storeId)) +{ + +} diff --git a/Plugins/BTCPayServer.Plugins.FujiOracle/Views/Shared/FujiOracle/StoreIntegrationFujiOracleOption.cshtml b/Plugins/BTCPayServer.Plugins.FujiOracle/Views/Shared/FujiOracle/StoreIntegrationFujiOracleOption.cshtml new file mode 100644 index 0000000..bf00734 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FujiOracle/Views/Shared/FujiOracle/StoreIntegrationFujiOracleOption.cshtml @@ -0,0 +1,58 @@ + +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Plugins.FujiOracle +@inject IScopeProvider ScopeProvider +@inject FujiOracleService FujiOracleService +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + + FujiOracleSettings settings = null; + if (!string.IsNullOrEmpty(storeId)) + { + try + { + settings = await FujiOracleService.GetFujiOracleForStore(storeId); + } + catch (Exception) + { + } + } +} +@if (!string.IsNullOrEmpty(storeId)) +{ +
  • +
    + + + Ticket Tailor + + + Sell tickets on Ticket Tailor using BTCPay Server + + + + @if (settings?.Enabled is true) + { + + + Active + + | + + Modify + + } + else + { + + + Disabled + + + Setup + + } + +
    +
  • +} diff --git a/Plugins/BTCPayServer.Plugins.FujiOracle/Views/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.FujiOracle/Views/_ViewImports.cshtml new file mode 100644 index 0000000..04173ab --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.FujiOracle/Views/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using BTCPayServer.Abstractions.Services + +@inject Safe Safe +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Plugins/BTCPayServer.Plugins.LSP/BTCPayServer.Plugins.LSP.csproj b/Plugins/BTCPayServer.Plugins.LSP/BTCPayServer.Plugins.LSP.csproj new file mode 100644 index 0000000..b48d219 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LSP/BTCPayServer.Plugins.LSP.csproj @@ -0,0 +1,19 @@ + + + net6.0 + true + false + true + 1.0.0 + + + + + + + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.LSP/LSPController.cs b/Plugins/BTCPayServer.Plugins.LSP/LSPController.cs new file mode 100644 index 0000000..0a043be --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LSP/LSPController.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Lightning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using NBitcoin; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Plugins.LSP +{ + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Route("plugins/{storeId}/LSP")] + public class LSPController : Controller + { + [AllowAnonymous] + [HttpGet("")] + public async Task View(string storeId) + { + var config = await _LSPService.GetLSPForStore(storeId); + try + { + if (config?.Enabled is true) + { + return View(new LSPViewModel() {Settings = config}); + } + } + catch (Exception e) + { + // ignored + } + + return NotFound(); + } + + + [AllowAnonymous] + [HttpPost("")] + public async Task Purchase(string storeId, string email, uint inbound, bool privateChannel) + { + var config = await _LSPService.GetLSPForStore(storeId); + try + { + if (config?.Enabled is not true || string.IsNullOrEmpty(email) || inbound < config.Minimum || + inbound > config.Maximum) + { + return RedirectToAction("View", new {storeId}); + } + + var price = Math.Ceiling((config.FeePerSat == 0 ? 0 : (config.FeePerSat * inbound)) + config.BaseFee); + var btcpayClient = await CreateClient(storeId); + var redirectUrl = Request.GetAbsoluteUri(Url.Action("Connect", + "LSP", new {storeId, invoiceId = "kukkskukkskukks"})); + redirectUrl = redirectUrl.Replace("kukkskukkskukks", "{InvoiceId}"); + var inv = await btcpayClient.CreateInvoice(storeId, + new CreateInvoiceRequest() + { + Amount = price, + Currency = "sats", + Type = InvoiceType.Standard, + AdditionalSearchTerms = new[] {"LSP"}, + Checkout = + { + RequiresRefundEmail = true, + RedirectAutomatically = price > 0, + RedirectURL = redirectUrl, + }, + Metadata = JObject.FromObject(new + { + buyerEmail = email, + privateChannel, + inbound, + config.BaseFee, + config.FeePerSat, + orderId = "LSP" + }) + }); + + while (inv.Amount == 0 && inv.Status == InvoiceStatus.New) + { + if (inv.Status == InvoiceStatus.New) + inv = await btcpayClient.GetInvoice(inv.StoreId, inv.Id); + } + + if (inv.Status == InvoiceStatus.Settled) + return RedirectToAction("Connect", new {storeId, invoiceId = inv.Id}); + return Redirect(inv.CheckoutLink); + } + catch (Exception e) + { + } + + return RedirectToAction("View", new {storeId}); + } + + + [AllowAnonymous] + [HttpGet("connect")] + public async Task Connect(string storeId, string invoiceId) + { + var btcpayClient = await CreateClient(storeId); + try + { + var config = await _LSPService.GetLSPForStore(storeId); + var result = new LSPConnectPage() {InvoiceId = invoiceId, Settings = config}; + var invoice = await btcpayClient.GetInvoice(storeId, invoiceId); + result.Status = invoice.Status; + if (invoice.Status != InvoiceStatus.Settled) return View(result); + if (invoice.Metadata.TryGetValue("lsp-channel-complete", out _)) + { + return Redirect(invoice.CheckoutLink); + } + + + result.Invoice = invoice; + result.LNURL = LNURL.LNURL.EncodeUri(new Uri(Request.GetAbsoluteUri(Url.Action( + "LNURLChannelRequest", + "LSP", new {storeId, invoiceId}))), "channelRequest", true).ToString(); + + return View(result); + } + catch (Exception e) + { + return NotFound(); + } + } + + private async Task CreateClient(string storeId) + { + return await _btcPayServerClientFactory.Create(null, new[] {storeId}, + new DefaultHttpContext() + { + Request = + { + Scheme = "https", Host = Request.Host, Path = Request.Path, PathBase = Request.PathBase + } + }); + } + + public class LSPConnectPage + { + public string LNURL; + public string InvoiceId { get; set; } + public InvoiceStatus Status { get; set; } + public LSPSettings Settings { get; set; } + public InvoiceData Invoice { get; set; } + } + + private readonly LSPService _LSPService; + private readonly IBTCPayServerClientFactory _btcPayServerClientFactory; + + public LSPController(IHttpClientFactory httpClientFactory, + LSPService LSPService, + IBTCPayServerClientFactory btcPayServerClientFactory) + { + _LSPService = LSPService; + _btcPayServerClientFactory = btcPayServerClientFactory; + } + + [HttpGet("update")] + public async Task UpdateLSPSettings(string storeId) + { + LSPSettings vm = null; + try + { + vm = await _LSPService.GetLSPForStore(storeId); + } + catch (Exception) + { + // ignored + } + + vm ??= new(); + + return View(vm); + } + + [HttpPost("update")] + public async Task UpdateLSPSettings(string storeId, + LSPSettings vm, + string command) + { + if (!ModelState.IsValid) + { + return View(vm); + } + + + switch (command?.ToLowerInvariant()) + { + case "save": + await _LSPService.SetLSPForStore(storeId, vm); + TempData["SuccessMessage"] = "LSP settings modified"; + return RedirectToAction(nameof(UpdateLSPSettings), new {storeId}); + + default: + return View(vm); + } + } + + + [AllowAnonymous] + [HttpGet("lnurlc-callback")] + public async Task LNURLChannelRequestCallback(string storeId, string k1, string remoteId) + { + if (!NodeInfo.TryParse(remoteId, out var remoteNode)) + { + return BadRequest(); + } + var btcPayClient = await CreateClient(storeId); + var invoice = await btcPayClient.GetInvoice(storeId, k1); + if (invoice?.Status != InvoiceStatus.Settled || invoice.Metadata.TryGetValue("lsp-channel-complete", out _)) + { + return NotFound(); + } + var settings = await _LSPService.GetLSPForStore(storeId); + if (settings?.Enabled is not true) + { + return BadRequest(); + } + if (!invoice.Metadata.TryGetValue("posData", out var posData)) + { + posData = JToken.Parse("{}"); + } + + var inbound = invoice.Metadata["inbound"].Value(); + try + { + await btcPayClient.ConnectToLightningNode(storeId, "BTC", new ConnectToNodeRequest(remoteNode)); + posData["LSP"] = JToken.FromObject(new Dictionary()); + posData["LSP"]["Remote Node"] = remoteId; + await btcPayClient.OpenLightningChannel(storeId, "BTC", new OpenLightningChannelRequest() + { + ChannelAmount = new Money(inbound, MoneyUnit.Satoshi), + + NodeURI = remoteNode + }); + posData["LSP"]["Channel Status"] = "Opened"; + invoice.Metadata["posData"] = posData; + invoice.Metadata["lsp-channel-complete"] = true; + await btcPayClient.UpdateInvoice(storeId, invoice.Id, + new UpdateInvoiceRequest() {Metadata = invoice.Metadata}); + return Ok(new LNURL.LNUrlStatusResponse() + { + Status = "OK" + }); + } + catch (Exception e) + { + posData["Error"] = + $"Channel could not be created. You should refund customer.{Environment.NewLine}{e.Message}"; + invoice.Metadata["posData"] = posData; + await btcPayClient.UpdateInvoice(storeId, invoice.Id, + new UpdateInvoiceRequest() {Metadata = invoice.Metadata}); + return Ok(new LNURL.LNUrlStatusResponse() + { + Status = "ERROR", Reason = $"Channel could not be created to {remoteId}" + }); + + } + } + + [AllowAnonymous] + [HttpGet("{invoiceId}/lnurlc")] + public async Task LNURLChannelRequest(string storeId, string invoiceId, string nodeUri) + { + var btcPayClient = await CreateClient(storeId); + var invoice = await btcPayClient.GetInvoice(storeId, invoiceId); + if (invoice?.Status != InvoiceStatus.Settled || invoice.Metadata.TryGetValue("lsp-channel-complete", out _)) + { + return NotFound(); + } + var settings = await _LSPService.GetLSPForStore(storeId); + if (settings?.Enabled is not true) + { + return BadRequest(); + } + return Ok(new LNURL.LNURLChannelRequest() + { + Tag = "channelRequest", + K1 = invoiceId, + Callback = new Uri(Request.GetAbsoluteUri(Url.Action("LNURLChannelRequestCallback", + "LSP", new {storeId}))), + Uri = nodeUri is null + ? (await btcPayClient.GetLightningNodeInfo(storeId, "BTC")).NodeURIs + .OrderBy(nodeInfo => nodeInfo.IsTor).First() + : NodeInfo.Parse(nodeUri) + }); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.LSP/LSPPlugin.cs b/Plugins/BTCPayServer.Plugins.LSP/LSPPlugin.cs new file mode 100644 index 0000000..7d49164 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LSP/LSPPlugin.cs @@ -0,0 +1,31 @@ +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Plugins.LSP +{ + public class LSPPlugin : BaseBTCPayServerPlugin + { + public override string Identifier => "BTCPayServer.Plugins.LSP"; + public override string Name => "LSP"; + + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new() {Identifier = nameof(BTCPayServer), Condition = ">=1.6.0.0"} + }; + + public override string Description => + "Allows you to become an LSP selling lightning channels with inbound liquidity"; + + public override void Execute(IServiceCollection applicationBuilder) + { + applicationBuilder.AddSingleton(); + applicationBuilder.AddSingleton(new UIExtension("LSP/StoreIntegrationLSPOption", + "store-integrations-list")); + applicationBuilder.AddSingleton(new UIExtension("LSP/LSPNav", + "store-integrations-nav")); + base.Execute(applicationBuilder); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.LSP/LSPService.cs b/Plugins/BTCPayServer.Plugins.LSP/LSPService.cs new file mode 100644 index 0000000..8467cb0 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LSP/LSPService.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using Microsoft.Extensions.Caching.Memory; + +namespace BTCPayServer.Plugins.LSP; + +public class LSPService +{ + private readonly IMemoryCache _memoryCache; + private readonly IStoreRepository _storeRepository; + + public LSPService(IMemoryCache memoryCache, + IStoreRepository storeRepository) + { + _memoryCache = memoryCache; + _storeRepository = storeRepository; + } + + + public async Task GetLSPForStore(string storeId) + { + var k = $"{nameof(LSPSettings)}_{storeId}"; + return await _memoryCache.GetOrCreateAsync(k, async _ => + { + var res = await _storeRepository.GetSettingAsync(storeId, + nameof(LSPSettings)); + return res; + }); + } + + public async Task SetLSPForStore(string storeId, LSPSettings lspSettings) + { + var k = $"{nameof(LSPSettings)}_{storeId}"; + await _storeRepository.UpdateSetting(storeId, nameof(LSPSettings), lspSettings); + _memoryCache.Set(k, lspSettings); + } +} diff --git a/Plugins/BTCPayServer.Plugins.LSP/Pack.ps1 b/Plugins/BTCPayServer.Plugins.LSP/Pack.ps1 new file mode 100644 index 0000000..2b22d27 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LSP/Pack.ps1 @@ -0,0 +1,2 @@ +dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.LSP +dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.LSP BTCPayServer.Plugins.LSP ../packed diff --git a/Plugins/BTCPayServer.Plugins.LSP/UpdateLSPViewModel.cs b/Plugins/BTCPayServer.Plugins.LSP/UpdateLSPViewModel.cs new file mode 100644 index 0000000..c8455ff --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LSP/UpdateLSPViewModel.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace BTCPayServer.Plugins.LSP; + +public class LSPSettings +{ + public bool Enabled { get; set; } = true; + public long Minimum { get; set; } = 100000; + public long Maximum { get; set; } = 10000000; + public decimal FeePerSat { get; set; } = 0.01m; + public long BaseFee { get; set; } = 0; + public string CustomCSS { get; set; } + public string Title { get; set; } = "Lightning Liquidity Peddler"; + public string Description { get; set; } = "

    Get an inbound channel

    This will open a public channel to your node.

    "; +} + +public class LSPViewModel +{ + public LSPSettings Settings { get; set; } +} diff --git a/Plugins/BTCPayServer.Plugins.LSP/Views/LSP/Connect.cshtml b/Plugins/BTCPayServer.Plugins.LSP/Views/LSP/Connect.cshtml new file mode 100644 index 0000000..b1db246 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LSP/Views/LSP/Connect.cshtml @@ -0,0 +1,73 @@ +@using BTCPayServer.Client.Models +@model BTCPayServer.Plugins.LSP.LSPController.LSPConnectPage +@inject ContentSecurityPolicies contentSecurityPolicies +@using BTCPayServer.Security +@using NBitcoin +@{ + var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32); + contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'"); + contentSecurityPolicies.AllowUnsafeHashes(); + Layout = "_LayoutSimple"; + var reloadPage = false; +} + + +
    +
    + +
    + +

    Thank you!

    +
    + @if (Model.Status == InvoiceStatus.Processing) + { + reloadPage = true; +
    + The invoice has detected a payment but is still waiting to be settled. This page will refresh periodically until it is settled. +
    + } + else if (Model.Status != InvoiceStatus.Settled) + { +
    + The invoice is not settled. +
    + } + else + { + Model.Invoice.Metadata.TryGetValue("inbound", out var inbound); +
    +
    + @await Component.InvokeAsync("QRCode", new {data = Model.LNURL.ToUpperInvariant()}) +
    + +

    Scan this QR with your wallet to proceed with opening the channel.

    + Open in wallet +

    Opening a channel of at least @inbound.ToString() sats.

    +
    + } +
    +
    + Powered by BTCPay Server +
    +
    +
    +
    + +@if (reloadPage) +{ + +} diff --git a/Plugins/BTCPayServer.Plugins.LSP/Views/LSP/UpdateLSPSettings.cshtml b/Plugins/BTCPayServer.Plugins.LSP/Views/LSP/UpdateLSPSettings.cshtml new file mode 100644 index 0000000..d13b797 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LSP/Views/LSP/UpdateLSPSettings.cshtml @@ -0,0 +1,78 @@ +@using BTCPayServer.Abstractions.Extensions +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using BTCPayServer.Abstractions.Contracts +@model BTCPayServer.Plugins.LSP.LSPSettings +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + Layout = "../Shared/_NavLayout.cshtml"; + ViewData["NavPartialName"] = "../UIStores/_Nav"; + ViewData.SetActivePage("LSP", "Update Store LSP Settings", null); +} + +

    @ViewData["Title"]

    +
    +
    +
    +
    +
    + + + +
    +
    + + + +
    + +
    + + + +
    +
    + + + +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    +
    + + @if (this.ViewContext.ModelState.IsValid && Model.Enabled) + { + + Purchase page + + } +
    +
    +
    +
    + +@section PageFootContent { + + + +} diff --git a/Plugins/BTCPayServer.Plugins.LSP/Views/LSP/View.cshtml b/Plugins/BTCPayServer.Plugins.LSP/Views/LSP/View.cshtml new file mode 100644 index 0000000..da04647 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LSP/Views/LSP/View.cshtml @@ -0,0 +1,88 @@ +@using Microsoft.AspNetCore.Routing +@using BTCPayServer.Plugins.LSP +@model BTCPayServer.Plugins.LSP.LSPViewModel +@inject ContentSecurityPolicies contentSecurityPolicies +@inject IScopeProvider ScopeProvider +@using BTCPayServer.Security +@using NBitcoin +@using BTCPayServer.Abstractions.Contracts +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32); + contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'"); + contentSecurityPolicies.AllowUnsafeHashes(); + Layout = "_LayoutSimple"; + +} + + + +
    +
    + + +

    @Model.Settings.Title

    + @if (!string.IsNullOrEmpty(Model.Settings.Description)) + { +
    +
    @Safe.Raw(Model.Settings.Description)
    +
    + } +
    +
    + +
    +
    + + +
    +
    +
    +
    + + +
    +
    + +
    +

    Base fee: @Model.Settings.BaseFee sats, fee per inbound sat: @Model.Settings.FeePerSat sats

    +

    +
    +
    + +
    +
    +
    + + +
    +
    + Powered by BTCPay Server +
    +
    +
    +
    diff --git a/Plugins/BTCPayServer.Plugins.LSP/Views/Shared/LSP/LSPNav.cshtml b/Plugins/BTCPayServer.Plugins.LSP/Views/Shared/LSP/LSPNav.cshtml new file mode 100644 index 0000000..7edfce4 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LSP/Views/Shared/LSP/LSPNav.cshtml @@ -0,0 +1,19 @@ +@using BTCPayServer.Plugins.LSP +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using BTCPayServer.Abstractions.Contracts +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null && + nameof(LSPController).StartsWith(controller?.ToString(), StringComparison.InvariantCultureIgnoreCase); +} +@if (!string.IsNullOrEmpty(storeId)) +{ + +} diff --git a/Plugins/BTCPayServer.Plugins.LSP/Views/Shared/LSP/StoreIntegrationLSPOption.cshtml b/Plugins/BTCPayServer.Plugins.LSP/Views/Shared/LSP/StoreIntegrationLSPOption.cshtml new file mode 100644 index 0000000..f245538 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LSP/Views/Shared/LSP/StoreIntegrationLSPOption.cshtml @@ -0,0 +1,61 @@ +@using BTCPayServer.Client +@using BTCPayServer.Plugins.LSP +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using Microsoft.AspNetCore.Routing +@using BTCPayServer.Abstractions.Contracts +@inject BTCPayServerClient BTCPayServerClient +@inject LSPService LSPService +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + + LSPSettings settings = null; + if (!string.IsNullOrEmpty(storeId)) + { + try + { + settings = await LSPService.GetLSPForStore(storeId); + } + catch (Exception) + { + } + } +} +@if (!string.IsNullOrEmpty(storeId)) +{ +
  • +
    + + + LSP + + + Sell lightning channel inbound liquidity using BTCPay Server + + + + @if (settings?.Enabled is true) + { + + + Active + + | + + Modify + + } + else + { + + + Disabled + + + Setup + + } + +
    +
  • +} diff --git a/Plugins/BTCPayServer.Plugins.LSP/Views/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.LSP/Views/_ViewImports.cshtml new file mode 100644 index 0000000..7ece6c4 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LSP/Views/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using BTCPayServer.Abstractions.Services +@using BTCPayServer.Abstractions.Extensions +@inject Safe Safe +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/BTCPayServer.Plugins.LiquidPlus.csproj b/Plugins/BTCPayServer.Plugins.LiquidPlus/BTCPayServer.Plugins.LiquidPlus.csproj new file mode 100644 index 0000000..207b665 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/BTCPayServer.Plugins.LiquidPlus.csproj @@ -0,0 +1,39 @@ + + + net6.0 + 10 + + + + + "Liquid+ + Enhanced support for the liquid network. + Kukks + 1.0.8 + + + + + true + false + true + + + + + + StaticWebAssetsEnabled=false + false + runtime;native;build;buildTransitive;contentFiles + + + + + + + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/Controllers/CustomLiquidAssetsController.cs b/Plugins/BTCPayServer.Plugins.LiquidPlus/Controllers/CustomLiquidAssetsController.cs new file mode 100644 index 0000000..7913e2f --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/Controllers/CustomLiquidAssetsController.cs @@ -0,0 +1,114 @@ +using System; +using System.Globalization; +using System.Net.Http; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Plugins.LiquidPlus.Models; +using BTCPayServer.Plugins.LiquidPlus.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Plugins.LiquidPlus.Controllers +{ + [Route("plugins/liquid/admin-settings")] + [Authorize(Policy = BTCPayServer.Client.Policies.CanModifyServerSettings, + AuthenticationSchemes = AuthenticationSchemes.Cookie)] + public class CustomLiquidAssetsController : Controller + { + private readonly CustomLiquidAssetsRepository _liquidAssetsRepository; + private readonly IHttpClientFactory _httpClientFactory; + + public CustomLiquidAssetsController(CustomLiquidAssetsRepository liquidAssetsRepository, + IHttpClientFactory httpClientFactory) + { + _liquidAssetsRepository = liquidAssetsRepository; + _httpClientFactory = httpClientFactory; + } + + [HttpGet("")] + public IActionResult Assets() + { + return View(new CustomLiquidAssetsViewModel() + { + Items = (_liquidAssetsRepository.Get()).Items, + PendingChanges = _liquidAssetsRepository.ChangesPending + }); + } + + [HttpPost("")] + public async Task Assets(CustomLiquidAssetsViewModel model, string command = null, + string import = null) + { + if (command == "add") + { + ModelState.Clear(); + model.Items.Add(new CustomLiquidAssetsSettings.LiquidAssetConfiguration()); + return View(model); + } + + if (command?.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase) is true) + { + ModelState.Clear(); + var index = int.Parse( + command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), + CultureInfo.InvariantCulture); + model.Items.RemoveAt(index); + return View(model); + } + + if (import != null) + { + try + { + if (!uint256.TryParse(import, out var _)) + { + TempData["ErrorMessage"] = + "Asset Id to import was invalid."; + return View(model); + } + + var data = JObject.Parse(await _httpClientFactory.CreateClient() + .GetStringAsync($"https://blockstream.info/liquid/api/asset/{import}")); + + model.Items.Add(new CustomLiquidAssetsSettings.LiquidAssetConfiguration() + { + DisplayName = data["name"].Value(), + Divisibility = data["precision"].Value(), + AssetId = data["asset_id"].Value(), + CryptoCode = data["ticker"].Value().Replace("-", "").Replace("_", "") + }); + } + catch (Exception) + { + TempData["ErrorMessage"] = + "Asset Id to import was invalid."; + return View(model); + } + } + + for (int i = 0; i < model.Items.Count; i++) + { + if (!string.IsNullOrEmpty(model.Items[i].AssetId) && + !uint256.TryParse(model.Items[i].AssetId, out var x)) + { + var inputName = + string.Format(CultureInfo.InvariantCulture, "Items[{0}].", + i.ToString(CultureInfo.InvariantCulture)) + + nameof(CustomLiquidAssetsSettings.LiquidAssetConfiguration.AssetId); + + ModelState.AddModelError(inputName, "Invalid asset id format"); + } + } + + if (!ModelState.IsValid) + { + return View(model); + } + + await _liquidAssetsRepository.Set(model); + return RedirectToAction(nameof(Assets)); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/Controllers/StoreLiquidController.cs b/Plugins/BTCPayServer.Plugins.LiquidPlus/Controllers/StoreLiquidController.cs new file mode 100644 index 0000000..cac0edd --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/Controllers/StoreLiquidController.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Client; +using BTCPayServer.Common; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using NBXplorer; + +namespace BTCPayServer.Plugins.LiquidPlus.Controllers +{ + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [AutoValidateAntiforgeryToken] + public class StoreLiquidController : Controller + { + private readonly BTCPayNetworkProvider _btcPayNetworkProvider; + private readonly BTCPayServerClient _client; + private readonly IExplorerClientProvider _explorerClientProvider; + + public StoreLiquidController(BTCPayNetworkProvider btcPayNetworkProvider, + BTCPayServerClient client, IExplorerClientProvider explorerClientProvider) + { + _btcPayNetworkProvider = btcPayNetworkProvider; + _client = client; + _explorerClientProvider = explorerClientProvider; + } + + [HttpGet("stores/{storeId}/liquid")] + public async Task GenerateLiquidScript(string storeId, Dictionary bitcoinExtKeys = null) + { + Dictionary generated = new Dictionary(); + var allNetworks = _btcPayNetworkProvider.GetAll().OfType() + .GroupBy(network => network.NetworkCryptoCode); + var allNetworkCodes = allNetworks + .SelectMany(networks => networks.Select(network => network.CryptoCode.ToUpperInvariant())) + .ToArray() + .Distinct(); + Dictionary privKeys = bitcoinExtKeys ?? new Dictionary(); + + + var paymentMethods = (await _client.GetStoreOnChainPaymentMethods(storeId)) + .Where(settings => allNetworkCodes.Contains(settings.CryptoCode)) + .GroupBy(data => _btcPayNetworkProvider.GetNetwork(data.CryptoCode).NetworkCryptoCode); + + if (paymentMethods.Any() is false) + { + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Info, + Message = "There are no wallets configured that use Liquid or an elements side-chain." + }); + return View(new GenerateLiquidImportScripts()); + } + + foreach (var der in paymentMethods) + { + var network = _btcPayNetworkProvider.GetNetwork(der.Key); + var nbxnet = network.NBXplorerNetwork; + + var sb = new StringBuilder(); + + var explorerClient = _explorerClientProvider.GetExplorerClient(der.Key); + var status = await explorerClient.GetStatusAsync(); + if (status.BitcoinStatus is null) + { + sb.AppendLine($"{der.Key} node is not available. Try again later."); + generated.Add(der.Key, sb.ToString()); + continue; + } + var derivationSchemesForNetwork = der.GroupBy(data => data.DerivationScheme); + + foreach (var paymentMethodDerivationScheme in derivationSchemesForNetwork) + { + var derivatonScheme = + nbxnet.DerivationStrategyFactory.Parse(paymentMethodDerivationScheme.Key); + var sameWalletCryptoCodes = paymentMethodDerivationScheme.Select(data => data.CryptoCode).ToArray(); + var matchedExistingKey = privKeys.Where(pair => sameWalletCryptoCodes.Contains(pair.Key)); + BitcoinExtKey key = null; + if (matchedExistingKey.Any()) + { + key = matchedExistingKey.First().Value; + } + else + { + + key = await explorerClient.GetMetadataAsync(derivatonScheme, + WellknownMetadataKeys.AccountHDKey); + } + + if (key != null) + { + + foreach (var paymentMethodData in paymentMethodDerivationScheme) + { + privKeys.TryAdd(paymentMethodData.CryptoCode, key); + } + } + + var utxos = await explorerClient.GetUTXOsAsync(derivatonScheme, CancellationToken.None); + + foreach (var utxo in utxos.GetUnspentUTXOs()) + { + var addr = nbxnet.CreateAddress(derivatonScheme, utxo.KeyPath, utxo.ScriptPubKey); + + if (key is null) + { + sb.AppendLine( + $"elements-cli importaddress \"{addr}\" \"{utxo.KeyPath} from {derivatonScheme}\" false"); + } + else + { + sb.AppendLine( + $"elements-cli importprivkey \"{key.Derive(utxo.KeyPath).PrivateKey.GetWif(nbxnet.NBitcoinNetwork)}\" \"{utxo.KeyPath} from {derivatonScheme}\" false"); + } + + if (!derivatonScheme.Unblinded()) + { + var blindingKey = + NBXplorerNetworkProvider.LiquidNBXplorerNetwork.GenerateBlindingKey( + derivatonScheme, utxo.KeyPath, utxo.ScriptPubKey, nbxnet.NBitcoinNetwork); + sb.AppendLine($"elements-cli importblindingkey {addr} {blindingKey.ToHex()}"); + } + } + } + + if (sb.Length > 0) + { + sb.AppendLine("elements-cli stop"); + sb.AppendLine("elementsd -rescan"); + + } + generated.Add(der.Key, sb.ToString()); + } + + return View(new GenerateLiquidImportScripts() + { + Wallets = paymentMethods.SelectMany(settings => + settings.Select(data => + new GenerateLiquidImportScripts.GenerateLiquidImportScriptWalletKeyVm() + { + CryptoCode = data.CryptoCode, + KeyPresent = privKeys.ContainsKey(data.CryptoCode), + ManualKey = null + }).ToArray()).ToArray(), + Scripts = generated + }); + } + + + [HttpPost("stores/{storeId}/liquid")] + public async Task GenerateLiquidScript(string storeId, GenerateLiquidImportScripts vm) + { + Dictionary privKeys = new Dictionary(); + for (var index = 0; index < vm.Wallets.Length; index++) + { + var wallet = vm.Wallets[index]; + if (string.IsNullOrEmpty(wallet.ManualKey)) + continue; + + var n = + _btcPayNetworkProvider.GetNetwork(wallet.CryptoCode); + ExtKey extKey = null; + try + { + var mnemonic = new Mnemonic(wallet.ManualKey); + extKey = mnemonic.DeriveExtKey(); + } + catch (Exception) + { + } + + if (extKey == null) + { + try + { + extKey = ExtKey.Parse(wallet.ManualKey, n.NBitcoinNetwork); + } + catch (Exception) + { + } + } + + if (extKey == null) + { + vm.AddModelError(scripts => scripts.Wallets[index].ManualKey, + "Invalid key (must be seed or root xprv or account xprv)", this); + continue; + } + + + + var der = n.NBXplorerNetwork.DerivationStrategyFactory.Parse( + (await _client.GetStoreOnChainPaymentMethod(storeId, wallet.CryptoCode)).DerivationScheme); + if (der.GetExtPubKeys().Count() > 1) + { + vm.AddModelError(scripts => scripts.Wallets[index].ManualKey, "cannot handle multsig", this); + continue; + } + + var first = der + .GetExtPubKeys().First(); + if (first != extKey.Neuter()) + { + KeyPath kp = null; + switch (der.ScriptPubKeyType()) + { + case ScriptPubKeyType.Legacy: + kp = new KeyPath($"m/44'/{n.CoinType}/0'"); + break; + case ScriptPubKeyType.Segwit: + + kp = new KeyPath($"m/84'/{n.CoinType}/0'"); + break; + case ScriptPubKeyType.SegwitP2SH: + kp = new KeyPath($"m/49'/{n.CoinType}/0'"); + break; + default: + vm.AddModelError(scripts => scripts.Wallets[index].ManualKey, "cannot handle wallet type", + this); + continue; + } + + extKey = extKey.Derive(kp); + if (first != extKey.Neuter()) + { + vm.AddModelError(scripts => scripts.Wallets[index].ManualKey, "key did not match", this); + continue; + } + } + + privKeys.TryAdd(wallet.CryptoCode, extKey.GetWif(n.NBitcoinNetwork)); + } + + if (!ModelState.IsValid) + { + return View(vm); + } + + return await GenerateLiquidScript(storeId, privKeys); + } +public class GenerateLiquidImportScripts + { + public class GenerateLiquidImportScriptWalletKeyVm + { + public string CryptoCode { get; set; } + public bool KeyPresent { get; set; } + public string ManualKey { get; set; } + } + + public GenerateLiquidImportScriptWalletKeyVm[] Wallets { get; set; } = + Array.Empty(); + + public Dictionary Scripts { get; set; } = new Dictionary(); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/LiquidPlusPlugin.cs b/Plugins/BTCPayServer.Plugins.LiquidPlus/LiquidPlusPlugin.cs new file mode 100644 index 0000000..01b0921 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/LiquidPlusPlugin.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using BTCPayServer.Plugins.LiquidPlus.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using NBitcoin; + +namespace BTCPayServer.Plugins.LiquidPlus +{ + public class LiquidPlusPlugin : BaseBTCPayServerPlugin + { + public override string Identifier { get; } = "BTCPayServer.Plugins.LiquidPlus"; + public override string Name { get; } = "Liquid+"; + + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new IBTCPayServerPlugin.PluginDependency() { Identifier = nameof(BTCPayServer), Condition = ">=1.7.0.0" } + }; + public override string Description { get; } = "Enhanced support for the liquid network."; + + public override void Execute(IServiceCollection services) + { + services.AddSingleton(new UIExtension("LiquidNav", "store-integrations-nav")); + services.AddSingleton(new UIExtension("CustomLiquidAssetsNavExtension", "server-nav")); + services.AddSingleton(new UIExtension("StoreNavLiquidExtension", "store-nav")); + services.AddSingleton(); + + var originalImplementationFactory = services.Single(descriptor => + descriptor.Lifetime == ServiceLifetime.Singleton && + descriptor.ServiceType == typeof(BTCPayNetworkProvider)); + services.Replace(ServiceDescriptor.Singleton(provider => + { + var _customLiquidAssetsRepository = provider.GetService(); + var _logger = provider.GetService>(); + var networkProvider = + (originalImplementationFactory.ImplementationInstance ?? + originalImplementationFactory.ImplementationFactory.Invoke(provider)) as BTCPayNetworkProvider; + if (networkProvider.Support("LBTC")) + { + var settings = _customLiquidAssetsRepository.Get(); + var template = networkProvider.GetNetwork("LBTC"); + var additionalNetworks = settings.Items.Select(configuration => new ElementsBTCPayNetwork() + { + CryptoCode = configuration.CryptoCode + .Replace("-", "") + .Replace("_", ""), + DefaultRateRules = configuration.DefaultRateRules ?? Array.Empty(), + AssetId = uint256.Parse(configuration.AssetId), + Divisibility = configuration.Divisibility, + DisplayName = configuration.DisplayName, + CryptoImagePath = configuration.CryptoImagePath, + NetworkCryptoCode = template.NetworkCryptoCode, + DefaultSettings = template.DefaultSettings, + ElectrumMapping = template.ElectrumMapping, + BlockExplorerLink = template.BlockExplorerLink, + ReadonlyWallet = template.ReadonlyWallet, + SupportLightning = false, + SupportPayJoin = false, + ShowSyncSummary = false, + WalletSupported = template.WalletSupported, + LightningImagePath = "", + NBXplorerNetwork = template.NBXplorerNetwork, + CoinType = template.CoinType, + VaultSupported = template.VaultSupported, + MaxTrackedConfirmation = template.MaxTrackedConfirmation, + BlockExplorerLinkDefault = template.BlockExplorerLinkDefault, + SupportRBF = template.SupportRBF + }); + var newCryptoCodes = settings.Items.Select(configuration => configuration.CryptoCode).ToArray(); + _logger.LogInformation($"Loaded {newCryptoCodes.Length} " + + $"{(!newCryptoCodes.Any()?string.Empty: $"({string.Join(',', newCryptoCodes)})")} additional liquid assets"); + var newSupportedChains = networkProvider.GetAll().Select(b => b.CryptoCode).Concat(newCryptoCodes).ToArray(); + return new BTCPayNetworkProviderOverride(networkProvider.NetworkType, additionalNetworks).Filter(newSupportedChains); + } + + return networkProvider; + })); + } + } + + public class BTCPayNetworkProviderOverride : BTCPayNetworkProvider + { + public BTCPayNetworkProviderOverride(ChainName networkType, + IEnumerable elementsBTCPayNetworks) : base(networkType) + { + foreach (ElementsBTCPayNetwork elementsBTCPayNetwork in elementsBTCPayNetworks) + { + _Networks.TryAdd(elementsBTCPayNetwork.CryptoCode.ToUpperInvariant(), elementsBTCPayNetwork); + } + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/Models/CustomLiquidAssetsSettings.cs b/Plugins/BTCPayServer.Plugins.LiquidPlus/Models/CustomLiquidAssetsSettings.cs new file mode 100644 index 0000000..fd17ddd --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/Models/CustomLiquidAssetsSettings.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace BTCPayServer.Plugins.LiquidPlus.Models +{ + public class CustomLiquidAssetsSettings + { + public List Items { get; set; } = new List(); + + public class LiquidAssetConfiguration + { + [Required] public string AssetId { get; set; } + + [Range(0, double.PositiveInfinity)] public int Divisibility { get; set; } = 8; + + [Required] + [Display(Name = "Display name")] + public string DisplayName { get; set; } + + [Display(Name = "Checkout icon url")] public string CryptoImagePath { get; set; } + + [Required] + [Display(Name = "Currency code")] + public string CryptoCode { get; set; } + + public string[] DefaultRateRules { get; set; } + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/Models/CustomLiquidAssetsViewModel.cs b/Plugins/BTCPayServer.Plugins.LiquidPlus/Models/CustomLiquidAssetsViewModel.cs new file mode 100644 index 0000000..978db40 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/Models/CustomLiquidAssetsViewModel.cs @@ -0,0 +1,7 @@ +namespace BTCPayServer.Plugins.LiquidPlus.Models +{ + public class CustomLiquidAssetsViewModel: CustomLiquidAssetsSettings + { + public bool PendingChanges { get; set; } + } +} diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/Pack.ps1 b/Plugins/BTCPayServer.Plugins.LiquidPlus/Pack.ps1 new file mode 100644 index 0000000..8f67aa9 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/Pack.ps1 @@ -0,0 +1,2 @@ +dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.LiquidPlus +dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.LiquidPlus BTCPayServer.Plugins.LiquidPlus ../packed diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/Services/CustomLiquidAssetsRepository.cs b/Plugins/BTCPayServer.Plugins.LiquidPlus/Services/CustomLiquidAssetsRepository.cs new file mode 100644 index 0000000..67a096a --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/Services/CustomLiquidAssetsRepository.cs @@ -0,0 +1,60 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using BTCPayServer.Configuration; +using BTCPayServer.Plugins.LiquidPlus.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Plugins.LiquidPlus.Services +{ + public class CustomLiquidAssetsRepository + { + private readonly ILogger _logger; + private readonly IOptions _options; + private string File => Path.Combine(_options.Value.DataDir, "custom-liquid-assets.json"); + + public CustomLiquidAssetsRepository(ILogger logger, IOptions options) + { + _logger = logger; + _options = options; + } + + public CustomLiquidAssetsSettings Get() + { + try + { + if (System.IO.File.Exists(File)) + { + return JObject.Parse(System.IO.File.ReadAllText(File)).ToObject(); + } + } + + catch (Exception e) + { + _logger.LogError(e, "could not parse custom liquid assets file"); + } + + return new CustomLiquidAssetsSettings(); + } + + public async Task Set(CustomLiquidAssetsSettings settings) + { + try + { + await System.IO.File.WriteAllTextAsync(File, JObject.FromObject(settings).ToString(Formatting.Indented)); + + ChangesPending = true; + } + + catch (Exception e) + { + _logger.LogError(e, "could not write custom liquid assets file"); + } + } + + public bool ChangesPending { get; private set; } + } +} diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/CustomLiquidAssets/Assets.cshtml b/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/CustomLiquidAssets/Assets.cshtml new file mode 100644 index 0000000..ed8d296 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/CustomLiquidAssets/Assets.cshtml @@ -0,0 +1,79 @@ +@inject ContentSecurityPolicies contentSecurityPolicies +@using BTCPayServer.Security +@using NBitcoin +@model BTCPayServer.Plugins.LiquidPlus.Models.CustomLiquidAssetsViewModel +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewData["NavPartialName"] = "../UIServer/_Nav"; + ViewData["Title"] = "Custom Liquid Assets"; + var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32); + contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'"); +} +@if (Model.PendingChanges) +{ +
    There are saved changes to the custom liquid assets that have not yet been applied. Restart BTCPay Server to load these changes.
    +} + +
    +
    + @if (!Model.Items.Any()) + { +

    No custom assets set up

    + } + @for (var index = 0; index < Model.Items.Count; index++) + { +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    + +
    + } + +
    + +
    + + + + +
    + +
    diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/Shared/CustomLiquidAssetsNavExtension.cshtml b/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/Shared/CustomLiquidAssetsNavExtension.cshtml new file mode 100644 index 0000000..7a47cb8 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/Shared/CustomLiquidAssetsNavExtension.cshtml @@ -0,0 +1,7 @@ +@using BTCPayServer.Plugins.LiquidPlus.Controllers +@{ + var isActive = ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null && + nameof(CustomLiquidAssetsController).StartsWith(controller?.ToString(), StringComparison.InvariantCultureIgnoreCase); +} + +Liquid Assets diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/Shared/LiquidNav.cshtml b/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/Shared/LiquidNav.cshtml new file mode 100644 index 0000000..17c9e01 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/Shared/LiquidNav.cshtml @@ -0,0 +1,23 @@ +@using BTCPayServer +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Plugins.LiquidPlus.Controllers +@using Microsoft.AspNetCore.Routing + +@inject BTCPayNetworkProvider BTCPayNetworkProvider; + +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null && + nameof(StoreLiquidController).StartsWith(controller?.ToString(), StringComparison.InvariantCultureIgnoreCase); +} +@if (!string.IsNullOrEmpty(storeId) && (BTCPayNetworkProvider.GetAll().OfType().Any())) +{ + +} diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/Shared/StoreNavLiquidExtension.cshtml b/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/Shared/StoreNavLiquidExtension.cshtml new file mode 100644 index 0000000..3750656 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/Shared/StoreNavLiquidExtension.cshtml @@ -0,0 +1,15 @@ +@using BTCPayServer +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Plugins.LiquidPlus.Controllers +@using Microsoft.AspNetCore.Routing +@inject BTCPayNetworkProvider BTCPayNetworkProvider; +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null && + nameof(StoreLiquidController).StartsWith(controller?.ToString(), StringComparison.InvariantCultureIgnoreCase); +} +@if (BTCPayNetworkProvider.GetAll().OfType().Any()) +{ + Liquid +} diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/StoreLiquid/GenerateLiquidScript.cshtml b/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/StoreLiquid/GenerateLiquidScript.cshtml new file mode 100644 index 0000000..812da37 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/StoreLiquid/GenerateLiquidScript.cshtml @@ -0,0 +1,65 @@ + +@model BTCPayServer.Plugins.LiquidPlus.Controllers.StoreLiquidController.GenerateLiquidImportScripts +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewData["NavPartialName"] = "../UIStores/_Nav"; + ViewBag.MainTitle = "Store settings"; + ViewData["title"] = "Liquid Import"; +} + +

    Liquid import

    +
    +
    +

    Generates commands to import your received liquid funds into an elements node

    +
    +
    +@if (Model.Wallets.Any()) +{ +
    +
      + +
    • Wallets
    • + + @for (var index = 0; index < Model.Wallets.Length; index++) + { + var x = Model.Wallets[index]; + + +
    • +
      + @x.CryptoCode + @if (!x.KeyPresent) + { + + } + else + { + Keys already available + } + +
      + + +
    • + } + @if (!Model.Wallets.All(vm => vm.KeyPresent)) + { +
    • + +
    • + } +
    • Scripts (per chain)
    • + @foreach (var script in Model.Scripts) + { +
    • @script.Key
    • +
    • + @if (string.IsNullOrEmpty(script.Value)) + { + Nothing to generate + } +
      @Html.Raw(script.Value)
      +
    • + } +
    +
    +} diff --git a/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/_ViewImports.cshtml new file mode 100644 index 0000000..afa82bb --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.LiquidPlus/Views/_ViewImports.cshtml @@ -0,0 +1 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Plugins/BTCPayServer.Plugins.NFC/BTCPayServer.Plugins.NFC.csproj b/Plugins/BTCPayServer.Plugins.NFC/BTCPayServer.Plugins.NFC.csproj new file mode 100644 index 0000000..94c029f --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.NFC/BTCPayServer.Plugins.NFC.csproj @@ -0,0 +1,40 @@ + + + + net6.0 + 10 + + + + + LNURL NFC Support + Allows you to support contactless card payments over NFC and LNURL Withdraw! + Kukks + 1.0.8 + + + + + true + false + true + + + + + + StaticWebAssetsEnabled=false + false + runtime;native;build;buildTransitive;contentFiles + + + + + + + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.NFC/NFCController.cs b/Plugins/BTCPayServer.Plugins.NFC/NFCController.cs new file mode 100644 index 0000000..209b7da --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.NFC/NFCController.cs @@ -0,0 +1,99 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Client; +using LNURL; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NBitcoin; + +namespace BTCPayServer.Plugins.NFC +{ + [Route("plugins/NFC")] + public class NFCController : Controller + { + private readonly IHttpClientFactory _httpClientFactory; + + public NFCController(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + public class SubmitRequest + { + public string Lnurl { get; set; } + public string Destination { get; set; } + } + + [AllowAnonymous] + public async Task SubmitLNURLWithdrawForInvoice([FromBody] SubmitRequest request) + { + Uri uri; + string tag; + try + { + uri = LNURL.LNURL.Parse(request.Lnurl, out tag); + if (uri is null) + { + return BadRequest("lnurl was malformed"); + } + } + catch (Exception e) + { + + return BadRequest(e.Message); + } + + + if (!string.IsNullOrEmpty(tag) && !tag.Equals("withdrawRequest")) + { + return BadRequest("lnurl was not lnurl-withdraw"); + } + + var httpClient = _httpClientFactory.CreateClient(uri.IsOnion() + ? "LightningLikePayoutHandlerOnionNamedClient" + : "LightningLikePayoutHandlerClearnetNamedClient"); + var info = (await + LNURL.LNURL.FetchInformation(uri, "withdrawRequest", httpClient)) as LNURLWithdrawRequest; + if (info is null) + { + return BadRequest("Could not fetch info from lnurl-withdraw "); + } + + httpClient = _httpClientFactory.CreateClient(info.Callback.IsOnion() + ? "LightningLikePayoutHandlerOnionNamedClient" + : "LightningLikePayoutHandlerClearnetNamedClient"); + + try + { + var destinationuri = LNURL.LNURL.Parse(request.Destination, out string _); + + var destinfo = (await + LNURL.LNURL.FetchInformation(destinationuri, "payRequest", httpClient)) as LNURLPayRequest; + + if (destinfo is null) + { + return BadRequest("Could not fetch bolt11 invoice to pay to."); + } + + httpClient = _httpClientFactory.CreateClient(destinfo.Callback.IsOnion() + ? "LightningLikePayoutHandlerOnionNamedClient" + : "LightningLikePayoutHandlerClearnetNamedClient"); + var destCallback = await destinfo.SendRequest(destinfo.MinSendable, Network.Main, httpClient); + request.Destination = destCallback.Pr; + } + catch (Exception e) + { + } + + var result = await info.SendRequest(request.Destination, httpClient); + if (result.Status.Equals("ok", StringComparison.InvariantCultureIgnoreCase)) + { + return Ok(result.Reason); + } + + return BadRequest(result.Reason); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.NFC/NFCPlugin.cs b/Plugins/BTCPayServer.Plugins.NFC/NFCPlugin.cs new file mode 100644 index 0000000..85aabea --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.NFC/NFCPlugin.cs @@ -0,0 +1,31 @@ +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Plugins.FixedFloat +{ + public class NFCPlugin : BaseBTCPayServerPlugin + { + public override string Identifier => "BTCPayServer.Plugins.NFC"; + public override string Name => "LNURL NFC Support"; + + + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new() { Identifier = nameof(BTCPayServer), Condition = ">1.7.1.0" } + }; + + public override string Description => + "Allows you to support contactless card payments over NFC and LNURL Withdraw!"; + + public override void Execute(IServiceCollection applicationBuilder) + { + applicationBuilder.AddSingleton(new UIExtension("NFC/CheckoutEnd", + "checkout-end")); + applicationBuilder.AddSingleton(new UIExtension("NFC/LightningCheckoutPostContent", + "checkout-lightning-post-content")); + base.Execute(applicationBuilder); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.NFC/Pack.ps1 b/Plugins/BTCPayServer.Plugins.NFC/Pack.ps1 new file mode 100644 index 0000000..14449fb --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.NFC/Pack.ps1 @@ -0,0 +1,2 @@ +dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.NFC +dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.NFC BTCPayServer.Plugins.NFC ../packed diff --git a/Plugins/BTCPayServer.Plugins.NFC/Resources/js/lnurlwnfc.js b/Plugins/BTCPayServer.Plugins.NFC/Resources/js/lnurlwnfc.js new file mode 100644 index 0000000..1f981f5 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.NFC/Resources/js/lnurlwnfc.js @@ -0,0 +1,80 @@ +Vue.component("LNURLWithdrawContactless", { + data: function () { + return { + supported: ('NDEFReader' in window && window.self === window.top), + scanning: false, + submitting: false, + readerAbortController: null, + } + }, + methods: { + startScan: async function () { + try { + if (this.scanning || this.submitting) { + return; + } + const self = this; + self.submitting = false; + self.scanning = true; + if (!this.supported) { + const result = prompt("enter lnurl withdraw"); + if (result) { + self.sendData.bind(self)(result); + return; + } + self.scanning = false; + } + ndef = new NDEFReader() + self.readerAbortController = new AbortController() + await ndef.scan({signal: self.readerAbortController.signal}) + + ndef.addEventListener('readingerror', () => { + self.scanning = false; + self.readerAbortController.abort() + }) + + ndef.addEventListener('reading', ({message, serialNumber}) => { + //Decode NDEF data from tag + const record = message.records[0] + const textDecoder = new TextDecoder('utf-8') + const lnurl = textDecoder.decode(record.data) + + //User feedback, show loader icon + self.scanning = false; + self.sendData.bind(self)(lnurl); + + }) + } catch(e) { + self.scanning = false; + self.submitting = false; + } + }, + sendData: function (lnurl) { + + this.submitting = true; + //Post LNURLW data to server + var xhr = new XMLHttpRequest() + xhr.open('POST', window.lnurlWithdrawSubmitUrl, true) + xhr.setRequestHeader('Content-Type', 'application/json') + xhr.send(JSON.stringify({lnurl, destination: this.$parent.srvModel.btcAddress})) + const self = this; + //User feedback, reset on failure + xhr.onload = function () { + if (xhr.readyState === xhr.DONE) { + console.log(xhr.response); + console.log(xhr.responseText); + self.scanning = false; + self.submitting = false; + + if(self.readerAbortController) { + self.readerAbortController.abort() + } + + if(xhr.response){ + alert(xhr.response) + } + } + } + } + } +}); diff --git a/Plugins/BTCPayServer.Plugins.NFC/Views/Shared/NFC/CheckoutEnd.cshtml b/Plugins/BTCPayServer.Plugins.NFC/Views/Shared/NFC/CheckoutEnd.cshtml new file mode 100644 index 0000000..e01fd0f --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.NFC/Views/Shared/NFC/CheckoutEnd.cshtml @@ -0,0 +1,14 @@ +@inject ContentSecurityPolicies contentSecurityPolicies +@using BTCPayServer.Abstractions.Extensions +@using BTCPayServer.Security +@using NBitcoin +@{ + var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32); + contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'"); + var url = Context.Request.GetAbsoluteUri(Url.Action("SubmitLNURLWithdrawForInvoice", "NFC")); +} + + + diff --git a/Plugins/BTCPayServer.Plugins.NFC/Views/Shared/NFC/LightningCheckoutPostContent.cshtml b/Plugins/BTCPayServer.Plugins.NFC/Views/Shared/NFC/LightningCheckoutPostContent.cshtml new file mode 100644 index 0000000..1450249 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.NFC/Views/Shared/NFC/LightningCheckoutPostContent.cshtml @@ -0,0 +1,12 @@ + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.NFC/Views/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.NFC/Views/_ViewImports.cshtml new file mode 100644 index 0000000..afa82bb --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.NFC/Views/_ViewImports.cshtml @@ -0,0 +1 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Plugins/BTCPayServer.Plugins.RockstarStylist/BTCPayServer.Plugins.RockstarStylist.csproj b/Plugins/BTCPayServer.Plugins.RockstarStylist/BTCPayServer.Plugins.RockstarStylist.csproj new file mode 100644 index 0000000..63d8a3c --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.RockstarStylist/BTCPayServer.Plugins.RockstarStylist.csproj @@ -0,0 +1,18 @@ + + + net6.0 + true + false + true + 1.0.2 + + + + + + + + + <_ContentIncludedByDefault Remove="Views\TestExtension\Index.cshtml" /> + + diff --git a/Plugins/BTCPayServer.Plugins.RockstarStylist/Pack.ps1 b/Plugins/BTCPayServer.Plugins.RockstarStylist/Pack.ps1 new file mode 100644 index 0000000..306c0a0 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.RockstarStylist/Pack.ps1 @@ -0,0 +1,2 @@ +dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.RockstarStylist +dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.RockstarStylist BTCPayServer.Plugins.RockstarStylist ../packed diff --git a/Plugins/BTCPayServer.Plugins.RockstarStylist/RockstarStyleProvider.cs b/Plugins/BTCPayServer.Plugins.RockstarStylist/RockstarStyleProvider.cs new file mode 100644 index 0000000..beedf3a --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.RockstarStylist/RockstarStyleProvider.cs @@ -0,0 +1,37 @@ +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Plugins.RockstarStylist +{ + public class RockstarStyleProvider + { + private HttpClient _githubClient; + + public RockstarStyleProvider(IHttpClientFactory httpClientFactory) + { + _githubClient = httpClientFactory.CreateClient(); + _githubClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("btcpayserver", "1")); + } + + public async Task Get() + { + var response = JArray.Parse(await _githubClient.GetStringAsync("https://api.github.com/repos/btcpayserver/BTCPayThemes/contents")); + return response.Where(token => token.Value("type") == "dir").Select(token => new RockstarStyle() + { + StyleName = token.Value("name"), + CssUrl = $"https://btcpayserver.github.io/BTCPayThemes/{token.Value("name")}/btcpay-checkout.custom.css", + PreviewUrl = $"https://btcpayserver.github.io/BTCPayThemes/{token.Value("name")}" + }).ToArray(); + } + } + + public class RockstarStyle + { + public string StyleName { get; set; } + public string CssUrl { get; set; } + public string PreviewUrl { get; set; } + } +} diff --git a/Plugins/BTCPayServer.Plugins.RockstarStylist/RockstarStylistPlugin.cs b/Plugins/BTCPayServer.Plugins.RockstarStylist/RockstarStylistPlugin.cs new file mode 100644 index 0000000..0af3880 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.RockstarStylist/RockstarStylistPlugin.cs @@ -0,0 +1,27 @@ +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Plugins.RockstarStylist +{ + public class RockstarStylistPlugin : BaseBTCPayServerPlugin + { + public override string Identifier { get; } = "BTCPayServer.Plugins.RockstarStylist"; + public override string Name { get; } = "Rockstar hairstylist"; + public override string Description { get; } = "Allows your checkout to get a rockstar approved makeover"; + + public override void Execute(IServiceCollection services) + { + services.AddSingleton(new UIExtension("InvoiceCheckoutThemeOptions", + "invoice-checkout-theme-options")); + services.AddSingleton(); + } + + + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new IBTCPayServerPlugin.PluginDependency() { Identifier = nameof(BTCPayServer), Condition = ">=1.4.6.0" } + }; + } +} diff --git a/Plugins/BTCPayServer.Plugins.RockstarStylist/Views/Shared/InvoiceCheckoutThemeOptions.cshtml b/Plugins/BTCPayServer.Plugins.RockstarStylist/Views/Shared/InvoiceCheckoutThemeOptions.cshtml new file mode 100644 index 0000000..e536781 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.RockstarStylist/Views/Shared/InvoiceCheckoutThemeOptions.cshtml @@ -0,0 +1,26 @@ +@using BTCPayServer.Plugins.RockstarStylist +@using BTCPayServer.Security +@using NBitcoin +@inject ContentSecurityPolicies contentSecurityPolicies +@inject RockstarStyleProvider RockstarStyleProvider +@{ + var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32); + contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'"); + var themes = await RockstarStyleProvider.Get(); +} +@foreach (var theme in themes) +{ + +} + + diff --git a/Plugins/BTCPayServer.Plugins.RockstarStylist/Views/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.RockstarStylist/Views/_ViewImports.cshtml new file mode 100644 index 0000000..afa82bb --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.RockstarStylist/Views/_ViewImports.cshtml @@ -0,0 +1 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Plugins/BTCPayServer.Plugins.SideShift/BTCPayServer.Plugins.SideShift.csproj b/Plugins/BTCPayServer.Plugins.SideShift/BTCPayServer.Plugins.SideShift.csproj new file mode 100644 index 0000000..d9964c2 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/BTCPayServer.Plugins.SideShift.csproj @@ -0,0 +1,40 @@ + + + + net6.0 + 10 + + + + + SideShift + Allows you to embed a SideShift conversion screen to allow customers to pay with altcoins. + Kukks + 1.0.9 + + + + + true + false + true + + + + + + StaticWebAssetsEnabled=false + false + runtime;native;build;buildTransitive;contentFiles + + + + + + + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.SideShift/Pack.ps1 b/Plugins/BTCPayServer.Plugins.SideShift/Pack.ps1 new file mode 100644 index 0000000..04e0c96 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/Pack.ps1 @@ -0,0 +1,2 @@ +dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.SideShift +dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.SideShift BTCPayServer.Plugins.SideShift ../packed diff --git a/Plugins/BTCPayServer.Plugins.SideShift/Resources/assets/sideshift.svg b/Plugins/BTCPayServer.Plugins.SideShift/Resources/assets/sideshift.svg new file mode 100644 index 0000000..7ecaaaa --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/Resources/assets/sideshift.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.SideShift/Resources/js/sideShiftComponent.js b/Plugins/BTCPayServer.Plugins.SideShift/Resources/js/sideShiftComponent.js new file mode 100644 index 0000000..8eecf6e --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/Resources/js/sideShiftComponent.js @@ -0,0 +1,38 @@ +Vue.component("side-shift", { + props: ["toCurrency", "toCurrencyDue", "toCurrencyAddress"], + methods: { + openDialog: function (e) { + if (e && e.preventDefault) { + e.preventDefault(); + } + let settleMethodId = ""; + let amount = !this.$parent.srvModel.isUnsetTopUp + ? this.toCurrencyDue + : undefined; + if (this.toCurrency.toLowerCase() === "lbtc") { + settleMethodId = "liquid"; + } else if (this.toCurrency.toLowerCase() === "usdt") { + settleMethodId = "usdtla"; + } else if ( + this.toCurrency.endsWith("LightningLike") || + this.toCurrency.endsWith("LNURLPay") + ) { + settleMethodId = "ln"; + } else { + settleMethodId = this.toCurrency + .replace("_BTCLike", "") + .replace("_MoneroLike", "") + .replace("_ZcashLike", "") + .toLowerCase(); + } + window.__SIDESHIFT__ = { + parentAffiliateId: "qg0OrfHJV", + defaultSettleMethodId: settleMethodId, + settleAddress: this.toCurrencyAddress, + settleAmount: amount, + type: !this.$parent.srvModel.isUnsetTopUp ? "fixed" : undefined, + }; + window.sideshift.show(); + }, + }, +}); diff --git a/Plugins/BTCPayServer.Plugins.SideShift/SideShiftController.cs b/Plugins/BTCPayServer.Plugins.SideShift/SideShiftController.cs new file mode 100644 index 0000000..15bb439 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/SideShiftController.cs @@ -0,0 +1,82 @@ +using System; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Client; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Plugins.SideShift +{ + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Route("plugins/{storeId}/SideShift")] + public class SideShiftController : Controller + { + private readonly BTCPayServerClient _btcPayServerClient; + private readonly SideShiftService _sideShiftService; + + public SideShiftController(BTCPayServerClient btcPayServerClient, SideShiftService sideShiftService) + { + _btcPayServerClient = btcPayServerClient; + _sideShiftService = sideShiftService; + } + + [HttpGet("")] + public async Task UpdateSideShiftSettings(string storeId) + { + var store = await _btcPayServerClient.GetStore(storeId); + + UpdateSideShiftSettingsViewModel vm = new UpdateSideShiftSettingsViewModel(); + vm.StoreName = store.Name; + SideShiftSettings SideShift = null; + try + { + SideShift = await _sideShiftService.GetSideShiftForStore(storeId); + } + catch (Exception) + { + // ignored + } + + SetExistingValues(SideShift, vm); + return View(vm); + } + + private void SetExistingValues(SideShiftSettings existing, UpdateSideShiftSettingsViewModel vm) + { + if (existing == null) + return; + vm.Enabled = existing.Enabled; + } + + [HttpPost("")] + public async Task UpdateSideShiftSettings(string storeId, UpdateSideShiftSettingsViewModel vm, + string command) + { + if (vm.Enabled) + { + if (!ModelState.IsValid) + { + return View(vm); + } + } + + var sideShiftSettings = new SideShiftSettings() + { + Enabled = vm.Enabled, + }; + + switch (command) + { + case "save": + await _sideShiftService.SetSideShiftForStore(storeId, sideShiftSettings); + TempData["SuccessMessage"] = "SideShift settings modified"; + return RedirectToAction(nameof(UpdateSideShiftSettings), new {storeId}); + + default: + return View(vm); + } + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.SideShift/SideShiftPlugin.cs b/Plugins/BTCPayServer.Plugins.SideShift/SideShiftPlugin.cs new file mode 100644 index 0000000..3b23787 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/SideShiftPlugin.cs @@ -0,0 +1,48 @@ +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Plugins.SideShift +{ + public class SideShiftPlugin : BaseBTCPayServerPlugin + { + public override string Identifier => "BTCPayServer.Plugins.SideShift"; + public override string Name => "SideShift"; + + + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new IBTCPayServerPlugin.PluginDependency() { Identifier = nameof(BTCPayServer), Condition = ">=1.7.0.0" } + }; + + public override string Description => + "Allows you to embed a SideShift conversion screen to allow customers to pay with altcoins."; + + public override void Execute(IServiceCollection applicationBuilder) + { + applicationBuilder.AddSingleton(); + applicationBuilder.AddSingleton(new UIExtension("SideShift/SideShiftNav", + "store-integrations-nav")); + applicationBuilder.AddSingleton(new UIExtension("SideShift/StoreIntegrationSideShiftOption", + "store-integrations-list")); + // Checkout v2 + applicationBuilder.AddSingleton(new UIExtension("SideShift/CheckoutPaymentMethodExtension", + "checkout-payment-method")); + applicationBuilder.AddSingleton(new UIExtension("SideShift/CheckoutPaymentExtension", + "checkout-payment")); + // Checkout Classic + applicationBuilder.AddSingleton(new UIExtension("SideShift/CheckoutContentExtension", + "checkout-bitcoin-post-content")); + applicationBuilder.AddSingleton(new UIExtension("SideShift/CheckoutContentExtension", + "checkout-lightning-post-content")); + applicationBuilder.AddSingleton(new UIExtension("SideShift/CheckoutTabExtension", + "checkout-bitcoin-post-tabs")); + applicationBuilder.AddSingleton(new UIExtension("SideShift/CheckoutTabExtension", + "checkout-lightning-post-tabs")); + applicationBuilder.AddSingleton(new UIExtension("SideShift/CheckoutEnd", + "checkout-end")); + base.Execute(applicationBuilder); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.SideShift/SideShiftService.cs b/Plugins/BTCPayServer.Plugins.SideShift/SideShiftService.cs new file mode 100644 index 0000000..668afaa --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/SideShiftService.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Client; +using Microsoft.Extensions.Caching.Memory; + +namespace BTCPayServer.Plugins.SideShift +{ + public class SideShiftService + { + private readonly ISettingsRepository _settingsRepository; + private readonly IMemoryCache _memoryCache; + private readonly IStoreRepository _storeRepository; + + public SideShiftService(ISettingsRepository settingsRepository, IMemoryCache memoryCache, IStoreRepository storeRepository) + { + _settingsRepository = settingsRepository; + _memoryCache = memoryCache; + _storeRepository = storeRepository; + } + + public async Task GetSideShiftForStore(string storeId) + { + var k = $"{nameof(SideShiftSettings)}_{storeId}"; + return await _memoryCache.GetOrCreateAsync(k, async _ => + { + var res = await _storeRepository.GetSettingAsync(storeId, + nameof(SideShiftSettings)); + if (res is not null) return res; + res = await _settingsRepository.GetSettingAsync(k); + + if (res is not null) + { + await SetSideShiftForStore(storeId, res); + } + + await _settingsRepository.UpdateSetting(null, k); + return res; + }); + } + + public async Task SetSideShiftForStore(string storeId, SideShiftSettings SideShiftSettings) + { + var k = $"{nameof(SideShiftSettings)}_{storeId}"; + await _storeRepository.UpdateSetting(storeId, nameof(SideShiftSettings), SideShiftSettings); + _memoryCache.Set(k, SideShiftSettings); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.SideShift/SideShiftSettings.cs b/Plugins/BTCPayServer.Plugins.SideShift/SideShiftSettings.cs new file mode 100644 index 0000000..448e15c --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/SideShiftSettings.cs @@ -0,0 +1,8 @@ +namespace BTCPayServer.Plugins.SideShift +{ + public class SideShiftSettings + { + public bool Enabled { get; set; } + public decimal AmountMarkupPercentage { get; set; } = 0; + } +} diff --git a/Plugins/BTCPayServer.Plugins.SideShift/UpdateSideShiftSettingsViewModel.cs b/Plugins/BTCPayServer.Plugins.SideShift/UpdateSideShiftSettingsViewModel.cs new file mode 100644 index 0000000..86b2418 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/UpdateSideShiftSettingsViewModel.cs @@ -0,0 +1,8 @@ +namespace BTCPayServer.Plugins.SideShift +{ + public class UpdateSideShiftSettingsViewModel + { + public bool Enabled { get; set; } + public string StoreName { get; set; } + } +} diff --git a/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutContentExtension.cshtml b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutContentExtension.cshtml new file mode 100644 index 0000000..58f5d6d --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutContentExtension.cshtml @@ -0,0 +1,26 @@ +@using BTCPayServer.Plugins.SideShift +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq +@inject SideShiftService SideShiftService +@{ + var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value(); + var settings = await SideShiftService.GetSideShiftForStore(storeId); + if (settings?.Enabled is true) + { +
    +
    + + {{$t("ConversionTab_BodyTop", srvModel)}} +

    + {{$t("ConversionTab_BodyDesc", srvModel)}} +
    +
    + + {{$t("Pay with SideShift")}} + +
    + } +} diff --git a/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutEnd.cshtml b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutEnd.cshtml new file mode 100644 index 0000000..46b0ee4 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutEnd.cshtml @@ -0,0 +1,16 @@ +@using BTCPayServer.Plugins.SideShift +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq +@inject BTCPayServer.Security.ContentSecurityPolicies csp +@inject SideShiftService SideShiftService +@{ + var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value(); + var settings = await SideShiftService.GetSideShiftForStore(storeId); + if (settings?.Enabled is true) + { + csp.Add("script-src", "https://sideshift.ai"); + csp.Add("script-src", "*.sideshift.ai"); + + + } +} diff --git a/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutPaymentExtension.cshtml b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutPaymentExtension.cshtml new file mode 100644 index 0000000..c19952c --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutPaymentExtension.cshtml @@ -0,0 +1,69 @@ +@using BTCPayServer.Plugins.SideShift +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq +@inject BTCPayServer.Security.ContentSecurityPolicies csp +@inject SideShiftService SideShiftService +@{ + var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value(); + var settings = await SideShiftService.GetSideShiftForStore(storeId); +} +@if (settings?.Enabled is true) +{ + csp.Add("script-src", "https://sideshift.ai"); + csp.Add("script-src", "*.sideshift.ai"); + + + + +} diff --git a/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutPaymentMethodExtension.cshtml b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutPaymentMethodExtension.cshtml new file mode 100644 index 0000000..a177fdf --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutPaymentMethodExtension.cshtml @@ -0,0 +1,15 @@ +@using BTCPayServer.Plugins.SideShift +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq +@inject SideShiftService SideShiftService +@{ + const string id = "SideShift"; + var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value(); + var settings = await SideShiftService.GetSideShiftForStore(storeId); + if (settings?.Enabled is true) + { + + @id + + } +} diff --git a/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutTabExtension.cshtml b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutTabExtension.cshtml new file mode 100644 index 0000000..74a4d8d --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/CheckoutTabExtension.cshtml @@ -0,0 +1,14 @@ +@using BTCPayServer.Plugins.SideShift +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq +@inject SideShiftService SideShiftService +@{ + var storeId = ((JObject)JObject.Parse(JsonConvert.SerializeObject(Model)))["StoreId"].Value(); + var settings = await SideShiftService.GetSideShiftForStore(storeId); + if (settings?.Enabled is true) + { +
    + {{$t("Altcoins (SideShift)")}} +
    + } +} diff --git a/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/SideShiftNav.cshtml b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/SideShiftNav.cshtml new file mode 100644 index 0000000..409b21d --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/SideShiftNav.cshtml @@ -0,0 +1,16 @@ +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Abstractions.Extensions +@using Microsoft.AspNetCore.Mvc.TagHelpers +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); +} +@if (!string.IsNullOrEmpty(storeId)) +{ + +} diff --git a/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/StoreIntegrationSideShiftOption.cshtml b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/StoreIntegrationSideShiftOption.cshtml new file mode 100644 index 0000000..9127400 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/Views/Shared/SideShift/StoreIntegrationSideShiftOption.cshtml @@ -0,0 +1,59 @@ +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Plugins.SideShift +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using Microsoft.AspNetCore.Routing +@inject SideShiftService SideShiftService +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + + SideShiftSettings settings = null; + if (!string.IsNullOrEmpty(storeId)) + { + try + { + settings = await SideShiftService.GetSideShiftForStore(storeId); + } + catch (Exception) + { + } + } +} +@if (!string.IsNullOrEmpty(storeId)) +{ +
  • +
    + + + SideShift + + + Allows your customers to pay with altcoins that are not supported by BTCPay Server. + + + + @if (settings?.Enabled is true) + { + + + Enabled + + | + + Modify + + } + else + { + + + Disabled + + + Setup + + } + +
    +
  • +} diff --git a/Plugins/BTCPayServer.Plugins.SideShift/Views/SideShift/UpdateSideShiftSettings.cshtml b/Plugins/BTCPayServer.Plugins.SideShift/Views/SideShift/UpdateSideShiftSettings.cshtml new file mode 100644 index 0000000..613aab3 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/Views/SideShift/UpdateSideShiftSettings.cshtml @@ -0,0 +1,28 @@ +@using BTCPayServer.Abstractions.Extensions +@using Microsoft.AspNetCore.Mvc.TagHelpers +@model BTCPayServer.Plugins.SideShift.UpdateSideShiftSettingsViewModel +@{ + ViewData.SetActivePage("SideShift", "SideShift", "SideShift"); +} + + + +

    @ViewData["Title"]

    + + + +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    diff --git a/Plugins/BTCPayServer.Plugins.SideShift/Views/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.SideShift/Views/_ViewImports.cshtml new file mode 100644 index 0000000..52e6837 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.SideShift/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@addTagHelper *, BTCPayServer.Abstractions +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/BTCPayServer.Plugins.TicketTailor.csproj b/Plugins/BTCPayServer.Plugins.TicketTailor/BTCPayServer.Plugins.TicketTailor.csproj new file mode 100644 index 0000000..62ab7ad --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/BTCPayServer.Plugins.TicketTailor.csproj @@ -0,0 +1,40 @@ + + + + net6.0 + 10 + + + + + TicketTailor + Allows you to integrate with TicketTailor.com to sell tickets for Bitcoin + Kukks + 1.0.5 + + + + + true + false + true + + + + + + StaticWebAssetsEnabled=false + false + runtime;native;build;buildTransitive;contentFiles + + + + + + + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/Pack.ps1 b/Plugins/BTCPayServer.Plugins.TicketTailor/Pack.ps1 new file mode 100644 index 0000000..21394c5 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/Pack.ps1 @@ -0,0 +1,2 @@ +dotnet publish -c Altcoins-Release -o bin/publish/BTCPayServer.Plugins.TicketTailor +dotnet run -p ../../BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.TicketTailor BTCPayServer.Plugins.TicketTailor ../packed diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/Resources/assets/tt.png b/Plugins/BTCPayServer.Plugins.TicketTailor/Resources/assets/tt.png new file mode 100644 index 0000000000000000000000000000000000000000..1199f9419ebd3de5bc4b33d55c795457dff032e3 GIT binary patch literal 1572 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={WBuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFl%InM3hAM`dB6B=jtVb)aX^@765fKFxc2v6eK2Rr0UTq__OB&@Hb09I0xZL0)vRD^GUf^&XRs)DJWnQpRynYn_wrJkXw zxw(nCj)IYap{c%svA(f^u92~oiGh`gkpdJb0c|TvNwW%aaf8|g1^l#~=$>Fbx5 zm+O@q>*W`v>l<2HTIw4Z=^Gj80#)c1SLT%@R_NvxE5l51Ni9w;$}A|!%+FH*nV6WA zUs__T1av9H3%LbwWAlok!2}F2{ffi_eM3D1ke6TzeSPsO&CP|YE-nd5MYtEM!Nnn! z1*!T$sm1xFMajU3OH&3}Rbb^@l$uzQUlfv`p92fUfQ`as9%gQ6EHx?w`VGz4P86EBbhPyVTSz%*Y3Ox*XrSfn#BFirAw zaSW-rm9%AhR>}g^gro(A35qveeR|Z=PVx0cwH@QyC>Z?j&rf$>ZlleyclUl_W!a$+ z!|dE@y!?HjamSjRD-V`2H*Q^PHM8)*i4zN!>Q0{e+N{vaMqAt1CBQAr!!1lAa%S&Z zs{qxLj#H*ReDh$9s;Vl-!?N%0>!a$LX1Ci~UKKoR5ol-^YI-u-eE#G#MuTq;mN_#? zbrig0uVUKI8qxIP#lzW0j(7FWWjk3|@I-)9OhdEV(~E0@mFM(S|Nm#-v~$M|!^MpK{1O|d zx$deen!H?jPUXixhyToJRebo&vhv-GuK$03%RjjM`Q_tsp`uS$wfpUBezeV5J|Wjh zIA*8W{Zn4K$G>%}un1lVJl=QbkO+I*)RrX;EEZuiXU_l zG2OZ(e9cK_Bbha`8+P&>Vq+4HOD!v#HsAjLgaEO*mbKrH_uqF>3*+Mp6An(j#AUsp z_}rXb-ZgCpLO(pz_Vqkfc%VrvB6LESyo_}jx5RCS++S^<*!!+M+|DrL$(GU-{rD=E zCjJJ`-=B`Ut4#5};neBSTFl~p@c7}w2ha9ST)I&;%pe90UiDlzxnE|Nqi2%pA*P&*A0GW|zjo~F)KqP~IrR+UHQ(O+{kfUn zBX8TCeP924UG0CMiBa#8F#S-Jaw*Y9g*#o52b?yh^w8PJu&-B8sq zkAugowcDohk64nGz@LAAw=1LzE`6)?BuXlxN8Z-SiBEl1Dmza`LJ?Ej`+v3J=jZKX mKXdO$^Z|<}(bpLm*%*GD{9JQk;rwn;x$Wud=d#Wzp$PzgY*ohq literal 0 HcmV?d00001 diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorClient.cs b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorClient.cs new file mode 100644 index 0000000..83c1a68 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorClient.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using NBitcoin.DataEncoders; +using NBitcoin.Logging; + +namespace BTCPayServer.Plugins.TicketTailor; + +public class TicketTailorClient : IDisposable +{ + private readonly HttpClient _httpClient; + + public TicketTailorClient(IHttpClientFactory httpClientFactory, string apiKey) + { + _httpClient = httpClientFactory.CreateClient(); + _httpClient.BaseAddress = new Uri("https://api.tickettailor.com"); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoding.ASCII.GetBytes(apiKey))); + } + + public async Task GetEvents() + { + return (await _httpClient.GetFromJsonAsync>("/v1/events"))?.Data; + } + + public async Task GetEvent(string id) + { + return await _httpClient.GetFromJsonAsync($"/v1/events/{id}"); + } + + public async Task<(IssuedTicket, string)> CreateTicket(IssueTicketRequest request) + { + var data = JsonSerializer.SerializeToElement(request).EnumerateObject().Select(property => + new KeyValuePair(property.Name, property.Value.GetString())).Where(pair =>pair.Value != null); + + + var response = await _httpClient.PostAsync($"/v1/issued_tickets", new FormUrlEncodedContent(data.ToArray())); + if (!response.IsSuccessStatusCode) + { + var error = await response.Content.ReadAsStringAsync(); + return (null, error); + } + return (await response.Content.ReadFromJsonAsync(), null); + } + + + public async Task GetTicket(string id) + { + return await _httpClient.GetFromJsonAsync($"/v1/issued_tickets/{id}"); + } + + public class DataHolder + { + [JsonPropertyName("data")] public T Data { get; set; } + } + + + public void Dispose() + { + _httpClient?.Dispose(); + } + + public class IssueTicketRequest + { + [JsonPropertyName("event_id")] public string EventId { get; set; } + [JsonPropertyName("ticket_type_id")] public string TicketTypeId { get; set; } + [JsonPropertyName("email")] public string Email { get; set; } + [JsonPropertyName("full_name")] public string FullName { get; set; } + [JsonPropertyName("reference")] public string Reference { get; set; } + [JsonPropertyName("barcode")] public string BarCode { get; set; } + } + + + public class EventEnd + { + [JsonPropertyName("date")] public string Date { get; set; } + + [JsonPropertyName("formatted")] public string Formatted { get; set; } + + [JsonPropertyName("iso")] public DateTime Iso { get; set; } + + [JsonPropertyName("time")] public string Time { get; set; } + + [JsonPropertyName("timezone")] public string Timezone { get; set; } + + [JsonPropertyName("unix")] public int Unix { get; set; } + } + + public class Images + { + [JsonPropertyName("header")] public string Header { get; set; } + + [JsonPropertyName("thumbnail")] public string Thumbnail { get; set; } + } + + public class PaymentMethod + { + [JsonPropertyName("external_id")] public string ExternalId { get; set; } + + [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("instructions")] public string Instructions { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("type")] public string Type { get; set; } + } + + public class Start + { + [JsonPropertyName("date")] public string Date { get; set; } + + [JsonPropertyName("formatted")] public string Formatted { get; set; } + + [JsonPropertyName("iso")] public DateTime Iso { get; set; } + + [JsonPropertyName("time")] public string Time { get; set; } + + [JsonPropertyName("timezone")] public string Timezone { get; set; } + + [JsonPropertyName("unix")] public int Unix { get; set; } + } + + public class TicketGroup + { + [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("max_per_order")] public object MaxPerOrder { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("sort_order")] public int SortOrder { get; set; } + + [JsonPropertyName("ticket_ids")] public List TicketIds { get; set; } + } + + public class TicketType + { + [JsonPropertyName("object")] public string Object { get; set; } + + [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("access_code")] public object AccessCode { get; set; } + + [JsonPropertyName("booking_fee")] public int BookingFee { get; set; } + + [JsonPropertyName("description")] public string Description { get; set; } + + [JsonPropertyName("group_id")] public string GroupId { get; set; } + + [JsonPropertyName("max_per_order")] public int MaxPerOrder { get; set; } + + [JsonPropertyName("min_per_order")] public int MinPerOrder { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("price")] public decimal Price { get; set; } + + [JsonPropertyName("status")] public string Status { get; set; } + + [JsonPropertyName("sort_order")] public int SortOrder { get; set; } + + [JsonPropertyName("type")] public string Type { get; set; } + + [JsonPropertyName("quantity")] public int Quantity { get; set; } + + [JsonPropertyName("quantity_held")] public int QuantityHeld { get; set; } + + [JsonPropertyName("quantity_issued")] public int QuantityIssued { get; set; } + + [JsonPropertyName("quantity_total")] public int QuantityTotal { get; set; } + } + + public class Venue + { + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("postal_code")] public string PostalCode { get; set; } + } + + public class IssuedTicket + { + [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("reference")] public string Reference { get; set; } + [JsonPropertyName("description")] public string Description { get; set; } + [JsonPropertyName("status")] public string Status { get; set; } + + [JsonPropertyName("full_name")] public string FullName { get; set; } + + + [JsonPropertyName("qr_code_url")] public string QrCodeUrl { get; set; } + [JsonPropertyName("barcode_url")] public string BarcodeUrl { get; set; } + [JsonPropertyName("barcode")] public string Barcode { get; set; } + [JsonPropertyName("ticket_type_id")] public string TicketTypeId { get; set; } + } + + public class Event + { + [JsonPropertyName("object")] public string Object { get; set; } + + [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("access_code")] public object AccessCode { get; set; } + + [JsonPropertyName("call_to_action")] public string CallToAction { get; set; } + + [JsonPropertyName("created_at")] public int CreatedAt { get; set; } + + [JsonPropertyName("currency")] public string Currency { get; set; } + + [JsonPropertyName("description")] public string Description { get; set; } + + [JsonPropertyName("end")] public EventEnd EventEnd { get; set; } + + [JsonPropertyName("hidden")] public string Hidden { get; set; } + + [JsonPropertyName("images")] public Images Images { get; set; } + + [JsonPropertyName("name")] public string Title { get; set; } + + [JsonPropertyName("online_event")] public string OnlineEvent { get; set; } + + [JsonPropertyName("payment_methods")] public List PaymentMethods { get; set; } + + [JsonPropertyName("private")] public string Private { get; set; } + + [JsonPropertyName("start")] public Start Start { get; set; } + + [JsonPropertyName("status")] public string Status { get; set; } + + [JsonPropertyName("ticket_groups")] public List TicketGroups { get; set; } + + [JsonPropertyName("ticket_types")] public List TicketTypes { get; set; } + + [JsonPropertyName("tickets_available")] + public string TicketsAvailable { get; set; } + + [JsonPropertyName("timezone")] public string Timezone { get; set; } + + [JsonPropertyName("total_holds")] public int TotalHolds { get; set; } + + [JsonPropertyName("total_issued_tickets")] + public int TotalIssuedTickets { get; set; } + + [JsonPropertyName("total_orders")] public int TotalOrders { get; set; } + + [JsonPropertyName("unavailable")] public string Unavailable { get; set; } + + [JsonPropertyName("unavailable_status")] + public object UnavailableStatus { get; set; } + + [JsonPropertyName("url")] public string Url { get; set; } + + [JsonPropertyName("venue")] public Venue Venue { get; set; } + } +} diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorController.cs b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorController.cs new file mode 100644 index 0000000..8045f99 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorController.cs @@ -0,0 +1,503 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AngleSharp; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using NBitcoin; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using AuthenticationSchemes = BTCPayServer.Abstractions.Constants.AuthenticationSchemes; +using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration; + +namespace BTCPayServer.Plugins.TicketTailor +{ + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Route("plugins/{storeId}/TicketTailor")] + public class TicketTailorController : Controller + { + [AllowAnonymous] + [HttpGet("")] + public async Task View(string storeId) + { + var config = await _ticketTailorService.GetTicketTailorForStore(storeId); + try + { + if (config?.ApiKey is not null && config?.EventId is not null) + { + var client = new TicketTailorClient(_httpClientFactory, config.ApiKey); + var evt = await client.GetEvent(config.EventId); + if (evt is null) + { + return NotFound(); + } + + return View(new TicketTailorViewModel() {Event = evt, Settings = config}); + } + } + catch (Exception e) + { + } + + return NotFound(); + } + + + [AllowAnonymous] + [HttpPost("")] + public async Task Purchase(string storeId, string ticketTypeId, string firstName, + string lastName, string email) + { + var config = await _ticketTailorService.GetTicketTailorForStore(storeId); + try + { + if (config?.ApiKey is not null && config?.EventId is not null) + { + var client = new TicketTailorClient(_httpClientFactory, config.ApiKey); + var evt = await client.GetEvent(config.EventId); + if (evt is null || (!config.BypassAvailabilityCheck && (evt.Unavailable == "true" || evt.TicketsAvailable == "false"))) + { + return NotFound(); + } + + var ticketType = evt.TicketTypes.FirstOrDefault(type => type.Id == ticketTypeId); + var specificTicket = + config.SpecificTickets?.SingleOrDefault(ticket => ticketType?.Id == ticket.TicketTypeId); + if (ticketType is not null && specificTicket is not null) + { + ticketType.Price = specificTicket.Price.GetValueOrDefault(ticketType.Price); + } + + if (ticketType is null || (specificTicket is null && ticketType.Status != "on_sale") || + ticketType.Quantity <= 0) + { + return NotFound(); + } + + var btcpayClient = await CreateClient(storeId); + var redirectUrl = Request.GetAbsoluteUri(Url.Action("Receipt", + "TicketTailor", new {storeId, invoiceId = "kukkskukkskukks"})); + redirectUrl = redirectUrl.Replace("kukkskukkskukks", "{InvoiceId}"); + var inv = await btcpayClient.CreateInvoice(storeId, + new CreateInvoiceRequest() + { + Amount = ticketType.Price, + Currency = evt.Currency, + Type = InvoiceType.Standard, + AdditionalSearchTerms = new[] {"tickettailor", ticketTypeId, evt.Id}, + Checkout = + { + RequiresRefundEmail = true, + RedirectAutomatically = ticketType.Price > 0, + RedirectURL = redirectUrl, + }, + Receipt = new InvoiceDataBase.ReceiptOptions() + { + Enabled = false + }, + Metadata = JObject.FromObject(new + { + buyerName = $"{firstName} {lastName}", buyerEmail = email, ticketTypeId,orderId="tickettailor" + }) + }); + + while (inv.Amount == 0 && inv.Status == InvoiceStatus.New) + { + if (inv.Status == InvoiceStatus.New) + inv = await btcpayClient.GetInvoice(inv.StoreId, inv.Id); + } + + if (inv.Status == InvoiceStatus.Settled) + return RedirectToAction("Receipt", new {storeId, invoiceId = inv.Id}); + return Redirect(inv.CheckoutLink); + } + } + catch (Exception e) + { + } + + return RedirectToAction("View", new {storeId}); + } + + + [AllowAnonymous] + [HttpGet("receipt")] + public async Task Receipt(string storeId, string invoiceId) + { + var btcpayClient = await CreateClient(storeId); + try + { + var result = new TicketReceiptPage() {InvoiceId = invoiceId}; + var invoice = await btcpayClient.GetInvoice(storeId, invoiceId); + result.Status = invoice.Status; + if (invoice.Status == InvoiceStatus.Settled) + { + + if (invoice.Metadata.TryGetValue("ticketId", out var ticketId)) + { + await SetTicketTailorTicketResult(storeId, result, ticketId); + } + else + { + invoice = await _ticketTailorService.Handle(invoice.Id, storeId, Request.GetAbsoluteRootUri()); + if (invoice.Metadata.TryGetValue("ticketId", out ticketId)) + { + await SetTicketTailorTicketResult(storeId, result, ticketId); + } + } + } + + return View(result); + } + catch (Exception e) + { + return NotFound(); + } + } + + private async Task SetTicketTailorTicketResult(string storeId, TicketReceiptPage result, JToken ticketId) + { + var settings = await _ticketTailorService.GetTicketTailorForStore(storeId); + var client = new TicketTailorClient(_httpClientFactory, settings.ApiKey); + result.Ticket = await client.GetTicket(ticketId.ToString()); + var evt = await client.GetEvent(settings.EventId); + result.Event = evt; + result.TicketType = + evt.TicketTypes.FirstOrDefault(type => type.Id == result.Ticket.TicketTypeId); + result.Settings = settings; + } + + private async Task CreateClient(string storeId) + { + return await _btcPayServerClientFactory.Create(null, new[] {storeId}, new DefaultHttpContext() + { + Request = + { + Scheme = "https", + Host = Request.Host, + Path = Request.Path, + PathBase = Request.PathBase + } + }); + } + + public class TicketReceiptPage + { + public string InvoiceId { get; set; } + public InvoiceStatus Status { get; set; } + public TicketTailorClient.IssuedTicket Ticket { get; set; } + public TicketTailorClient.Event Event { get; set; } + public TicketTailorClient.TicketType TicketType { get; set; } + public TicketTailorSettings Settings { get; set; } + } + + + private readonly IHttpClientFactory _httpClientFactory; + private readonly TicketTailorService _ticketTailorService; + private readonly IBTCPayServerClientFactory _btcPayServerClientFactory; + private readonly IConfiguration _configuration; + private readonly LinkGenerator _linkGenerator; + + public TicketTailorController(IHttpClientFactory httpClientFactory, + TicketTailorService ticketTailorService, + IBTCPayServerClientFactory btcPayServerClientFactory, + IConfiguration configuration, + LinkGenerator linkGenerator ) + { + + _httpClientFactory = httpClientFactory; + _ticketTailorService = ticketTailorService; + _btcPayServerClientFactory = btcPayServerClientFactory; + _configuration = configuration; + _linkGenerator = linkGenerator; + } + + [HttpGet("update")] + public async Task UpdateTicketTailorSettings(string storeId) + { + UpdateTicketTailorSettingsViewModel vm = new(); + TicketTailorSettings TicketTailor = null; + try + { + TicketTailor = await _ticketTailorService.GetTicketTailorForStore(storeId); + if (TicketTailor is not null) + { + vm.ApiKey = TicketTailor.ApiKey; + vm.EventId = TicketTailor.EventId; + vm.ShowDescription = TicketTailor.ShowDescription; + vm.BypassAvailabilityCheck = TicketTailor.BypassAvailabilityCheck; + vm.CustomCSS = TicketTailor.CustomCSS; + vm.SpecificTickets = TicketTailor.SpecificTickets; + } + } + catch (Exception) + { + // ignored + } + + vm = await SetValues(vm); + + return View(vm); + } + + private async Task SetValues(UpdateTicketTailorSettingsViewModel vm) + { + try + { + if (!string.IsNullOrEmpty(vm.ApiKey)) + { + TicketTailorClient.Event? evt = null; + var client = new TicketTailorClient(_httpClientFactory, vm.ApiKey); + var evts = await client.GetEvents(); + if (vm.EventId is not null && evts.All(e => e.Id != vm.EventId)) + { + vm.EventId = null; + vm.SpecificTickets = new List(); + } + else + { + if (vm.EventId is null) + { + vm.SpecificTickets = new List(); + } + else + { + evt = evts.SingleOrDefault(e => e.Id == vm.EventId); + } + } + + evts = evts.Prepend(new TicketTailorClient.Event() {Id = null, Title = "Select an event"}) + .ToArray(); + vm.Events = new SelectList(evts, nameof(TicketTailorClient.Event.Id), + nameof(TicketTailorClient.Event.Title), vm.EventId); + + if (vm.EventId is not null) + { + vm.TicketTypes = evt?.TicketTypes?.ToArray(); + } + } + } + catch (Exception e) + { + ModelState.AddModelError(nameof(vm.ApiKey), "Api key did not work."); + } + + return vm; + } + + + [HttpPost("update")] + public async Task UpdateTicketTailorSettings(string storeId, + UpdateTicketTailorSettingsViewModel vm, + string command, + [FromServices] BTCPayServerClient btcPayServerClient) + { + vm = await SetValues(vm); + + if (command == "add-specific-ticket" && vm.NewSpecificTicket is not null) + { + vm.SpecificTickets ??= new List(); + vm.SpecificTickets.Add(new() {TicketTypeId = vm.NewSpecificTicket}); + vm.NewSpecificTicket = null; + return View(vm); + } + + if (command.StartsWith("remove-specific-ticket")) + { + var i = int.Parse(command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + + 1)); + vm.SpecificTickets.RemoveAt(i); + return View(vm); + } + + if (!ModelState.IsValid) + { + return View(vm); + } + ModelState.Clear(); + var settings = new TicketTailorSettings() + { + ApiKey = vm.ApiKey, + EventId = vm.EventId, + ShowDescription = vm.ShowDescription, + CustomCSS = vm.CustomCSS, + SpecificTickets = vm.SpecificTickets, + BypassAvailabilityCheck = vm.BypassAvailabilityCheck + }; + + var bindAddress = _configuration.GetValue("bind", IPAddress.Loopback); + if (Equals(bindAddress, IPAddress.Any)) + { + bindAddress = IPAddress.Loopback; + } + if (Equals(bindAddress, IPAddress.IPv6Any)) + { + bindAddress = IPAddress.IPv6Loopback; + } + int bindPort = _configuration.GetValue("port", 443); + + string rootPath = _configuration.GetValue("rootpath", "/"); + string attempt1 = null; + if (bindAddress is not null) + { + attempt1 = _linkGenerator.GetUriByAction("Callback", + "TicketTailor", new {storeId,test= true}, "https", new HostString(bindAddress?.ToString(), bindPort), + new PathString(rootPath)); + } + + var attempt2 = Request.GetAbsoluteUri(Url.Action("Callback", + "TicketTailor", new {storeId, test= true})); + + + HttpRequestMessage Create(string uri) + { + return new HttpRequestMessage(HttpMethod.Post, uri) + { + Content = new StringContent( + JsonConvert.SerializeObject(new WebhookInvoiceEvent(WebhookEventType.InvoiceSettled)) + ,Encoding.UTF8, + "application/json"), + + }; + } + + HttpClient CreateClient(string uri) + { + var link = new Uri(uri); + if (link.IsLoopback) + { + return _httpClientFactory.CreateClient("greenfield-webhook.loopback"); + + }else if (link.Host.EndsWith("onion")) + { + return _httpClientFactory.CreateClient("greenfield-webhook.onion"); + } + else + { + return _httpClientFactory.CreateClient("greenfield-webhook.clearnet"); + } + } + + + HttpResponseMessage result = null; + if (attempt1 is not null) + { + + try + { + + result = await CreateClient(attempt1).SendAsync(Create(attempt1), CancellationToken.None); + } + catch (Exception e) + { + + } + } + + string webhookUrl = null; + if (result?.IsSuccessStatusCode is true) + { + webhookUrl = _linkGenerator.GetUriByAction("Callback", + "TicketTailor", new {storeId}, "http", new HostString(bindAddress.ToString(), bindPort), + new PathString(rootPath));; + } + else + { + try + { + result = null; + result = await CreateClient(attempt2).SendAsync(Create(attempt2), CancellationToken.None); + } + catch (Exception e) + { + } + if (result?.IsSuccessStatusCode is true) + { + webhookUrl = Request.GetAbsoluteUri(Url.Action("Callback", + "TicketTailor", new {storeId}));; + } + + } + + if (webhookUrl is null) + { + ModelState.AddModelError("", $"{attempt1} or {attempt2} was not reachable by BTCPayServer."); + + return View(vm); + + }else if (vm.ApiKey is not null && vm.EventId is not null) + { + var webhooks = await btcPayServerClient.GetWebhooks(storeId); + var webhook = webhooks.FirstOrDefault(data => data.Enabled && data.Url == webhookUrl && (data.AuthorizedEvents.Everything || data.AuthorizedEvents.SpecificEvents.Contains(WebhookEventType.InvoiceSettled))); + if (webhook is null) + { + await CreateWebhook(storeId, btcPayServerClient, webhookUrl); + } + } + + switch (command?.ToLowerInvariant()) + { + case "save": + await _ticketTailorService.SetTicketTailorForStore(storeId, settings); + TempData["SuccessMessage"] = "TicketTailor settings modified"; + return RedirectToAction(nameof(UpdateTicketTailorSettings), new {storeId}); + + default: + return View(vm); + } + } + + private static async Task CreateWebhook(string storeId, BTCPayServerClient btcPayServerClient, + string webhookUrl) + { + var wh = await btcPayServerClient.CreateWebhook(storeId, + new CreateStoreWebhookRequest() + { + Enabled = true, + Url = webhookUrl, + AuthorizedEvents = new StoreWebhookBaseData.AuthorizedEventsData() + { + Everything = false, + SpecificEvents = new[] {WebhookEventType.InvoiceSettled} + }, + AutomaticRedelivery = true + }); + return wh.Id; + } + + [AllowAnonymous] + [HttpPost("callback")] + public async Task Callback(string storeId, [FromBody] WebhookInvoiceSettledEvent response, [FromQuery ]bool test) + { + if (test) + { + return Ok(); + } + if (response.StoreId != storeId && response.Type != WebhookEventType.InvoiceSettled) + { + return BadRequest(); + } + + await _ticketTailorService.Handle(response.InvoiceId, response.StoreId, Request.GetAbsoluteRootUri()); + + return Ok(); + } + + + } +} diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorPlugin.cs b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorPlugin.cs new file mode 100644 index 0000000..7751cb7 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorPlugin.cs @@ -0,0 +1,33 @@ +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Plugins.TicketTailor +{ + public class TicketTailorPlugin : BaseBTCPayServerPlugin + { + public override string Identifier => "BTCPayServer.Plugins.TicketTailor"; + public override string Name => "TicketTailor"; + + + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new() { Identifier = nameof(BTCPayServer), Condition = ">=1.6.0.0" } + }; + + public override string Description => + "Allows you to integrate with TicketTailor.com to sell tickets for Bitcoin"; + + public override void Execute(IServiceCollection applicationBuilder) + { + applicationBuilder.AddSingleton(); + applicationBuilder.AddHostedService(s=>s.GetRequiredService()); + applicationBuilder.AddSingleton(new UIExtension("TicketTailor/StoreIntegrationTicketTailorOption", + "store-integrations-list")); + applicationBuilder.AddSingleton(new UIExtension("TicketTailor/TicketTailorNav", + "store-integrations-nav")); + base.Execute(applicationBuilder); + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorService.cs b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorService.cs new file mode 100644 index 0000000..30c961c --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorService.cs @@ -0,0 +1,236 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Plugins.TicketTailor; + +public class TicketTailorService : IHostedService +{ + private readonly ISettingsRepository _settingsRepository; + private readonly IMemoryCache _memoryCache; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IStoreRepository _storeRepository; + private readonly ILogger _logger; + private readonly IBTCPayServerClientFactory _btcPayServerClientFactory; + private readonly LinkGenerator _linkGenerator; + + public TicketTailorService(ISettingsRepository settingsRepository, IMemoryCache memoryCache, + IHttpClientFactory httpClientFactory, + IStoreRepository storeRepository, ILogger logger, + IBTCPayServerClientFactory btcPayServerClientFactory, LinkGenerator linkGenerator) + { + _settingsRepository = settingsRepository; + _memoryCache = memoryCache; + _httpClientFactory = httpClientFactory; + _storeRepository = storeRepository; + _logger = logger; + _btcPayServerClientFactory = btcPayServerClientFactory; + _linkGenerator = linkGenerator; + } + + + public async Task GetTicketTailorForStore(string storeId) + { + var k = $"{nameof(TicketTailorSettings)}_{storeId}"; + return await _memoryCache.GetOrCreateAsync(k, async _ => + { + var res = await _storeRepository.GetSettingAsync(storeId, + nameof(TicketTailorSettings)); + if (res is not null) return res; + res = await _settingsRepository.GetSettingAsync(k); + + if (res is not null) + { + await SetTicketTailorForStore(storeId, res); + } + + await _settingsRepository.UpdateSetting(null, k); + return res; + }); + } + + public async Task SetTicketTailorForStore(string storeId, TicketTailorSettings TicketTailorSettings) + { + var k = $"{nameof(TicketTailorSettings)}_{storeId}"; + await _storeRepository.UpdateSetting(storeId, nameof(TicketTailorSettings), TicketTailorSettings); + _memoryCache.Set(k, TicketTailorSettings); + } + + + public Task Handle(string invoiceId, string storeId, Uri host) + { + var tcs = new TaskCompletionSource(); + _events.Writer.TryWrite(new IssueTicket() {Task = tcs, InvoiceId = invoiceId, StoreId = storeId, Host = host}); + return tcs.Task; + } + + internal class IssueTicket + { + public string InvoiceId { get; set; } + public string StoreId { get; set; } + public TaskCompletionSource Task { get; set; } + public Uri Host { get; set; } + } + + + readonly Channel _events = Channel.CreateUnbounded(); + + public Task StartAsync(CancellationToken cancellationToken) + { + _ = ProcessEvents(cancellationToken); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public async Task CreateClient(string storeId, Uri host) + { + return await _btcPayServerClientFactory.Create(null, new []{storeId}, new DefaultHttpContext() + { + Request = + { + Scheme = host.Scheme, + Host = new HostString(host.Host), + Path = new PathString(host.AbsolutePath), + PathBase = new PathString(), + } + }); + } + + private async Task ProcessEvents(CancellationToken cancellationToken) + { + while (await _events.Reader.WaitToReadAsync(cancellationToken)) + { + if (!_events.Reader.TryRead(out var evt)) continue; + + async Task HandleIssueTicketError(JToken posData, string e, InvoiceData invoiceData, + BTCPayServerClient btcPayClient) + { + posData["Error"] = + $"Ticket could not be created. You should refund customer.{Environment.NewLine}{e}"; + invoiceData.Metadata["posData"] = posData; + await btcPayClient.UpdateInvoice(evt.StoreId, invoiceData.Id, + new UpdateInvoiceRequest() {Metadata = invoiceData.Metadata}, cancellationToken); + try + { + await btcPayClient.MarkInvoiceStatus(evt.StoreId, invoiceData.Id, + new MarkInvoiceStatusRequest() {Status = InvoiceStatus.Invalid}, cancellationToken); + } + catch (Exception exception) + { + _logger.LogError(exception, $"Failed to update invoice {invoiceData.Id} status from {invoiceData.Status} to Invalid after failing to issue ticket from ticket tailor"); + } + } + + InvoiceData invoice = null; + try + { + var settings = await GetTicketTailorForStore(evt.StoreId); + if (settings is null || settings.ApiKey is null) + { + evt.Task.SetResult(null); + continue; + } + + var btcPayClient = await CreateClient(evt.StoreId, evt.Host); + invoice = await btcPayClient.GetInvoice(evt.StoreId, evt.InvoiceId, cancellationToken); + if (invoice.Status != InvoiceStatus.Settled) + { + evt.Task.SetResult(null); + continue; + } + + if (invoice.Metadata.ContainsKey("ticketId")) + { + evt.Task.SetResult(null); + continue; + } + + var ticketTypeId = invoice.Metadata["ticketTypeId"].ToString(); + var email = invoice.Metadata["buyerEmail"].ToString(); + var name = invoice.Metadata["buyerName"]?.ToString(); + invoice.Metadata.TryGetValue("posData", out var posData); + posData ??= new JObject(); + var client = new TicketTailorClient(_httpClientFactory, settings.ApiKey); + try + { + var ticketResult = await client.CreateTicket(new TicketTailorClient.IssueTicketRequest() + { + Reference = invoice.Id, + Email = email, + EventId = settings.EventId, + TicketTypeId = ticketTypeId, + FullName = name, + }); + + if (ticketResult.Item2 is not null) + { + await HandleIssueTicketError(posData, ticketResult.Item2, invoice, btcPayClient); + + continue; + } + + var ticket = ticketResult.Item1; + invoice.Metadata["ticketId"] = ticket.Id; + invoice.Metadata["orderId"] = $"tickettailor_{ticket.Id}"; + + posData["Ticket Code"] = ticket.Barcode; + posData["Ticket Id"] = ticket.Id; + invoice.Metadata["posData"] = posData; + await btcPayClient.UpdateInvoice(evt.StoreId, invoice.Id, + new UpdateInvoiceRequest() {Metadata = invoice.Metadata}, cancellationToken); + + var url = + _linkGenerator.GetUriByAction("Receipt", + "TicketTailor", + new {evt.StoreId, invoiceId = invoice.Id}, + evt.Host.Scheme, + new HostString(evt.Host.Host), + evt.Host.AbsolutePath); + + try + { + await btcPayClient.SendEmail(evt.StoreId, + new SendEmailRequest() + { + Subject = "Your ticket is available now.", + Email = email, + Body = + $"Your payment has been settled and the event ticket has been issued successfully. Please go to {url}" + }, cancellationToken); + } + catch (Exception e) + { + // ignored + } + } + catch (Exception e) + { + await HandleIssueTicketError(posData, e.Message, invoice, btcPayClient); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to issue ticket"); + } + finally + { + evt.Task.SetResult(invoice); + } + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorSettings.cs b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorSettings.cs new file mode 100644 index 0000000..d797ff9 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorSettings.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace BTCPayServer.Plugins.TicketTailor +{ + public class TicketTailorSettings + { + public string ApiKey { get; set; } + public string EventId { get; set; } + + public bool ShowDescription { get; set; } + public string CustomCSS { get; set; } + public List SpecificTickets { get; set; } + public bool BypassAvailabilityCheck { get; set; } + } +} diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/UpdateTicketTailorSettingsViewModel.cs b/Plugins/BTCPayServer.Plugins.TicketTailor/UpdateTicketTailorSettingsViewModel.cs new file mode 100644 index 0000000..e4e85fc --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/UpdateTicketTailorSettingsViewModel.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace BTCPayServer.Plugins.TicketTailor; + +public class UpdateTicketTailorSettingsViewModel +{ + public string NewSpecificTicket { get; set; } + public string ApiKey { get; set; } + public SelectList Events { get; set; } + public string EventId { get; set; } + public bool ShowDescription { get; set; } + public string CustomCSS { get; set; } + public TicketTailorClient.TicketType[] TicketTypes { get; set; } + + public List SpecificTickets { get; set; } + public bool BypassAvailabilityCheck { get; set; } +} + +public class SpecificTicket +{ + public string TicketTypeId { get; set; } + public decimal? Price { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public bool Hidden { get; set; } +} + +public class TicketTailorViewModel +{ + public TicketTailorClient.Event Event { get; set; } + public TicketTailorSettings Settings { get; set; } +} diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/Views/Shared/TicketTailor/StoreIntegrationTicketTailorOption.cshtml b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/Shared/TicketTailor/StoreIntegrationTicketTailorOption.cshtml new file mode 100644 index 0000000..8c426e3 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/Shared/TicketTailor/StoreIntegrationTicketTailorOption.cshtml @@ -0,0 +1,59 @@ +@using BTCPayServer.Client +@using BTCPayServer.Plugins.TicketTailor +@using Microsoft.AspNetCore.Routing +@using BTCPayServer.Abstractions.Contracts +@inject IScopeProvider ScopeProvider +@inject TicketTailorService TicketTailorService +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + + TicketTailorSettings settings = null; + if (!string.IsNullOrEmpty(storeId)) + { + try + { + settings = await TicketTailorService.GetTicketTailorForStore(storeId); + } + catch (Exception) + { + } + } +} +@if (!string.IsNullOrEmpty(storeId)) +{ +
  • +
    + + + Ticket Tailor + + + Sell tickets on Ticket Tailor using BTCPay Server + + + + @if (settings?.ApiKey is not null) + { + + + Active + + | + + Modify + + } + else + { + + + Disabled + + + Setup + + } + +
    +
  • +} diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/Views/Shared/TicketTailor/TicketTailorNav.cshtml b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/Shared/TicketTailor/TicketTailorNav.cshtml new file mode 100644 index 0000000..f8e77f6 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/Shared/TicketTailor/TicketTailorNav.cshtml @@ -0,0 +1,17 @@ +@using BTCPayServer.Plugins.TicketTailor +@inject IScopeProvider ScopeProvider +@using BTCPayServer.Abstractions.Contracts +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null && + nameof(TicketTailorController).StartsWith(controller?.ToString(), StringComparison.InvariantCultureIgnoreCase); +} +@if (!string.IsNullOrEmpty(storeId)) +{ + +} diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/Receipt.cshtml b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/Receipt.cshtml new file mode 100644 index 0000000..77ab3fb --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/Receipt.cshtml @@ -0,0 +1,133 @@ +@using BTCPayServer.Client.Models +@model BTCPayServer.Plugins.TicketTailor.TicketTailorController.TicketReceiptPage +@inject ContentSecurityPolicies contentSecurityPolicies +@using BTCPayServer.Security +@using NBitcoin +@{ + var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32); + contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'"); + contentSecurityPolicies.AllowUnsafeHashes(); + Layout = "_LayoutSimple"; + var reloadPage = false; +} + +
    +
    + +
    + +

    Ticket Receipt

    + +
    + @if (Model.Status == InvoiceStatus.Processing) + { + reloadPage = true; +
    + The invoice has detected a payment but is still waiting to be settled. This page will refresh periodically until it is settled. + +
    + } + else if (Model.Status != InvoiceStatus.Settled) + { +
    + The invoice is not settled. +
    + } + else if (Model.Ticket is null) + { + + reloadPage = true; +
    + The invoice is settled but the ticket has not been issued yet. This page will refresh periodically until it is issued. +
    + } + else + { + var specificTicketName = Model.Settings?.SpecificTickets?.FirstOrDefault(ticket => ticket.TicketTypeId == Model.TicketType.Id)?.Name ?? Model.TicketType.Name; + +
    +
    +
    +

    Ticket Details

    +
    +
    + Please ensure you can see this QR barcode +
    +
    + Please ensure you can see this barcode +
    +
    +
    +
    @Model.Ticket.Barcode
    TICKET CODE
    +
    + @if (!string.IsNullOrEmpty(Model.Ticket.Reference)) + { +
    +
    @Model.Ticket.Reference
    REFERENCE
    +
    + } + + @if (!string.IsNullOrEmpty(Model.Ticket.FullName)) + { +
    +
    @Model.Ticket.FullName
    ATTENDEE NAME
    +
    + } +
    +
    @specificTicketName
    TICKET TYPE
    +
    +
    +
    +
    +
    +

    Event Details

    +
    +
    @Model.Event.Title
    EVENT
    +
    +
    +
    + @Model.Event.Url +
    EVENT URL
    +
    +
    +
    @Model.Event.Start.Formatted - @Model.Event.EventEnd.Formatted
    +
    Date
    +
    +
    +
    @Model.Event.Venue.Name
    +
    Venue
    +
    +
    +
    +
    + } +
    +
    + Powered by BTCPay Server +
    +
    +
    +
    + +@if (reloadPage) +{ + +} + diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/UpdateTicketTailorSettings.cshtml b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/UpdateTicketTailorSettings.cshtml new file mode 100644 index 0000000..fa0e93a --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/UpdateTicketTailorSettings.cshtml @@ -0,0 +1,134 @@ +@using BTCPayServer.Abstractions.Extensions +@using BTCPayServer.Client +@using BTCPayServer.Plugins.TicketTailor +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using Microsoft.AspNetCore.Routing +@using BTCPayServer.Abstractions.Contracts +@model BTCPayServer.Plugins.TicketTailor.UpdateTicketTailorSettingsViewModel +@inject IScopeProvider ScopeProvider +@{ + var storeId = ScopeProvider.GetCurrentStoreId(); + Layout = "../Shared/_NavLayout.cshtml"; + ViewData["NavPartialName"] = "../UIStores/_Nav"; + ViewData.SetActivePage("TicketTailor", "Update Store TicketTailor Settings", null); + Model.SpecificTickets ??= new List(); + +} + +

    @ViewData["Title"]

    +@if (ViewContext.ModelState.IsValid && Model.EventId is not null) +{ +
    + Please ensure that emails in your store are configured if you wish to send the tickets via email to customers as TicketTailor does not handle it for tickets issued via its API. +
    + Ensure that the url used for btcpayserver is accessible publicly. +
    +} +
    +
    +
    +
    + +
    + + + +
    + + @if (Model.Events is not null) + { +
    + + +
    + } + + @if (Model.EventId is not null) + { +
    + + + +
    +
    + + + +
    + +
    + + + +
    + + @if (Model.TicketTypes?.Any() is true) + { + var uniqueRemaining = Model.TicketTypes.Where(type => Model.SpecificTickets?.Any(ticket => ticket.TicketTypeId == type.Id) is false); +
    +
    Specific tickets
    +

    Specific tickets allow you to override explicitly which tickets you wish to allow to sell, and also override the price, name, and description of these tickets.

    + @if (uniqueRemaining.Any()) + { + SelectList sl = new SelectList(uniqueRemaining, nameof(TicketTailorClient.TicketType.Id), nameof(TicketTailorClient.TicketType.Name)); +
    + +
    + + + +
    +
    + } + @for (var index = 0; index < Model.SpecificTickets.Count; index++) + { + var specific = Model.SpecificTickets[index]; + var ticketType = Model.TicketTypes.SingleOrDefault(type => type.Id == specific.TicketTypeId); + if (ticketType is null) + { + continue; + } +
    +
    + @ticketType.Name +
    +
    +
    +
    + + + +
    +
    + +
    + +
    +
    + + + +
    +
    + +
    + } +
    + } + } +
    + + @if (this.ViewContext.ModelState.IsValid && Model.EventId is not null) + { + + Ticket purchase page + + } +
    +
    +
    +
    + + diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/View.cshtml b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/View.cshtml new file mode 100644 index 0000000..5f66d28 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/TicketTailor/View.cshtml @@ -0,0 +1,163 @@ +@using Microsoft.AspNetCore.Routing +@using BTCPayServer.Plugins.TicketTailor +@model BTCPayServer.Plugins.TicketTailor.TicketTailorViewModel + +@inject BTCPayServer.Security.ContentSecurityPolicies csp; +@{ + Layout = "_LayoutSimple"; + var available = Model.Settings.BypassAvailabilityCheck || (Model.Event.Unavailable != "true" && Model.Event.TicketsAvailable == "true"); + Model.Settings.SpecificTickets ??= new List(); +} + +
    +
    + + +

    @Model.Event.Title

    +

    @Model.Event.Start.Formatted - @Model.Event.EventEnd.Formatted

    + @if (Model.Settings.ShowDescription && !string.IsNullOrEmpty(Model.Event.Description)) + { +
    +
    @Safe.Raw(Model.Event.Description)
    +
    + } +
    +
    + +
    +
    + + +
    +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + @for (int x = 0; x < Model.Event.TicketTypes.Count; x++) + { + var item = Model.Event.TicketTypes[x]; + var availableToShow = new[] {"on_sale", "sold_out", "unavailable"}.Contains(item.Status); + var specific = false; + + if (Model.Settings.SpecificTickets?.Any() is true) + { + var matched = Model.Settings.SpecificTickets.FirstOrDefault(ticket => ticket.TicketTypeId == item.Id); + if (matched is null || matched.Hidden) + { + continue; + } + if (matched.Price is not null) + { + item.Price = matched.Price.Value; + } + if (!string.IsNullOrEmpty(matched.Name)) + { + item.Name = matched.Name; + } + if (!string.IsNullOrEmpty(matched.Description)) + { + item.Description = matched.Description; + } + availableToShow = true; + specific = true; + } + if (!availableToShow) + { + continue; + } +
    + + @{ CardBody(item.Name, item.Description); } + +
    + } +
    +
    + + +
    + +
    + Powered by BTCPay Server +
    +
    +
    +
    + +@functions { + + + private void CardBody(string title, string description) + { +
    +
    @title
    + @if (!String.IsNullOrWhiteSpace(description)) + { +

    @Html.Raw(description)

    + } +
    + } + +} diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/Views/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/_ViewImports.cshtml new file mode 100644 index 0000000..04173ab --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/Views/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using BTCPayServer.Abstractions.Services + +@inject Safe Safe +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs new file mode 100644 index 0000000..9933efa --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; +using Microsoft.Extensions.Logging; +using NBitcoin; +using WalletWasabi.Blockchain.TransactionOutputs; +using WalletWasabi.Crypto.Randomness; +using WalletWasabi.Extensions; +using WalletWasabi.WabiSabi.Backend.Rounds; +using WalletWasabi.WabiSabi.Client; +using WalletWasabi.Wallets; + +namespace BTCPayServer.Plugins.Wabisabi; + +public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector +{ + private readonly BTCPayWallet _wallet; + private readonly ILogger _logger; + + public BTCPayCoinjoinCoinSelector(BTCPayWallet wallet, ILogger logger) + { + _wallet = wallet; + _logger = logger; + } + + public async Task> SelectCoinsAsync(IEnumerable coinCandidates, + UtxoSelectionParameters utxoSelectionParameters, + Money liquidityClue, SecureRandom secureRandom) + { + coinCandidates = + coinCandidates + .Where(coin => utxoSelectionParameters.AllowedInputScriptTypes.Contains(coin.ScriptType)) + .Where(coin => utxoSelectionParameters.AllowedInputAmounts.Contains(coin.Amount)) + .Where(coin => + { + var effV = coin.EffectiveValue(utxoSelectionParameters.MiningFeeRate, + utxoSelectionParameters.CoordinationFeeRate); + var percentageLeft = (effV.ToDecimal(MoneyUnit.BTC) / coin.Amount.ToDecimal(MoneyUnit.BTC)); + // filter out low value coins where 50% of the value would be eaten up by fees + return effV > 0 && percentageLeft >= 0.5m; + }); + var payments = + _wallet.BatchPayments + ? await _wallet.DestinationProvider.GetPendingPaymentsAsync(utxoSelectionParameters) + : Array.Empty(); + var minCoins = new Dictionary(); + if (_wallet.RedCoinIsolation) + { + minCoins.Add(AnonsetType.Red, 1); + } + + var solution = SelectCoinsInternal(utxoSelectionParameters, coinCandidates, payments, + Random.Shared.Next(10, 31), + minCoins, + new Dictionary() {{AnonsetType.Red, 1}, {AnonsetType.Orange, 1}, {AnonsetType.Green, 1}}, + _wallet.ConsolidationMode, liquidityClue); + _logger.LogInformation(solution.ToString()); + // SubsetSolution bestSolution = null; + // for (int i = 0; i < 100; i++) + // { + // var minCoins = new Dictionary(); + // if (_wallet.RedCoinIsolation) + // { + // minCoins.Add(AnonsetType.Red, 1); + // } + // var solution = SelectCoinsInternal(utxoSelectionParameters, coinCandidates, payments, Random.Shared.Next(10,31), + // minCoins, new Dictionary() + // { + // + // {AnonsetType.Red, 1}, + // {AnonsetType.Orange, 1}, + // {AnonsetType.Green, 1} + // },_wallet.ConsolidationMode, liquidityClue); + // if (bestSolution is null || solution.Score() > bestSolution.Score()) + // { + // bestSolution = solution; + // } + // } + // _logger.LogInformation(bestSolution.ToString()); + // return bestSolution.Coins.ToImmutableList(); + return solution.Coins.ToImmutableList(); + } + + private SubsetSolution SelectCoinsInternal(UtxoSelectionParameters utxoSelectionParameters, + IEnumerable coins, IEnumerable pendingPayments, + int maxCoins, + Dictionary maxPerType, Dictionary idealMinimumPerType, + bool consolidationMode, Money liquidityClue) + { + var stopwatch = Stopwatch.StartNew(); + + // Sort the coins by their anon score and then by descending order their value, and then slightly randomize in 2 ways: + //attempt to shift coins that comes from the same tx AND also attempt to shift coins based on percentage probability + var remainingCoins = SlightlyShiftOrder(RandomizeCoins( + coins.OrderBy(coin => coin.CoinColor(_wallet.AnonymitySetTarget)).ThenByDescending(x => + x.EffectiveValue(utxoSelectionParameters.MiningFeeRate, + utxoSelectionParameters.CoordinationFeeRate)) + .ToList(), liquidityClue), 10); + var remainingPendingPayments = new List(pendingPayments); + var solution = new SubsetSolution(remainingPendingPayments.Count, _wallet.AnonymitySetTarget, + utxoSelectionParameters); + + if (remainingCoins.All(coin => coin.CoinColor(_wallet.AnonymitySetTarget) == AnonsetType.Green) && + !remainingPendingPayments.Any()) + { + // var decidedAmt = Random.Shared.Next(10, maxCoins); + // // all the coins are mixed and we have no payments to do.. + // //if we are trying to reduce our utxoset, and we + // if (consolidationMode && remainingCoins.Count >= decidedAmt) + // { + // + // for (int i = 0; i < decidedAmt; i++) + // { + // + // var anonsetOrderedCoin = + // remainingCoins.OrderBy(coin => coin.AnonymitySet).BiasedRandomElement(70); + // solution.Coins.Add(anonsetOrderedCoin); + // remainingCoins.Remove(anonsetOrderedCoin); + // } + // } + // else + // { + //still good to have a chance to proceed with a join to reduce timing analysis + + var rand = Random.Shared.Next(1, 101); + if (rand > 5) + { + _logger.LogInformation($"All coins are private and we have no pending payments. Skipping join."); + return solution; + } + + _logger.LogInformation( + "All coins are private and we have no pending payments but will join just to reduce timing analysis"); + //} + } + + while (remainingCoins.Any()) + { + var coinColorCount = solution.SortedCoins.ToDictionary(pair => pair.Key, pair => pair.Value.Length); + + var predicate = new Func(_ => true); + foreach (var coinColor in idealMinimumPerType.ToShuffled()) + { + if (coinColor.Value != 0) + { + coinColorCount.TryGetValue(coinColor.Key, out var currentCoinColorCount); + if (currentCoinColorCount < coinColor.Value) + { + predicate = coin1 => coin1.CoinColor(_wallet.AnonymitySetTarget) == coinColor.Key; + break; + } + } + else + { + //if the ideal amount = 0, then we should de-prioritize. + predicate = coin1 => coin1.CoinColor(_wallet.AnonymitySetTarget) != coinColor.Key; + break; + } + } + + var coin = remainingCoins.FirstOrDefault(predicate) ?? remainingCoins.First(); + var color = coin.CoinColor(_wallet.AnonymitySetTarget); + // If the selected coins list is at its maximum size, break out of the loop + if (solution.Coins.Count == maxCoins) + { + break; + } + + remainingCoins.Remove(coin); + if (maxPerType.TryGetValue(color, out var maxColor) && + solution.Coins.Count(coin1 => coin1.CoinColor(_wallet.AnonymitySetTarget) == color) == maxColor) + { + continue; + } + + solution.Coins.Add(coin); + + // Loop through the pending payments and handle each payment by subtracting the payment amount from the total value of the selected coins + var potentialPayments = remainingPendingPayments + .Where(payment => + payment.ToTxOut().EffectiveCost(utxoSelectionParameters.MiningFeeRate).ToDecimal(MoneyUnit.BTC) <= + solution.LeftoverValue).ToShuffled(); + + while (potentialPayments.Any()) + { + var payment = potentialPayments.First(); + solution.HandledPayments.Add(payment); + remainingPendingPayments.Remove(payment); + potentialPayments = remainingPendingPayments.Where(payment => + payment.ToTxOut().EffectiveCost(utxoSelectionParameters.MiningFeeRate).ToDecimal(MoneyUnit.BTC) <= + solution.LeftoverValue).ToShuffled(); + } + + if (!remainingPendingPayments.Any()) + { + //if we're in consolidation mode, we should use more than one coin at the very least + if (solution.Coins.Count == 1 && consolidationMode) + { + continue; + } + + var rand = Random.Shared.Next(1, 101); + //let's check how many coins we are allowed to add max and how many we added, and use that percentage as the random chance of not adding it. + // if max coins = 20, and current coins = 5 then 5/20 = 0.25 * 100 = 25 + var maxCoinCapacityPercentage = Math.Floor((solution.Coins.Count / (decimal)maxCoins) * 100); + //aggressively attempt to reach max coin target if consolidation mode is on + var chance = consolidationMode ? 90 : 100 - maxCoinCapacityPercentage; + _logger.LogDebug( + $"coin selection: no payms left but at {solution.Coins.Count()} coins. random chance to add another coin if: {chance} <= {rand} (random 0-100) "); + if (chance <= rand) + { + break; + } + } + } + + stopwatch.Stop(); + solution.TimeElapsed = stopwatch.Elapsed; + return solution; + } + + static List SlightlyShiftOrder(List list, int chanceOfShiftPercentage) + { + // Create a random number generator + var rand = new Random(); + List workingList = new List(list); +// Loop through the coins and determine whether to swap the positions of two consecutive coins in the list + for (int i = 0; i < workingList.Count() - 1; i++) + { + // If a random number between 0 and 1 is less than or equal to 0.1, swap the positions of the current and next coins in the list + if (rand.NextDouble() <= ((double)chanceOfShiftPercentage / 100)) + { + // Swap the positions of the current and next coins in the list + (workingList[i], workingList[i + 1]) = (workingList[i + 1], workingList[i]); + } + } + + return workingList; + } + + private List RandomizeCoins(List coins, Money liquidityClue) + { + var remainingCoins = new List(coins); + var workingList = new List(); + while (remainingCoins.Any()) + { + var currentCoin = remainingCoins.First(); + remainingCoins.RemoveAt(0); + var lastCoin = workingList.LastOrDefault(); + if (lastCoin is null || currentCoin.CoinColor(_wallet.AnonymitySetTarget) == AnonsetType.Green || + !remainingCoins.Any() || + (remainingCoins.Count == 1 && remainingCoins.First().TransactionId == currentCoin.TransactionId) || + lastCoin.TransactionId != currentCoin.TransactionId || + liquidityClue <= currentCoin.Amount || + Random.Shared.Next(0, 10) < 5) + { + workingList.Add(currentCoin); + } + else + { + remainingCoins.Insert(1, currentCoin); + } + } + + + return workingList.ToList(); + } +} + +public static class SmartCoinExtensions +{ + public static AnonsetType CoinColor(this SmartCoin coin, int anonsetTarget) + { + return coin.AnonymitySet <= 1 ? AnonsetType.Red : + coin.AnonymitySet >= anonsetTarget ? AnonsetType.Green : AnonsetType.Orange; + } +} + +public enum AnonsetType +{ + Red, + Orange, + Green +} + +public class SubsetSolution : IEquatable +{ + private readonly UtxoSelectionParameters _utxoSelectionParameters; + + public SubsetSolution(int totalPaymentsGross, int anonsetTarget, UtxoSelectionParameters utxoSelectionParameters) + { + _utxoSelectionParameters = utxoSelectionParameters; + TotalPaymentsGross = totalPaymentsGross; + AnonsetTarget = anonsetTarget; + } + + public TimeSpan TimeElapsed { get; set; } + public List Coins { get; set; } = new(); + public List HandledPayments { get; set; } = new(); + + public decimal TotalValue => Coins.Sum(coin => + coin.EffectiveValue(_utxoSelectionParameters.MiningFeeRate, _utxoSelectionParameters.CoordinationFeeRate) + .ToDecimal(MoneyUnit.BTC)); + + public Dictionary SortedCoins => + Coins.GroupBy(coin => coin.CoinColor(AnonsetTarget)).ToDictionary(coins => coins.Key, coins => coins.ToArray()); + + public int TotalPaymentsGross { get; } + public int AnonsetTarget { get; } + + public decimal TotalPaymentCost => HandledPayments.Sum(payment => + payment.ToTxOut().EffectiveCost(_utxoSelectionParameters.MiningFeeRate).ToDecimal(MoneyUnit.BTC)); + + public decimal LeftoverValue => TotalValue - TotalPaymentCost; + + public decimal Score() + { + var score = 0m; + + decimal ComputeCoinScore(List coins) + { + var w = 0m; + foreach (var smartCoin in coins) + { + var val = smartCoin.EffectiveValue(_utxoSelectionParameters.MiningFeeRate, + _utxoSelectionParameters.CoordinationFeeRate).ToDecimal(MoneyUnit.BTC); + if (smartCoin.AnonymitySet <= 0) + { + w += val; + } + else + { + w += val / (decimal)smartCoin.AnonymitySet; + } + } + + return w; // / (coins.Count == 0 ? 1 : coins.Count); + } + + decimal ComputePaymentScore(List pendingPayments) + { + return TotalPaymentsGross == 0 ? 100 : (pendingPayments.Count / (decimal)TotalPaymentsGross) * 100; + } + + score += ComputeCoinScore(Coins); + score += ComputePaymentScore(HandledPayments); + + return score; + } + + + public string GetId() + { + return string.Join("-", + Coins.OrderBy(coin => coin.Outpoint).Select(coin => coin.Outpoint.ToString()) + .Concat(HandledPayments.OrderBy(arg => arg.Value).Select(p => p.Value.ToString()))); + } + + public override string ToString() + { + var sb = new StringBuilder(); + if (!Coins.Any()) + { + return "Solution yielded no selection of coins"; + } + + var sc = SortedCoins; + sc.TryGetValue(AnonsetType.Green, out var gcoins); + sc.TryGetValue(AnonsetType.Orange, out var ocoins); + sc.TryGetValue(AnonsetType.Red, out var rcoins); + sb.AppendLine( + $"Solution total coins:{Coins.Count} R:{rcoins?.Length ?? 0} O:{ocoins?.Length ?? 0} G:{gcoins?.Length ?? 0} AL:{GetAnonLoss(Coins)} total value: {TotalValue} total payments:{TotalPaymentCost}/{TotalPaymentsGross} leftover: {LeftoverValue} score: {Score()} Compute time: {TimeElapsed} "); + sb.AppendLine( + $"Used coins: {string.Join(", ", Coins.Select(coin => coin.Outpoint + " " + coin.Amount.ToString() + " A" + coin.AnonymitySet))}"); + if (HandledPayments.Any()) + sb.AppendLine($"handled payments: {string.Join(", ", HandledPayments.Select(p => p.Value))} "); + return sb.ToString(); + } + + public bool Equals(SubsetSolution? other) + { + return GetId() == other?.GetId(); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((SubsetSolution)obj); + } + + private static decimal GetAnonLoss(IEnumerable coins) + where TCoin : SmartCoin + { + double minimumAnonScore = coins.Min(x => x.AnonymitySet); + var rawSum = coins.Sum(x => x.Amount); + return coins.Sum(x => + ((decimal)x.AnonymitySet - (decimal)minimumAnonScore) * x.Amount.ToDecimal(MoneyUnit.BTC)) / rawSum; + } +} diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayKeyChain.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayKeyChain.cs new file mode 100644 index 0000000..e99826d --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayKeyChain.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NBitcoin; +using NBXplorer; +using NBXplorer.DerivationStrategy; +using WalletWasabi.Blockchain.Keys; +using WalletWasabi.Crypto; +using WalletWasabi.Extensions; +using WalletWasabi.WabiSabi.Client; + +namespace BTCPayServer.Plugins.Wabisabi; + +public class BTCPayKeyChain : IKeyChain +{ + private readonly ExplorerClient _explorerClient; + private readonly DerivationStrategyBase _derivationStrategy; + private readonly ExtKey _masterKey; + private readonly ExtKey _accountKey; + + public bool KeysAvailable => _masterKey is not null && _accountKey is not null; + + public BTCPayKeyChain(ExplorerClient explorerClient, DerivationStrategyBase derivationStrategy, ExtKey masterKey, + ExtKey accountKey) + { + _explorerClient = explorerClient; + _derivationStrategy = derivationStrategy; + _masterKey = masterKey; + _accountKey = accountKey; + } + + + public OwnershipProof GetOwnershipProof(IDestination destination, CoinJoinInputCommitmentData committedData) + { + return NBitcoinExtensions.GetOwnershipProof(_masterKey.PrivateKey, GetBitcoinSecret(destination.ScriptPubKey), + destination.ScriptPubKey, committedData); + } + + public Transaction Sign(Transaction transaction, Coin coin, PrecomputedTransactionData precomputeTransactionData) + { + transaction = transaction.Clone(); + + if (transaction.Inputs.Count == 0) + { + throw new ArgumentException("No inputs to sign.", nameof(transaction)); + } + + var txInput = transaction.Inputs.AsIndexedInputs().FirstOrDefault(input => input.PrevOut == coin.Outpoint); + + if (txInput is null) + { + throw new InvalidOperationException("Missing input."); + } + + + BitcoinSecret secret = GetBitcoinSecret(coin.ScriptPubKey); + + TransactionBuilder builder = Network.Main.CreateTransactionBuilder(); + builder.AddKeys(secret); + builder.AddCoins(coin); + builder.SetSigningOptions(new SigningOptions(TaprootSigHash.All, + (TaprootReadyPrecomputedTransactionData)precomputeTransactionData)); + builder.SignTransactionInPlace(transaction); + + return transaction; + } + + public void TrySetScriptStates(KeyState state, IEnumerable diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/UpdateWabisabiStoreSettings.cshtml b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/UpdateWabisabiStoreSettings.cshtml new file mode 100644 index 0000000..d668792 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/WabisabiStore/UpdateWabisabiStoreSettings.cshtml @@ -0,0 +1,322 @@ +@using BTCPayServer.Plugins.Wabisabi +@using BTCPayServer.Abstractions.Extensions +@using BTCPayServer.Abstractions.Contracts +@using BTCPayServer.Security +@using NBitcoin +@using System.Security.Claims +@using BTCPayServer +@using BTCPayServer.Client +@using BTCPayServer.Common +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using WalletWasabi.Backend.Controllers +@model BTCPayServer.Plugins.Wabisabi.WabisabiStoreSettings +@inject ContentSecurityPolicies contentSecurityPolicies +@inject WabisabiCoordinatorClientInstanceManager WabisabiCoordinatorClientInstanceManager +@inject IScopeProvider _scopeProvider +@inject IExplorerClientProvider ExplorerClientProvider; +@inject IBTCPayServerClientFactory ClientFactory +@inject WalletProvider WalletProvider +@{ + var storeId = _scopeProvider.GetCurrentStoreId(); + Layout = "../Shared/_NavLayout.cshtml"; + ViewData["NavPartialName"] = "../UIStores/_Nav"; + ViewData.SetActivePage("Plugins", "BTCPayServer.Views.Stores.StoreNavPages", "Wabisabi coinjoin support", storeId); + var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32); + contentSecurityPolicies.Add("script-src", $"'nonce-{nonce}'"); + contentSecurityPolicies.AllowUnsafeHashes(); + var explorerClient = ExplorerClientProvider.GetExplorerClient("BTC"); + var userid = Context.User.Claims.Single(claim => claim.Type == ClaimTypes.NameIdentifier).Value; + var anyEnabled = Model.Settings.Any(settings => settings.Enabled); + var Client = await ClientFactory.Create(userid, storeId); + ScriptPubKeyType? scriptType = null; + try + { + var pm = await Client.GetStoreOnChainPaymentMethod(storeId, "BTC"); + scriptType = explorerClient.Network.DerivationStrategyFactory.Parse(pm.DerivationScheme).ScriptPubKeyType(); + } + catch (Exception e) + { + } + Client = await ClientFactory.Create(userid); + var stores = (await Client.GetStores()) + .Where(data => data.Id != storeId) + .ToDictionary(s => s.Id, async s => + { + try + { + var sclient = await ClientFactory.Create(userid, s.Id, storeId); + var pm = await sclient.GetStoreOnChainPaymentMethod(s.Id, "BTC"); + if (explorerClient.Network.DerivationStrategyFactory.Parse(pm.DerivationScheme).ScriptPubKeyType() != scriptType) + { + return null; + } + return s.Name; + } + catch (Exception e) + { + return null; + } + }); + await Task.WhenAll(stores.Values); + var selectStores = + stores.Where(pair => pair.Value.Result is not null) + .Select(pair => new SelectListItem(pair.Value.Result, pair.Key, Model.MixToOtherWallet == pair.Key)).Prepend(new SelectListItem("None", "")); +} + + +

    Coinjoin configuration

    + +
    + @{ + var wallet = await WalletProvider.GetWalletAsync(storeId); + if (wallet is BTCPayWallet btcPayWallet) + { + @if (btcPayWallet.OnChainPaymentMethodData?.Enabled is not true) + { + + } + else if (!((BTCPayKeyChain) wallet.KeyChain).KeysAvailable) + { + + } + } + } + + +
    +
    +
    +
    + + +

    I just want to coinjoin.

    +
    +
    +
    +
    + + +

    The world is broken and I need to be vigilant about my bitcoin practices.

    +
    +
    +
    +
    +
    + + + + +

    Scores your coinjoined utxos based on how many other utxos in the coinjoin (and other previous coinjoin rounds) had the same value.
    Anonset score computation is not an exact science, and when using coordinators with massive liquidity, is not that important as all rounds (past, present, future) contribute to your privacy.

    +
    +
    + + +

    Feed as many coins to the coinjoin as possible.

    +
    +
    + + +

    Only allow a single non-private coin into a coinjoin.

    +
    +
    + + +

    Batch your pending payments (on-chain payouts awaiting payment) inside coinjoins.

    +
    +
    + + +

    Send coins that have been created in a coinjoin in a standard denomination to another wallet

    +
    + +
    +
    Only mix coins with these labels
    + @if (Model.InputLabelsAllowed?.Any() is not true) + { +
    No label filter applied
    + } + else + { + @for (var xIndex = 0; xIndex < Model.InputLabelsAllowed.Count; xIndex++) + { +
    +
    + + +
    +
    + } + } +
    + +
    +
    +
    +
    Only mix coins without these labels
    + @if (Model.InputLabelsExcluded?.Any() is not true) + { +
    No label filter applied
    + } + else + { + @for (var xIndex = 0; xIndex < Model.InputLabelsExcluded.Count; xIndex++) + { +
    + +
    + + +
    +
    + } + } +
    + +
    +
    + +
    +
    + + @for (var index = 0; index < Model.Settings.Count; index++) + { + + var s = Model.Settings[index]; + + if (! WabisabiCoordinatorClientInstanceManager.HostedServices.TryGetValue(s.Coordinator, out var coordinator)) + { + continue; + } +
    +
    +
    +
    + +

    @coordinator.CoordinatorDisplayName

    + +
    + + @coordinator.Coordinator +
    + @if (!coordinator.WasabiCoordinatorStatusFetcher.Connected) + { +

    Coordinator Status: Not connected

    + } + else + { +

    + Coordinator Status: Connected + + T&C + +

    + } +
    +
    +
    + + +
    + @if (coordinator.CoordinatorName != "local" && coordinator.CoordinatorName != "zksnacks") + { + + } +
    + + +
    + } + @if (ViewBag.DiscoveredCoordinators is List discoveredCoordinators) + { + foreach (var coordinator in discoveredCoordinators) + { + +
    +
    +
    +
    + +

    @coordinator.Name

    + +
    + + @coordinator.Uri +
    +
    + +
    +
    + + +
    + + } + } + + Coordinator runner + + Enable Discrete payments - Coming soon + + + + + +@section PageFootContent { + +} + + diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/Views/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/_ViewImports.cshtml new file mode 100644 index 0000000..04173ab --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/Views/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using BTCPayServer.Abstractions.Services + +@inject Safe Safe +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiCoordinatorClientInstance.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiCoordinatorClientInstance.cs new file mode 100644 index 0000000..361ee49 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiCoordinatorClientInstance.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Payments.PayJoin; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NBitcoin; +using WalletWasabi.Services; +using WalletWasabi.Tor.Socks5.Pool.Circuits; +using WalletWasabi.Userfacing; +using WalletWasabi.WabiSabi.Client; +using WalletWasabi.WabiSabi.Client.RoundStateAwaiters; +using WalletWasabi.WabiSabi.Client.StatusChangedEvents; +using WalletWasabi.WebClients.Wasabi; + +namespace BTCPayServer.Plugins.Wabisabi; + +public class WabisabiCoordinatorClientInstanceManager:IHostedService +{ + private readonly IServiceProvider _provider; + private readonly WalletProvider _walletProvider; + public Dictionary HostedServices { get; set; } = new(); + + public WabisabiCoordinatorClientInstanceManager(IServiceProvider provider, WalletProvider walletProvider ) + { + _provider = provider; + _walletProvider = walletProvider; + _walletProvider.WalletUnloaded += WalletProviderOnWalletUnloaded; + + } + + private void WalletProviderOnWalletUnloaded(object sender, WalletProvider.WalletUnloadEventArgs e) + { + _ =StopWallet(e.Wallet); + } + + private bool started = false; + public LocalisedUTXOLocker UTXOLocker; + + public async Task StartAsync(CancellationToken cancellationToken) + { + started = true; + foreach (KeyValuePair coordinatorManager in HostedServices) + { + await coordinatorManager.Value.StartAsync(cancellationToken); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + foreach (KeyValuePair coordinatorManager in HostedServices) + { + await coordinatorManager.Value.StopAsync(cancellationToken); + } + } + + public async Task StopWallet(string name) + { + foreach (var servicesValue in HostedServices.Values) + { + await servicesValue.StopWallet(name); + } + } + + + + public void AddCoordinator(string displayName, string name, + Func fetcher, string termsConditions = null) + { + if (HostedServices.ContainsKey(name)) + { + return; + } + var instance = new WabisabiCoordinatorClientInstance( + displayName, + name, fetcher.Invoke(_provider), _provider.GetService(), _provider, UTXOLocker, + _provider.GetService()); + if (HostedServices.TryAdd(instance.CoordinatorName, instance)) + { + if(started) + _ = instance.StartAsync(CancellationToken.None); + } + } + + public async Task RemoveCoordinator(string name) + { + if (!HostedServices.TryGetValue(name, out var s)) + { + return; + } + + await s.StopAsync(CancellationToken.None); + HostedServices.Remove(name); + } +} + +public class WabisabiCoordinatorClientInstance +{ + private readonly IUTXOLocker _utxoLocker; + private readonly ILogger _logger; + public string CoordinatorDisplayName { get; } + public string CoordinatorName { get; set; } + public Uri Coordinator { get; set; } + public WalletProvider WalletProvider { get; } + public HttpClientFactory WasabiHttpClientFactory { get; set; } + public RoundStateUpdater RoundStateUpdater { get; set; } + public WasabiCoordinatorStatusFetcher WasabiCoordinatorStatusFetcher { get; set; } + public CoinJoinManager CoinJoinManager { get; set; } + + public WabisabiCoordinatorClientInstance(string coordinatorDisplayName, + string coordinatorName, + Uri coordinator, + ILoggerFactory loggerFactory, + IServiceProvider serviceProvider, + IUTXOLocker utxoLocker, + WalletProvider walletProvider) + { + _utxoLocker = utxoLocker; + var config = serviceProvider.GetService(); + var socksEndpoint = config.GetValue("socksendpoint"); + EndPointParser.TryParse(socksEndpoint,9050, out var torEndpoint); + if (torEndpoint is not null && torEndpoint is DnsEndPoint dnsEndPoint) + { + torEndpoint = new IPEndPoint(Dns.GetHostAddresses(dnsEndPoint.Host).First(), dnsEndPoint.Port); + } + CoordinatorDisplayName = coordinatorDisplayName; + CoordinatorName = coordinatorName; + Coordinator = coordinator; + WalletProvider = walletProvider; + _logger = loggerFactory.CreateLogger(coordinatorName); + WasabiHttpClientFactory = new HttpClientFactory(torEndpoint, () => Coordinator); + var roundStateUpdaterCircuit = new PersonCircuit(); + var roundStateUpdaterHttpClient = + WasabiHttpClientFactory.NewHttpClient(Mode.SingleCircuitPerLifetime, roundStateUpdaterCircuit); + var sharedWabisabiClient = new WabiSabiHttpApiClient(roundStateUpdaterHttpClient); + WasabiCoordinatorStatusFetcher = new WasabiCoordinatorStatusFetcher(sharedWabisabiClient, _logger); + + RoundStateUpdater = new RoundStateUpdater(TimeSpan.FromSeconds(5),sharedWabisabiClient, WasabiCoordinatorStatusFetcher); + CoinJoinManager = new CoinJoinManager(coordinatorName,WalletProvider, RoundStateUpdater, WasabiHttpClientFactory, + WasabiCoordinatorStatusFetcher, "CoinJoinCoordinatorIdentifier"); + CoinJoinManager.StatusChanged += OnStatusChanged; + CoinJoinManager.OnBan += (sender, args) => + { + WalletProvider.OnBan(coordinatorName, args); + }; + + } + + public async Task StopWallet(string walletName) + { + await CoinJoinManager.StopAsyncByName(walletName, CancellationToken.None); + } + + private void OnStatusChanged(object sender, StatusChangedEventArgs e) + { + + switch (e) + { + case CoinJoinStatusEventArgs coinJoinStatusEventArgs: + _logger.LogInformation(coinJoinStatusEventArgs.CoinJoinProgressEventArgs.GetType().ToString() + " :" + + e.Wallet.WalletName); + break; + case CompletedEventArgs completedEventArgs: + + var result = completedEventArgs.CoinJoinResult; + + if (completedEventArgs.CompletionStatus == CompletionStatus.Success) + { + Task.Run(async () => + { + + var wallet = (BTCPayWallet) e.Wallet; + await wallet.RegisterCoinjoinTransaction(result, CoordinatorName); + + }); + } + else + { + Task.Run(async () => + { + // _logger.LogInformation("unlocking coins because round failed"); + await _utxoLocker.TryUnlock( + result.RegisteredCoins.Select(coin => coin.Outpoint).ToArray()); + }); + break; + } + _logger.LogInformation("Coinjoin complete! :" + + e.Wallet.WalletName); + break; + case LoadedEventArgs loadedEventArgs: + var stopWhenAllMixed = !((BTCPayWallet)loadedEventArgs.Wallet).BatchPayments; + _ = CoinJoinManager.StartAsync(loadedEventArgs.Wallet, stopWhenAllMixed, false, CancellationToken.None); + _logger.LogInformation( "Loaded wallet :" + e.Wallet.WalletName + $"stopWhenAllMixed: {stopWhenAllMixed}"); + break; + case StartErrorEventArgs errorArgs: + _logger.LogInformation("Could not start wallet for coinjoin:" + errorArgs.Error.ToString() + " :" + e.Wallet.WalletName); + break; + case StoppedEventArgs stoppedEventArgs: + _logger.LogInformation("Stopped wallet for coinjoin: " + stoppedEventArgs.Reason + " :" + e.Wallet.WalletName); + break; + default: + _logger.LogInformation(e.GetType() + " :" + e.Wallet.WalletName); + break; + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + + RoundStateUpdater.StartAsync(cancellationToken); + WasabiCoordinatorStatusFetcher.StartAsync(cancellationToken); + CoinJoinManager.StartAsync(cancellationToken); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + RoundStateUpdater.StopAsync(cancellationToken); + WasabiCoordinatorStatusFetcher.StopAsync(cancellationToken); + CoinJoinManager.StopAsync(cancellationToken); + return Task.CompletedTask; + } +} diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiPlugin.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiPlugin.cs new file mode 100644 index 0000000..36910b9 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiPlugin.cs @@ -0,0 +1,119 @@ +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using BTCPayServer.Common; +using BTCPayServer.Payments.PayJoin; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using NBitcoin; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using WalletWasabi.Backend.Controllers; +using WalletWasabi.Logging; +using WalletWasabi.WabiSabi.Client; +using WalletWasabi.WabiSabi.Models.Serialization; +using LogLevel = WalletWasabi.Logging.LogLevel; + +namespace BTCPayServer.Plugins.Wabisabi; + +public class WabisabiPlugin : BaseBTCPayServerPlugin +{ + public override string Identifier => "BTCPayServer.Plugins.Wabisabi"; + public override string Name => "Coinjoin"; + + + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new() {Identifier = nameof(BTCPayServer), Condition = ">=1.7.3.0"} + }; + + public override string Description => + "Allows you to integrate your btcpayserver store with coinjoins."; + + + public override void Execute(IServiceCollection applicationBuilder) + { + var utxoLocker = new LocalisedUTXOLocker(); + applicationBuilder.AddSingleton( + provider => + { + var res = ActivatorUtilities.CreateInstance(provider); + res.UTXOLocker = utxoLocker; + res.AddCoordinator("zkSNACKS Coordinator", "zksnacks", provider => + { + var chain = provider.GetService().GetExplorerClient("BTC").Network + .NBitcoinNetwork.ChainName; + if (chain == ChainName.Mainnet) + { + return new Uri("https://wasabiwallet.io/"); + } + + if (chain == ChainName.Testnet) + { + return new Uri("https://wasabiwallet.co/"); + } + + return new Uri("http://localhost:37127"); + }); + return res; + }); + applicationBuilder.AddHostedService(provider => + provider.GetRequiredService()); + applicationBuilder.AddSingleton(); + applicationBuilder.AddSingleton(provider => new( + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + utxoLocker + )); + applicationBuilder.AddWabisabiCoordinator(); + applicationBuilder.AddSingleton(provider => provider.GetRequiredService()); + applicationBuilder.AddHostedService(provider => provider.GetRequiredService()); + ; + applicationBuilder.AddSingleton(new UIExtension("Wabisabi/StoreIntegrationWabisabiOption", + "store-integrations-list")); + applicationBuilder.AddSingleton(new UIExtension("Wabisabi/WabisabiNav", + "store-integrations-nav")); + applicationBuilder.AddSingleton(new UIExtension("Wabisabi/WabisabiDashboard", + "dashboard")); + + Logger.SetMinimumLevel(LogLevel.Info); + Logger.SetModes(LogMode.DotNetLoggers); + + + base.Execute(applicationBuilder); + } + + + public override void Execute(IApplicationBuilder applicationBuilder, + IServiceProvider applicationBuilderApplicationServices) + { + Task.Run(async () => + { + var walletProvider = + (WalletProvider)applicationBuilderApplicationServices.GetRequiredService(); + await walletProvider.ResetWabisabiStuckPayouts(); + }); + + Logger.DotnetLogger = applicationBuilderApplicationServices.GetService>(); + base.Execute(applicationBuilder, applicationBuilderApplicationServices); + } +} diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiService.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiService.cs new file mode 100644 index 0000000..889cea7 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiService.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Client; +using Microsoft.Extensions.Caching.Memory; +using WalletWasabi.WabiSabi.Client; + +namespace BTCPayServer.Plugins.Wabisabi +{ + public class WabisabiService + { + private readonly IStoreRepository _storeRepository; + private readonly WabisabiCoordinatorClientInstanceManager _coordinatorClientInstanceManager; + private readonly WalletProvider _walletProvider; + private string[] _ids => _coordinatorClientInstanceManager.HostedServices.Keys.ToArray(); + + public WabisabiService( IStoreRepository storeRepository, + WabisabiCoordinatorClientInstanceManager coordinatorClientInstanceManager, + WalletProvider walletProvider) + { + _storeRepository = storeRepository; + _coordinatorClientInstanceManager = coordinatorClientInstanceManager; + _walletProvider = walletProvider; + } + + public async Task GetWabisabiForStore(string storeId) + { + + var res = await _storeRepository.GetSettingAsync(storeId, nameof(WabisabiStoreSettings)); + res ??= new WabisabiStoreSettings(); + res.Settings = res.Settings.Where(settings => _ids.Contains(settings.Coordinator)).ToList(); + foreach (var wabisabiCoordinatorManager in _coordinatorClientInstanceManager.HostedServices) + { + if (res.Settings.All(settings => settings.Coordinator != wabisabiCoordinatorManager.Key)) + { + res.Settings.Add(new WabisabiStoreCoordinatorSettings() + { + Coordinator = wabisabiCoordinatorManager.Key, + }); + } + } + + return res; + } + + public async Task SetWabisabiForStore(string storeId, WabisabiStoreSettings wabisabiSettings) + { + + foreach (var setting in wabisabiSettings.Settings) + { + if (setting.Enabled) continue; + if(_coordinatorClientInstanceManager.HostedServices.TryGetValue(setting.Coordinator, out var coordinator)) + _ = coordinator.StopWallet(storeId); + } + + if (wabisabiSettings.Settings.All(settings => !settings.Enabled)) + { + + await _storeRepository.UpdateSetting(storeId, nameof(WabisabiStoreSettings), null!); + } + else + { + await _storeRepository.UpdateSetting(storeId, nameof(WabisabiStoreSettings), wabisabiSettings!); + } + + await _walletProvider.SettingsUpdated(storeId, wabisabiSettings); + + } + } + +} diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStore.Wabisabi.csproj b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStore.Wabisabi.csproj new file mode 100644 index 0000000..acf3e39 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStore.Wabisabi.csproj @@ -0,0 +1,41 @@ + + + net6.0 + true + false + true + 0.0.10 + + + + + Debug;Release;Altcoins-Release;Altcoins-Debug + AnyCPU + + + + + true + true + + + $(DefineConstants);DEBUG + true + + + + + + + + + + + + + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreController.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreController.cs new file mode 100644 index 0000000..46a584f --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreController.cs @@ -0,0 +1,375 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using AngleSharp.Dom.Events; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Common; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using NBitcoin; +using NBitcoin.Payment; +using NBXplorer; +using Newtonsoft.Json.Linq; +using NNostr.Client; +using WalletWasabi.Backend.Controllers; +using WalletWasabi.Blockchain.TransactionBuilding; +using WalletWasabi.Blockchain.TransactionOutputs; + +namespace BTCPayServer.Plugins.Wabisabi +{ + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Route("plugins/{storeId}/Wabisabi")] + public partial class WabisabiStoreController : Controller + { + private readonly WabisabiService _WabisabiService; + private readonly WalletProvider _walletProvider; + private readonly IBTCPayServerClientFactory _btcPayServerClientFactory; + private readonly IExplorerClientProvider _explorerClientProvider; + private readonly WabisabiCoordinatorService _wabisabiCoordinatorService; + private readonly WabisabiCoordinatorClientInstanceManager _instanceManager; + + public WabisabiStoreController(WabisabiService WabisabiService, WalletProvider walletProvider, + IBTCPayServerClientFactory btcPayServerClientFactory, + IExplorerClientProvider explorerClientProvider, + WabisabiCoordinatorService wabisabiCoordinatorService, + WabisabiCoordinatorClientInstanceManager instanceManager) + { + _WabisabiService = WabisabiService; + _walletProvider = walletProvider; + _btcPayServerClientFactory = btcPayServerClientFactory; + _explorerClientProvider = explorerClientProvider; + _wabisabiCoordinatorService = wabisabiCoordinatorService; + _instanceManager = instanceManager; + } + + [HttpGet("")] + public async Task UpdateWabisabiStoreSettings(string storeId) + { + WabisabiStoreSettings Wabisabi = null; + try + { + Wabisabi = await _WabisabiService.GetWabisabiForStore(storeId); + } + catch (Exception) + { + // ignored + } + + return View(Wabisabi); + } + + public const int coordinatorEventKind = 15750; + + [HttpPost("")] + public async Task UpdateWabisabiStoreSettings(string storeId, WabisabiStoreSettings vm, + string command) + { + var pieces = command.Split(":"); + var actualCommand = pieces[0]; + var commandIndex = pieces.Length > 1 ? pieces[1] : null; + var coordinator = pieces.Length > 2 ? pieces[2] : null; + var coord = vm.Settings.SingleOrDefault(settings => settings.Coordinator == coordinator); + ModelState.Clear(); + + WabisabiCoordinatorSettings coordSettings; + switch (actualCommand) + { + case "discover": + coordSettings = await _wabisabiCoordinatorService.GetSettings(); + var relay = commandIndex ?? + (await _wabisabiCoordinatorService.GetSettings())?.NostrRelay.ToString(); + + if (Uri.TryCreate(relay, UriKind.Absolute, out var relayUri)) + { + using var nostrClient = new NostrClient(relayUri); + await nostrClient.CreateSubscription("nostr-wabisabi-coordinators", + new[] + { + new NostrSubscriptionFilter() + { + Kinds = new[] {coordinatorEventKind}, + Since = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromHours(1)), + } + }); + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + await nostrClient.ConnectAndWaitUntilConnected(cts.Token); + _ = nostrClient.ListenForMessages(); + var result = new List(); + var tcs = new TaskCompletionSource(); + Stopwatch stopwatch = new(); + stopwatch.Start(); + nostrClient.MessageReceived += (sender, s) => + { + if (JArray.Parse(s).FirstOrDefault()?.Value() == "EOSE") + { + tcs.SetResult(); + } + }; + nostrClient.EventsReceived += (sender, tuple) => + { + stopwatch.Restart(); + result.AddRange(tuple.events); + }; + while (!tcs.Task.IsCompleted && !cts.IsCancellationRequested && + stopwatch.ElapsedMilliseconds < 10000) + { + await Task.Delay(1000, cts.Token); + } + + nostrClient.Dispose(); + + var network = _explorerClientProvider.GetExplorerClient("BTC").Network.NBitcoinNetwork.Name + .ToLower(); + ViewBag.DiscoveredCoordinators = result.Where(@event => + @event.CreatedAt < DateTimeOffset.UtcNow.AddMinutes(15) && + @event.Verify() && + @event.Tags.Any(tag => + tag.TagIdentifier == "uri" && + tag.Data.Any(s => Uri.IsWellFormedUriString(s, UriKind.Absolute))) && + @event.Tags.Any(tag => + tag.TagIdentifier == "network" && tag.Data.FirstOrDefault() == network) + ).Select(@event => new DiscoveredCoordinator() + { + Name = @event.PublicKey, + Uri = new Uri(@event.GetTaggedData("uri") + .First(s => Uri.IsWellFormedUriString(s, UriKind.Absolute))) + }).Where(discoveredCoordinator => string.IsNullOrEmpty(coordSettings.NostrIdentity) || discoveredCoordinator.Name != coordSettings.PubKey?.ToHex()).ToList(); + } + else + { + TempData["ErrorMessage"] = $"No relay uri was provided"; + } + + return View(vm); + case "add-coordinator": + var name = commandIndex; + var uri = coordinator; + + coordSettings = await _wabisabiCoordinatorService.GetSettings(); + if (coordSettings.DiscoveredCoordinators.All(discoveredCoordinator => + discoveredCoordinator.Name != name)) + { + coordSettings.DiscoveredCoordinators.Add(new DiscoveredCoordinator() {Name = name,}); + await _wabisabiCoordinatorService.UpdateSettings(coordSettings); + _instanceManager.AddCoordinator($"nostr[{name}]", name, provider => new Uri(uri)); + + TempData["SuccessMessage"] = $"Coordinator {commandIndex} added and started"; + return RedirectToAction(nameof(UpdateWabisabiStoreSettings), new {storeId}); + } + + else + { + TempData["ErrorMessage"] = + $"Coordinator {commandIndex} could not be added because the name was not unique"; + return View(vm); + } + + break; + case "remove-coordinator": + coordSettings = await _wabisabiCoordinatorService.GetSettings(); + if (coordSettings.DiscoveredCoordinators.RemoveAll(discoveredCoordinator => + discoveredCoordinator.Name == commandIndex) > 0) + { + TempData["SuccessMessage"] = $"Coordinator {commandIndex} stopped and removed"; + await _wabisabiCoordinatorService.UpdateSettings(coordSettings); + await _instanceManager.RemoveCoordinator(commandIndex); + return RedirectToAction(nameof(UpdateWabisabiStoreSettings), new {storeId}); + } + else + { + TempData["ErrorMessage"] = + $"Coordinator {commandIndex} could not be removed because it was not found"; + } + + return View(vm); + break; + case "check": + await _walletProvider.Check(storeId, CancellationToken.None); + TempData["SuccessMessage"] = "Store wallet re-checked"; + return RedirectToAction(nameof(UpdateWabisabiStoreSettings), new {storeId}); + case "exclude-label-add": + vm.InputLabelsExcluded.Add(""); + return View(vm); + + case "exclude-label-remove": + vm.InputLabelsExcluded.Remove(commandIndex); + return View(vm); + case "include-label-add": + vm.InputLabelsAllowed.Add(""); + return View(vm); + case "include-label-remove": + vm.InputLabelsAllowed.Remove(commandIndex); + return View(vm); + + case "save": + foreach (WabisabiStoreCoordinatorSettings settings in vm.Settings) + { + vm.InputLabelsAllowed = vm.InputLabelsAllowed.Where(s => !string.IsNullOrEmpty(s)).Distinct() + .ToList(); + vm.InputLabelsExcluded = vm.InputLabelsExcluded.Where(s => !string.IsNullOrEmpty(s)).Distinct() + .ToList(); + } + + await _WabisabiService.SetWabisabiForStore(storeId, vm); + TempData["SuccessMessage"] = "Wabisabi settings modified"; + return RedirectToAction(nameof(UpdateWabisabiStoreSettings), new {storeId}); + + default: + return View(vm); + } + } + + [HttpGet("spend")] + public async Task Spend(string storeId) + { + if ((await _walletProvider.GetWalletAsync(storeId)) is not BTCPayWallet wallet) + { + return NotFound(); + } + + return View(new SpendViewModel() { }); + } + + [HttpPost("spend")] + public async Task Spend(string storeId, SpendViewModel spendViewModel, string command) + { + if ((await _walletProvider.GetWalletAsync(storeId)) is not BTCPayWallet wallet) + { + return NotFound(); + } + + var n = _explorerClientProvider.GetExplorerClient("BTC").Network.NBitcoinNetwork; + if (string.IsNullOrEmpty(spendViewModel.Destination)) + { + ModelState.AddModelError(nameof(spendViewModel.Destination), + "A destination is required"); + } + else + { + try + { + BitcoinAddress.Create(spendViewModel.Destination, n); + } + catch (Exception e) + { + try + { + new BitcoinUrlBuilder(spendViewModel.Destination, n); + } + catch (Exception exception) + { + ModelState.AddModelError(nameof(spendViewModel.Destination), + "A destination must be a bitcoin address or a bip21 payment link"); + } + } + } + + if (spendViewModel.Amount is null) + { + try + { + spendViewModel.Amount = + new BitcoinUrlBuilder(spendViewModel.Destination, n).Amount.ToDecimal(MoneyUnit.BTC); + } + catch (Exception e) + { + ModelState.AddModelError(nameof(spendViewModel.Amount), + "An amount was not specified and the destination did not have an amount specified"); + } + } + + if (!ModelState.IsValid) + { + return View(); + } + + if (command == "payout") + { + var client = await _btcPayServerClientFactory.Create(null, storeId); + await client.CreatePayout(storeId, + new CreatePayoutThroughStoreRequest() + { + Approved = true, Amount = spendViewModel.Amount, Destination = spendViewModel.Destination + }); + + TempData["SuccessMessage"] = + "The payment has been scheduled. If payment batching is enabled in the coinjoin settings, and the coordinator supports sending that amount and that address type, it will be batched."; + + return RedirectToAction("UpdateWabisabiStoreSettings", new {storeId}); + } + + var coins = await wallet.GetAllCoins(); + if (command == "compute-with-selection") + { + if (spendViewModel.SelectedCoins?.Any() is true) + { + coins = (CoinsView)coins.FilterBy(coin => + spendViewModel.SelectedCoins.Contains(coin.Outpoint.ToString())); + } + } + + if (command == "compute-with-selection" || command == "compute") + { + if (spendViewModel.Amount is null) + { + spendViewModel.Amount = + new BitcoinUrlBuilder(spendViewModel.Destination, n).Amount.ToDecimal(MoneyUnit.BTC); + } + + var defaultCoinSelector = new DefaultCoinSelector(); + var defaultSelection = + (defaultCoinSelector.Select(coins.Select(coin => coin.Coin).ToArray(), + new Money((decimal)spendViewModel.Amount, MoneyUnit.BTC)) ?? Array.Empty()) + .ToArray(); + var selector = new SmartCoinSelector(coins.ToList()); + var smartSelection = selector.Select(defaultSelection, + new Money((decimal)spendViewModel.Amount, MoneyUnit.BTC)); + spendViewModel.SelectedCoins = smartSelection.Select(coin => coin.Outpoint.ToString()).ToArray(); + return View(spendViewModel); + } + + if (command == "send") + { + var userid = HttpContext.User.Claims.Single(claim => claim.Type == ClaimTypes.NameIdentifier).Value; + var client = await _btcPayServerClientFactory.Create(userid, storeId); + var tx = await client.CreateOnChainTransaction(storeId, "BTC", + new CreateOnChainTransactionRequest() + { + SelectedInputs = spendViewModel.SelectedCoins?.Select(OutPoint.Parse).ToList(), + Destinations = + new List() + { + new CreateOnChainTransactionRequest.CreateOnChainTransactionRequestDestination() + { + Destination = spendViewModel.Destination, Amount = spendViewModel.Amount + } + } + }); + + TempData["SuccessMessage"] = + $"The tx {tx.TransactionHash} has been broadcast."; + + return RedirectToAction("UpdateWabisabiStoreSettings", new {storeId}); + } + + return View(spendViewModel); + } + + + public class SpendViewModel + { + public string Destination { get; set; } + public decimal? Amount { get; set; } + public string[] SelectedCoins { get; set; } + } + } +} diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreSettings.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreSettings.cs new file mode 100644 index 0000000..5e79a57 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WabisabiStoreSettings.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; + +namespace BTCPayServer.Plugins.Wabisabi; + +public class WabisabiStoreSettings +{ + public List Settings { get; set; } = new(); + + + public string MixToOtherWallet { get; set; } + + public bool PlebMode { get; set; } = true; + + public List InputLabelsAllowed { get; set; } = new(); + public List InputLabelsExcluded { get; set; } = new(); + public bool ConsolidationMode { get; set; } = false; + public bool RedCoinIsolation { get; set; } = false; + public int AnonymitySetTarget { get; set; } = 5; + + public bool BatchPayments { get; set; } = true; + + +} + +public class WabisabiStoreCoordinatorSettings +{ + public string Coordinator { get; set; } + public bool Enabled { get; set; } = false; + + +} diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WalletProvider.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WalletProvider.cs new file mode 100644 index 0000000..886102a --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WalletProvider.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Client.Models; +using BTCPayServer.Common; +using BTCPayServer.Payments.PayJoin; +using Microsoft.Extensions.Logging; +using NBitcoin; +using NBXplorer; +using WalletWasabi.Bases; +using WalletWasabi.Blockchain.TransactionOutputs; +using WalletWasabi.WabiSabi.Client; +using WalletWasabi.Wallets; + +namespace BTCPayServer.Plugins.Wabisabi; + +public class WalletProvider : PeriodicRunner,IWalletProvider +{ + private Dictionary? _cachedSettings; + private readonly IBTCPayServerClientFactory _btcPayServerClientFactory; + private readonly IExplorerClientProvider _explorerClientProvider; + public IUTXOLocker UtxoLocker { get; set; } + private readonly ILoggerFactory _loggerFactory; + + public WalletProvider(IStoreRepository storeRepository, IBTCPayServerClientFactory btcPayServerClientFactory, + IExplorerClientProvider explorerClientProvider, ILoggerFactory loggerFactory, IUTXOLocker utxoLocker ) : base(TimeSpan.FromMinutes(5)) + { + UtxoLocker = utxoLocker; + _btcPayServerClientFactory = btcPayServerClientFactory; + _explorerClientProvider = explorerClientProvider; + _loggerFactory = loggerFactory; + initialLoad = Task.Run(async () => + { + _cachedSettings = + await storeRepository.GetSettingsAsync(nameof(WabisabiStoreSettings)); + }); + } + + public readonly ConcurrentDictionary> LoadedWallets = new(); + public ConcurrentDictionary> BannedCoins = new(); + + + public class WalletUnloadEventArgs : EventArgs + { + public string Wallet { get; } + + public WalletUnloadEventArgs(string wallet) + { + Wallet = wallet; + } + } + + public event EventHandler? WalletUnloaded; + public async Task GetWalletAsync(string name) + { + await initialLoad; + return await LoadedWallets.GetOrAddAsync(name, async s => + { + + if (!_cachedSettings.TryGetValue(name, out var wabisabiStoreSettings)) + { + return null; + } + + var client = await _btcPayServerClientFactory.Create(null, name); + var pm = await client.GetStoreOnChainPaymentMethod(name, "BTC"); + var explorerClient = _explorerClientProvider.GetExplorerClient("BTC"); + var derivationStrategy = + explorerClient.Network.DerivationStrategyFactory.Parse(pm.DerivationScheme); + + var masterKey = await explorerClient.GetMetadataAsync(derivationStrategy, + WellknownMetadataKeys.MasterHDKey); + var accountKey = await explorerClient.GetMetadataAsync(derivationStrategy, + WellknownMetadataKeys.AccountHDKey); + + var keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, masterKey, accountKey); + + var destinationProvider = + new NBXInternalDestinationProvider(explorerClient, _btcPayServerClientFactory, derivationStrategy, client, name, + wabisabiStoreSettings); + + var smartifier = new Smartifier(explorerClient, derivationStrategy, _btcPayServerClientFactory, name, + CoinOnPropertyChanged); + + return (IWallet)new BTCPayWallet(pm, derivationStrategy, explorerClient, keychain, destinationProvider, + _btcPayServerClientFactory, name, wabisabiStoreSettings, UtxoLocker, + _loggerFactory, smartifier, BannedCoins); + + }); + + } + + private Task initialLoad = null; + public async Task> GetWalletsAsync() + { + var explorerClient = _explorerClientProvider.GetExplorerClient("BTC"); + var status = await explorerClient.GetStatusAsync(); + if (!status.IsFullySynched) + { + return Array.Empty(); + } + + await initialLoad; + return (await Task.WhenAll(_cachedSettings + .Select(pair => GetWalletAsync(pair.Key)))) + .Where(wallet => wallet is not null); + } + + private void CoinOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (sender is SmartCoin smartCoin) + { + if (e.PropertyName == nameof(SmartCoin.CoinJoinInProgress)) + { + // _logger.LogInformation($"{smartCoin.Outpoint}.CoinJoinInProgress = {smartCoin.CoinJoinInProgress}"); + if (UtxoLocker is not null) + { + _ = (smartCoin.CoinJoinInProgress + ? UtxoLocker.TryLock(smartCoin.Outpoint) + : UtxoLocker.TryUnlock(smartCoin.Outpoint)).ContinueWith(task => + { + // _logger.LogInformation( + // $"{(task.Result ? "Success" : "Fail")}: {(smartCoin.CoinJoinInProgress ? "" : "un")}locking coin for coinjoin: {smartCoin.Outpoint} "); + }); + } + } + } + } + + public async Task ResetWabisabiStuckPayouts() + { + var wallets = await GetWalletsAsync(); + foreach (BTCPayWallet wallet in wallets) + { + var client = await _btcPayServerClientFactory.Create(null, wallet.StoreId); + var payouts = await client.GetStorePayouts(wallet.StoreId); + var inProgressPayouts = payouts.Where(data => + data.State == PayoutState.InProgress && data.PaymentMethod == "BTC" && + data.PaymentProof?.Value("proofType") == "Wabisabi"); + foreach (PayoutData payout in inProgressPayouts) + { + try + { + var paymentproof = + payout.PaymentProof.ToObject(); + if (paymentproof.Candidates?.Any() is not true) + await client.MarkPayout(wallet.StoreId, payout.Id, + new MarkPayoutRequest() {State = PayoutState.AwaitingPayment}); + } + catch (Exception e) + { + } + } + } + } + + protected override async Task ActionAsync(CancellationToken cancel) + { + + var toCheck = LoadedWallets.Keys.ToList(); + while (toCheck.Any()) + { + var storeid = toCheck.First(); + await Check(storeid, cancel); + toCheck.Remove(storeid); + } + } + + public async Task Check(string storeId, CancellationToken cancellationToken) + { + var client = await _btcPayServerClientFactory.Create(null, storeId); + try + { + if (LoadedWallets.TryGetValue(storeId, out var currentWallet)) + { + var wallet = (BTCPayWallet)await currentWallet; + var kc = (BTCPayKeyChain)wallet.KeyChain; + var pm = await client.GetStoreOnChainPaymentMethod(storeId, "BTC", cancellationToken); + if (pm.DerivationScheme != wallet.OnChainPaymentMethodData.DerivationScheme) + { + await UnloadWallet(storeId); + } + else + { + wallet.OnChainPaymentMethodData = pm; + } + + if (!kc.KeysAvailable) + { + await UnloadWallet(storeId); + } + } + } + catch (Exception e) + { + await UnloadWallet(storeId); + } + } + + private async Task UnloadWallet(string name) + { + + LoadedWallets.TryRemove(name, out _); + WalletUnloaded?.Invoke(this, new WalletUnloadEventArgs(name)); + } + + public async Task SettingsUpdated(string storeId, WabisabiStoreSettings wabisabiSettings) + { + + if (wabisabiSettings.Settings.All(settings => !settings.Enabled)) + { + _cachedSettings?.Remove(storeId); + await UnloadWallet(storeId); + }else if (LoadedWallets.TryGetValue(storeId, out var existingWallet)) + { + + _cachedSettings.AddOrReplace(storeId, wabisabiSettings); + var btcpayWalet = (BTCPayWallet) await existingWallet; + if (btcpayWalet is null) + { + + LoadedWallets.TryRemove(storeId, out _); + var w = await GetWalletAsync(storeId); + if (w is null) + { + await UnloadWallet(storeId); + } + } + else + { + + btcpayWalet.WabisabiStoreSettings = wabisabiSettings; + } + } + else + { + _cachedSettings.AddOrReplace(storeId, wabisabiSettings); + await GetWalletAsync(storeId); + } + } + + public void OnBan(string coordinatorName, BannedCoinEventArgs args) + { + BannedCoins.AddOrUpdate(coordinatorName, + s => new Dictionary() {{args.Utxo, args.BannedTime}}, + (s, offsets) => + { + offsets.TryAdd(args.Utxo, args.BannedTime); + return offsets; + }); + } +} diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/WasabiCoordinatorStatusFetcher.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/WasabiCoordinatorStatusFetcher.cs new file mode 100644 index 0000000..9f11697 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/WasabiCoordinatorStatusFetcher.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using NBitcoin; +using WalletWasabi.Backend.Models.Responses; +using WalletWasabi.Bases; +using WalletWasabi.WabiSabi.Client; +using WalletWasabi.WabiSabi.Models; +using WalletWasabi.WebClients.Wasabi; + +namespace BTCPayServer.Plugins.Wabisabi; + +public class WasabiCoordinatorStatusFetcher : PeriodicRunner, IWasabiBackendStatusProvider +{ + private readonly WabiSabiHttpApiClient _wasabiClient; + private readonly ILogger _logger; + public bool Connected { get; set; } = false; + public WasabiCoordinatorStatusFetcher(WabiSabiHttpApiClient wasabiClient, ILogger logger) : + base(TimeSpan.FromSeconds(30)) + { + _wasabiClient = wasabiClient; + _logger = logger; + } + + protected override async Task ActionAsync(CancellationToken cancel) + { + try + { + await _wasabiClient.GetStatusAsync(new RoundStateRequest(ImmutableList.Empty), cancel); + if (!Connected) + { + _logger.LogInformation("Connected to coordinator" ); + } + + Connected = true; + } + catch (Exception e) + { + Connected = false; + _logger.LogError(e, "Could not connect to the coordinator "); + throw; + } + } +} diff --git a/submodules/btcpayserver b/submodules/btcpayserver new file mode 160000 index 0000000..068b717 --- /dev/null +++ b/submodules/btcpayserver @@ -0,0 +1 @@ +Subproject commit 068b717a7530a6f904502f41678df40a97d63b03 diff --git a/submodules/walletwasabi b/submodules/walletwasabi new file mode 160000 index 0000000..0fb49c8 --- /dev/null +++ b/submodules/walletwasabi @@ -0,0 +1 @@ +Subproject commit 0fb49c8b6145acf1bd2402b765ed1edcd1e8a895