From 51c2b9f243274ea5abe44eb8e9c64a812bb47683 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Mon, 6 Jan 2025 22:23:16 +0900 Subject: [PATCH] Remove ZCash and Monero from core code --- .../docker-compose.altcoins.yml | 24 -- .../Filters/OnlyIfSupportAttribute.cs | 31 -- .../Plugins/Altcoins/AltcoinsPlugin.cs | 4 - .../Altcoins/Monero/AltcoinsPlugin.Monero.cs | 122 ------ .../Monero/MoneroLikeSpecificBtcPayNetwork.cs | 8 - .../Altcoins/Monero/RPC/JsonRpcClient.cs | 121 ------ .../Monero/RPC/Models/CreateAccountRequest.cs | 9 - .../RPC/Models/CreateAccountResponse.cs | 10 - .../Monero/RPC/Models/CreateAddressRequest.cs | 10 - .../RPC/Models/CreateAddressResponse.cs | 10 - .../Monero/RPC/Models/GetAccountsRequest.cs | 9 - .../Monero/RPC/Models/GetAccountsResponse.cs | 14 - .../RPC/Models/GetFeeEstimateRequest.cs | 9 - .../RPC/Models/GetFeeEstimateResponse.cs | 11 - .../Monero/RPC/Models/GetHeightResponse.cs | 9 - .../Monero/RPC/Models/GetInfoResponse.cs | 13 - .../GetTransferByTransactionIdRequest.cs | 11 - .../GetTransferByTransactionIdResponse.cs | 31 -- .../Monero/RPC/Models/GetTransfersRequest.cs | 19 - .../Monero/RPC/Models/GetTransfersResponse.cs | 35 -- .../Altcoins/Monero/RPC/Models/Info.cs | 33 -- .../Monero/RPC/Models/MakeUriRequest.cs | 13 - .../Monero/RPC/Models/MakeUriResponse.cs | 9 - .../RPC/Models/OpenWallerErrorResponse.cs | 10 - .../Monero/RPC/Models/OpenWalletRequest.cs | 10 - .../Monero/RPC/Models/OpenWalletResponse.cs | 12 - .../Monero/RPC/Models/ParseStringConverter.cs | 40 -- .../Altcoins/Monero/RPC/Models/Peer.cs | 9 - .../Monero/RPC/Models/SubaddrIndex.cs | 10 - .../Monero/RPC/Models/SubaddressAccount.cs | 14 - .../Altcoins/Monero/Utils/MoneroMoney.cs | 20 - .../Altcoins/Zcash/AltcoinsPlugin.Zcash.cs | 118 ------ .../Altcoins/Zcash/RPC/JsonRpcClient.cs | 102 ----- .../Zcash/RPC/Models/CreateAccountRequest.cs | 9 - .../Zcash/RPC/Models/CreateAccountResponse.cs | 10 - .../Zcash/RPC/Models/CreateAddressRequest.cs | 10 - .../Zcash/RPC/Models/CreateAddressResponse.cs | 10 - .../Zcash/RPC/Models/GetAccountsRequest.cs | 9 - .../Zcash/RPC/Models/GetAccountsResponse.cs | 14 - .../Zcash/RPC/Models/GetFeeEstimateRequest.cs | 9 - .../RPC/Models/GetFeeEstimateResponse.cs | 11 - .../Zcash/RPC/Models/GetHeightResponse.cs | 9 - .../GetTransferByTransactionIdRequest.cs | 11 - .../GetTransferByTransactionIdResponse.cs | 31 -- .../Zcash/RPC/Models/GetTransfersRequest.cs | 19 - .../Zcash/RPC/Models/GetTransfersResponse.cs | 35 -- .../Plugins/Altcoins/Zcash/RPC/Models/Info.cs | 33 -- .../Zcash/RPC/Models/MakeUriRequest.cs | 13 - .../Zcash/RPC/Models/MakeUriResponse.cs | 9 - .../Zcash/RPC/Models/ParseStringConverter.cs | 40 -- .../Plugins/Altcoins/Zcash/RPC/Models/Peer.cs | 9 - .../Altcoins/Zcash/RPC/Models/SubaddrIndex.cs | 10 - .../Zcash/RPC/Models/SubaddressAccount.cs | 14 - .../Zcash/RPC/Models/SyncInfoResponse.cs | 13 - .../Altcoins/Zcash/Utils/ZcashMoney.cs | 20 - .../Zcash/ZcashLikeSpecificBtcPayNetwork.cs | 7 - BTCPayServer/Properties/launchSettings.json | 8 +- .../Configuration/MoneroLikeConfiguration.cs | 20 - .../Payments/MoneroCheckoutModelExtension.cs | 52 --- .../MoneroLikeOnChainPaymentMethodDetails.cs | 11 - .../Monero/Payments/MoneroLikePaymentData.cs | 20 - .../MoneroLikePaymentMethodHandler.cs | 120 ------ .../Payments/MoneroPaymentLinkExtension.cs | 27 -- .../Payments/MoneroPaymentPromptDetails.cs | 11 - .../RPC/MoneroDaemonCallbackController.cs | 38 -- .../Altcoins/Monero/RPC/MoneroEvent.cs | 15 - .../MoneroLikeSummaryUpdaterHostedService.cs | 70 --- .../Monero/Services/MoneroListener.cs | 401 ------------------ .../Monero/Services/MoneroRPCProvider.cs | 118 ------ .../Services/MoneroSyncSummaryProvider.cs | 45 -- .../Monero/UI/MoneroLikeStoreController.cs | 393 ----------------- .../Monero/UI/MoneroPaymentViewModel.cs | 17 - .../Configuration/ZcashLikeConfiguration.cs | 18 - .../Payments/ZcashCheckoutModelExtension.cs | 51 --- .../Zcash/Payments/ZcashLikePaymentData.cs | 17 - .../Payments/ZcashLikePaymentMethodHandler.cs | 114 ----- .../Payments/ZcashPaymentLinkExtension.cs | 27 -- .../Payments/ZcashPaymentMethodConfig.cs | 10 - .../Payments/ZcashPaymentPromptDetails.cs | 11 - .../RPC/ZcashDaemonCallbackController.cs | 38 -- .../Services/Altcoins/Zcash/RPC/ZcashEvent.cs | 15 - .../ZcashLikeSummaryUpdaterHostedService.cs | 71 ---- .../Altcoins/Zcash/Services/ZcashListener.cs | 356 ---------------- .../Zcash/Services/ZcashRPCProvider.cs | 123 ------ .../Services/ZcashSyncSummaryProvider.cs | 45 -- .../Zcash/UI/ZcashLikeStoreController.cs | 311 -------------- .../Zcash/UI/ZcashPaymentViewModel.cs | 17 - .../Shared/Monero/MoneroSyncSummary.cshtml | 26 -- .../Monero/StoreNavMoneroExtension.cshtml | 16 - .../StoreWalletsNavMoneroExtension.cshtml | 34 -- .../Monero/ViewMoneroLikePaymentData.cshtml | 68 --- .../Zcash/StoreNavZcashExtension.cshtml | 16 - .../Zcash/ViewZcashLikePaymentData.cshtml | 69 --- .../Shared/Zcash/ZcashSyncSummary.cshtml | 26 -- .../GetStoreMoneroLikePaymentMethod.cshtml | 148 ------- .../GetStoreMoneroLikePaymentMethods.cshtml | 57 --- .../GetStoreZcashLikePaymentMethod.cshtml | 112 ----- .../GetStoreZcashLikePaymentMethods.cshtml | 58 --- BTCPayServer/wwwroot/imlegacy/monero.svg | 40 -- BTCPayServer/wwwroot/imlegacy/zcash.png | Bin 123472 -> 0 bytes 100 files changed, 2 insertions(+), 4523 deletions(-) delete mode 100644 BTCPayServer/Filters/OnlyIfSupportAttribute.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/AltcoinsPlugin.Monero.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/MoneroLikeSpecificBtcPayNetwork.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/JsonRpcClient.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAccountRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAccountResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAddressRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAddressResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetAccountsRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetAccountsResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetFeeEstimateRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetFeeEstimateResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetHeightResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetInfoResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransferByTransactionIdRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransferByTransactionIdResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransfersRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransfersResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/Info.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/MakeUriRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/MakeUriResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/OpenWallerErrorResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/OpenWalletRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/OpenWalletResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/ParseStringConverter.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/Peer.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/SubaddrIndex.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/SubaddressAccount.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Monero/Utils/MoneroMoney.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/AltcoinsPlugin.Zcash.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/JsonRpcClient.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAccountRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAccountResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAddressRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAddressResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetAccountsRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetAccountsResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetFeeEstimateRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetFeeEstimateResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetHeightResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransfersRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransfersResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/Info.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/MakeUriRequest.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/MakeUriResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/ParseStringConverter.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/Peer.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/SubaddrIndex.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/SubaddressAccount.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/SyncInfoResponse.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/Utils/ZcashMoney.cs delete mode 100644 BTCPayServer/Plugins/Altcoins/Zcash/ZcashLikeSpecificBtcPayNetwork.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/Configuration/MoneroLikeConfiguration.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/Payments/MoneroCheckoutModelExtension.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentData.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentLinkExtension.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentPromptDetails.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/RPC/MoneroDaemonCallbackController.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/RPC/MoneroEvent.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/Services/MoneroLikeSummaryUpdaterHostedService.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/Services/MoneroRPCProvider.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/Services/MoneroSyncSummaryProvider.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/UI/MoneroPaymentViewModel.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/Configuration/ZcashLikeConfiguration.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashCheckoutModelExtension.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentData.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentMethodHandler.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentLinkExtension.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentMethodConfig.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentPromptDetails.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashDaemonCallbackController.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashEvent.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/Services/ZcashLikeSummaryUpdaterHostedService.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/Services/ZcashRPCProvider.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/Services/ZcashSyncSummaryProvider.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/UI/ZcashLikeStoreController.cs delete mode 100644 BTCPayServer/Services/Altcoins/Zcash/UI/ZcashPaymentViewModel.cs delete mode 100644 BTCPayServer/Views/Shared/Monero/MoneroSyncSummary.cshtml delete mode 100644 BTCPayServer/Views/Shared/Monero/StoreNavMoneroExtension.cshtml delete mode 100644 BTCPayServer/Views/Shared/Monero/StoreWalletsNavMoneroExtension.cshtml delete mode 100644 BTCPayServer/Views/Shared/Monero/ViewMoneroLikePaymentData.cshtml delete mode 100644 BTCPayServer/Views/Shared/Zcash/StoreNavZcashExtension.cshtml delete mode 100644 BTCPayServer/Views/Shared/Zcash/ViewZcashLikePaymentData.cshtml delete mode 100644 BTCPayServer/Views/Shared/Zcash/ZcashSyncSummary.cshtml delete mode 100644 BTCPayServer/Views/UIMoneroLikeStore/GetStoreMoneroLikePaymentMethod.cshtml delete mode 100644 BTCPayServer/Views/UIMoneroLikeStore/GetStoreMoneroLikePaymentMethods.cshtml delete mode 100644 BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethod.cshtml delete mode 100644 BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethods.cshtml delete mode 100644 BTCPayServer/wwwroot/imlegacy/monero.svg delete mode 100644 BTCPayServer/wwwroot/imlegacy/zcash.png diff --git a/BTCPayServer.Tests/docker-compose.altcoins.yml b/BTCPayServer.Tests/docker-compose.altcoins.yml index 3f3e09f31..41a6a4849 100644 --- a/BTCPayServer.Tests/docker-compose.altcoins.yml +++ b/BTCPayServer.Tests/docker-compose.altcoins.yml @@ -58,7 +58,6 @@ services: - merchant_lnd - sshd - tor - - monero_wallet sshd: build: @@ -317,28 +316,6 @@ services: - "torrcdir:/usr/local/etc/tor" - "tor_servicesdir:/var/lib/tor/hidden_services" - monerod: - image: btcpayserver/monero:0.18.3.3 - restart: unless-stopped - container_name: xmr_monerod - entrypoint: monerod --fixed-difficulty 200 --rpc-bind-ip=0.0.0.0 --confirm-external-bind --rpc-bind-port=18081 --block-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/block?cryptoCode=xmr&hash=%s" --testnet --no-igd --hide-my-port --offline --non-interactive - volumes: - - "monero_data:/home/monero/.bitmonero" - ports: - - "18081:18081" - - monero_wallet: - image: btcpayserver/monero:0.18.3.3 - restart: unless-stopped - container_name: xmr_wallet_rpc - entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-dir=/wallet --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s" - ports: - - "18082:18082" - volumes: - - "./monero_wallet:/wallet" - depends_on: - - monerod - litecoind: restart: unless-stopped image: btcpayserver/litecoin:0.18.1 @@ -400,7 +377,6 @@ volumes: tor_datadir: torrcdir: tor_servicesdir: - monero_data: networks: default: diff --git a/BTCPayServer/Filters/OnlyIfSupportAttribute.cs b/BTCPayServer/Filters/OnlyIfSupportAttribute.cs deleted file mode 100644 index 7ca67f4f9..000000000 --- a/BTCPayServer/Filters/OnlyIfSupportAttribute.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Threading.Tasks; -using BTCPayServer.Payments; -using BTCPayServer.Services.Invoices; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; - -namespace BTCPayServer.Filters -{ - public class OnlyIfSupportAttribute : Attribute, IAsyncActionFilter - { - private readonly string _paymentMethodId; - - public OnlyIfSupportAttribute(string paymentMethodId) - { - _paymentMethodId = paymentMethodId; - } - - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - var handlers = context.HttpContext.RequestServices.GetService(); - if (!handlers.Support(PaymentMethodId.Parse(_paymentMethodId))) - { - context.Result = new NotFoundResult(); - return; - } - await next(); - } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/AltcoinsPlugin.cs b/BTCPayServer/Plugins/Altcoins/AltcoinsPlugin.cs index 7bd9ff9f5..a356cc0bb 100644 --- a/BTCPayServer/Plugins/Altcoins/AltcoinsPlugin.cs +++ b/BTCPayServer/Plugins/Altcoins/AltcoinsPlugin.cs @@ -52,10 +52,6 @@ namespace BTCPayServer.Plugins.Altcoins InitDash(services); if (selectedChains.Contains("GRS")) InitGroestlcoin(services); - if (selectedChains.Contains("XMR")) - InitMonero(services); - if (selectedChains.Contains("ZEC")) - InitZcash(services); } } } diff --git a/BTCPayServer/Plugins/Altcoins/Monero/AltcoinsPlugin.Monero.cs b/BTCPayServer/Plugins/Altcoins/Monero/AltcoinsPlugin.Monero.cs deleted file mode 100644 index 3736e21e8..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/AltcoinsPlugin.Monero.cs +++ /dev/null @@ -1,122 +0,0 @@ -using BTCPayServer.Abstractions.Contracts; -using BTCPayServer.Abstractions.Services; -using System.Net.Http; -using System.Net; -using BTCPayServer.Hosting; -using BTCPayServer.Payments; -using BTCPayServer.Payments.Bitcoin; -using BTCPayServer.Services.Altcoins.Monero.Configuration; -using BTCPayServer.Services.Altcoins.Monero.Payments; -using BTCPayServer.Services.Altcoins.Monero.Services; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.Extensions.DependencyInjection; -using NBitcoin; -using BTCPayServer.Configuration; -using System.Linq; -using System; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace BTCPayServer.Plugins.Altcoins; - -public partial class AltcoinsPlugin -{ - public void InitMonero(IServiceCollection services) - { - var network = new MoneroLikeSpecificBtcPayNetwork() - { - CryptoCode = "XMR", - DisplayName = "Monero", - Divisibility = 12, - DefaultRateRules = new[] - { - "XMR_X = XMR_BTC * BTC_X", - "XMR_BTC = kraken(XMR_BTC)" - }, - CryptoImagePath = "/imlegacy/monero.svg", - UriScheme = "monero" - }; - var blockExplorerLink = ChainName == ChainName.Mainnet - ? "https://www.exploremonero.com/transaction/{0}" - : "https://testnet.xmrchain.net/tx/{0}"; - var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("XMR"); - services.AddDefaultPrettyName(pmi, network.DisplayName); - services.AddBTCPayNetwork(network) - .AddTransactionLinkProvider(pmi, new SimpleTransactionLinkProvider(blockExplorerLink)); - - - services.AddSingleton(provider => - ConfigureMoneroLikeConfiguration(provider)); - services.AddHttpClient("XMRclient") - .ConfigurePrimaryHttpMessageHandler(provider => - { - var configuration = provider.GetRequiredService(); - if (!configuration.MoneroLikeConfigurationItems.TryGetValue("XMR", out var xmrConfig) || xmrConfig.Username is null || xmrConfig.Password is null) - { - return new HttpClientHandler(); - } - return new HttpClientHandler - { - Credentials = new NetworkCredential(xmrConfig.Username, xmrConfig.Password), - PreAuthenticate = true - }; - }); - services.AddSingleton(); - services.AddHostedService(); - services.AddHostedService(); - services.AddSingleton(provider => - (IPaymentMethodHandler)ActivatorUtilities.CreateInstance(provider, typeof(MoneroLikePaymentMethodHandler), new object[] { network })); - services.AddSingleton(provider => -(IPaymentLinkExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroPaymentLinkExtension), new object[] { network, pmi })); - services.AddSingleton(provider => -(ICheckoutModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroCheckoutModelExtension), new object[] { network, pmi })); - - services.AddUIExtension("store-nav", "Monero/StoreNavMoneroExtension"); - services.AddUIExtension("store-wallets-nav", "Monero/StoreWalletsNavMoneroExtension"); - services.AddUIExtension("store-invoices-payments", "Monero/ViewMoneroLikePaymentData"); - services.AddSingleton(); - } - private static MoneroLikeConfiguration ConfigureMoneroLikeConfiguration(IServiceProvider serviceProvider) - { - var configuration = serviceProvider.GetService(); - var btcPayNetworkProvider = serviceProvider.GetService(); - var result = new MoneroLikeConfiguration(); - - var supportedNetworks = btcPayNetworkProvider.GetAll() - .OfType(); - - foreach (var moneroLikeSpecificBtcPayNetwork in supportedNetworks) - { - var daemonUri = - configuration.GetOrDefault($"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_uri", - null); - var walletDaemonUri = - configuration.GetOrDefault( - $"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_uri", null); - var walletDaemonWalletDirectory = - configuration.GetOrDefault( - $"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_walletdir", null); - var daemonUsername = - configuration.GetOrDefault( - $"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_username", null); - var daemonPassword = - configuration.GetOrDefault( - $"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_password", null); - if (daemonUri == null || walletDaemonUri == null || walletDaemonWalletDirectory == null) - { - throw new ConfigException($"{moneroLikeSpecificBtcPayNetwork.CryptoCode} is misconfigured"); - } - - result.MoneroLikeConfigurationItems.Add(moneroLikeSpecificBtcPayNetwork.CryptoCode, new MoneroLikeConfigurationItem() - { - DaemonRpcUri = daemonUri, - Username = daemonUsername, - Password = daemonPassword, - InternalWalletRpcUri = walletDaemonUri, - WalletDirectory = walletDaemonWalletDirectory - }); - } - return result; - } -} - diff --git a/BTCPayServer/Plugins/Altcoins/Monero/MoneroLikeSpecificBtcPayNetwork.cs b/BTCPayServer/Plugins/Altcoins/Monero/MoneroLikeSpecificBtcPayNetwork.cs deleted file mode 100644 index 9f4fd4530..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/MoneroLikeSpecificBtcPayNetwork.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace BTCPayServer.Plugins.Altcoins; - -public class MoneroLikeSpecificBtcPayNetwork : BTCPayNetworkBase -{ - public int MaxTrackedConfirmation = 10; - public string UriScheme { get; set; } -} - diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/JsonRpcClient.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/JsonRpcClient.cs deleted file mode 100644 index d8051c99c..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/JsonRpcClient.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC -{ - public class JsonRpcClient - { - private readonly Uri _address; - private readonly string _username; - private readonly string _password; - private readonly HttpClient _httpClient; - - public JsonRpcClient(Uri address, string username, string password, HttpClient client = null) - { - _address = address; - _username = username; - _password = password; - _httpClient = client ?? new HttpClient(); - } - - - public async Task SendCommandAsync(string method, TRequest data, - CancellationToken cts = default(CancellationToken)) - { - var jsonSerializer = new JsonSerializerSettings - { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }; - var httpRequest = new HttpRequestMessage() - { - Method = HttpMethod.Post, - RequestUri = new Uri(_address, "json_rpc"), - Content = new StringContent( - JsonConvert.SerializeObject(new JsonRpcCommand(method, data), jsonSerializer), - Encoding.UTF8, "application/json") - }; - httpRequest.Headers.Accept.Clear(); - httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic", - Convert.ToBase64String(Encoding.Default.GetBytes($"{_username}:{_password}"))); - - HttpResponseMessage rawResult = await _httpClient.SendAsync(httpRequest, cts); - rawResult.EnsureSuccessStatusCode(); - var rawJson = await rawResult.Content.ReadAsStringAsync(); - - JsonRpcResult response; - try - { - response = JsonConvert.DeserializeObject>(rawJson, jsonSerializer); - } - catch (Exception e) - { - Console.WriteLine(e.Message); - Console.WriteLine(rawJson); - throw; - } - - if (response.Error != null) - { - throw new JsonRpcApiException() - { - Error = response.Error - }; - } - - return response.Result; - } - - public class NoRequestModel - { - public static NoRequestModel Instance = new NoRequestModel(); - } - - internal class JsonRpcApiException : Exception - { - public JsonRpcResultError Error { get; set; } - - public override string Message => Error?.Message; - } - - public class JsonRpcResultError - { - [JsonProperty("code")] public int Code { get; set; } - [JsonProperty("message")] public string Message { get; set; } - [JsonProperty("data")] dynamic Data { get; set; } - } - internal class JsonRpcResult - { - - - [JsonProperty("result")] public T Result { get; set; } - [JsonProperty("error")] public JsonRpcResultError Error { get; set; } - [JsonProperty("id")] public string Id { get; set; } - } - - internal class JsonRpcCommand - { - [JsonProperty("jsonRpc")] public string JsonRpc { get; set; } = "2.0"; - [JsonProperty("id")] public string Id { get; set; } = Guid.NewGuid().ToString(); - [JsonProperty("method")] public string Method { get; set; } - - [JsonProperty("params")] public T Parameters { get; set; } - - public JsonRpcCommand() - { - } - - public JsonRpcCommand(string method, T parameters) - { - Method = method; - Parameters = parameters; - } - } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAccountRequest.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAccountRequest.cs deleted file mode 100644 index 10ab914b8..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAccountRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class CreateAccountRequest - { - [JsonProperty("label")] public string Label { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAccountResponse.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAccountResponse.cs deleted file mode 100644 index ed60cb677..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAccountResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class CreateAccountResponse - { - [JsonProperty("account_index")] public long AccountIndex { get; set; } - [JsonProperty("address")] public string Address { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAddressRequest.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAddressRequest.cs deleted file mode 100644 index 057f4f101..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAddressRequest.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class CreateAddressRequest - { - [JsonProperty("account_index")] public long AccountIndex { get; set; } - [JsonProperty("label")] public string Label { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAddressResponse.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAddressResponse.cs deleted file mode 100644 index 657b3d0c9..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/CreateAddressResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class CreateAddressResponse - { - [JsonProperty("address")] public string Address { get; set; } - [JsonProperty("address_index")] public long AddressIndex { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetAccountsRequest.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetAccountsRequest.cs deleted file mode 100644 index 86dd55ee7..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetAccountsRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class GetAccountsRequest - { - [JsonProperty("tag")] public string Tag { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetAccountsResponse.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetAccountsResponse.cs deleted file mode 100644 index 6d6da2987..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetAccountsResponse.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class GetAccountsResponse - { - [JsonProperty("subaddress_accounts")] public List SubaddressAccounts { get; set; } - [JsonProperty("total_balance")] public decimal TotalBalance { get; set; } - - [JsonProperty("total_unlocked_balance")] - public decimal TotalUnlockedBalance { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetFeeEstimateRequest.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetFeeEstimateRequest.cs deleted file mode 100644 index 936af70c9..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetFeeEstimateRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public class GetFeeEstimateRequest - { - [JsonProperty("grace_blocks")] public int? GraceBlocks { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetFeeEstimateResponse.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetFeeEstimateResponse.cs deleted file mode 100644 index 6e383bc34..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetFeeEstimateResponse.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public class GetFeeEstimateResponse - { - [JsonProperty("fee")] public long Fee { get; set; } - [JsonProperty("status")] public string Status { get; set; } - [JsonProperty("untrusted")] public bool Untrusted { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetHeightResponse.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetHeightResponse.cs deleted file mode 100644 index 1040b51a1..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetHeightResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class GetHeightResponse - { - [JsonProperty("height")] public long Height { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetInfoResponse.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetInfoResponse.cs deleted file mode 100644 index 864c42953..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetInfoResponse.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class GetInfoResponse - { - [JsonProperty("height")] public long Height { get; set; } - [JsonProperty("busy_syncing")] public bool BusySyncing { get; set; } - [JsonProperty("status")] public string Status { get; set; } - [JsonProperty("target_height")] public long? TargetHeight { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransferByTransactionIdRequest.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransferByTransactionIdRequest.cs deleted file mode 100644 index d11f9ac58..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransferByTransactionIdRequest.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public class GetTransferByTransactionIdRequest - { - [JsonProperty("txid")] public string TransactionId { get; set; } - - [JsonProperty("account_index", DefaultValueHandling = DefaultValueHandling.Ignore)] public long? AccountIndex { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransferByTransactionIdResponse.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransferByTransactionIdResponse.cs deleted file mode 100644 index 025c22151..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransferByTransactionIdResponse.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class GetTransferByTransactionIdResponse - { - [JsonProperty("transfer")] public TransferItem Transfer { get; set; } - [JsonProperty("transfers")] public IEnumerable Transfers { get; set; } - - public partial class TransferItem - { - [JsonProperty("address")] public string Address { get; set; } - [JsonProperty("amount")] public long Amount { get; set; } - [JsonProperty("confirmations")] public long Confirmations { get; set; } - [JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; } - [JsonProperty("height")] public long Height { get; set; } - [JsonProperty("note")] public string Note { get; set; } - [JsonProperty("payment_id")] public string PaymentId { get; set; } - [JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; } - - [JsonProperty("suggested_confirmations_threshold")] - public long SuggestedConfirmationsThreshold { get; set; } - - [JsonProperty("timestamp")] public long Timestamp { get; set; } - [JsonProperty("txid")] public string Txid { get; set; } - [JsonProperty("type")] public string Type { get; set; } - [JsonProperty("unlock_time")] public long UnlockTime { get; set; } - } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransfersRequest.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransfersRequest.cs deleted file mode 100644 index 506a96483..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransfersRequest.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class GetTransfersRequest - { - [JsonProperty("in")] public bool In { get; set; } - [JsonProperty("out")] public bool Out { get; set; } - [JsonProperty("pending")] public bool Pending { get; set; } - [JsonProperty("failed")] public bool Failed { get; set; } - [JsonProperty("pool")] public bool Pool { get; set; } - [JsonProperty("filter_by_height ")] public bool FilterByHeight { get; set; } - [JsonProperty("min_height")] public long MinHeight { get; set; } - [JsonProperty("max_height")] public long MaxHeight { get; set; } - [JsonProperty("account_index")] public long AccountIndex { get; set; } - [JsonProperty("subaddr_indices")] public List SubaddrIndices { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransfersResponse.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransfersResponse.cs deleted file mode 100644 index f0d3b4076..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/GetTransfersResponse.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class GetTransfersResponse - { - [JsonProperty("in")] public List In { get; set; } - [JsonProperty("out")] public List Out { get; set; } - [JsonProperty("pending")] public List Pending { get; set; } - [JsonProperty("failed")] public List Failed { get; set; } - [JsonProperty("pool")] public List Pool { get; set; } - - public partial class GetTransfersResponseItem - - { - [JsonProperty("address")] public string Address { get; set; } - [JsonProperty("amount")] public long Amount { get; set; } - [JsonProperty("confirmations")] public long Confirmations { get; set; } - [JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; } - [JsonProperty("height")] public long Height { get; set; } - [JsonProperty("note")] public string Note { get; set; } - [JsonProperty("payment_id")] public string PaymentId { get; set; } - [JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; } - - [JsonProperty("suggested_confirmations_threshold")] - public long SuggestedConfirmationsThreshold { get; set; } - - [JsonProperty("timestamp")] public long Timestamp { get; set; } - [JsonProperty("txid")] public string Txid { get; set; } - [JsonProperty("type")] public string Type { get; set; } - [JsonProperty("unlock_time")] public long UnlockTime { get; set; } - } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/Info.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/Info.cs deleted file mode 100644 index ca9f55741..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/Info.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class Info - { - [JsonProperty("address")] public string Address { get; set; } - [JsonProperty("avg_download")] public long AvgDownload { get; set; } - [JsonProperty("avg_upload")] public long AvgUpload { get; set; } - [JsonProperty("connection_id")] public string ConnectionId { get; set; } - [JsonProperty("current_download")] public long CurrentDownload { get; set; } - [JsonProperty("current_upload")] public long CurrentUpload { get; set; } - [JsonProperty("height")] public long Height { get; set; } - [JsonProperty("host")] public string Host { get; set; } - [JsonProperty("incoming")] public bool Incoming { get; set; } - [JsonProperty("ip")] public string Ip { get; set; } - [JsonProperty("live_time")] public long LiveTime { get; set; } - [JsonProperty("local_ip")] public bool LocalIp { get; set; } - [JsonProperty("localhost")] public bool Localhost { get; set; } - [JsonProperty("peer_id")] public string PeerId { get; set; } - - [JsonProperty("port")] - [JsonConverter(typeof(ParseStringConverter))] - public long Port { get; set; } - - [JsonProperty("recv_count")] public long RecvCount { get; set; } - [JsonProperty("recv_idle_time")] public long RecvIdleTime { get; set; } - [JsonProperty("send_count")] public long SendCount { get; set; } - [JsonProperty("send_idle_time")] public long SendIdleTime { get; set; } - [JsonProperty("state")] public string State { get; set; } - [JsonProperty("support_flags")] public long SupportFlags { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/MakeUriRequest.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/MakeUriRequest.cs deleted file mode 100644 index ba1a503b3..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/MakeUriRequest.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class MakeUriRequest - { - [JsonProperty("address")] public string Address { get; set; } - [JsonProperty("amount")] public long Amount { get; set; } - [JsonProperty("payment_id")] public string PaymentId { get; set; } - [JsonProperty("tx_description")] public string TxDescription { get; set; } - [JsonProperty("recipient_name")] public string RecipientName { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/MakeUriResponse.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/MakeUriResponse.cs deleted file mode 100644 index 160297139..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/MakeUriResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class MakeUriResponse - { - [JsonProperty("uri")] public string Uri { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/OpenWallerErrorResponse.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/OpenWallerErrorResponse.cs deleted file mode 100644 index 90c4e6e22..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/OpenWallerErrorResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class OpenWalletErrorResponse - { - [JsonProperty("code")] public int Code { get; set; } - [JsonProperty("message")] public string Message { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/OpenWalletRequest.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/OpenWalletRequest.cs deleted file mode 100644 index 3da005827..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/OpenWalletRequest.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class OpenWalletRequest - { - [JsonProperty("filename")] public string Filename { get; set; } - [JsonProperty("password")] public string Password { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/OpenWalletResponse.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/OpenWalletResponse.cs deleted file mode 100644 index 75c2a3a86..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/OpenWalletResponse.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class OpenWalletResponse - { - [JsonProperty("id")] public string Id { get; set; } - [JsonProperty("jsonrpc")] public string Jsonrpc { get; set; } - [JsonProperty("result")] public object Result { get; set; } - [JsonProperty("error")] public OpenWalletErrorResponse Error { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/ParseStringConverter.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/ParseStringConverter.cs deleted file mode 100644 index 3e929f3da..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/ParseStringConverter.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Globalization; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - internal class ParseStringConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(long) || t == typeof(long?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - return null; - var value = serializer.Deserialize(reader); - long l; - if (Int64.TryParse(value, out l)) - { - return l; - } - - throw new Exception("Cannot unmarshal type long"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - - var value = (long)untypedValue; - serializer.Serialize(writer, value.ToString(CultureInfo.InvariantCulture)); - return; - } - - public static readonly ParseStringConverter Singleton = new ParseStringConverter(); - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/Peer.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/Peer.cs deleted file mode 100644 index 48f5a2d82..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/Peer.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class Peer - { - [JsonProperty("info")] public Info Info { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/SubaddrIndex.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/SubaddrIndex.cs deleted file mode 100644 index dab5c3c56..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/SubaddrIndex.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class SubaddrIndex - { - [JsonProperty("major")] public long Major { get; set; } - [JsonProperty("minor")] public long Minor { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/SubaddressAccount.cs b/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/SubaddressAccount.cs deleted file mode 100644 index 2f6a7913b..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/RPC/Models/SubaddressAccount.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models -{ - public partial class SubaddressAccount - { - [JsonProperty("account_index")] public long AccountIndex { get; set; } - [JsonProperty("balance")] public decimal Balance { get; set; } - [JsonProperty("base_address")] public string BaseAddress { get; set; } - [JsonProperty("label")] public string Label { get; set; } - [JsonProperty("tag")] public string Tag { get; set; } - [JsonProperty("unlocked_balance")] public decimal UnlockedBalance { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Monero/Utils/MoneroMoney.cs b/BTCPayServer/Plugins/Altcoins/Monero/Utils/MoneroMoney.cs deleted file mode 100644 index fc4a73005..000000000 --- a/BTCPayServer/Plugins/Altcoins/Monero/Utils/MoneroMoney.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Globalization; - -namespace BTCPayServer.Services.Altcoins.Monero.Utils -{ - public class MoneroMoney - { - public static decimal Convert(long piconero) - { - var amt = piconero.ToString(CultureInfo.InvariantCulture).PadLeft(12, '0'); - amt = amt.Length == 12 ? $"0.{amt}" : amt.Insert(amt.Length - 12, "."); - - return decimal.Parse(amt, CultureInfo.InvariantCulture); - } - - public static long Convert(decimal monero) - { - return System.Convert.ToInt64(monero * 1000000000000); - } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/AltcoinsPlugin.Zcash.cs b/BTCPayServer/Plugins/Altcoins/Zcash/AltcoinsPlugin.Zcash.cs deleted file mode 100644 index 5ac5dc5f1..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/AltcoinsPlugin.Zcash.cs +++ /dev/null @@ -1,118 +0,0 @@ -#nullable enable -using BTCPayServer.Services; -using System.Globalization; -using System.Linq; -using NBitcoin; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.Extensions.DependencyInjection; -using BTCPayServer.Hosting; -using BTCPayServer.Payments; -using BTCPayServer.Payments.Bitcoin; -using BTCPayServer.Abstractions.Contracts; -using BTCPayServer.Abstractions.Services; -using BTCPayServer.Services.Altcoins.Zcash.Payments; -using BTCPayServer.Services.Altcoins.Zcash.Services; -using BTCPayServer.Configuration; -using BTCPayServer.Services.Altcoins.Zcash.Configuration; -using System; -using Microsoft.Extensions.Configuration; - -namespace BTCPayServer.Plugins.Altcoins; - -public partial class AltcoinsPlugin -{ - // Change this if you want another zcash coin - public void InitZcash(IServiceCollection services) - { - var network = new ZcashLikeSpecificBtcPayNetwork() - { - CryptoCode = "ZEC", - DisplayName = "Zcash", - Divisibility = 8, - DefaultRateRules = new[] - { - "ZEC_X = ZEC_BTC * BTC_X", - "ZEC_BTC = kraken(ZEC_BTC)" - }, - CryptoImagePath = "/imlegacy/zcash.png", - UriScheme = "zcash" - }; - var blockExplorerLink = ChainName == ChainName.Mainnet - ? "https://www.exploreZcash.com/transaction/{0}" - : "https://testnet.xmrchain.net/tx/{0}"; - var pmi = PaymentTypes.CHAIN.GetPaymentMethodId("ZEC"); - services.AddDefaultPrettyName(pmi, network.DisplayName); - services.AddBTCPayNetwork(network) - .AddTransactionLinkProvider(pmi, new SimpleTransactionLinkProvider(blockExplorerLink)); - - - services.AddSingleton(provider => - ConfigureZcashLikeConfiguration(provider)); - services.AddSingleton(); - services.AddHostedService(); - services.AddHostedService(); - - - services.AddSingleton(provider => - (IPaymentMethodHandler)ActivatorUtilities.CreateInstance(provider, typeof(ZcashLikePaymentMethodHandler), new object[] { network })); - services.AddSingleton(provider => -(IPaymentLinkExtension)ActivatorUtilities.CreateInstance(provider, typeof(ZcashPaymentLinkExtension), new object[] { network, pmi })); - services.AddSingleton(provider => -(ICheckoutModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(ZcashCheckoutModelExtension), new object[] { network, pmi })); - - services.AddSingleton(); - services.AddSingleton(provider => provider.GetRequiredService()); - services.AddUIExtension("store-nav", "Zcash/StoreNavZcashExtension"); - services.AddUIExtension("store-invoices-payments", "Zcash/ViewZcashLikePaymentData"); - services.AddSingleton(); - - } - static ZcashLikeConfiguration ConfigureZcashLikeConfiguration(IServiceProvider serviceProvider) - { - var configuration = serviceProvider.GetService(); - var btcPayNetworkProvider = serviceProvider.GetRequiredService(); - var result = new ZcashLikeConfiguration(); - - var supportedNetworks = btcPayNetworkProvider.GetAll() - .OfType(); - - foreach (var ZcashLikeSpecificBtcPayNetwork in supportedNetworks) - { - var daemonUri = - configuration.GetOrDefault($"{ZcashLikeSpecificBtcPayNetwork.CryptoCode}_daemon_uri", - null); - var walletDaemonUri = - configuration.GetOrDefault( - $"{ZcashLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_uri", null); - var walletDaemonWalletDirectory = - configuration.GetOrDefault( - $"{ZcashLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_walletdir", null); - if (daemonUri == null || walletDaemonUri == null || walletDaemonWalletDirectory == null) - { - throw new ConfigException($"{ZcashLikeSpecificBtcPayNetwork.CryptoCode} is misconfigured"); - } - - result.ZcashLikeConfigurationItems.Add(ZcashLikeSpecificBtcPayNetwork.CryptoCode, new ZcashLikeConfigurationItem() - { - DaemonRpcUri = daemonUri, - InternalWalletRpcUri = walletDaemonUri, - WalletDirectory = walletDaemonWalletDirectory - }); - } - return result; - } - class SimpleTransactionLinkProvider : DefaultTransactionLinkProvider - { - public SimpleTransactionLinkProvider(string blockExplorerLink) : base(blockExplorerLink) - { - } - - public override string? GetTransactionLink(string paymentId) - { - if (string.IsNullOrEmpty(BlockExplorerLink)) - return null; - return string.Format(CultureInfo.InvariantCulture, BlockExplorerLink, paymentId); - } - } -} - diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/JsonRpcClient.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/JsonRpcClient.cs deleted file mode 100644 index fd6cb1bd1..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/JsonRpcClient.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC -{ - public class JsonRpcClient - { - private readonly Uri _address; - private readonly string _username; - private readonly string _password; - private readonly HttpClient _httpClient; - - public JsonRpcClient(Uri address, string username, string password, HttpClient client = null) - { - _address = address; - _username = username; - _password = password; - _httpClient = client ?? new HttpClient(); - } - - - public async Task SendCommandAsync(string method, TRequest data, - CancellationToken cts = default(CancellationToken)) - { - var jsonSerializer = new JsonSerializerSettings - { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }; - var httpRequest = new HttpRequestMessage() - { - Method = HttpMethod.Post, - RequestUri = new Uri(_address, method), - Content = new StringContent( - JsonConvert.SerializeObject(data, jsonSerializer), - Encoding.UTF8, "application/json") - }; - // httpRequest.Headers.Accept.Clear(); - // httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - // httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic", - // Convert.ToBase64String(Encoding.Default.GetBytes($"{_username}:{_password}"))); - - var rawResult = await _httpClient.SendAsync(httpRequest, cts); - - var rawJson = await rawResult.Content.ReadAsStringAsync(); - rawResult.EnsureSuccessStatusCode(); - var response = JsonConvert.DeserializeObject(rawJson, jsonSerializer); - return response; - } - - public class NoRequestModel - { - public static NoRequestModel Instance = new NoRequestModel(); - } - - internal class JsonRpcApiException : Exception - { - public JsonRpcResultError Error { get; set; } - - public override string Message => Error?.Message; - } - - public class JsonRpcResultError - { - [JsonProperty("code")] public int Code { get; set; } - [JsonProperty("message")] public string Message { get; set; } - [JsonProperty("data")] dynamic Data { get; set; } - } - internal class JsonRpcResult - { - - - [JsonProperty("result")] public T Result { get; set; } - [JsonProperty("error")] public JsonRpcResultError Error { get; set; } - [JsonProperty("id")] public string Id { get; set; } - } - - internal class JsonRpcCommand - { - [JsonProperty("jsonRpc")] public string JsonRpc { get; set; } = "2.0"; - [JsonProperty("id")] public string Id { get; set; } = Guid.NewGuid().ToString(); - [JsonProperty("method")] public string Method { get; set; } - - [JsonProperty("params")] public T Parameters { get; set; } - - public JsonRpcCommand() - { - } - - public JsonRpcCommand(string method, T parameters) - { - Method = method; - Parameters = parameters; - } - } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAccountRequest.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAccountRequest.cs deleted file mode 100644 index 6ce4a3f80..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAccountRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class CreateAccountRequest - { - [JsonProperty("label")] public string Label { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAccountResponse.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAccountResponse.cs deleted file mode 100644 index 16f8c6587..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAccountResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class CreateAccountResponse - { - [JsonProperty("account_index")] public long AccountIndex { get; set; } - [JsonProperty("address")] public string Address { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAddressRequest.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAddressRequest.cs deleted file mode 100644 index 752579007..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAddressRequest.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class CreateAddressRequest - { - [JsonProperty("account_index")] public long AccountIndex { get; set; } - [JsonProperty("label")] public string Label { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAddressResponse.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAddressResponse.cs deleted file mode 100644 index 9603363b8..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/CreateAddressResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class CreateAddressResponse - { - [JsonProperty("address")] public string Address { get; set; } - [JsonProperty("address_index")] public long AddressIndex { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetAccountsRequest.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetAccountsRequest.cs deleted file mode 100644 index fa70211b9..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetAccountsRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class GetAccountsRequest - { - [JsonProperty("tag")] public string Tag { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetAccountsResponse.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetAccountsResponse.cs deleted file mode 100644 index 77cbe5703..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetAccountsResponse.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class GetAccountsResponse - { - [JsonProperty("subaddress_accounts")] public List SubaddressAccounts { get; set; } - [JsonProperty("total_balance")] public decimal TotalBalance { get; set; } - - [JsonProperty("total_unlocked_balance")] - public decimal TotalUnlockedBalance { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetFeeEstimateRequest.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetFeeEstimateRequest.cs deleted file mode 100644 index 8f3979bd8..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetFeeEstimateRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public class GetFeeEstimateRequest - { - [JsonProperty("grace_blocks")] public int? GraceBlocks { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetFeeEstimateResponse.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetFeeEstimateResponse.cs deleted file mode 100644 index f778f1d4b..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetFeeEstimateResponse.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public class GetFeeEstimateResponse - { - [JsonProperty("fee")] public long Fee { get; set; } - [JsonProperty("status")] public string Status { get; set; } - [JsonProperty("untrusted")] public bool Untrusted { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetHeightResponse.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetHeightResponse.cs deleted file mode 100644 index 22a867721..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetHeightResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class GetHeightResponse - { - [JsonProperty("height")] public long Height { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdRequest.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdRequest.cs deleted file mode 100644 index 5e1da36a9..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdRequest.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public class GetTransferByTransactionIdRequest - { - [JsonProperty("txid")] public string TransactionId { get; set; } - - [JsonProperty("account_index")] public long AccountIndex { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdResponse.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdResponse.cs deleted file mode 100644 index c985c4425..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransferByTransactionIdResponse.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class GetTransferByTransactionIdResponse - { - [JsonProperty("transfer")] public TransferItem Transfer { get; set; } - [JsonProperty("transfers")] public IEnumerable Transfers { get; set; } - - public partial class TransferItem - { - [JsonProperty("address")] public string Address { get; set; } - [JsonProperty("amount")] public long Amount { get; set; } - [JsonProperty("confirmations")] public long Confirmations { get; set; } - [JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; } - [JsonProperty("height")] public long Height { get; set; } - [JsonProperty("note")] public string Note { get; set; } - [JsonProperty("payment_id")] public string PaymentId { get; set; } - [JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; } - - [JsonProperty("suggested_confirmations_threshold")] - public long SuggestedConfirmationsThreshold { get; set; } - - [JsonProperty("timestamp")] public long Timestamp { get; set; } - [JsonProperty("txid")] public string Txid { get; set; } - [JsonProperty("type")] public string Type { get; set; } - [JsonProperty("unlock_time")] public long UnlockTime { get; set; } - } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransfersRequest.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransfersRequest.cs deleted file mode 100644 index 8776b958e..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransfersRequest.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class GetTransfersRequest - { - [JsonProperty("in")] public bool In { get; set; } - [JsonProperty("out")] public bool Out { get; set; } - [JsonProperty("pending")] public bool Pending { get; set; } - [JsonProperty("failed")] public bool Failed { get; set; } - [JsonProperty("pool")] public bool Pool { get; set; } - [JsonProperty("filter_by_height ")] public bool FilterByHeight { get; set; } - [JsonProperty("min_height")] public long MinHeight { get; set; } - [JsonProperty("max_height")] public long MaxHeight { get; set; } - [JsonProperty("account_index")] public long AccountIndex { get; set; } - [JsonProperty("subaddr_indices")] public List SubaddrIndices { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransfersResponse.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransfersResponse.cs deleted file mode 100644 index faba474ab..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/GetTransfersResponse.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class GetTransfersResponse - { - [JsonProperty("in")] public List In { get; set; } - [JsonProperty("out")] public List Out { get; set; } - [JsonProperty("pending")] public List Pending { get; set; } - [JsonProperty("failed")] public List Failed { get; set; } - [JsonProperty("pool")] public List Pool { get; set; } - - public partial class GetTransfersResponseItem - - { - [JsonProperty("address")] public string Address { get; set; } - [JsonProperty("amount")] public long Amount { get; set; } - [JsonProperty("confirmations")] public long Confirmations { get; set; } - [JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; } - [JsonProperty("height")] public long Height { get; set; } - [JsonProperty("note")] public string Note { get; set; } - [JsonProperty("payment_id")] public string PaymentId { get; set; } - [JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; } - - [JsonProperty("suggested_confirmations_threshold")] - public long SuggestedConfirmationsThreshold { get; set; } - - [JsonProperty("timestamp")] public long Timestamp { get; set; } - [JsonProperty("txid")] public string Txid { get; set; } - [JsonProperty("type")] public string Type { get; set; } - [JsonProperty("unlock_time")] public long UnlockTime { get; set; } - } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/Info.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/Info.cs deleted file mode 100644 index 6f2ce5e61..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/Info.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class Info - { - [JsonProperty("address")] public string Address { get; set; } - [JsonProperty("avg_download")] public long AvgDownload { get; set; } - [JsonProperty("avg_upload")] public long AvgUpload { get; set; } - [JsonProperty("connection_id")] public string ConnectionId { get; set; } - [JsonProperty("current_download")] public long CurrentDownload { get; set; } - [JsonProperty("current_upload")] public long CurrentUpload { get; set; } - [JsonProperty("height")] public long Height { get; set; } - [JsonProperty("host")] public string Host { get; set; } - [JsonProperty("incoming")] public bool Incoming { get; set; } - [JsonProperty("ip")] public string Ip { get; set; } - [JsonProperty("live_time")] public long LiveTime { get; set; } - [JsonProperty("local_ip")] public bool LocalIp { get; set; } - [JsonProperty("localhost")] public bool Localhost { get; set; } - [JsonProperty("peer_id")] public string PeerId { get; set; } - - [JsonProperty("port")] - [JsonConverter(typeof(ParseStringConverter))] - public long Port { get; set; } - - [JsonProperty("recv_count")] public long RecvCount { get; set; } - [JsonProperty("recv_idle_time")] public long RecvIdleTime { get; set; } - [JsonProperty("send_count")] public long SendCount { get; set; } - [JsonProperty("send_idle_time")] public long SendIdleTime { get; set; } - [JsonProperty("state")] public string State { get; set; } - [JsonProperty("support_flags")] public long SupportFlags { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/MakeUriRequest.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/MakeUriRequest.cs deleted file mode 100644 index 0ea22f945..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/MakeUriRequest.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class MakeUriRequest - { - [JsonProperty("address")] public string Address { get; set; } - [JsonProperty("amount")] public long Amount { get; set; } - [JsonProperty("payment_id")] public string PaymentId { get; set; } - [JsonProperty("tx_description")] public string TxDescription { get; set; } - [JsonProperty("recipient_name")] public string RecipientName { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/MakeUriResponse.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/MakeUriResponse.cs deleted file mode 100644 index 6a4235dd1..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/MakeUriResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class MakeUriResponse - { - [JsonProperty("uri")] public string Uri { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/ParseStringConverter.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/ParseStringConverter.cs deleted file mode 100644 index 3b419d519..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/ParseStringConverter.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Globalization; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - internal class ParseStringConverter : JsonConverter - { - public override bool CanConvert(Type t) => t == typeof(long) || t == typeof(long?); - - public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - return null; - var value = serializer.Deserialize(reader); - long l; - if (Int64.TryParse(value, out l)) - { - return l; - } - - throw new Exception("Cannot unmarshal type long"); - } - - public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) - { - if (untypedValue == null) - { - serializer.Serialize(writer, null); - return; - } - - var value = (long)untypedValue; - serializer.Serialize(writer, value.ToString(CultureInfo.InvariantCulture)); - return; - } - - public static readonly ParseStringConverter Singleton = new ParseStringConverter(); - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/Peer.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/Peer.cs deleted file mode 100644 index 3425049a0..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/Peer.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class Peer - { - [JsonProperty("info")] public Info Info { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/SubaddrIndex.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/SubaddrIndex.cs deleted file mode 100644 index 516ee81cc..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/SubaddrIndex.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class SubaddrIndex - { - [JsonProperty("major")] public long Major { get; set; } - [JsonProperty("minor")] public long Minor { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/SubaddressAccount.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/SubaddressAccount.cs deleted file mode 100644 index 6523f0ba0..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/SubaddressAccount.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class SubaddressAccount - { - [JsonProperty("account_index")] public long AccountIndex { get; set; } - [JsonProperty("balance")] public decimal Balance { get; set; } - [JsonProperty("base_address")] public string BaseAddress { get; set; } - [JsonProperty("label")] public string Label { get; set; } - [JsonProperty("tag")] public string Tag { get; set; } - [JsonProperty("unlocked_balance")] public decimal UnlockedBalance { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/SyncInfoResponse.cs b/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/SyncInfoResponse.cs deleted file mode 100644 index 989ad3d74..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/RPC/Models/SyncInfoResponse.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models -{ - public partial class SyncInfoResponse - { - [JsonProperty("height")] public long Height { get; set; } - [JsonProperty("peers")] public List Peers { get; set; } - [JsonProperty("status")] public string Status { get; set; } - [JsonProperty("target_height")] public long? TargetHeight { get; set; } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/Utils/ZcashMoney.cs b/BTCPayServer/Plugins/Altcoins/Zcash/Utils/ZcashMoney.cs deleted file mode 100644 index fe109e0c7..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/Utils/ZcashMoney.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Globalization; - -namespace BTCPayServer.Services.Altcoins.Zcash.Utils -{ - public class ZcashMoney - { - public static decimal Convert(long zat) - { - var amt = zat.ToString(CultureInfo.InvariantCulture).PadLeft(8, '0'); - amt = amt.Length == 8 ? $"0.{amt}" : amt.Insert(amt.Length - 8, "."); - - return decimal.Parse(amt, CultureInfo.InvariantCulture); - } - - public static long Convert(decimal Zcash) - { - return System.Convert.ToInt64(Zcash * 100000000); - } - } -} diff --git a/BTCPayServer/Plugins/Altcoins/Zcash/ZcashLikeSpecificBtcPayNetwork.cs b/BTCPayServer/Plugins/Altcoins/Zcash/ZcashLikeSpecificBtcPayNetwork.cs deleted file mode 100644 index 98fc75e31..000000000 --- a/BTCPayServer/Plugins/Altcoins/Zcash/ZcashLikeSpecificBtcPayNetwork.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace BTCPayServer.Plugins.Altcoins; - -public class ZcashLikeSpecificBtcPayNetwork : BTCPayNetworkBase -{ - public int MaxTrackedConfirmation = 10; - public string UriScheme { get; set; } -} diff --git a/BTCPayServer/Properties/launchSettings.json b/BTCPayServer/Properties/launchSettings.json index f471ce64a..9d26da915 100644 --- a/BTCPayServer/Properties/launchSettings.json +++ b/BTCPayServer/Properties/launchSettings.json @@ -181,8 +181,7 @@ "BTCPAY_CHEATMODE": "true", "BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer", "BTCPAY_XMR_DAEMON_URI": "http://127.0.0.1:18081", - "BTCPAY_XMR_WALLET_DAEMON_URI": "http://127.0.0.1:18082", - "BTCPAY_XMR_WALLET_DAEMON_WALLETDIR": "/path/to/monero_wallet" + "BTCPAY_XMR_WALLET_DAEMON_URI": "http://127.0.0.1:18082" }, "applicationUrl": "https://localhost:14142/" }, @@ -221,10 +220,7 @@ "BTCPAY_DOCKERDEPLOYMENT": "true", "BTCPAY_RECOMMENDED-PLUGINS": "", "BTCPAY_CHEATMODE": "true", - "BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer", - "BTCPAY_XMR_DAEMON_URI": "http://127.0.0.1:18081", - "BTCPAY_XMR_WALLET_DAEMON_URI": "http://127.0.0.1:18082", - "BTCPAY_XMR_WALLET_DAEMON_WALLETDIR": "/path/to/monero_wallet" + "BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer" }, "applicationUrl": "https://localhost:14142/" } diff --git a/BTCPayServer/Services/Altcoins/Monero/Configuration/MoneroLikeConfiguration.cs b/BTCPayServer/Services/Altcoins/Monero/Configuration/MoneroLikeConfiguration.cs deleted file mode 100644 index c99002abe..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/Configuration/MoneroLikeConfiguration.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace BTCPayServer.Services.Altcoins.Monero.Configuration -{ - public class MoneroLikeConfiguration - { - public Dictionary MoneroLikeConfigurationItems { get; set; } = - new Dictionary(); - } - - public class MoneroLikeConfigurationItem - { - public Uri DaemonRpcUri { get; set; } - public Uri InternalWalletRpcUri { get; set; } - public string WalletDirectory { get; set; } - public string Username { get; set; } - public string Password { get; set; } - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroCheckoutModelExtension.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroCheckoutModelExtension.cs deleted file mode 100644 index 7d00784b0..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroCheckoutModelExtension.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using BTCPayServer.Payments; -using BTCPayServer.Payments.Bitcoin; -using BTCPayServer.Payments.Lightning; -using BTCPayServer.Services.Altcoins.Monero.Services; -using BTCPayServer.Services.Invoices; - -namespace BTCPayServer.Services.Altcoins.Monero.Payments -{ - public class MoneroCheckoutModelExtension : ICheckoutModelExtension - { - private readonly BTCPayNetworkBase _network; - private readonly PaymentMethodHandlerDictionary _handlers; - private readonly IPaymentLinkExtension paymentLinkExtension; - - public MoneroCheckoutModelExtension( - PaymentMethodId paymentMethodId, - IEnumerable paymentLinkExtensions, - BTCPayNetworkBase network, - PaymentMethodHandlerDictionary handlers) - { - PaymentMethodId = paymentMethodId; - _network = network; - _handlers = handlers; - paymentLinkExtension = paymentLinkExtensions.Single(p => p.PaymentMethodId == PaymentMethodId); - } - public PaymentMethodId PaymentMethodId { get; } - - public string Image => _network.CryptoImagePath; - public string Badge => ""; - - public void ModifyCheckoutModel(CheckoutModelContext context) - { - if (context is not { Handler: MoneroLikePaymentMethodHandler handler }) - return; - context.Model.CheckoutBodyComponentName = BitcoinCheckoutModelExtension.CheckoutBodyComponentName; - var details = context.InvoiceEntity.GetPayments(true) - .Select(p => p.GetDetails(handler)) - .Where(p => p is not null) - .FirstOrDefault(); - if (details is not null) - { - context.Model.ReceivedConfirmations = details.ConfirmationCount; - context.Model.RequiredConfirmations = (int)MoneroListener.ConfirmationsRequired(details, context.InvoiceEntity.SpeedPolicy); - } - - context.Model.InvoiceBitcoinUrl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper); - context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrl; - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs deleted file mode 100644 index a0d29ec23..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs +++ /dev/null @@ -1,11 +0,0 @@ -using BTCPayServer.Payments; - -namespace BTCPayServer.Services.Altcoins.Monero.Payments -{ - public class MoneroLikeOnChainPaymentMethodDetails - { - public long AccountIndex { get; set; } - public long AddressIndex { get; set; } - public long? InvoiceSettledConfirmationThreshold { get; set; } - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentData.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentData.cs deleted file mode 100644 index 6a3d79b3d..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentData.cs +++ /dev/null @@ -1,20 +0,0 @@ -using BTCPayServer.Client.Models; -using BTCPayServer.Payments; -using BTCPayServer.Plugins.Altcoins; -using BTCPayServer.Services.Altcoins.Monero.Utils; -using BTCPayServer.Services.Invoices; - -namespace BTCPayServer.Services.Altcoins.Monero.Payments -{ - public class MoneroLikePaymentData - { - public long SubaddressIndex { get; set; } - public long SubaccountIndex { get; set; } - public long BlockHeight { get; set; } - public long ConfirmationCount { get; set; } - public string TransactionId { get; set; } - public long? InvoiceSettledConfirmationThreshold { get; set; } - - public long LockTime { get; set; } = 0; - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs deleted file mode 100644 index 5101b73f2..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using AngleSharp.Dom; -using BTCPayServer.Abstractions.Extensions; -using BTCPayServer.BIP78.Sender; -using BTCPayServer.Data; -using BTCPayServer.Logging; -using BTCPayServer.Models; -using BTCPayServer.Models.InvoicingModels; -using BTCPayServer.Payments; -using BTCPayServer.Plugins.Altcoins; -using BTCPayServer.Rating; -using BTCPayServer.Services.Altcoins.Monero.RPC.Models; -using BTCPayServer.Services.Altcoins.Monero.Services; -using BTCPayServer.Services.Altcoins.Monero.Utils; -using BTCPayServer.Services.Altcoins.Zcash.Payments; -using BTCPayServer.Services.Invoices; -using BTCPayServer.Services.Rates; -using NBitcoin; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Services.Altcoins.Monero.Payments -{ - public class MoneroLikePaymentMethodHandler : IPaymentMethodHandler - { - private readonly MoneroLikeSpecificBtcPayNetwork _network; - public MoneroLikeSpecificBtcPayNetwork Network => _network; - public JsonSerializer Serializer { get; } - private readonly MoneroRPCProvider _moneroRpcProvider; - - public PaymentMethodId PaymentMethodId { get; } - - public MoneroLikePaymentMethodHandler(MoneroLikeSpecificBtcPayNetwork network, MoneroRPCProvider moneroRpcProvider) - { - PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode); - _network = network; - Serializer = BlobSerializer.CreateSerializer().Serializer; - _moneroRpcProvider = moneroRpcProvider; - } - - public Task BeforeFetchingRates(PaymentMethodContext context) - { - context.Prompt.Currency = _network.CryptoCode; - context.Prompt.Divisibility = _network.Divisibility; - if (context.Prompt.Activated) - { - var supportedPaymentMethod = ParsePaymentMethodConfig(context.PaymentMethodConfig); - var walletClient = _moneroRpcProvider.WalletRpcClients[_network.CryptoCode]; - var daemonClient = _moneroRpcProvider.DaemonRpcClients[_network.CryptoCode]; - context.State = new Prepare() - { - GetFeeRate = daemonClient.SendCommandAsync("get_fee_estimate", new GetFeeEstimateRequest()), - ReserveAddress = s => walletClient.SendCommandAsync("create_address", new CreateAddressRequest() { Label = $"btcpay invoice #{s}", AccountIndex = supportedPaymentMethod.AccountIndex }), - AccountIndex = supportedPaymentMethod.AccountIndex - }; - } - return Task.CompletedTask; - } - - public async Task ConfigurePrompt(PaymentMethodContext context) - { - if (!_moneroRpcProvider.IsAvailable(_network.CryptoCode)) - throw new PaymentMethodUnavailableException($"Node or wallet not available"); - var invoice = context.InvoiceEntity; - Prepare moneroPrepare = (Prepare)context.State; - var feeRatePerKb = await moneroPrepare.GetFeeRate; - var address = await moneroPrepare.ReserveAddress(invoice.Id); - - var feeRatePerByte = feeRatePerKb.Fee / 1024; - var details = new MoneroLikeOnChainPaymentMethodDetails() - { - AccountIndex = moneroPrepare.AccountIndex, - AddressIndex = address.AddressIndex, - InvoiceSettledConfirmationThreshold = ParsePaymentMethodConfig(context.PaymentMethodConfig).InvoiceSettledConfirmationThreshold - }; - context.Prompt.Destination = address.Address; - context.Prompt.PaymentMethodFee = MoneroMoney.Convert(feeRatePerByte * 100); - context.Prompt.Details = JObject.FromObject(details, Serializer); - context.TrackedDestinations.Add(address.Address); - } - private MoneroPaymentPromptDetails ParsePaymentMethodConfig(JToken config) - { - return config.ToObject(Serializer) ?? throw new FormatException($"Invalid {nameof(MoneroLikePaymentMethodHandler)}"); - } - object IPaymentMethodHandler.ParsePaymentMethodConfig(JToken config) - { - return ParsePaymentMethodConfig(config); - } - - class Prepare - { - public Task GetFeeRate; - public Func> ReserveAddress; - - public long AccountIndex { get; internal set; } - } - - public MoneroLikeOnChainPaymentMethodDetails ParsePaymentPromptDetails(Newtonsoft.Json.Linq.JToken details) - { - return details.ToObject(Serializer); - } - object IPaymentMethodHandler.ParsePaymentPromptDetails(Newtonsoft.Json.Linq.JToken details) - { - return ParsePaymentPromptDetails(details); - } - - public MoneroLikePaymentData ParsePaymentDetails(JToken details) - { - return details.ToObject(Serializer) ?? throw new FormatException($"Invalid {nameof(MoneroLikePaymentMethodHandler)}"); - } - object IPaymentMethodHandler.ParsePaymentDetails(JToken details) - { - return ParsePaymentDetails(details); - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentLinkExtension.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentLinkExtension.cs deleted file mode 100644 index 88a3718df..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentLinkExtension.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable enable -using System.Globalization; -using BTCPayServer.Payments; -using BTCPayServer.Plugins.Altcoins; -using BTCPayServer.Services.Invoices; -using Microsoft.AspNetCore.Mvc; - -namespace BTCPayServer.Services.Altcoins.Monero.Payments -{ - public class MoneroPaymentLinkExtension : IPaymentLinkExtension - { - private readonly MoneroLikeSpecificBtcPayNetwork _network; - - public MoneroPaymentLinkExtension(PaymentMethodId paymentMethodId, MoneroLikeSpecificBtcPayNetwork network) - { - PaymentMethodId = paymentMethodId; - _network = network; - } - public PaymentMethodId PaymentMethodId { get; } - - public string? GetPaymentLink(PaymentPrompt prompt, IUrlHelper? urlHelper) - { - var due = prompt.Calculate().Due; - return $"{_network.UriScheme}:{prompt.Destination}?tx_amount={due.ToString(CultureInfo.InvariantCulture)}"; - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentPromptDetails.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentPromptDetails.cs deleted file mode 100644 index 7e7212b91..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentPromptDetails.cs +++ /dev/null @@ -1,11 +0,0 @@ -using BTCPayServer.Payments; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Monero.Payments -{ - public class MoneroPaymentPromptDetails - { - public long AccountIndex { get; set; } - public long? InvoiceSettledConfirmationThreshold { get; set; } - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/RPC/MoneroDaemonCallbackController.cs b/BTCPayServer/Services/Altcoins/Monero/RPC/MoneroDaemonCallbackController.cs deleted file mode 100644 index 3b2634d94..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/RPC/MoneroDaemonCallbackController.cs +++ /dev/null @@ -1,38 +0,0 @@ -using BTCPayServer.Filters; -using Microsoft.AspNetCore.Mvc; - -namespace BTCPayServer.Services.Altcoins.Monero.RPC -{ - [Route("[controller]")] - [OnlyIfSupportAttribute("XMR-CHAIN")] - public class MoneroLikeDaemonCallbackController : Controller - { - private readonly EventAggregator _eventAggregator; - - public MoneroLikeDaemonCallbackController(EventAggregator eventAggregator) - { - _eventAggregator = eventAggregator; - } - [HttpGet("block")] - public IActionResult OnBlockNotify(string hash, string cryptoCode) - { - _eventAggregator.Publish(new MoneroEvent() - { - BlockHash = hash, - CryptoCode = cryptoCode.ToUpperInvariant() - }); - return Ok(); - } - [HttpGet("tx")] - public IActionResult OnTransactionNotify(string hash, string cryptoCode) - { - _eventAggregator.Publish(new MoneroEvent() - { - TransactionHash = hash, - CryptoCode = cryptoCode.ToUpperInvariant() - }); - return Ok(); - } - - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/RPC/MoneroEvent.cs b/BTCPayServer/Services/Altcoins/Monero/RPC/MoneroEvent.cs deleted file mode 100644 index 4821730ec..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/RPC/MoneroEvent.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace BTCPayServer.Services.Altcoins.Monero.RPC -{ - public class MoneroEvent - { - public string BlockHash { get; set; } - public string TransactionHash { get; set; } - public string CryptoCode { get; set; } - - public override string ToString() - { - return - $"{CryptoCode}: {(string.IsNullOrEmpty(TransactionHash) ? string.Empty : "Tx Update")}{(string.IsNullOrEmpty(BlockHash) ? string.Empty : "New Block")} ({TransactionHash ?? string.Empty}{BlockHash ?? string.Empty})"; - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroLikeSummaryUpdaterHostedService.cs b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroLikeSummaryUpdaterHostedService.cs deleted file mode 100644 index 0aeff2c58..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroLikeSummaryUpdaterHostedService.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using BTCPayServer.Logging; -using BTCPayServer.Services.Altcoins.Monero.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace BTCPayServer.Services.Altcoins.Monero.Services -{ - public class MoneroLikeSummaryUpdaterHostedService : IHostedService - { - private readonly MoneroRPCProvider _MoneroRpcProvider; - private readonly MoneroLikeConfiguration _moneroLikeConfiguration; - - public Logs Logs { get; } - - private CancellationTokenSource _Cts; - public MoneroLikeSummaryUpdaterHostedService(MoneroRPCProvider moneroRpcProvider, MoneroLikeConfiguration moneroLikeConfiguration, Logs logs) - { - _MoneroRpcProvider = moneroRpcProvider; - _moneroLikeConfiguration = moneroLikeConfiguration; - Logs = logs; - } - public Task StartAsync(CancellationToken cancellationToken) - { - _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - foreach (var moneroLikeConfigurationItem in _moneroLikeConfiguration.MoneroLikeConfigurationItems) - { - _ = StartLoop(_Cts.Token, moneroLikeConfigurationItem.Key); - } - return Task.CompletedTask; - } - - private async Task StartLoop(CancellationToken cancellation, string cryptoCode) - { - Logs.PayServer.LogInformation($"Starting listening Monero-like daemons ({cryptoCode})"); - try - { - while (!cancellation.IsCancellationRequested) - { - try - { - await _MoneroRpcProvider.UpdateSummary(cryptoCode); - if (_MoneroRpcProvider.IsAvailable(cryptoCode)) - { - await Task.Delay(TimeSpan.FromMinutes(1), cancellation); - } - else - { - await Task.Delay(TimeSpan.FromSeconds(10), cancellation); - } - } - catch (Exception ex) when (!cancellation.IsCancellationRequested) - { - Logs.PayServer.LogError(ex, $"Unhandled exception in Summary updater ({cryptoCode})"); - await Task.Delay(TimeSpan.FromSeconds(10), cancellation); - } - } - } - catch when (cancellation.IsCancellationRequested) { } - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _Cts?.Cancel(); - return Task.CompletedTask; - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs deleted file mode 100644 index dd2cdac37..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs +++ /dev/null @@ -1,401 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using BTCPayServer.Client.Models; -using BTCPayServer.Data; -using BTCPayServer.Events; -using BTCPayServer.HostedServices; -using BTCPayServer.Payments; -using BTCPayServer.Plugins.Altcoins; -using BTCPayServer.Services.Altcoins.Monero.Configuration; -using BTCPayServer.Services.Altcoins.Monero.Payments; -using BTCPayServer.Services.Altcoins.Monero.RPC; -using BTCPayServer.Services.Altcoins.Monero.RPC.Models; -using BTCPayServer.Services.Altcoins.Monero.Utils; -using BTCPayServer.Services.Altcoins.Zcash.Utils; -using BTCPayServer.Services.Invoices; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using NBitcoin; -using NBXplorer; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Services.Altcoins.Monero.Services -{ - public class MoneroListener : EventHostedServiceBase - { - private readonly InvoiceRepository _invoiceRepository; - private readonly EventAggregator _eventAggregator; - private readonly MoneroRPCProvider _moneroRpcProvider; - private readonly MoneroLikeConfiguration _MoneroLikeConfiguration; - private readonly BTCPayNetworkProvider _networkProvider; - private readonly ILogger _logger; - private readonly PaymentMethodHandlerDictionary _handlers; - private readonly InvoiceActivator _invoiceActivator; - private readonly PaymentService _paymentService; - - public MoneroListener(InvoiceRepository invoiceRepository, - EventAggregator eventAggregator, - MoneroRPCProvider moneroRpcProvider, - MoneroLikeConfiguration moneroLikeConfiguration, - BTCPayNetworkProvider networkProvider, - ILogger logger, - PaymentMethodHandlerDictionary handlers, - InvoiceActivator invoiceActivator, - PaymentService paymentService) : base(eventAggregator, logger) - { - _invoiceRepository = invoiceRepository; - _eventAggregator = eventAggregator; - _moneroRpcProvider = moneroRpcProvider; - _MoneroLikeConfiguration = moneroLikeConfiguration; - _networkProvider = networkProvider; - _logger = logger; - _handlers = handlers; - _invoiceActivator = invoiceActivator; - _paymentService = paymentService; - } - - protected override void SubscribeToEvents() - { - base.SubscribeToEvents(); - Subscribe(); - Subscribe(); - } - - protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) - { - if (evt is MoneroRPCProvider.MoneroDaemonStateChange stateChange) - { - if (_moneroRpcProvider.IsAvailable(stateChange.CryptoCode)) - { - _logger.LogInformation($"{stateChange.CryptoCode} just became available"); - _ = UpdateAnyPendingMoneroLikePayment(stateChange.CryptoCode); - } - else - { - _logger.LogInformation($"{stateChange.CryptoCode} just became unavailable"); - } - } - else if (evt is MoneroEvent moneroEvent) - { - if (!_moneroRpcProvider.IsAvailable(moneroEvent.CryptoCode)) - return; - - if (!string.IsNullOrEmpty(moneroEvent.BlockHash)) - { - await OnNewBlock(moneroEvent.CryptoCode); - } - if (!string.IsNullOrEmpty(moneroEvent.TransactionHash)) - { - await OnTransactionUpdated(moneroEvent.CryptoCode, moneroEvent.TransactionHash); - } - } - } - - private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment) - { - _logger.LogInformation( - $"Invoice {invoice.Id} received payment {payment.Value} {payment.Currency} {payment.Id}"); - - var prompt = invoice.GetPaymentPrompt(payment.PaymentMethodId); - - if (prompt != null && - prompt.Activated && - prompt.Destination == payment.Destination && - prompt.Calculate().Due > 0.0m) - { - await _invoiceActivator.ActivateInvoicePaymentMethod(invoice.Id, payment.PaymentMethodId, true); - invoice = await _invoiceRepository.GetInvoice(invoice.Id); - } - - _eventAggregator.Publish( - new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment }); - } - - private async Task UpdatePaymentStates(string cryptoCode, InvoiceEntity[] invoices) - { - if (!invoices.Any()) - { - return; - } - - var moneroWalletRpcClient = _moneroRpcProvider.WalletRpcClients[cryptoCode]; - var network = _networkProvider.GetNetwork(cryptoCode); - var paymentId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode); - var handler = (MoneroLikePaymentMethodHandler)_handlers[paymentId]; - - //get all the required data in one list (invoice, its existing payments and the current payment method details) - var expandedInvoices = invoices.Select(entity => (Invoice: entity, - ExistingPayments: GetAllMoneroLikePayments(entity, cryptoCode), - Prompt: entity.GetPaymentPrompt(paymentId), - PaymentMethodDetails: handler.ParsePaymentPromptDetails(entity.GetPaymentPrompt(paymentId).Details))) - .Select(tuple => ( - tuple.Invoice, - tuple.PaymentMethodDetails, - tuple.Prompt, - ExistingPayments: tuple.ExistingPayments.Select(entity => - (Payment: entity, PaymentData: handler.ParsePaymentDetails(entity.Details), - tuple.Invoice)) - )); - - var existingPaymentData = expandedInvoices.SelectMany(tuple => tuple.ExistingPayments); - - var accountToAddressQuery = new Dictionary>(); - //create list of subaddresses to account to query the monero wallet - foreach (var expandedInvoice in expandedInvoices) - { - var addressIndexList = - accountToAddressQuery.GetValueOrDefault(expandedInvoice.PaymentMethodDetails.AccountIndex, - new List()); - - addressIndexList.AddRange( - expandedInvoice.ExistingPayments.Select(tuple => tuple.PaymentData.SubaddressIndex)); - addressIndexList.Add(expandedInvoice.PaymentMethodDetails.AddressIndex); - accountToAddressQuery.AddOrReplace(expandedInvoice.PaymentMethodDetails.AccountIndex, addressIndexList); - } - - var tasks = accountToAddressQuery.ToDictionary(datas => datas.Key, - datas => moneroWalletRpcClient.SendCommandAsync( - "get_transfers", - new GetTransfersRequest() - { - AccountIndex = datas.Key, - In = true, - SubaddrIndices = datas.Value.Distinct().ToList() - })); - - await Task.WhenAll(tasks.Values); - - - var transferProcessingTasks = new List(); - - var updatedPaymentEntities = new List<(PaymentEntity Payment, InvoiceEntity invoice)>(); - foreach (var keyValuePair in tasks) - { - var transfers = keyValuePair.Value.Result.In; - if (transfers == null) - { - continue; - } - - transferProcessingTasks.AddRange(transfers.Select(transfer => - { - InvoiceEntity invoice = null; - var existingMatch = existingPaymentData.SingleOrDefault(tuple => - tuple.Payment.Destination == transfer.Address && - tuple.PaymentData.TransactionId == transfer.Txid); - - if (existingMatch.Invoice != null) - { - invoice = existingMatch.Invoice; - } - else - { - var newMatch = expandedInvoices.SingleOrDefault(tuple => - tuple.Prompt.Destination == transfer.Address); - - if (newMatch.Invoice == null) - { - return Task.CompletedTask; - } - - invoice = newMatch.Invoice; - } - - - return HandlePaymentData(cryptoCode, transfer.Address, transfer.Amount, transfer.SubaddrIndex.Major, - transfer.SubaddrIndex.Minor, transfer.Txid, transfer.Confirmations, transfer.Height, transfer.UnlockTime,invoice, - updatedPaymentEntities); - })); - } - - transferProcessingTasks.Add( - _paymentService.UpdatePayments(updatedPaymentEntities.Select(tuple => tuple.Item1).ToList())); - await Task.WhenAll(transferProcessingTasks); - foreach (var valueTuples in updatedPaymentEntities.GroupBy(entity => entity.Item2)) - { - if (valueTuples.Any()) - { - _eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(valueTuples.Key.Id)); - } - } - } - - private async Task OnNewBlock(string cryptoCode) - { - await UpdateAnyPendingMoneroLikePayment(cryptoCode); - _eventAggregator.Publish(new NewBlockEvent() { PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode) }); - } - - private async Task OnTransactionUpdated(string cryptoCode, string transactionHash) - { - var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode); - var transfer = await GetTransferByTxId(cryptoCode, transactionHash, this.CancellationToken); - - var paymentsToUpdate = new List<(PaymentEntity Payment, InvoiceEntity invoice)>(); - - //group all destinations of the tx together and loop through the sets - foreach (var destination in transfer.Transfers.GroupBy(destination => destination.Address)) - { - //find the invoice corresponding to this address, else skip - var invoice = await _invoiceRepository.GetInvoiceFromAddress(paymentMethodId, destination.Key); - if (invoice == null) - continue; - - var index = destination.First().SubaddrIndex; - - await HandlePaymentData(cryptoCode, - destination.Key, - destination.Sum(destination1 => destination1.Amount), - index.Major, - index.Minor, - transfer.Transfer.Txid, - transfer.Transfer.Confirmations, - transfer.Transfer.Height - , transfer.Transfer.UnlockTime,invoice, paymentsToUpdate); - } - - if (paymentsToUpdate.Any()) - { - await _paymentService.UpdatePayments(paymentsToUpdate.Select(tuple => tuple.Payment).ToList()); - foreach (var valueTuples in paymentsToUpdate.GroupBy(entity => entity.invoice)) - { - if (valueTuples.Any()) - { - _eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(valueTuples.Key.Id)); - } - } - } - } - - private async Task GetTransferByTxId(string cryptoCode, - string transactionHash, CancellationToken cancellationToken) - { - var accounts = await _moneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync("get_accounts", new GetAccountsRequest(), cancellationToken); - var accountIndexes = accounts - .SubaddressAccounts - .Select(a => new long?(a.AccountIndex)) - .ToList(); - if (accountIndexes.Count is 0) - accountIndexes.Add(null); - var req = accountIndexes - .Select(i => GetTransferByTxId(cryptoCode, transactionHash, i)) - .ToArray(); - foreach (var task in req) - { - var result = await task; - if (result != null) - return result; - } - - return null; - } - - private async Task GetTransferByTxId(string cryptoCode, string transactionHash, long? accountIndex) - { - try - { - var result = await _moneroRpcProvider.WalletRpcClients[cryptoCode] - .SendCommandAsync( - "get_transfer_by_txid", - new GetTransferByTransactionIdRequest() - { - TransactionId = transactionHash, - AccountIndex = accountIndex - }); - return result; - } - catch (JsonRpcClient.JsonRpcApiException) - { - return null; - } - } - - private async Task HandlePaymentData(string cryptoCode, string address, long totalAmount, long subaccountIndex, - long subaddressIndex, - string txId, long confirmations, long blockHeight, long locktime, InvoiceEntity invoice, - List<(PaymentEntity Payment, InvoiceEntity invoice)> paymentsToUpdate) - { - var network = _networkProvider.GetNetwork(cryptoCode); - var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode); - var handler = (MoneroLikePaymentMethodHandler)_handlers[pmi]; - var promptDetails = handler.ParsePaymentPromptDetails(invoice.GetPaymentPrompt(pmi).Details); - var details = new MoneroLikePaymentData() - { - SubaccountIndex = subaccountIndex, - SubaddressIndex = subaddressIndex, - TransactionId = txId, - ConfirmationCount = confirmations, - BlockHeight = blockHeight, - LockTime = locktime, - InvoiceSettledConfirmationThreshold = promptDetails.InvoiceSettledConfirmationThreshold - }; - var status = GetStatus(details, invoice.SpeedPolicy) ? PaymentStatus.Settled : PaymentStatus.Processing; - var paymentData = new Data.PaymentData() - { - Status = status, - Amount = MoneroMoney.Convert(totalAmount), - Created = DateTimeOffset.UtcNow, - Id = $"{txId}#{subaccountIndex}#{subaddressIndex}", - Currency = network.CryptoCode, - InvoiceDataId = invoice.Id, - }.Set(invoice, handler, details); - - - //check if this tx exists as a payment to this invoice already - var alreadyExistingPaymentThatMatches = GetAllMoneroLikePayments(invoice, cryptoCode) - .SingleOrDefault(c => c.Id == paymentData.Id && c.PaymentMethodId == pmi); - - //if it doesnt, add it and assign a new monerolike address to the system if a balance is still due - if (alreadyExistingPaymentThatMatches == null) - { - var payment = await _paymentService.AddPayment(paymentData, [txId]); - if (payment != null) - await ReceivedPayment(invoice, payment); - } - else - { - //else update it with the new data - alreadyExistingPaymentThatMatches.Status = status; - alreadyExistingPaymentThatMatches.Details = JToken.FromObject(details, handler.Serializer); - paymentsToUpdate.Add((alreadyExistingPaymentThatMatches, invoice)); - } - } - - private bool GetStatus(MoneroLikePaymentData details, SpeedPolicy speedPolicy) - => ConfirmationsRequired(details, speedPolicy) <= details.ConfirmationCount; - - public static long ConfirmationsRequired(MoneroLikePaymentData details, SpeedPolicy speedPolicy) - => (details, speedPolicy) switch - { - (_, _) when details.ConfirmationCount < details.LockTime => details.LockTime - details.ConfirmationCount, - ({ InvoiceSettledConfirmationThreshold: long v }, _) => v, - (_, SpeedPolicy.HighSpeed) => 0, - (_, SpeedPolicy.MediumSpeed) => 1, - (_, SpeedPolicy.LowMediumSpeed) => 2, - (_, SpeedPolicy.LowSpeed) => 6, - _ => 6, - }; - - - private async Task UpdateAnyPendingMoneroLikePayment(string cryptoCode) - { - var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode); - var invoices = await _invoiceRepository.GetMonitoredInvoices(paymentMethodId); - if (!invoices.Any()) - return; - invoices = invoices.Where(entity => entity.GetPaymentPrompt(paymentMethodId)?.Activated is true).ToArray(); - await UpdatePaymentStates(cryptoCode, invoices); - } - - private IEnumerable GetAllMoneroLikePayments(InvoiceEntity invoice, string cryptoCode) - { - return invoice.GetPayments(false) - .Where(p => p.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)); - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroRPCProvider.cs b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroRPCProvider.cs deleted file mode 100644 index 72a81bf9f..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroRPCProvider.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Immutable; -using System.Net.Http; -using System.Threading.Tasks; -using BTCPayServer.Services.Altcoins.Monero.Configuration; -using BTCPayServer.Services.Altcoins.Monero.RPC; -using BTCPayServer.Services.Altcoins.Monero.RPC.Models; -using NBitcoin; - -namespace BTCPayServer.Services.Altcoins.Monero.Services -{ - public class MoneroRPCProvider - { - private readonly MoneroLikeConfiguration _moneroLikeConfiguration; - private readonly EventAggregator _eventAggregator; - public ImmutableDictionary DaemonRpcClients; - public ImmutableDictionary WalletRpcClients; - - private readonly ConcurrentDictionary _summaries = - new ConcurrentDictionary(); - - public ConcurrentDictionary Summaries => _summaries; - - public MoneroRPCProvider(MoneroLikeConfiguration moneroLikeConfiguration, EventAggregator eventAggregator, IHttpClientFactory httpClientFactory) - { - _moneroLikeConfiguration = moneroLikeConfiguration; - _eventAggregator = eventAggregator; - DaemonRpcClients = - _moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key, - pair => new JsonRpcClient(pair.Value.DaemonRpcUri, pair.Value.Username, pair.Value.Password, httpClientFactory.CreateClient($"{pair.Key}client"))); - WalletRpcClients = - _moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key, - pair => new JsonRpcClient(pair.Value.InternalWalletRpcUri, "", "", httpClientFactory.CreateClient($"{pair.Key}client"))); - } - - public bool IsAvailable(string cryptoCode) - { - cryptoCode = cryptoCode.ToUpperInvariant(); - return _summaries.ContainsKey(cryptoCode) && IsAvailable(_summaries[cryptoCode]); - } - - private bool IsAvailable(MoneroLikeSummary summary) - { - return summary.Synced && - summary.WalletAvailable; - } - - public async Task UpdateSummary(string cryptoCode) - { - if (!DaemonRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var daemonRpcClient) || - !WalletRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var walletRpcClient)) - { - return null; - } - - var summary = new MoneroLikeSummary(); - try - { - var daemonResult = - await daemonRpcClient.SendCommandAsync("get_info", - JsonRpcClient.NoRequestModel.Instance); - summary.TargetHeight = daemonResult.TargetHeight.GetValueOrDefault(0); - summary.CurrentHeight = daemonResult.Height; - summary.TargetHeight = summary.TargetHeight == 0 ? summary.CurrentHeight : summary.TargetHeight; - summary.Synced = !daemonResult.BusySyncing; - summary.UpdatedAt = DateTime.UtcNow; - summary.DaemonAvailable = true; - } - catch - { - summary.DaemonAvailable = false; - } - - try - { - var walletResult = - await walletRpcClient.SendCommandAsync( - "get_height", JsonRpcClient.NoRequestModel.Instance); - - summary.WalletHeight = walletResult.Height; - summary.WalletAvailable = true; - } - catch - { - summary.WalletAvailable = false; - } - - var changed = !_summaries.ContainsKey(cryptoCode) || IsAvailable(cryptoCode) != IsAvailable(summary); - - _summaries.AddOrReplace(cryptoCode, summary); - if (changed) - { - _eventAggregator.Publish(new MoneroDaemonStateChange() { Summary = summary, CryptoCode = cryptoCode }); - } - - return summary; - } - - - public class MoneroDaemonStateChange - { - public string CryptoCode { get; set; } - public MoneroLikeSummary Summary { get; set; } - } - - public class MoneroLikeSummary - { - public bool Synced { get; set; } - public long CurrentHeight { get; set; } - public long WalletHeight { get; set; } - public long TargetHeight { get; set; } - public DateTime UpdatedAt { get; set; } - public bool DaemonAvailable { get; set; } - public bool WalletAvailable { get; set; } - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroSyncSummaryProvider.cs b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroSyncSummaryProvider.cs deleted file mode 100644 index 0376fa006..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroSyncSummaryProvider.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using BTCPayServer.Abstractions.Contracts; -using BTCPayServer.Client.Models; -using BTCPayServer.Payments; - -namespace BTCPayServer.Services.Altcoins.Monero.Services -{ - public class MoneroSyncSummaryProvider : ISyncSummaryProvider - { - private readonly MoneroRPCProvider _moneroRpcProvider; - - public MoneroSyncSummaryProvider(MoneroRPCProvider moneroRpcProvider) - { - _moneroRpcProvider = moneroRpcProvider; - } - - public bool AllAvailable() - { - return _moneroRpcProvider.Summaries.All(pair => pair.Value.WalletAvailable); - } - - public string Partial { get; } = "Monero/MoneroSyncSummary"; - public IEnumerable GetStatuses() - { - return _moneroRpcProvider.Summaries.Select(pair => new MoneroSyncStatus() - { - Summary = pair.Value, PaymentMethodId = PaymentMethodId.Parse(pair.Key).ToString() - }); - } - } - - public class MoneroSyncStatus: SyncStatus, ISyncStatus - { - public override bool Available - { - get - { - return Summary?.WalletAvailable ?? false; - } - } - - public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; } - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs b/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs deleted file mode 100644 index 8b84620da..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs +++ /dev/null @@ -1,393 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Abstractions.Constants; -using BTCPayServer.Abstractions.Extensions; -using BTCPayServer.Abstractions.Models; -using BTCPayServer.Client; -using BTCPayServer.Data; -using BTCPayServer.Filters; -using BTCPayServer.Payments; -using BTCPayServer.Services.Altcoins.Monero.Configuration; -using BTCPayServer.Services.Altcoins.Monero.Payments; -using BTCPayServer.Services.Altcoins.Monero.RPC.Models; -using BTCPayServer.Services.Altcoins.Monero.Services; -using BTCPayServer.Services.Invoices; -using BTCPayServer.Services.Stores; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.Extensions.Localization; - -namespace BTCPayServer.Services.Altcoins.Monero.UI -{ - [Route("stores/{storeId}/monerolike")] - [OnlyIfSupportAttribute("XMR-CHAIN")] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public class UIMoneroLikeStoreController : Controller - { - private readonly MoneroLikeConfiguration _MoneroLikeConfiguration; - private readonly StoreRepository _StoreRepository; - private readonly MoneroRPCProvider _MoneroRpcProvider; - private readonly PaymentMethodHandlerDictionary _handlers; - private IStringLocalizer StringLocalizer { get; } - - public UIMoneroLikeStoreController(MoneroLikeConfiguration moneroLikeConfiguration, - StoreRepository storeRepository, MoneroRPCProvider moneroRpcProvider, - PaymentMethodHandlerDictionary handlers, - IStringLocalizer stringLocalizer) - { - _MoneroLikeConfiguration = moneroLikeConfiguration; - _StoreRepository = storeRepository; - _MoneroRpcProvider = moneroRpcProvider; - _handlers = handlers; - StringLocalizer = stringLocalizer; - } - - public StoreData StoreData => HttpContext.GetStoreData(); - - [HttpGet()] - public async Task GetStoreMoneroLikePaymentMethods() - { - return View(await GetVM(StoreData)); - } -[NonAction] - public async Task GetVM(StoreData storeData) - { - var excludeFilters = storeData.GetStoreBlob().GetExcludedPaymentMethods(); - - var accountsList = _MoneroLikeConfiguration.MoneroLikeConfigurationItems.ToDictionary(pair => pair.Key, - pair => GetAccounts(pair.Key)); - - await Task.WhenAll(accountsList.Values); - return new MoneroLikePaymentMethodListViewModel() - { - Items = _MoneroLikeConfiguration.MoneroLikeConfigurationItems.Select(pair => - GetMoneroLikePaymentMethodViewModel(storeData, pair.Key, excludeFilters, - accountsList[pair.Key].Result)) - }; - } - - private Task GetAccounts(string cryptoCode) - { - try - { - if (_MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary) && summary.WalletAvailable) - { - - return _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync("get_accounts", new GetAccountsRequest()); - } - } - catch { } - return Task.FromResult(null); - } - - private MoneroLikePaymentMethodViewModel GetMoneroLikePaymentMethodViewModel( - StoreData storeData, string cryptoCode, - IPaymentFilter excludeFilters, GetAccountsResponse accountsResponse) - { - var monero = storeData.GetPaymentMethodConfigs(_handlers) - .Where(s => s.Value is MoneroPaymentPromptDetails) - .Select(s => (PaymentMethodId: s.Key, Details: (MoneroPaymentPromptDetails)s.Value)); - var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode); - var settings = monero.Where(method => method.PaymentMethodId == pmi).Select(m => m.Details).SingleOrDefault(); - _MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary); - _MoneroLikeConfiguration.MoneroLikeConfigurationItems.TryGetValue(cryptoCode, - out var configurationItem); - var fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet"); - var accounts = accountsResponse?.SubaddressAccounts?.Select(account => - new SelectListItem( - $"{account.AccountIndex} - {(string.IsNullOrEmpty(account.Label) ? "No label" : account.Label)}", - account.AccountIndex.ToString(CultureInfo.InvariantCulture))); - - var settlementThresholdChoice = MoneroLikeSettlementThresholdChoice.StoreSpeedPolicy; - if (settings != null && settings.InvoiceSettledConfirmationThreshold is { } confirmations) - { - settlementThresholdChoice = confirmations switch - { - 0 => MoneroLikeSettlementThresholdChoice.ZeroConfirmation, - 1 => MoneroLikeSettlementThresholdChoice.AtLeastOne, - 10 => MoneroLikeSettlementThresholdChoice.AtLeastTen, - _ => MoneroLikeSettlementThresholdChoice.Custom - }; - } - - return new MoneroLikePaymentMethodViewModel() - { - WalletFileFound = System.IO.File.Exists(fileAddress), - Enabled = - settings != null && - !excludeFilters.Match(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)), - Summary = summary, - CryptoCode = cryptoCode, - AccountIndex = settings?.AccountIndex ?? accountsResponse?.SubaddressAccounts?.FirstOrDefault()?.AccountIndex ?? 0, - Accounts = accounts == null ? null : new SelectList(accounts, nameof(SelectListItem.Value), - nameof(SelectListItem.Text)), - SettlementConfirmationThresholdChoice = settlementThresholdChoice, - CustomSettlementConfirmationThreshold = - settings != null && - settlementThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom - ? settings.InvoiceSettledConfirmationThreshold - : null - }; - } - - [HttpGet("{cryptoCode}")] - public async Task GetStoreMoneroLikePaymentMethod(string cryptoCode) - { - cryptoCode = cryptoCode.ToUpperInvariant(); - if (!_MoneroLikeConfiguration.MoneroLikeConfigurationItems.ContainsKey(cryptoCode)) - { - return NotFound(); - } - - var vm = GetMoneroLikePaymentMethodViewModel(StoreData, cryptoCode, - StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode)); - return View(nameof(GetStoreMoneroLikePaymentMethod), vm); - } - - [HttpPost("{cryptoCode}")] - [DisableRequestSizeLimit] - public async Task GetStoreMoneroLikePaymentMethod(MoneroLikePaymentMethodViewModel viewModel, string command, string cryptoCode) - { - cryptoCode = cryptoCode.ToUpperInvariant(); - if (!_MoneroLikeConfiguration.MoneroLikeConfigurationItems.TryGetValue(cryptoCode, - out var configurationItem)) - { - return NotFound(); - } - - if (command == "add-account") - { - try - { - var newAccount = await _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync("create_account", new CreateAccountRequest() - { - Label = viewModel.NewAccountLabel - }); - viewModel.AccountIndex = newAccount.AccountIndex; - } - catch (Exception) - { - ModelState.AddModelError(nameof(viewModel.AccountIndex), StringLocalizer["Could not create a new account."]); - } - - } - else if (command == "upload-wallet") - { - var valid = true; - if (viewModel.WalletFile == null) - { - ModelState.AddModelError(nameof(viewModel.WalletFile), StringLocalizer["Please select the view-only wallet file"]); - valid = false; - } - if (viewModel.WalletKeysFile == null) - { - ModelState.AddModelError(nameof(viewModel.WalletKeysFile), StringLocalizer["Please select the view-only wallet keys file"]); - valid = false; - } - - if (valid) - { - if (_MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary)) - { - if (summary.WalletAvailable) - { - TempData.SetStatusMessageModel(new StatusMessageModel - { - Severity = StatusMessageModel.StatusSeverity.Error, - Message = StringLocalizer["There is already an active wallet configured for {0}. Replacing it would break any existing invoices!", cryptoCode].Value - }); - return RedirectToAction(nameof(GetStoreMoneroLikePaymentMethod), - new { cryptoCode }); - } - } - - var fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet"); - using (var fileStream = new FileStream(fileAddress, FileMode.Create)) - { - await viewModel.WalletFile.CopyToAsync(fileStream); - try - { - Exec($"chmod 666 {fileAddress}"); - } - catch - { - } - } - - fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet.keys"); - using (var fileStream = new FileStream(fileAddress, FileMode.Create)) - { - await viewModel.WalletKeysFile.CopyToAsync(fileStream); - try - { - Exec($"chmod 666 {fileAddress}"); - } - catch - { - } - - } - - fileAddress = Path.Combine(configurationItem.WalletDirectory, "password"); - using (var fileStream = new StreamWriter(fileAddress, false)) - { - await fileStream.WriteAsync(viewModel.WalletPassword); - try - { - Exec($"chmod 666 {fileAddress}"); - } - catch - { - } - } - - try - { - var response = await _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync("open_wallet", new OpenWalletRequest - { - Filename = "wallet", - Password = viewModel.WalletPassword - }); - if (response?.Error != null) - { - throw new Exception(response.Error.Message); - } - } - catch (Exception ex) - { - ModelState.AddModelError(nameof(viewModel.AccountIndex), StringLocalizer["Could not open the wallet: {0}", ex.Message]); - return View(viewModel); - } - - TempData.SetStatusMessageModel(new StatusMessageModel - { - Severity = StatusMessageModel.StatusSeverity.Info, - Message = StringLocalizer["View-only wallet files uploaded. The wallet will soon become available."].Value - }); - return RedirectToAction(nameof(GetStoreMoneroLikePaymentMethod), new { cryptoCode }); - } - } - - if (!ModelState.IsValid) - { - - var vm = GetMoneroLikePaymentMethodViewModel(StoreData, cryptoCode, - StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode)); - - vm.Enabled = viewModel.Enabled; - vm.NewAccountLabel = viewModel.NewAccountLabel; - vm.AccountIndex = viewModel.AccountIndex; - vm.SettlementConfirmationThresholdChoice = viewModel.SettlementConfirmationThresholdChoice; - vm.CustomSettlementConfirmationThreshold = viewModel.CustomSettlementConfirmationThreshold; - return View(vm); - } - - var storeData = StoreData; - var blob = storeData.GetStoreBlob(); - storeData.SetPaymentMethodConfig(_handlers[PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)], new MoneroPaymentPromptDetails() - { - AccountIndex = viewModel.AccountIndex, - InvoiceSettledConfirmationThreshold = viewModel.SettlementConfirmationThresholdChoice switch - { - MoneroLikeSettlementThresholdChoice.ZeroConfirmation => 0, - MoneroLikeSettlementThresholdChoice.AtLeastOne => 1, - MoneroLikeSettlementThresholdChoice.AtLeastTen => 10, - MoneroLikeSettlementThresholdChoice.Custom when viewModel.CustomSettlementConfirmationThreshold is { } custom => custom, - _ => null - } - }); - - blob.SetExcluded(PaymentTypes.CHAIN.GetPaymentMethodId(viewModel.CryptoCode), !viewModel.Enabled); - storeData.SetStoreBlob(blob); - await _StoreRepository.UpdateStore(storeData); - return RedirectToAction("GetStoreMoneroLikePaymentMethods", - new { StatusMessage = $"{cryptoCode} settings updated successfully", storeId = StoreData.Id }); - } - - private void Exec(string cmd) - { - - var escapedArgs = cmd.Replace("\"", "\\\"", StringComparison.InvariantCulture); - - var process = new Process - { - StartInfo = new ProcessStartInfo - { - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - FileName = "/bin/sh", - Arguments = $"-c \"{escapedArgs}\"" - } - }; - -#pragma warning disable CA1416 // Validate platform compatibility - process.Start(); -#pragma warning restore CA1416 // Validate platform compatibility - process.WaitForExit(); - } - - public class MoneroLikePaymentMethodListViewModel - { - public IEnumerable Items { get; set; } - } - - public class MoneroLikePaymentMethodViewModel : IValidatableObject - { - public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; } - public string CryptoCode { get; set; } - public string NewAccountLabel { get; set; } - public long AccountIndex { get; set; } - public bool Enabled { get; set; } - - public IEnumerable Accounts { get; set; } - public bool WalletFileFound { get; set; } - [Display(Name = "View-Only Wallet File")] - public IFormFile WalletFile { get; set; } - [Display(Name = "Wallet Keys File")] - public IFormFile WalletKeysFile { get; set; } - [Display(Name = "Wallet Password")] - public string WalletPassword { get; set; } - [Display(Name = "Consider the invoice settled when the payment transaction …")] - public MoneroLikeSettlementThresholdChoice SettlementConfirmationThresholdChoice { get; set; } - [Display(Name = "Required Confirmations"), Range(0, 100)] - public long? CustomSettlementConfirmationThreshold { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - if (SettlementConfirmationThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom - && CustomSettlementConfirmationThreshold is null) - { - yield return new ValidationResult( - "You must specify the number of required confirmations when using a custom threshold.", - new[] { nameof(CustomSettlementConfirmationThreshold) }); - } - } - } - - public enum MoneroLikeSettlementThresholdChoice - { - [Display(Name = "Store Speed Policy", Description = "Use the store's speed policy")] - StoreSpeedPolicy, - [Display(Name = "Zero Confirmation", Description = "Is unconfirmed")] - ZeroConfirmation, - [Display(Name = "At Least One", Description = "Has at least 1 confirmation")] - AtLeastOne, - [Display(Name = "At Least Ten", Description = "Has at least 10 confirmations")] - AtLeastTen, - [Display(Name = "Custom", Description = "Custom")] - Custom - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroPaymentViewModel.cs b/BTCPayServer/Services/Altcoins/Monero/UI/MoneroPaymentViewModel.cs deleted file mode 100644 index 68d172ffb..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroPaymentViewModel.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using BTCPayServer.Payments; - -namespace BTCPayServer.Services.Altcoins.Monero.UI -{ - public class MoneroPaymentViewModel - { - public PaymentMethodId PaymentMethodId { get; set; } - public string Confirmations { get; set; } - public string DepositAddress { get; set; } - public string Amount { get; set; } - public string TransactionId { get; set; } - public DateTimeOffset ReceivedTime { get; set; } - public string TransactionLink { get; set; } - public string Currency { get; set; } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/Configuration/ZcashLikeConfiguration.cs b/BTCPayServer/Services/Altcoins/Zcash/Configuration/ZcashLikeConfiguration.cs deleted file mode 100644 index 3d223c02b..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/Configuration/ZcashLikeConfiguration.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace BTCPayServer.Services.Altcoins.Zcash.Configuration -{ - public class ZcashLikeConfiguration - { - public Dictionary ZcashLikeConfigurationItems { get; set; } = - new Dictionary(); - } - - public class ZcashLikeConfigurationItem - { - public Uri DaemonRpcUri { get; set; } - public Uri InternalWalletRpcUri { get; set; } - public string WalletDirectory { get; set; } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashCheckoutModelExtension.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashCheckoutModelExtension.cs deleted file mode 100644 index 3d17be1c8..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashCheckoutModelExtension.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using BTCPayServer.Payments; -using BTCPayServer.Payments.Bitcoin; -using BTCPayServer.Services.Altcoins.Monero.Payments; -using BTCPayServer.Services.Altcoins.Zcash.Services; -using BTCPayServer.Services.Invoices; - -namespace BTCPayServer.Services.Altcoins.Zcash.Payments -{ - public class ZcashCheckoutModelExtension : ICheckoutModelExtension - { - private readonly BTCPayNetworkBase _network; - private readonly PaymentMethodHandlerDictionary _handlers; - private readonly IPaymentLinkExtension paymentLinkExtension; - - public ZcashCheckoutModelExtension( - PaymentMethodId paymentMethodId, - IEnumerable paymentLinkExtensions, - BTCPayNetworkBase network, - PaymentMethodHandlerDictionary handlers) - { - PaymentMethodId = paymentMethodId; - _network = network; - _handlers = handlers; - paymentLinkExtension = paymentLinkExtensions.Single(p => p.PaymentMethodId == PaymentMethodId); - } - public PaymentMethodId PaymentMethodId { get; } - - public string Image => _network.CryptoImagePath; - public string Badge => ""; - - public void ModifyCheckoutModel(CheckoutModelContext context) - { - if (context is not { Handler: ZcashLikePaymentMethodHandler handler }) - return; - context.Model.CheckoutBodyComponentName = BitcoinCheckoutModelExtension.CheckoutBodyComponentName; - var details = context.InvoiceEntity.GetPayments(true) - .Select(p => p.GetDetails(handler)) - .Where(p => p is not null) - .FirstOrDefault(); - if (details is not null) - { - context.Model.ReceivedConfirmations = details.ConfirmationCount; - context.Model.RequiredConfirmations = (int)ZcashListener.ConfirmationsRequired(context.InvoiceEntity.SpeedPolicy); - } - context.Model.InvoiceBitcoinUrl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper); - context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrl; - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentData.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentData.cs deleted file mode 100644 index a30922987..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentData.cs +++ /dev/null @@ -1,17 +0,0 @@ -using BTCPayServer.Client.Models; -using BTCPayServer.Payments; -using BTCPayServer.Plugins.Altcoins; -using BTCPayServer.Services.Altcoins.Zcash.Utils; -using BTCPayServer.Services.Invoices; - -namespace BTCPayServer.Services.Altcoins.Zcash.Payments -{ - public class ZcashLikePaymentData - { - public long SubaddressIndex { get; set; } - public long SubaccountIndex { get; set; } - public long BlockHeight { get; set; } - public long ConfirmationCount { get; set; } - public string TransactionId { get; set; } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentMethodHandler.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentMethodHandler.cs deleted file mode 100644 index a9cc55f47..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashLikePaymentMethodHandler.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using AngleSharp.Dom; -using BTCPayServer.Common; -using BTCPayServer.Data; -using BTCPayServer.Logging; -using BTCPayServer.Models; -using BTCPayServer.Models.InvoicingModels; -using BTCPayServer.Payments; -using BTCPayServer.Plugins.Altcoins; -using BTCPayServer.Rating; -using BTCPayServer.Services.Altcoins.Zcash.RPC.Models; -using BTCPayServer.Services.Altcoins.Zcash.Services; -using BTCPayServer.Services.Altcoins.Zcash.Utils; -using BTCPayServer.Services.Invoices; -using BTCPayServer.Services.Rates; -using NBitcoin; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace BTCPayServer.Services.Altcoins.Zcash.Payments -{ - public class ZcashLikePaymentMethodHandler : IPaymentMethodHandler - { - private readonly ZcashLikeSpecificBtcPayNetwork _network; - public ZcashLikeSpecificBtcPayNetwork Network => _network; - public JsonSerializer Serializer { get; } - private readonly ZcashRPCProvider _ZcashRpcProvider; - public PaymentMethodId PaymentMethodId { get; } - public ZcashLikePaymentMethodHandler(BTCPayNetworkBase network, ZcashRPCProvider ZcashRpcProvider) - { - _network = (ZcashLikeSpecificBtcPayNetwork)network; - PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(_network.CryptoCode); - Serializer = BlobSerializer.CreateSerializer().Serializer; - _ZcashRpcProvider = ZcashRpcProvider; - } - public Task BeforeFetchingRates(PaymentMethodContext context) - { - context.Prompt.Currency = _network.CryptoCode; - context.Prompt.Divisibility = _network.Divisibility; - if (context.Prompt.Activated) - { - var walletClient = _ZcashRpcProvider.WalletRpcClients[_network.CryptoCode]; - var daemonClient = _ZcashRpcProvider.DaemonRpcClients[_network.CryptoCode]; - var config = ParsePaymentMethodConfig(context.PaymentMethodConfig); - context.State = new Prepare() - { - GetFeeRate = daemonClient.SendCommandAsync("get_fee_estimate", new GetFeeEstimateRequest()), - ReserveAddress = s => walletClient.SendCommandAsync("create_address", new CreateAddressRequest() { Label = $"btcpay invoice #{s}", AccountIndex = config.AccountIndex }), - AccountIndex = config.AccountIndex - }; - } - return Task.CompletedTask; - } - public async Task ConfigurePrompt(PaymentMethodContext context) - { - if (!_ZcashRpcProvider.IsAvailable(_network.CryptoCode)) - throw new PaymentMethodUnavailableException($"Node or wallet not available"); - var invoice = context.InvoiceEntity; - var ZcashPrepare = (Prepare)context.State; - var feeRatePerKb = await ZcashPrepare.GetFeeRate; - var address = await ZcashPrepare.ReserveAddress(invoice.Id); - - var feeRatePerByte = feeRatePerKb.Fee / 1024; - - context.Prompt.PaymentMethodFee = ZcashMoney.Convert(feeRatePerByte * 100); - context.Prompt.Details = JObject.FromObject(new ZcashPaymentPromptDetails() - { - AccountIndex = ZcashPrepare.AccountIndex, - AddressIndex = address.AddressIndex, - DepositAddress = address.Address - }, Serializer); - context.TrackedDestinations.Add(address.Address); - } - - public ZcashPaymentPromptDetails ParsePaymentPromptDetails(Newtonsoft.Json.Linq.JToken details) - { - return details.ToObject(Serializer); - } - object IPaymentMethodHandler.ParsePaymentPromptDetails(Newtonsoft.Json.Linq.JToken details) - { - return ParsePaymentPromptDetails(details); - } - object IPaymentMethodHandler.ParsePaymentMethodConfig(JToken config) - { - return ParsePaymentMethodConfig(config); - } - public ZcashPaymentMethodConfig ParsePaymentMethodConfig(JToken config) - { - return config.ToObject(Serializer) ?? throw new FormatException($"Invalid {nameof(ZcashPaymentMethodConfig)}"); - } - - - - class Prepare - { - public Task GetFeeRate; - public Func> ReserveAddress; - public long AccountIndex { get; internal set; } - } - - public ZcashLikePaymentData ParsePaymentDetails(JToken details) - { - return details.ToObject(Serializer) ?? throw new FormatException($"Invalid {nameof(ZcashLikePaymentData)}"); - } - object IPaymentMethodHandler.ParsePaymentDetails(JToken details) - { - return ParsePaymentDetails(details); - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentLinkExtension.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentLinkExtension.cs deleted file mode 100644 index 27b9f3d5e..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentLinkExtension.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable enable -using System.Globalization; -using BTCPayServer.Payments; -using BTCPayServer.Plugins.Altcoins; -using BTCPayServer.Services.Invoices; -using Microsoft.AspNetCore.Mvc; - -namespace BTCPayServer.Services.Altcoins.Zcash.Payments -{ - public class ZcashPaymentLinkExtension : IPaymentLinkExtension - { - private readonly ZcashLikeSpecificBtcPayNetwork _network; - - public ZcashPaymentLinkExtension(PaymentMethodId paymentMethodId, ZcashLikeSpecificBtcPayNetwork network) - { - PaymentMethodId = paymentMethodId; - _network = network; - } - public PaymentMethodId PaymentMethodId { get; } - - public string? GetPaymentLink(PaymentPrompt prompt, IUrlHelper? urlHelper) - { - var due = prompt.Calculate().Due; - return $"{_network.UriScheme}:{prompt.Destination}?amount={due.ToString(CultureInfo.InvariantCulture)}"; - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentMethodConfig.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentMethodConfig.cs deleted file mode 100644 index 9b83b15c4..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentMethodConfig.cs +++ /dev/null @@ -1,10 +0,0 @@ -using BTCPayServer.Payments; -using Newtonsoft.Json; - -namespace BTCPayServer.Services.Altcoins.Zcash.Payments -{ - public class ZcashPaymentMethodConfig - { - public long AccountIndex { get; set; } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentPromptDetails.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentPromptDetails.cs deleted file mode 100644 index c4bfa73b9..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashPaymentPromptDetails.cs +++ /dev/null @@ -1,11 +0,0 @@ -using BTCPayServer.Payments; - -namespace BTCPayServer.Services.Altcoins.Zcash.Payments -{ - public class ZcashPaymentPromptDetails - { - public long AccountIndex { get; set; } - public long AddressIndex { get; set; } - public string DepositAddress { get; set; } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashDaemonCallbackController.cs b/BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashDaemonCallbackController.cs deleted file mode 100644 index 327952c54..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashDaemonCallbackController.cs +++ /dev/null @@ -1,38 +0,0 @@ -using BTCPayServer.Filters; -using Microsoft.AspNetCore.Mvc; - -namespace BTCPayServer.Services.Altcoins.Zcash.RPC -{ - [Route("[controller]")] - [OnlyIfSupportAttribute("ZEC-CHAIN")] - public class ZcashLikeDaemonCallbackController : Controller - { - private readonly EventAggregator _eventAggregator; - - public ZcashLikeDaemonCallbackController(EventAggregator eventAggregator) - { - _eventAggregator = eventAggregator; - } - [HttpGet("block")] - public IActionResult OnBlockNotify(string hash, string cryptoCode) - { - _eventAggregator.Publish(new ZcashEvent() - { - BlockHash = hash, - CryptoCode = cryptoCode.ToUpperInvariant() - }); - return Ok(); - } - [HttpGet("tx")] - public IActionResult OnTransactionNotify(string hash, string cryptoCode) - { - _eventAggregator.Publish(new ZcashEvent() - { - TransactionHash = hash, - CryptoCode = cryptoCode.ToUpperInvariant() - }); - return Ok(); - } - - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashEvent.cs b/BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashEvent.cs deleted file mode 100644 index 486e9d542..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/RPC/ZcashEvent.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace BTCPayServer.Services.Altcoins.Zcash.RPC -{ - public class ZcashEvent - { - public string BlockHash { get; set; } - public string TransactionHash { get; set; } - public string CryptoCode { get; set; } - - public override string ToString() - { - return - $"{CryptoCode}: {(string.IsNullOrEmpty(TransactionHash) ? string.Empty : "Tx Update")}{(string.IsNullOrEmpty(BlockHash) ? string.Empty : "New Block")} ({TransactionHash ?? string.Empty}{BlockHash ?? string.Empty})"; - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashLikeSummaryUpdaterHostedService.cs b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashLikeSummaryUpdaterHostedService.cs deleted file mode 100644 index 274dff203..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashLikeSummaryUpdaterHostedService.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using BTCPayServer.Logging; -using BTCPayServer.Services.Altcoins.Zcash.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace BTCPayServer.Services.Altcoins.Zcash.Services -{ - public class ZcashLikeSummaryUpdaterHostedService : IHostedService - { - private readonly ZcashRPCProvider _ZcashRpcProvider; - private readonly ZcashLikeConfiguration _ZcashLikeConfiguration; - private CancellationTokenSource _Cts; - - public Logs Logs { get; } - - public ZcashLikeSummaryUpdaterHostedService(ZcashRPCProvider ZcashRpcProvider, ZcashLikeConfiguration ZcashLikeConfiguration, Logs logs) - { - _ZcashRpcProvider = ZcashRpcProvider; - _ZcashLikeConfiguration = ZcashLikeConfiguration; - Logs = logs; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - foreach (var ZcashLikeConfigurationItem in _ZcashLikeConfiguration.ZcashLikeConfigurationItems) - { - _ = StartLoop(_Cts.Token, ZcashLikeConfigurationItem.Key); - } - return Task.CompletedTask; - } - - private async Task StartLoop(CancellationToken cancellation, string cryptoCode) - { - Logs.PayServer.LogInformation($"Starting listening Zcash-like daemons ({cryptoCode})"); - try - { - while (!cancellation.IsCancellationRequested) - { - try - { - await _ZcashRpcProvider.UpdateSummary(cryptoCode); - if (_ZcashRpcProvider.IsAvailable(cryptoCode)) - { - await Task.Delay(TimeSpan.FromMinutes(1), cancellation); - } - else - { - await Task.Delay(TimeSpan.FromSeconds(10), cancellation); - } - } - catch (Exception ex) when (!cancellation.IsCancellationRequested) - { - Logs.PayServer.LogError(ex, $"Unhandled exception in Summary updater ({cryptoCode})"); - await Task.Delay(TimeSpan.FromSeconds(10), cancellation); - } - } - } - catch when (cancellation.IsCancellationRequested) { } - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _Cts?.Cancel(); - return Task.CompletedTask; - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs deleted file mode 100644 index c903fc625..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashListener.cs +++ /dev/null @@ -1,356 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using BTCPayServer.Client.Models; -using BTCPayServer.Data; -using BTCPayServer.Events; -using BTCPayServer.HostedServices; -using BTCPayServer.Payments; -using BTCPayServer.Plugins.Altcoins; -using BTCPayServer.Services.Altcoins.Zcash.Configuration; -using BTCPayServer.Services.Altcoins.Zcash.Payments; -using BTCPayServer.Services.Altcoins.Zcash.RPC; -using BTCPayServer.Services.Altcoins.Zcash.RPC.Models; -using BTCPayServer.Services.Altcoins.Zcash.Utils; -using BTCPayServer.Services.Invoices; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using NBitcoin; -using NBitpayClient; -using NBXplorer; -using Newtonsoft.Json.Linq; -using static BTCPayServer.Client.Models.InvoicePaymentMethodDataModel; - -namespace BTCPayServer.Services.Altcoins.Zcash.Services -{ - public class ZcashListener : EventHostedServiceBase - { - private readonly InvoiceRepository _invoiceRepository; - private readonly EventAggregator _eventAggregator; - private readonly ZcashRPCProvider _ZcashRpcProvider; - private readonly ZcashLikeConfiguration _ZcashLikeConfiguration; - private readonly BTCPayNetworkProvider _networkProvider; - private readonly ILogger _logger; - private readonly PaymentService _paymentService; - private readonly InvoiceActivator _invoiceActivator; - private readonly PaymentMethodHandlerDictionary _handlers; - - public ZcashListener(InvoiceRepository invoiceRepository, - EventAggregator eventAggregator, - ZcashRPCProvider ZcashRpcProvider, - ZcashLikeConfiguration ZcashLikeConfiguration, - BTCPayNetworkProvider networkProvider, - ILogger logger, - PaymentService paymentService, - InvoiceActivator invoiceActivator, - PaymentMethodHandlerDictionary handlers) : base(eventAggregator, logger) - { - _invoiceRepository = invoiceRepository; - _eventAggregator = eventAggregator; - _ZcashRpcProvider = ZcashRpcProvider; - _ZcashLikeConfiguration = ZcashLikeConfiguration; - _networkProvider = networkProvider; - _logger = logger; - _paymentService = paymentService; - _invoiceActivator = invoiceActivator; - _handlers = handlers; - } - - protected override void SubscribeToEvents() - { - base.SubscribeToEvents(); - Subscribe(); - Subscribe(); - } - - protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) - { - if (evt is ZcashRPCProvider.ZcashDaemonStateChange stateChanged) - { - if (_ZcashRpcProvider.IsAvailable(stateChanged.CryptoCode)) - { - _logger.LogInformation($"{stateChanged.CryptoCode} just became available"); - _ = UpdateAnyPendingZcashLikePayment(stateChanged.CryptoCode); - } - else - { - _logger.LogInformation($"{stateChanged.CryptoCode} just became unavailable"); - } - } - else if (evt is ZcashEvent zcashEvent) - { - if (!_ZcashRpcProvider.IsAvailable(zcashEvent.CryptoCode)) - return; - - if (!string.IsNullOrEmpty(zcashEvent.BlockHash)) - { - await OnNewBlock(zcashEvent.CryptoCode); - } - if (!string.IsNullOrEmpty(zcashEvent.TransactionHash)) - { - await OnTransactionUpdated(zcashEvent.CryptoCode, zcashEvent.TransactionHash); - } - } - } - - private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment) - { - _logger.LogInformation( - $"Invoice {invoice.Id} received payment {payment.Value} {payment.Currency} {payment.Id}"); - - - var prompt = invoice.GetPaymentPrompt(payment.PaymentMethodId); - - if (prompt != null && - prompt.Activated && - prompt.Destination == payment.Destination && - prompt.Calculate().Due > 0.0m) - { - await _invoiceActivator.ActivateInvoicePaymentMethod(invoice.Id, payment.PaymentMethodId, true); - invoice = await _invoiceRepository.GetInvoice(invoice.Id); - } - - _eventAggregator.Publish( - new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment }); - } - - private async Task UpdatePaymentStates(string cryptoCode, InvoiceEntity[] invoices) - { - if (!invoices.Any()) - { - return; - } - - var ZcashWalletRpcClient = _ZcashRpcProvider.WalletRpcClients[cryptoCode]; - var network = _networkProvider.GetNetwork(cryptoCode); - - var paymentId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode); - var handler = (ZcashLikePaymentMethodHandler)_handlers[paymentId]; - - //get all the required data in one list (invoice, its existing payments and the current payment method details) - var expandedInvoices = invoices.Select(entity => (Invoice: entity, - ExistingPayments: GetAllZcashLikePayments(entity, cryptoCode), - Prompt: entity.GetPaymentPrompt(paymentId), - PaymentMethodDetails: handler.ParsePaymentPromptDetails(entity.GetPaymentPrompt(paymentId).Details))) - .Select(tuple => ( - tuple.Invoice, - tuple.PaymentMethodDetails, - tuple.Prompt, - ExistingPayments: tuple.ExistingPayments.Select(entity => - (Payment: entity, PaymentData: handler.ParsePaymentDetails(entity.Details), - tuple.Invoice)) - )); - - var existingPaymentData = expandedInvoices.SelectMany(tuple => tuple.ExistingPayments); - - var accountToAddressQuery = new Dictionary>(); - //create list of subaddresses to account to query the Zcash wallet - foreach (var expandedInvoice in expandedInvoices) - { - var addressIndexList = - accountToAddressQuery.GetValueOrDefault(expandedInvoice.PaymentMethodDetails.AccountIndex, - new List()); - - addressIndexList.AddRange( - expandedInvoice.ExistingPayments.Select(tuple => tuple.PaymentData.SubaddressIndex)); - addressIndexList.Add(expandedInvoice.PaymentMethodDetails.AddressIndex); - accountToAddressQuery.AddOrReplace(expandedInvoice.PaymentMethodDetails.AccountIndex, addressIndexList); - } - - var tasks = accountToAddressQuery.ToDictionary(datas => datas.Key, - datas => ZcashWalletRpcClient.SendCommandAsync( - "get_transfers", - new GetTransfersRequest() - { - AccountIndex = datas.Key, - In = true, - SubaddrIndices = datas.Value.Distinct().ToList() - })); - - await Task.WhenAll(tasks.Values); - - - var transferProcessingTasks = new List(); - - var updatedPaymentEntities = new List<(PaymentEntity Payment, InvoiceEntity invoice)>(); - foreach (var keyValuePair in tasks) - { - var transfers = keyValuePair.Value.Result.In; - if (transfers == null) - { - continue; - } - - transferProcessingTasks.AddRange(transfers.Select(transfer => - { - InvoiceEntity invoice = null; - var existingMatch = existingPaymentData.SingleOrDefault(tuple => - tuple.Payment.Destination == transfer.Address && - tuple.PaymentData.TransactionId == transfer.Txid); - - if (existingMatch.Invoice != null) - { - invoice = existingMatch.Invoice; - } - else - { - var newMatch = expandedInvoices.SingleOrDefault(tuple => - tuple.Prompt.Destination == transfer.Address); - - if (newMatch.Invoice == null) - { - return Task.CompletedTask; - } - - invoice = newMatch.Invoice; - } - - - return HandlePaymentData(cryptoCode, transfer.Address, transfer.Amount, transfer.SubaddrIndex.Major, - transfer.SubaddrIndex.Minor, transfer.Txid, transfer.Confirmations, transfer.Height, invoice, - updatedPaymentEntities); - })); - } - - transferProcessingTasks.Add( - _paymentService.UpdatePayments(updatedPaymentEntities.Select(tuple => tuple.Item1).ToList())); - await Task.WhenAll(transferProcessingTasks); - foreach (var valueTuples in updatedPaymentEntities.GroupBy(entity => entity.Item2)) - { - if (valueTuples.Any()) - { - _eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(valueTuples.Key.Id)); - } - } - } - - private async Task OnNewBlock(string cryptoCode) - { - await UpdateAnyPendingZcashLikePayment(cryptoCode); - _eventAggregator.Publish(new NewBlockEvent() { PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode) }); - } - - private async Task OnTransactionUpdated(string cryptoCode, string transactionHash) - { - var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode); - var transfer = await _ZcashRpcProvider.WalletRpcClients[cryptoCode] - .SendCommandAsync( - "get_transfer_by_txid", - new GetTransferByTransactionIdRequest() { TransactionId = transactionHash }); - - var paymentsToUpdate = new List<(PaymentEntity Payment, InvoiceEntity invoice)>(); - - //group all destinations of the tx together and loop through the sets - foreach (var destination in transfer.Transfers.GroupBy(destination => destination.Address)) - { - //find the invoice corresponding to this address, else skip - var invoice = await _invoiceRepository.GetInvoiceFromAddress(paymentMethodId, destination.Key); - if (invoice == null) - continue; - - var index = destination.First().SubaddrIndex; - - await HandlePaymentData(cryptoCode, - destination.Key, - destination.Sum(destination1 => destination1.Amount), - index.Major, - index.Minor, - transfer.Transfer.Txid, - transfer.Transfer.Confirmations, - transfer.Transfer.Height - , invoice, paymentsToUpdate); - } - - if (paymentsToUpdate.Any()) - { - await _paymentService.UpdatePayments(paymentsToUpdate.Select(tuple => tuple.Payment).ToList()); - foreach (var valueTuples in paymentsToUpdate.GroupBy(entity => entity.invoice)) - { - if (valueTuples.Any()) - { - _eventAggregator.Publish(new Events.InvoiceNeedUpdateEvent(valueTuples.Key.Id)); - } - } - } - } - - private async Task HandlePaymentData(string cryptoCode, string address, long totalAmount, long subaccountIndex, - long subaddressIndex, - string txId, long confirmations, long blockHeight, InvoiceEntity invoice, - List<(PaymentEntity Payment, InvoiceEntity invoice)> paymentsToUpdate) - { - var network = _networkProvider.GetNetwork(cryptoCode); - var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode); - var handler = (ZcashLikePaymentMethodHandler)_handlers[pmi]; - - var details = new ZcashLikePaymentData() - { - SubaccountIndex = subaccountIndex, - SubaddressIndex = subaddressIndex, - TransactionId = txId, - ConfirmationCount = confirmations, - BlockHeight = blockHeight - }; - var status = GetStatus(details, invoice.SpeedPolicy) ? PaymentStatus.Settled : PaymentStatus.Processing; - var paymentData = new Data.PaymentData() - { - Status = status, - Amount = ZcashMoney.Convert(totalAmount), - Created = DateTimeOffset.UtcNow, - Id = $"{txId}#{subaccountIndex}#{subaddressIndex}", - Currency = network.CryptoCode - }.Set(invoice, handler, details); - - - var alreadyExistingPaymentThatMatches = GetAllZcashLikePayments(invoice, cryptoCode) - .SingleOrDefault(c => c.Id == paymentData.Id && c.PaymentMethodId == pmi); - - //if it doesnt, add it and assign a new Zcashlike address to the system if a balance is still due - if (alreadyExistingPaymentThatMatches == null) - { - var payment = await _paymentService.AddPayment(paymentData, [txId]); - if (payment != null) - await ReceivedPayment(invoice, payment); - } - else - { - //else update it with the new data - alreadyExistingPaymentThatMatches.Status = status; - alreadyExistingPaymentThatMatches.Details = JToken.FromObject(details, handler.Serializer); - paymentsToUpdate.Add((alreadyExistingPaymentThatMatches, invoice)); - } - } - - private bool GetStatus(ZcashLikePaymentData details, SpeedPolicy speedPolicy) - => details.ConfirmationCount >= ConfirmationsRequired(speedPolicy); - public static int ConfirmationsRequired(SpeedPolicy speedPolicy) - => speedPolicy switch - { - SpeedPolicy.HighSpeed => 0, - SpeedPolicy.MediumSpeed => 1, - SpeedPolicy.LowMediumSpeed => 2, - SpeedPolicy.LowSpeed => 6, - _ => 6, - }; - - private async Task UpdateAnyPendingZcashLikePayment(string cryptoCode) - { - var paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode); - var invoices = await _invoiceRepository.GetMonitoredInvoices(paymentMethodId); - if (!invoices.Any()) - return; - invoices = invoices.Where(entity => entity.GetPaymentPrompt(paymentMethodId).Activated).ToArray(); - await UpdatePaymentStates(cryptoCode, invoices); - } - - private IEnumerable GetAllZcashLikePayments(InvoiceEntity invoice, string cryptoCode) - { - return invoice.GetPayments(false) - .Where(p => p.PaymentMethodId == PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)); - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashRPCProvider.cs b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashRPCProvider.cs deleted file mode 100644 index 82e2cb6e5..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashRPCProvider.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Immutable; -using System.Globalization; -using System.Net.Http; -using System.Threading.Tasks; -using BTCPayServer.Services.Altcoins.Zcash.Configuration; -using BTCPayServer.Services.Altcoins.Zcash.RPC; -using BTCPayServer.Services.Altcoins.Zcash.RPC.Models; -using NBitcoin; - -namespace BTCPayServer.Services.Altcoins.Zcash.Services -{ - public class ZcashRPCProvider - { - private readonly ZcashLikeConfiguration _ZcashLikeConfiguration; - private readonly EventAggregator _eventAggregator; - public ImmutableDictionary DaemonRpcClients; - public ImmutableDictionary WalletRpcClients; - - private readonly ConcurrentDictionary _summaries = - new ConcurrentDictionary(); - - public ConcurrentDictionary Summaries => _summaries; - - public ZcashRPCProvider(ZcashLikeConfiguration ZcashLikeConfiguration, EventAggregator eventAggregator, IHttpClientFactory httpClientFactory) - { - _ZcashLikeConfiguration = ZcashLikeConfiguration; - _eventAggregator = eventAggregator; - DaemonRpcClients = - _ZcashLikeConfiguration.ZcashLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key, - pair => new JsonRpcClient(pair.Value.DaemonRpcUri, "", "", httpClientFactory.CreateClient())); - WalletRpcClients = - _ZcashLikeConfiguration.ZcashLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key, - pair => new JsonRpcClient(pair.Value.InternalWalletRpcUri, "", "", httpClientFactory.CreateClient())); - } - - public bool IsAvailable(string cryptoCode) - { - cryptoCode = cryptoCode.ToUpperInvariant(); - return _summaries.ContainsKey(cryptoCode) && IsAvailable(_summaries[cryptoCode]); - } - - private bool IsAvailable(ZcashLikeSummary summary) - { - return summary.Synced && - summary.WalletAvailable; - } - - public async Task UpdateSummary(string cryptoCode) - { - if (!DaemonRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var daemonRpcClient) || - !WalletRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var walletRpcClient)) - { - return null; - } - - var summary = new ZcashLikeSummary(); - try - { - var daemonResult = - await daemonRpcClient.SendCommandAsync("sync_info", - JsonRpcClient.NoRequestModel.Instance); - - summary.TargetHeight = daemonResult.TargetHeight.GetValueOrDefault(0); - summary.CurrentHeight = daemonResult.Height; - summary.TargetHeight = summary.TargetHeight == 0 ? summary.CurrentHeight : summary.TargetHeight; - summary.Synced = daemonResult.Height >= summary.TargetHeight && summary.CurrentHeight > 0; - summary.UpdatedAt = DateTime.Now; - summary.DaemonAvailable = true; - } - catch (Exception e) - { - Console.WriteLine(e.Message); - summary.DaemonAvailable = false; - } - - try - { - var walletResult = - await walletRpcClient.SendCommandAsync( - "get_height", JsonRpcClient.NoRequestModel.Instance); - - summary.WalletHeight = walletResult.Height; - summary.WalletAvailable = true; - } - catch - { - summary.WalletAvailable = false; - } - - var changed = !_summaries.ContainsKey(cryptoCode) || IsAvailable(cryptoCode) != IsAvailable(summary); - - _summaries.AddOrReplace(cryptoCode, summary); - if (changed) - { - _eventAggregator.Publish(new ZcashDaemonStateChange() { Summary = summary, CryptoCode = cryptoCode }); - } - - return summary; - } - - - public class ZcashDaemonStateChange - { - public string CryptoCode { get; set; } - public ZcashLikeSummary Summary { get; set; } - } - - public class ZcashLikeSummary - { - public bool Synced { get; set; } - public long CurrentHeight { get; set; } - public long WalletHeight { get; set; } - public long TargetHeight { get; set; } - public DateTime UpdatedAt { get; set; } - public bool DaemonAvailable { get; set; } - public bool WalletAvailable { get; set; } - - public override String ToString() { return String.Format(CultureInfo.InvariantCulture, "{0} {1} {2} {3} {4} {5}", Synced, CurrentHeight, TargetHeight, WalletHeight, DaemonAvailable, WalletAvailable); } - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashSyncSummaryProvider.cs b/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashSyncSummaryProvider.cs deleted file mode 100644 index 35f1b11ee..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/Services/ZcashSyncSummaryProvider.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using BTCPayServer.Abstractions.Contracts; -using BTCPayServer.Client.Models; -using BTCPayServer.Payments; - -namespace BTCPayServer.Services.Altcoins.Zcash.Services -{ - public class ZcashSyncSummaryProvider : ISyncSummaryProvider - { - private readonly ZcashRPCProvider _ZcashRpcProvider; - - public ZcashSyncSummaryProvider(ZcashRPCProvider ZcashRpcProvider) - { - _ZcashRpcProvider = ZcashRpcProvider; - } - - public bool AllAvailable() - { - return _ZcashRpcProvider.Summaries.All(pair => pair.Value.WalletAvailable); - } - - public string Partial { get; } = "Zcash/ZcashSyncSummary"; - public IEnumerable GetStatuses() - { - return _ZcashRpcProvider.Summaries.Select(pair => new ZcashSyncStatus() - { - Summary = pair.Value, PaymentMethodId = PaymentMethodId.Parse(pair.Key).ToString() - }); - } - } - - public class ZcashSyncStatus: SyncStatus, ISyncStatus - { - public override bool Available - { - get - { - return Summary?.WalletAvailable ?? false; - } - } - - public ZcashRPCProvider.ZcashLikeSummary Summary { get; set; } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/UI/ZcashLikeStoreController.cs b/BTCPayServer/Services/Altcoins/Zcash/UI/ZcashLikeStoreController.cs deleted file mode 100644 index c7db2ddb1..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/UI/ZcashLikeStoreController.cs +++ /dev/null @@ -1,311 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Abstractions.Constants; -using BTCPayServer.Abstractions.Extensions; -using BTCPayServer.Abstractions.Models; -using BTCPayServer.Client; -using BTCPayServer.Data; -using BTCPayServer.Filters; -using BTCPayServer.Payments; -using BTCPayServer.Payments.Bitcoin; -using BTCPayServer.Services.Altcoins.Zcash.Configuration; -using BTCPayServer.Services.Altcoins.Zcash.Payments; -using BTCPayServer.Services.Altcoins.Zcash.RPC.Models; -using BTCPayServer.Services.Altcoins.Zcash.Services; -using BTCPayServer.Services.Invoices; -using BTCPayServer.Services.Stores; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.Extensions.Localization; - -namespace BTCPayServer.Services.Altcoins.Zcash.UI -{ - [Route("stores/{storeId}/Zcashlike")] - [OnlyIfSupportAttribute("ZEC-CHAIN")] - [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] - public class UIZcashLikeStoreController : Controller - { - private readonly ZcashLikeConfiguration _ZcashLikeConfiguration; - private readonly StoreRepository _StoreRepository; - private readonly ZcashRPCProvider _ZcashRpcProvider; - private readonly PaymentMethodHandlerDictionary _handlers; - private IStringLocalizer StringLocalizer { get; } - - public UIZcashLikeStoreController(ZcashLikeConfiguration ZcashLikeConfiguration, - StoreRepository storeRepository, ZcashRPCProvider ZcashRpcProvider, - PaymentMethodHandlerDictionary handlers, - IStringLocalizer stringLocalizer) - { - _ZcashLikeConfiguration = ZcashLikeConfiguration; - _StoreRepository = storeRepository; - _ZcashRpcProvider = ZcashRpcProvider; - _handlers = handlers; - StringLocalizer = stringLocalizer; - } - - public StoreData StoreData => HttpContext.GetStoreData(); - - [HttpGet()] - public async Task GetStoreZcashLikePaymentMethods() - { - var Zcash = StoreData.GetPaymentMethodConfigs(_handlers); - - var excludeFilters = StoreData.GetStoreBlob().GetExcludedPaymentMethods(); - - var accountsList = _ZcashLikeConfiguration.ZcashLikeConfigurationItems.ToDictionary(pair => pair.Key, - pair => GetAccounts(pair.Key)); - - await Task.WhenAll(accountsList.Values); - return View(new ZcashLikePaymentMethodListViewModel() - { - Items = _ZcashLikeConfiguration.ZcashLikeConfigurationItems.Select(pair => - GetZcashLikePaymentMethodViewModel(StoreData, pair.Key, excludeFilters, - accountsList[pair.Key].Result)) - }); - } - - private Task GetAccounts(string cryptoCode) - { - try - { - if (_ZcashRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary) && summary.WalletAvailable) - { - - return _ZcashRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync("get_accounts", new GetAccountsRequest()); - } - } - catch { } - return Task.FromResult(null); - } - - private ZcashLikePaymentMethodViewModel GetZcashLikePaymentMethodViewModel( - StoreData store, string cryptoCode, - IPaymentFilter excludeFilters, GetAccountsResponse accountsResponse) - { - var Zcash = store.GetPaymentMethodConfigs(_handlers); - var settings = Zcash.SingleOrDefault(method => ((IHasNetwork)_handlers[method.Key]).Network.CryptoCode == cryptoCode).Value; - _ZcashRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary); - _ZcashLikeConfiguration.ZcashLikeConfigurationItems.TryGetValue(cryptoCode, - out var configurationItem); - var fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet"); - var accounts = accountsResponse?.SubaddressAccounts?.Select(account => - new SelectListItem( - $"{account.AccountIndex} - {(string.IsNullOrEmpty(account.Label) ? "No label" : account.Label)}", - account.AccountIndex.ToString(CultureInfo.InvariantCulture))); - return new ZcashLikePaymentMethodViewModel() - { - WalletFileFound = System.IO.File.Exists(fileAddress), - Enabled = - settings != null && - !excludeFilters.Match(PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)), - Summary = summary, - CryptoCode = cryptoCode, - AccountIndex = settings?.AccountIndex ?? accountsResponse?.SubaddressAccounts?.FirstOrDefault()?.AccountIndex ?? 0, - Accounts = accounts == null ? null : new SelectList(accounts, nameof(SelectListItem.Value), - nameof(SelectListItem.Text)) - }; - } - - [HttpGet("{cryptoCode}")] - public async Task GetStoreZcashLikePaymentMethod(string cryptoCode) - { - cryptoCode = cryptoCode.ToUpperInvariant(); - if (!_ZcashLikeConfiguration.ZcashLikeConfigurationItems.ContainsKey(cryptoCode)) - { - return NotFound(); - } - - var vm = GetZcashLikePaymentMethodViewModel(StoreData, cryptoCode, - StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode)); - return View(nameof(GetStoreZcashLikePaymentMethod), vm); - } - - [DisableRequestSizeLimit] - [HttpPost("{cryptoCode}")] - public async Task GetStoreZcashLikePaymentMethod(ZcashLikePaymentMethodViewModel viewModel, string command, string cryptoCode) - { - cryptoCode = cryptoCode.ToUpperInvariant(); - if (!_ZcashLikeConfiguration.ZcashLikeConfigurationItems.TryGetValue(cryptoCode, - out var configurationItem)) - { - return NotFound(); - } - - if (command == "add-account") - { - try - { - var newAccount = await _ZcashRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync("create_account", new CreateAccountRequest() - { - Label = viewModel.NewAccountLabel - }); - viewModel.AccountIndex = newAccount.AccountIndex; - } - catch (Exception) - { - ModelState.AddModelError(nameof(viewModel.AccountIndex), StringLocalizer["Could not create new account."]); - } - - } - else if (command == "upload-wallet") - { - var valid = true; - if (viewModel.WalletFile == null) - { - ModelState.AddModelError(nameof(viewModel.WalletFile), StringLocalizer["Please select the wallet file"]); - valid = false; - } - if (viewModel.WalletKeysFile == null) - { - ModelState.AddModelError(nameof(viewModel.WalletKeysFile), StringLocalizer["Please select the wallet.keys file"]); - valid = false; - } - - if (valid) - { - if (_ZcashRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary)) - { - if (summary.WalletAvailable) - { - TempData.SetStatusMessageModel(new StatusMessageModel - { - Severity = StatusMessageModel.StatusSeverity.Error, - Message = StringLocalizer["There is already an active wallet configured for {0}. Replacing it would break any existing invoices!", cryptoCode].Value - }); - return RedirectToAction(nameof(GetStoreZcashLikePaymentMethod), - new { cryptoCode }); - } - } - - var fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet"); - using (var fileStream = new FileStream(fileAddress, FileMode.Create)) - { - await viewModel.WalletFile.CopyToAsync(fileStream); - try - { - Exec($"chmod 666 {fileAddress}"); - } - catch - { - } - } - - fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet.keys"); - using (var fileStream = new FileStream(fileAddress, FileMode.Create)) - { - await viewModel.WalletKeysFile.CopyToAsync(fileStream); - try - { - Exec($"chmod 666 {fileAddress}"); - } - catch - { - } - - } - - fileAddress = Path.Combine(configurationItem.WalletDirectory, "password"); - using (var fileStream = new StreamWriter(fileAddress, false)) - { - await fileStream.WriteAsync(viewModel.WalletPassword); - try - { - Exec($"chmod 666 {fileAddress}"); - } - catch - { - } - } - - return RedirectToAction(nameof(GetStoreZcashLikePaymentMethod), new - { - cryptoCode, - StatusMessage = "Wallet files uploaded. If it was valid, the wallet will become available soon" - - }); - } - } - - if (!ModelState.IsValid) - { - - var vm = GetZcashLikePaymentMethodViewModel(StoreData, cryptoCode, - StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode)); - - vm.Enabled = viewModel.Enabled; - vm.NewAccountLabel = viewModel.NewAccountLabel; - vm.AccountIndex = viewModel.AccountIndex; - return View(vm); - } - - var storeData = StoreData; - var blob = storeData.GetStoreBlob(); - var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode); - storeData.SetPaymentMethodConfig(_handlers[pmi], new ZcashPaymentMethodConfig() - { - AccountIndex = viewModel.AccountIndex - }); - - blob.SetExcluded(PaymentTypes.CHAIN.GetPaymentMethodId(viewModel.CryptoCode), !viewModel.Enabled); - storeData.SetStoreBlob(blob); - await _StoreRepository.UpdateStore(storeData); - return RedirectToAction("GetStoreZcashLikePaymentMethods", - new { StatusMessage = $"{cryptoCode} settings updated successfully", storeId = StoreData.Id }); - } - - private void Exec(string cmd) - { - - var escapedArgs = cmd.Replace("\"", "\\\"", StringComparison.InvariantCulture); - - var process = new Process - { - StartInfo = new ProcessStartInfo - { - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - FileName = "/bin/sh", - Arguments = $"-c \"{escapedArgs}\"" - } - }; - -#pragma warning disable CA1416 // Validate platform compatibility - process.Start(); -#pragma warning restore CA1416 // Validate platform compatibility - process.WaitForExit(); - } - - public class ZcashLikePaymentMethodListViewModel - { - public IEnumerable Items { get; set; } - } - - public class ZcashLikePaymentMethodViewModel - { - public ZcashRPCProvider.ZcashLikeSummary Summary { get; set; } - public string CryptoCode { get; set; } - public string NewAccountLabel { get; set; } - public long AccountIndex { get; set; } - public bool Enabled { get; set; } - - public IEnumerable Accounts { get; set; } - public bool WalletFileFound { get; set; } - [Display(Name = "View-Only Wallet File")] - public IFormFile WalletFile { get; set; } - public IFormFile WalletKeysFile { get; set; } - public string WalletPassword { get; set; } - } - } -} diff --git a/BTCPayServer/Services/Altcoins/Zcash/UI/ZcashPaymentViewModel.cs b/BTCPayServer/Services/Altcoins/Zcash/UI/ZcashPaymentViewModel.cs deleted file mode 100644 index 2972c5993..000000000 --- a/BTCPayServer/Services/Altcoins/Zcash/UI/ZcashPaymentViewModel.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using BTCPayServer.Payments; - -namespace BTCPayServer.Services.Altcoins.Zcash.UI -{ - public class ZcashPaymentViewModel - { - public PaymentMethodId PaymentMethodId { get; set; } - public string Confirmations { get; set; } - public string DepositAddress { get; set; } - public string Amount { get; set; } - public string TransactionId { get; set; } - public DateTimeOffset ReceivedTime { get; set; } - public string TransactionLink { get; set; } - public string Currency { get; set; } - } -} diff --git a/BTCPayServer/Views/Shared/Monero/MoneroSyncSummary.cshtml b/BTCPayServer/Views/Shared/Monero/MoneroSyncSummary.cshtml deleted file mode 100644 index 231882e6f..000000000 --- a/BTCPayServer/Views/Shared/Monero/MoneroSyncSummary.cshtml +++ /dev/null @@ -1,26 +0,0 @@ -@using BTCPayServer.Services.Altcoins.Monero.Services -@inject MoneroRPCProvider MoneroRpcProvider -@inject SignInManager SignInManager; - -@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroRpcProvider.Summaries.Any()) -{ - @foreach (var summary in MoneroRpcProvider.Summaries) - { - @if (summary.Value != null) - { - var status = summary.Value.DaemonAvailable - ? summary.Value.Synced ? "enabled" : "pending" - : "disabled"; -
- - @summary.Key -
-
    -
  • Node available: @summary.Value.DaemonAvailable
  • -
  • Wallet available: @summary.Value.WalletAvailable
  • -
  • Last updated: @summary.Value.UpdatedAt
  • -
  • Synced: @summary.Value.Synced (@summary.Value.CurrentHeight / @summary.Value.TargetHeight)
  • -
- } - } -} diff --git a/BTCPayServer/Views/Shared/Monero/StoreNavMoneroExtension.cshtml b/BTCPayServer/Views/Shared/Monero/StoreNavMoneroExtension.cshtml deleted file mode 100644 index fc5ca8bdb..000000000 --- a/BTCPayServer/Views/Shared/Monero/StoreNavMoneroExtension.cshtml +++ /dev/null @@ -1,16 +0,0 @@ -@using BTCPayServer.Services.Altcoins.Monero.Configuration -@using BTCPayServer.Services.Altcoins.Monero.UI -@using Microsoft.AspNetCore.Mvc.TagHelpers -@using BTCPayServer.Abstractions.Contracts -@inject SignInManager SignInManager; -@inject MoneroLikeConfiguration MoneroLikeConfiguration; -@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(UIMoneroLikeStoreController).StartsWith(controller.ToString() ?? string.Empty, StringComparison.InvariantCultureIgnoreCase); -} -@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any()) -{ - Monero -} diff --git a/BTCPayServer/Views/Shared/Monero/StoreWalletsNavMoneroExtension.cshtml b/BTCPayServer/Views/Shared/Monero/StoreWalletsNavMoneroExtension.cshtml deleted file mode 100644 index 2c5bcaa2e..000000000 --- a/BTCPayServer/Views/Shared/Monero/StoreWalletsNavMoneroExtension.cshtml +++ /dev/null @@ -1,34 +0,0 @@ -@using BTCPayServer.Services.Altcoins.Monero.Configuration -@using BTCPayServer.Services.Altcoins.Monero.UI -@using BTCPayServer.Abstractions.Contracts -@inject SignInManager SignInManager; -@inject MoneroLikeConfiguration MoneroLikeConfiguration; -@inject IScopeProvider ScopeProvider -@inject UIMoneroLikeStoreController UIMoneroLikeStore; -@{ - var storeId = ScopeProvider.GetCurrentStoreId(); - -} -@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any()) -{ - var store = Context.GetStoreData(); - var result = await UIMoneroLikeStore.GetVM(store); - - foreach (var item in result.Items) - { - - var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null && - nameof(UIMoneroLikeStoreController).StartsWith(controller.ToString() ?? string.Empty, StringComparison.InvariantCultureIgnoreCase) && - ViewContext.RouteData.Values.TryGetValue("cryptoCode", out var cryptoCode) && cryptoCode is not null && cryptoCode.ToString() == item.CryptoCode; - - } -} diff --git a/BTCPayServer/Views/Shared/Monero/ViewMoneroLikePaymentData.cshtml b/BTCPayServer/Views/Shared/Monero/ViewMoneroLikePaymentData.cshtml deleted file mode 100644 index 5219b0a09..000000000 --- a/BTCPayServer/Views/Shared/Monero/ViewMoneroLikePaymentData.cshtml +++ /dev/null @@ -1,68 +0,0 @@ -@using System.Globalization -@using BTCPayServer.Plugins.Altcoins; -@using BTCPayServer.Services -@using BTCPayServer.Services.Altcoins.Monero.Payments -@using BTCPayServer.Services.Altcoins.Monero.Services -@using BTCPayServer.Services.Altcoins.Monero.UI -@using BTCPayServer.Services.Invoices -@inject DisplayFormatter DisplayFormatter -@model InvoiceDetailsModel -@inject TransactionLinkProviders TransactionLinkProviders -@inject PaymentMethodHandlerDictionary handlers - -@{ - var payments = Model.Payments.Select(payment => - { - if (!handlers.TryGetValue(payment.PaymentMethodId, out var h) || h is not MoneroLikePaymentMethodHandler handler) - return null; - var m = new MoneroPaymentViewModel(); - var onChainPaymentData = handler.ParsePaymentDetails(payment.Details); - m.PaymentMethodId = handler.PaymentMethodId; - m.DepositAddress = payment.Destination; - m.Amount = payment.Value.ToString(CultureInfo.InvariantCulture); - - var confReq = MoneroListener.ConfirmationsRequired(onChainPaymentData, payment.InvoiceEntity.SpeedPolicy); - var confCount = onChainPaymentData.ConfirmationCount; - confCount = Math.Min(confReq, confCount); - m.Confirmations = $"{confCount} / {confReq}"; - - m.TransactionId = onChainPaymentData.TransactionId; - m.ReceivedTime = payment.ReceivedTime; - if (onChainPaymentData.TransactionId != null) - m.TransactionLink = TransactionLinkProviders.GetTransactionLink(m.PaymentMethodId, onChainPaymentData.TransactionId); - m.Currency = payment.Currency; - return m; - }).Where(c => c != null).ToList(); -} - -@if (payments.Any()) -{ -
-
Monero Payments
- - - - - - - - - - - - @foreach (var payment in payments) - { - - - - - - - - } - -
Payment MethodDestinationPayment ProofConfirmationsPaid
@payment.PaymentMethodId@payment.Confirmations - @DisplayFormatter.Currency(payment.Amount, payment.Currency) -
-
-} diff --git a/BTCPayServer/Views/Shared/Zcash/StoreNavZcashExtension.cshtml b/BTCPayServer/Views/Shared/Zcash/StoreNavZcashExtension.cshtml deleted file mode 100644 index 9761c1b73..000000000 --- a/BTCPayServer/Views/Shared/Zcash/StoreNavZcashExtension.cshtml +++ /dev/null @@ -1,16 +0,0 @@ -@using BTCPayServer.Services.Altcoins.Zcash.Configuration -@using BTCPayServer.Services.Altcoins.Zcash.UI -@using Microsoft.AspNetCore.Mvc.TagHelpers -@using BTCPayServer.Abstractions.Contracts -@inject SignInManager SignInManager; -@inject ZcashLikeConfiguration ZcashLikeConfiguration; -@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(UIZcashLikeStoreController).StartsWith(controller.ToString() ?? string.Empty, StringComparison.InvariantCultureIgnoreCase); -} -@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && ZcashLikeConfiguration.ZcashLikeConfigurationItems.Any()) -{ - Zcash -} diff --git a/BTCPayServer/Views/Shared/Zcash/ViewZcashLikePaymentData.cshtml b/BTCPayServer/Views/Shared/Zcash/ViewZcashLikePaymentData.cshtml deleted file mode 100644 index 41d291edf..000000000 --- a/BTCPayServer/Views/Shared/Zcash/ViewZcashLikePaymentData.cshtml +++ /dev/null @@ -1,69 +0,0 @@ -@using System.Globalization -@using BTCPayServer.Plugins.Altcoins; -@using BTCPayServer.Components.TruncateCenter -@using BTCPayServer.Services -@using BTCPayServer.Services.Altcoins.Zcash.Payments -@using BTCPayServer.Services.Altcoins.Zcash.Services -@using BTCPayServer.Services.Altcoins.Zcash.UI -@using BTCPayServer.Services.Invoices -@inject DisplayFormatter DisplayFormatter -@model InvoiceDetailsModel -@inject TransactionLinkProviders TransactionLinkProviders -@inject PaymentMethodHandlerDictionary handlers - -@{ - var payments = Model.Payments.Select(payment => - { - if (!handlers.TryGetValue(payment.PaymentMethodId, out var h) || h is not ZcashLikePaymentMethodHandler handler) - return null; - var m = new ZcashPaymentViewModel(); - var onChainPaymentData = handler.ParsePaymentDetails(payment.Details); - m.PaymentMethodId = handler.PaymentMethodId; - m.DepositAddress = payment.Destination; - m.Amount = payment.Value.ToString(CultureInfo.InvariantCulture); - - var confReq = ZcashListener.ConfirmationsRequired(payment.InvoiceEntity.SpeedPolicy); - var confCount = onChainPaymentData.ConfirmationCount; - confCount = Math.Min(confReq, confCount); - m.Confirmations = $"{confCount} / {confReq}"; - - m.TransactionId = onChainPaymentData.TransactionId; - m.ReceivedTime = payment.ReceivedTime; - if (onChainPaymentData.TransactionId != null) - m.TransactionLink = TransactionLinkProviders.GetTransactionLink(m.PaymentMethodId, onChainPaymentData.TransactionId); - m.Currency = payment.Currency; - return m; - }).Where(c => c != null).ToList(); -} - -@if (payments.Any()) -{ -
-
Zcash Payments
- - - - - - - - - - - - @foreach (var payment in payments) - { - - - - - - - - } - -
Payment MethodDestinationPayment ProofConfirmationsPaid
@payment.PaymentMethodId@payment.Confirmations - @DisplayFormatter.Currency(payment.Amount, payment.Currency) -
-
-} diff --git a/BTCPayServer/Views/Shared/Zcash/ZcashSyncSummary.cshtml b/BTCPayServer/Views/Shared/Zcash/ZcashSyncSummary.cshtml deleted file mode 100644 index 1274aa17e..000000000 --- a/BTCPayServer/Views/Shared/Zcash/ZcashSyncSummary.cshtml +++ /dev/null @@ -1,26 +0,0 @@ -@using BTCPayServer.Services.Altcoins.Zcash.Services -@inject ZcashRPCProvider ZcashRpcProvider -@inject SignInManager SignInManager; - -@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && ZcashRpcProvider.Summaries.Any()) -{ - @foreach (var summary in ZcashRpcProvider.Summaries) - { - @if (summary.Value != null) - { - var status = summary.Value.DaemonAvailable - ? summary.Value.Synced ? "enabled" : "pending" - : "disabled"; -
- - @summary.Key -
-
    -
  • Node available: @summary.Value.DaemonAvailable
  • -
  • Wallet available: @summary.Value.WalletAvailable
  • -
  • Last updated: @summary.Value.UpdatedAt
  • -
  • Synced: @summary.Value.Synced (@summary.Value.CurrentHeight / @summary.Value.TargetHeight)
  • -
- } - } -} diff --git a/BTCPayServer/Views/UIMoneroLikeStore/GetStoreMoneroLikePaymentMethod.cshtml b/BTCPayServer/Views/UIMoneroLikeStore/GetStoreMoneroLikePaymentMethod.cshtml deleted file mode 100644 index 343174464..000000000 --- a/BTCPayServer/Views/UIMoneroLikeStore/GetStoreMoneroLikePaymentMethod.cshtml +++ /dev/null @@ -1,148 +0,0 @@ -@using MoneroLikePaymentMethodViewModel = BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikePaymentMethodViewModel -@using MoneroLikeSettlementThresholdChoice = BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikeSettlementThresholdChoice; -@model MoneroLikePaymentMethodViewModel - -@{ - ViewData.SetActivePage(Model.CryptoCode, StringLocalizer["{0} Settings", Model.CryptoCode], Model.CryptoCode); -} - - - -
-
- @if (!ViewContext.ModelState.IsValid) - { -
- } - @if (Model.Summary != null) - { -
-
    -
  • Node available: @Model.Summary.DaemonAvailable
  • -
  • Wallet available: @Model.Summary.WalletAvailable (@(Model.WalletFileFound ? "Wallet file present" : "Wallet file not found"))
  • -
  • Last updated: @Model.Summary.UpdatedAt
  • -
  • Synced: @Model.Summary.Synced (@Model.Summary.CurrentHeight / @Model.Summary.TargetHeight)
  • -
-
- } - - @if (!Model.WalletFileFound || Model.Summary.WalletHeight == default) - { -
- -
-

Upload Wallet

-
- - - -
-
- - - -
-
- - - -
- -
-
- } -
- - - @if (!Model.WalletFileFound || Model.Summary.WalletHeight == default) - { - - } - else - { -
- - @if (@Model.Accounts != null && Model.Accounts.Any()) - { - - - } - else - { - No accounts available on the current wallet - - } -
-
-
- - -
-
- } - -
- - - -
- -
- - - - - - - -
- - - -
- - - - Back to list - -
-
-
-
- -@section PageFootContent { - @await Html.PartialAsync("_ValidationScriptsPartial") -} diff --git a/BTCPayServer/Views/UIMoneroLikeStore/GetStoreMoneroLikePaymentMethods.cshtml b/BTCPayServer/Views/UIMoneroLikeStore/GetStoreMoneroLikePaymentMethods.cshtml deleted file mode 100644 index 67a64a5dd..000000000 --- a/BTCPayServer/Views/UIMoneroLikeStore/GetStoreMoneroLikePaymentMethods.cshtml +++ /dev/null @@ -1,57 +0,0 @@ -@model BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikePaymentMethodListViewModel - -@{ - ViewData.SetActivePage("Monero Settings", StringLocalizer["{0} Settings", "Monero"], "Monero Settings"); -} - -
-
- @if (!ViewContext.ModelState.IsValid) - { -
- } -
- - - - - - - - - - - @foreach (var item in Model.Items) - { - - - - - - - } - -
CryptoAccount IndexEnabledActions
@item.CryptoCode@item.AccountIndex - @if (item.Enabled) - { - - } - else - { - - } - - - Modify - -
-
-
-
- -@section PageFootContent { - @await Html.PartialAsync("_ValidationScriptsPartial") -} diff --git a/BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethod.cshtml b/BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethod.cshtml deleted file mode 100644 index 20351be06..000000000 --- a/BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethod.cshtml +++ /dev/null @@ -1,112 +0,0 @@ -@using BTCPayServer.Views.Stores -@model BTCPayServer.Services.Altcoins.Zcash.UI.UIZcashLikeStoreController.ZcashLikePaymentMethodViewModel - -@{ - ViewData.SetActivePage(StoreNavPages.OnchainSettings, StringLocalizer["{0} Settings", Model.CryptoCode], $"{Context.GetStoreData().Id}-{Model.CryptoCode}"); -} - -
-
- @if (!ViewContext.ModelState.IsValid) - { -
- } - @if (Model.Summary != null) - { -
-
    -
  • Node available: @Model.Summary.DaemonAvailable
  • -
  • Wallet available: @Model.Summary.WalletAvailable (@(Model.WalletFileFound ? "Wallet file present" : "Wallet file not found"))
  • -
  • Last updated: @Model.Summary.UpdatedAt
  • -
  • Synced: @Model.Summary.Synced (@Model.Summary.CurrentHeight / @Model.Summary.TargetHeight)
  • -
-
- } - - @if (!Model.WalletFileFound || Model.Summary.WalletHeight == default) - { -
- -
-

Upload Wallet

-
- - - -
-
- - - -
-
- - - -
- -
-
- } -
- - - @if (!Model.WalletFileFound || Model.Summary.WalletHeight == default) - { - - } - else - { -
- - @if (@Model.Accounts != null && Model.Accounts.Any()) - { - - - } - else - { - No accounts available on the current wallet - - } -
-
-
- - -
-
- } - -
- - - -
- -
- - - - Back to list - -
-
-
-
- -@section PageFootContent { - @await Html.PartialAsync("_ValidationScriptsPartial") -} diff --git a/BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethods.cshtml b/BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethods.cshtml deleted file mode 100644 index 870469965..000000000 --- a/BTCPayServer/Views/UIZcashLikeStore/GetStoreZcashLikePaymentMethods.cshtml +++ /dev/null @@ -1,58 +0,0 @@ -@using BTCPayServer.Views.Stores -@model BTCPayServer.Services.Altcoins.Zcash.UI.UIZcashLikeStoreController.ZcashLikePaymentMethodListViewModel - -@{ - ViewData.SetActivePage(StoreNavPages.OnchainSettings, StringLocalizer["{0} Settings", "Zcash"], $"{Context.GetStoreData().Id}-ZEC"); -} - -
-
- @if (!ViewContext.ModelState.IsValid) - { -
- } -
- - - - - - - - - - - @foreach (var item in Model.Items) - { - - - - - - - } - -
CryptoAccount IndexEnabledActions
@item.CryptoCode@item.AccountIndex - @if (item.Enabled) - { - - } - else - { - - } - - - Modify - -
-
-
-
- -@section PageFootContent { - @await Html.PartialAsync("_ValidationScriptsPartial") -} diff --git a/BTCPayServer/wwwroot/imlegacy/monero.svg b/BTCPayServer/wwwroot/imlegacy/monero.svg deleted file mode 100644 index 05d80151e..000000000 --- a/BTCPayServer/wwwroot/imlegacy/monero.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/BTCPayServer/wwwroot/imlegacy/zcash.png b/BTCPayServer/wwwroot/imlegacy/zcash.png deleted file mode 100644 index 82de5b7ac1de0e9ff468a680980311b61c84c653..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123472 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

$9+r_knkkIfTwf1vI#>`st{b)eAVwSUleMVZ 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