diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml index 7b0191a..c847767 100644 --- a/.github/workflows/codacy.yml +++ b/.github/workflows/codacy.yml @@ -13,7 +13,7 @@ permissions: jobs: report-coverage: - if: github.repository == 'btcpay-monero/btcpayserver-monero-plugin' && github.event.workflow_run.conclusion == 'success' + if: github.repository == 'btcpay-monero/btcpayserver-monero-plugin' runs-on: ubuntu-latest steps: - name: Download coverage report diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index e88daae..f025bc0 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -41,7 +41,7 @@ jobs: - name: Run unit tests run: | - dotnet tool install --global JetBrains.dotCover.CommandLineTools + dotnet tool install --global JetBrains.dotCover.CommandLineTools --version 2025.1.6 dotCover cover-dotnet --TargetArguments="test BTCPayServer.Plugins.UnitTests -c Release --no-build" --output=coverage/dotCover.UnitTests.output.dcvr --filters="-:Assembly=BTCPayServer.Plugins.UnitTests;-:Assembly=testhost;-:Assembly=BTCPayServer;-:Class=AspNetCoreGeneratedDocument.*" - name: Run integration tests diff --git a/BTCPayServer.Plugins.IntegrationTests/Dockerfile b/BTCPayServer.Plugins.IntegrationTests/Dockerfile index b607dc0..bcd5292 100644 --- a/BTCPayServer.Plugins.IntegrationTests/Dockerfile +++ b/BTCPayServer.Plugins.IntegrationTests/Dockerfile @@ -32,7 +32,7 @@ RUN mkdir -p ${MONERO_PLUGIN_FOLDER} RUN cd Plugins/Monero && dotnet build BTCPayServer.Plugins.Monero.sln --configuration ${CONFIGURATION_NAME} /p:RazorCompileOnBuild=true --output ${MONERO_PLUGIN_FOLDER} RUN cd BTCPayServer.Plugins.IntegrationTests && dotnet build --configuration ${CONFIGURATION_NAME} /p:CI_TESTS=true /p:RazorCompileOnBuild=true RUN dotnet tool install --global Microsoft.Playwright.CLI -RUN dotnet tool install --global JetBrains.DotCover.CommandLineTools +RUN dotnet tool install --global JetBrains.DotCover.CommandLineTools --version 2025.1.6 ENV PATH="$PATH:/root/.dotnet/tools" RUN playwright install chromium --with-deps WORKDIR /source/BTCPayServer.Plugins.IntegrationTests diff --git a/BTCPayServer.Plugins.IntegrationTests/Monero/MoneroPluginIntegrationTest.cs b/BTCPayServer.Plugins.IntegrationTests/Monero/MoneroPluginIntegrationTest.cs index 7c4db0a..670e03b 100644 --- a/BTCPayServer.Plugins.IntegrationTests/Monero/MoneroPluginIntegrationTest.cs +++ b/BTCPayServer.Plugins.IntegrationTests/Monero/MoneroPluginIntegrationTest.cs @@ -36,6 +36,11 @@ public class MoneroPluginIntegrationTest(ITestOutputHelper helper) : MoneroAndBi await s.RegisterNewUser(true); await s.CreateNewStore(preferredExchange: "Kraken"); await s.Page.Locator("a.nav-link[href*='monerolike/XMR']").ClickAsync(); + await s.Page.Locator("input#PrimaryAddress").FillAsync("43Pnj6ZKGFTJhaLhiecSFfLfr64KPJZw7MyGH73T6PTDekBBvsTAaWEUSM4bmJqDuYLizhA13jQkMRPpz9VXBCBqQQb6y5L"); + await s.Page.Locator("input#PrivateViewKey").FillAsync("1bfa03b0c78aa6bc8292cf160ec9875657d61e889c41d0ebe5c54fd3a2c4b40e"); + await s.Page.Locator("input#RestoreHeight").FillAsync("0"); + await s.Page.Locator("input#WalletPassword").FillAsync("pass123"); + await s.Page.ClickAsync("button[name='command'][value='set-wallet-details']"); await s.Page.CheckAsync("#Enabled"); await s.Page.SelectOptionAsync("#SettlementConfirmationThresholdChoice", "2"); await s.Page.ClickAsync("#SaveButton"); diff --git a/BTCPayServer.Plugins.IntegrationTests/docker-compose.yml b/BTCPayServer.Plugins.IntegrationTests/docker-compose.yml index 0b01503..64e1d79 100644 --- a/BTCPayServer.Plugins.IntegrationTests/docker-compose.yml +++ b/BTCPayServer.Plugins.IntegrationTests/docker-compose.yml @@ -32,7 +32,6 @@ services: - nbxplorer - postgres - xmr_wallet - - xmr_cashcow_wallet nbxplorer: image: nicolasdorier/nbxplorer:2.5.25 @@ -90,36 +89,24 @@ services: - "bitcoin_datadir:/data" monerod: - image: btcpayserver/monero:0.18.4.0 + image: btcpayserver/monero:0.18.4.2 restart: unless-stopped container_name: monerod - entrypoint: monerod --fixed-difficulty 1 --log-level=2 --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" --regtest --no-igd --hide-my-port --offline --non-interactive + command: monerod --fixed-difficulty 1 --log-level=2 --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" --regtest --no-igd --hide-my-port --offline --non-interactive volumes: - - "xmr_data:/home/monero/.bitmonero" + - xmr_data:/data ports: - "18081:18081" xmr_wallet: - image: btcpayserver/monero:0.18.4.0 + image: btcpayserver/monero:0.18.4.2 restart: unless-stopped container_name: xmr_wallet - entrypoint: monero-wallet-rpc --log-level 2 --allow-mismatched-daemon-version --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" + command: monero-wallet-rpc --log-level 2 --allow-mismatched-daemon-version --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: - - "xmr_wallet:/wallet" - depends_on: - - monerod - - xmr_cashcow_wallet: - image: btcpayserver/monero:0.18.4.0 - restart: unless-stopped - container_name: xmr_cashcow_wallet - entrypoint: monero-wallet-rpc --log-level 2 --allow-mismatched-daemon-version --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18092 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-dir=/wallet - ports: - - "18092:18092" - volumes: - - "xmr_cashcow_wallet:/wallet" + - xmr_wallet:/wallet depends_on: - monerod @@ -137,7 +124,6 @@ volumes: bitcoin_datadir: xmr_data: xmr_wallet: - xmr_cashcow_wallet: networks: default: diff --git a/BTCPayServer.Plugins.UnitTests/Monero/Configuration/MoneroLikeConfigurationTests.cs b/BTCPayServer.Plugins.UnitTests/Monero/Configuration/MoneroLikeConfigurationTests.cs index d5a4e46..256f9a7 100644 --- a/BTCPayServer.Plugins.UnitTests/Monero/Configuration/MoneroLikeConfigurationTests.cs +++ b/BTCPayServer.Plugins.UnitTests/Monero/Configuration/MoneroLikeConfigurationTests.cs @@ -26,8 +26,7 @@ namespace BTCPayServer.Plugins.UnitTests.Monero.Configuration InternalWalletRpcUri = new Uri("http://localhost:18082"), WalletDirectory = "/wallets", Username = "user", - Password = "password", - CashCowWalletRpcUri = new Uri("http://localhost:18083") + Password = "password" }; Assert.Equal("http://localhost:18081/", configItem.DaemonRpcUri.ToString()); @@ -35,7 +34,6 @@ namespace BTCPayServer.Plugins.UnitTests.Monero.Configuration Assert.Equal("/wallets", configItem.WalletDirectory); Assert.Equal("user", configItem.Username); Assert.Equal("password", configItem.Password); - Assert.Equal("http://localhost:18083/", configItem.CashCowWalletRpcUri.ToString()); } [Trait("Category", "Unit")] diff --git a/Plugins/Monero/Configuration/MoneroLikeConfiguration.cs b/Plugins/Monero/Configuration/MoneroLikeConfiguration.cs index 8fb399a..3d790d0 100644 --- a/Plugins/Monero/Configuration/MoneroLikeConfiguration.cs +++ b/Plugins/Monero/Configuration/MoneroLikeConfiguration.cs @@ -15,6 +15,5 @@ namespace BTCPayServer.Plugins.Monero.Configuration public string WalletDirectory { get; set; } public string Username { get; set; } public string Password { get; set; } - public Uri CashCowWalletRpcUri { get; set; } } } \ No newline at end of file diff --git a/Plugins/Monero/Controllers/GenerateFromKeysException.cs b/Plugins/Monero/Controllers/GenerateFromKeysException.cs new file mode 100644 index 0000000..e26b379 --- /dev/null +++ b/Plugins/Monero/Controllers/GenerateFromKeysException.cs @@ -0,0 +1,5 @@ +using System; + +namespace BTCPayServer.Plugins.Monero.Controllers; + +public class GenerateFromKeysException(string message) : Exception(message); \ No newline at end of file diff --git a/Plugins/Monero/Controllers/MoneroLikeStoreController.cs b/Plugins/Monero/Controllers/MoneroLikeStoreController.cs index aa675dd..ffdd1ae 100644 --- a/Plugins/Monero/Controllers/MoneroLikeStoreController.cs +++ b/Plugins/Monero/Controllers/MoneroLikeStoreController.cs @@ -1,9 +1,7 @@ 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; @@ -21,7 +19,6 @@ 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; @@ -183,22 +180,22 @@ namespace BTCPayServer.Plugins.Monero.Controllers } } - else if (command == "upload-wallet") + else if (command == "set-wallet-details") { var valid = true; - if (viewModel.WalletFile == null) + if (viewModel.PrimaryAddress == null) { - ModelState.AddModelError(nameof(viewModel.WalletFile), StringLocalizer["Please select the view-only wallet file"]); + ModelState.AddModelError(nameof(viewModel.PrimaryAddress), StringLocalizer["Please set your primary public address"]); valid = false; } - if (viewModel.WalletKeysFile == null) + if (viewModel.PrivateViewKey == null) { - ModelState.AddModelError(nameof(viewModel.WalletKeysFile), StringLocalizer["Please select the view-only wallet keys file"]); + ModelState.AddModelError(nameof(viewModel.PrivateViewKey), StringLocalizer["Please set your private view key"]); valid = false; } if (configurationItem.WalletDirectory == null) { - ModelState.AddModelError(nameof(viewModel.WalletFile), StringLocalizer["This installation doesn't support wallet import (BTCPAY_XMR_WALLET_DAEMON_WALLETDIR is not set)"]); + ModelState.AddModelError(nameof(viewModel.PrimaryAddress), StringLocalizer["This installation doesn't support wallet creation (BTCPAY_XMR_WALLET_DAEMON_WALLETDIR is not set)"]); valid = false; } if (valid) @@ -216,71 +213,31 @@ namespace BTCPayServer.Plugins.Monero.Controllers 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 - { - // ignored - } - } - - 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 - { - // ignored - } - } - - fileAddress = Path.Combine(configurationItem.WalletDirectory, "password"); - using (var fileStream = new StreamWriter(fileAddress, false)) - { - await fileStream.WriteAsync(viewModel.WalletPassword); - try - { - Exec($"chmod 666 {fileAddress}"); - } - catch - { - // ignored - } - } - try { - var response = await _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync("open_wallet", new OpenWalletRequest + var response = await _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync("generate_from_keys", new GenerateFromKeysRequest { - Filename = "wallet", + PrimaryAddress = viewModel.PrimaryAddress, + PrivateViewKey = viewModel.PrivateViewKey, + WalletFileName = "view_wallet", + RestoreHeight = viewModel.RestoreHeight, Password = viewModel.WalletPassword }); if (response?.Error != null) { - throw new WalletOpenException(response.Error.Message); + throw new GenerateFromKeysException(response.Error.Message); } } catch (Exception ex) { - ModelState.AddModelError(nameof(viewModel.AccountIndex), StringLocalizer["Could not open the wallet: {0}", ex.Message]); + ModelState.AddModelError(nameof(viewModel.AccountIndex), StringLocalizer["Could not generate view wallet from keys: {0}", ex.Message]); return View("/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml", viewModel); } TempData.SetStatusMessageModel(new StatusMessageModel { Severity = StatusMessageModel.StatusSeverity.Info, - Message = StringLocalizer["View-only wallet files uploaded. The wallet will soon become available."].Value + Message = StringLocalizer["View-only wallet created. The wallet will soon become available."].Value }); return RedirectToAction(nameof(GetStoreMoneroLikePaymentMethod), new { cryptoCode }); } @@ -297,7 +254,6 @@ namespace BTCPayServer.Plugins.Monero.Controllers vm.AccountIndex = viewModel.AccountIndex; vm.SettlementConfirmationThresholdChoice = viewModel.SettlementConfirmationThresholdChoice; vm.CustomSettlementConfirmationThreshold = viewModel.CustomSettlementConfirmationThreshold; - vm.SupportWalletExport = configurationItem.WalletDirectory is not null; return View("/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml", vm); } @@ -323,30 +279,6 @@ namespace BTCPayServer.Plugins.Monero.Controllers 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; } @@ -355,7 +287,6 @@ namespace BTCPayServer.Plugins.Monero.Controllers public class MoneroLikePaymentMethodViewModel : IValidatableObject { public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; } - public bool SupportWalletExport { get; set; } public string CryptoCode { get; set; } public string NewAccountLabel { get; set; } public long AccountIndex { get; set; } @@ -363,10 +294,12 @@ namespace BTCPayServer.Plugins.Monero.Controllers 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 = "Primary Public Address")] + public string PrimaryAddress { get; set; } + [Display(Name = "Private View Key")] + public string PrivateViewKey { get; set; } + [Display(Name = "Restore Height")] + public int RestoreHeight { get; set; } [Display(Name = "Wallet Password")] public string WalletPassword { get; set; } [Display(Name = "Consider the invoice settled when the payment transaction …")] diff --git a/Plugins/Monero/Controllers/WalletOpenException.cs b/Plugins/Monero/Controllers/WalletOpenException.cs deleted file mode 100644 index e1b1e00..0000000 --- a/Plugins/Monero/Controllers/WalletOpenException.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System; - -namespace BTCPayServer.Plugins.Monero.Controllers; - -public class WalletOpenException(string message) : Exception(message); \ No newline at end of file diff --git a/Plugins/Monero/MoneroPlugin.cs b/Plugins/Monero/MoneroPlugin.cs index 33fbaac..cabf09d 100644 --- a/Plugins/Monero/MoneroPlugin.cs +++ b/Plugins/Monero/MoneroPlugin.cs @@ -78,15 +78,12 @@ public class MoneroPlugin : BaseBTCPayServerPlugin 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.AddSingleton(provider => - (ICheckoutCheatModeExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroCheckoutCheatModeExtension), new object[] { network, pmi })); + services.AddSingleton(provider => + (IPaymentMethodHandler)ActivatorUtilities.CreateInstance(provider, typeof(MoneroLikePaymentMethodHandler), network)); + services.AddSingleton(provider => +(IPaymentLinkExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroPaymentLinkExtension), network, pmi)); + services.AddSingleton(provider => +(ICheckoutModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroCheckoutModelExtension), network, pmi)); services.AddUIExtension("store-nav", "/Views/Monero/StoreNavMoneroExtension.cshtml"); services.AddUIExtension("store-wallets-nav", "/Views/Monero/StoreWalletsNavMoneroExtension.cshtml"); @@ -126,9 +123,6 @@ public class MoneroPlugin : BaseBTCPayServerPlugin var walletDaemonUri = configuration.GetOrDefault( $"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_uri", null); - var cashCowWalletDaemonUri = - configuration.GetOrDefault( - $"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_cashcow_wallet_daemon_uri", null); var walletDaemonWalletDirectory = configuration.GetOrDefault( $"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_walletdir", null); @@ -154,14 +148,13 @@ public class MoneroPlugin : BaseBTCPayServerPlugin } else { - result.MoneroLikeConfigurationItems.Add(moneroLikeSpecificBtcPayNetwork.CryptoCode, new MoneroLikeConfigurationItem() + result.MoneroLikeConfigurationItems.Add(moneroLikeSpecificBtcPayNetwork.CryptoCode, new MoneroLikeConfigurationItem { DaemonRpcUri = daemonUri, Username = daemonUsername, Password = daemonPassword, InternalWalletRpcUri = walletDaemonUri, - WalletDirectory = walletDaemonWalletDirectory, - CashCowWalletRpcUri = cashCowWalletDaemonUri, + WalletDirectory = walletDaemonWalletDirectory }); } } diff --git a/Plugins/Monero/RPC/Models/OpenWallerErrorResponse.cs b/Plugins/Monero/RPC/Models/ErrorResponse.cs similarity index 81% rename from Plugins/Monero/RPC/Models/OpenWallerErrorResponse.cs rename to Plugins/Monero/RPC/Models/ErrorResponse.cs index 0fbfb42..2576843 100644 --- a/Plugins/Monero/RPC/Models/OpenWallerErrorResponse.cs +++ b/Plugins/Monero/RPC/Models/ErrorResponse.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace BTCPayServer.Plugins.Monero.RPC.Models { - public partial class OpenWalletErrorResponse + public class ErrorResponse { [JsonProperty("code")] public int Code { get; set; } [JsonProperty("message")] public string Message { get; set; } diff --git a/Plugins/Monero/RPC/Models/GenerateFromKeysRequest.cs b/Plugins/Monero/RPC/Models/GenerateFromKeysRequest.cs new file mode 100644 index 0000000..60ea762 --- /dev/null +++ b/Plugins/Monero/RPC/Models/GenerateFromKeysRequest.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace BTCPayServer.Plugins.Monero.RPC.Models; + +public class GenerateFromKeysRequest +{ + [JsonProperty("address")] public string PrimaryAddress { get; set; } + [JsonProperty("viewkey")] public string PrivateViewKey { get; set; } + [JsonProperty("filename")] public string WalletFileName { get; set; } + [JsonProperty("restore_height")] public int RestoreHeight { get; set; } + [JsonProperty("password")] public string Password { get; set; } +} \ No newline at end of file diff --git a/Plugins/Monero/RPC/Models/GenerateFromKeysResponse.cs b/Plugins/Monero/RPC/Models/GenerateFromKeysResponse.cs new file mode 100644 index 0000000..c28e68d --- /dev/null +++ b/Plugins/Monero/RPC/Models/GenerateFromKeysResponse.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace BTCPayServer.Plugins.Monero.RPC.Models; + +public class GenerateFromKeysResponse +{ + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("jsonrpc")] public string Jsonrpc { get; set; } + [JsonProperty("result")] public GenerateFromKeysResult Result { get; set; } + [JsonProperty("error")] public ErrorResponse Error { get; set; } +} \ No newline at end of file diff --git a/Plugins/Monero/RPC/Models/GenerateFromKeysResult.cs b/Plugins/Monero/RPC/Models/GenerateFromKeysResult.cs new file mode 100644 index 0000000..bfc6c10 --- /dev/null +++ b/Plugins/Monero/RPC/Models/GenerateFromKeysResult.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace BTCPayServer.Plugins.Monero.RPC.Models; + +public class GenerateFromKeysResult +{ + [JsonProperty("address")] public string ViewWalletAddress { get; set; } + [JsonProperty("info")] public string CreationInfo { get; set; } +} \ No newline at end of file diff --git a/Plugins/Monero/RPC/Models/OpenWalletRequest.cs b/Plugins/Monero/RPC/Models/OpenWalletRequest.cs deleted file mode 100644 index 4d1ae17..0000000 --- a/Plugins/Monero/RPC/Models/OpenWalletRequest.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Plugins.Monero.RPC.Models -{ - public partial class OpenWalletRequest - { - [JsonProperty("filename")] public string Filename { get; set; } - [JsonProperty("password")] public string Password { get; set; } - } -} \ No newline at end of file diff --git a/Plugins/Monero/RPC/Models/OpenWalletResponse.cs b/Plugins/Monero/RPC/Models/OpenWalletResponse.cs deleted file mode 100644 index f64579f..0000000 --- a/Plugins/Monero/RPC/Models/OpenWalletResponse.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace BTCPayServer.Plugins.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; } - } -} \ No newline at end of file diff --git a/Plugins/Monero/Services/MoneroCheckoutCheatModeExtension.cs b/Plugins/Monero/Services/MoneroCheckoutCheatModeExtension.cs deleted file mode 100644 index 57e8b7f..0000000 --- a/Plugins/Monero/Services/MoneroCheckoutCheatModeExtension.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Threading.Tasks; - -using BTCPayServer.Payments; -using BTCPayServer.Plugins.Monero.RPC; -using BTCPayServer.Plugins.Monero.RPC.Models; - -namespace BTCPayServer.Plugins.Monero.Services; - -public class MoneroCheckoutCheatModeExtension : ICheckoutCheatModeExtension -{ - private readonly MoneroRPCProvider _rpcProvider; - private readonly MoneroLikeSpecificBtcPayNetwork _network; - private readonly PaymentMethodId _paymentMethodId; - - public MoneroCheckoutCheatModeExtension( - MoneroRPCProvider rpcProvider, - MoneroLikeSpecificBtcPayNetwork network, - PaymentMethodId paymentMethodId) - { - _rpcProvider = rpcProvider; - _network = network; - _paymentMethodId = paymentMethodId; - } - - public bool Handle(PaymentMethodId paymentMethodId) => _paymentMethodId == paymentMethodId; - - public async Task PayInvoice(ICheckoutCheatModeExtension.PayInvoiceContext payInvoiceContext) - { - var amount = payInvoiceContext.Amount; - for (int i = 0; i < _network.Divisibility; i++) - { - amount *= 10; - } - - var cashcow = _rpcProvider.CashCowWalletRpcClients[_network.CryptoCode]; - var result = await cashcow.SendCommandAsync("transfer", - new TransferRequest() - { - Destinations = new[] { new TransferDestination() - { - Amount = (long)amount, - Address = payInvoiceContext.PaymentPrompt.Destination - } - } - }); - return new ICheckoutCheatModeExtension.PayInvoiceResult(result.TransactionHash); - } - - public async Task MineBlock(ICheckoutCheatModeExtension.MineBlockContext mineBlockContext) - { - var cashcow = _rpcProvider.CashCowWalletRpcClients[_network.CryptoCode]; - var deamon = _rpcProvider.DaemonRpcClients[_network.CryptoCode]; - var address = (await cashcow.SendCommandAsync("get_address", new() - { - AccountIndex = 0 - })).Address; - await deamon.SendCommandAsync("generateblocks", new GenerateBlocks() - { - WalletAddress = address, - AmountOfBlocks = mineBlockContext.BlockCount - }); - return new ICheckoutCheatModeExtension.MineBlockResult(); - } -} \ No newline at end of file diff --git a/Plugins/Monero/Services/MoneroRPCProvider.cs b/Plugins/Monero/Services/MoneroRPCProvider.cs index 37cf160..5d96070 100644 --- a/Plugins/Monero/Services/MoneroRPCProvider.cs +++ b/Plugins/Monero/Services/MoneroRPCProvider.cs @@ -1,16 +1,12 @@ using System; using System.Collections.Concurrent; using System.Collections.Immutable; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; using BTCPayServer.Plugins.Monero.Configuration; using BTCPayServer.Plugins.Monero.RPC; using BTCPayServer.Plugins.Monero.RPC.Models; -using BTCPayServer.Services; - -using Microsoft.Extensions.Logging; using NBitcoin; @@ -19,9 +15,7 @@ namespace BTCPayServer.Plugins.Monero.Services public class MoneroRPCProvider { private readonly MoneroLikeConfiguration _moneroLikeConfiguration; - private readonly ILogger _logger; private readonly EventAggregator _eventAggregator; - private readonly BTCPayServerEnvironment environment; public ImmutableDictionary DaemonRpcClients; public ImmutableDictionary WalletRpcClients; @@ -30,14 +24,11 @@ namespace BTCPayServer.Plugins.Monero.Services public ConcurrentDictionary Summaries => _summaries; public MoneroRPCProvider(MoneroLikeConfiguration moneroLikeConfiguration, - ILogger logger, EventAggregator eventAggregator, - IHttpClientFactory httpClientFactory, BTCPayServerEnvironment environment) + IHttpClientFactory httpClientFactory) { _moneroLikeConfiguration = moneroLikeConfiguration; - _logger = logger; _eventAggregator = eventAggregator; - this.environment = environment; DaemonRpcClients = _moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key, pair => new JsonRpcClient(pair.Value.DaemonRpcUri, pair.Value.Username, pair.Value.Password, @@ -46,18 +37,8 @@ namespace BTCPayServer.Plugins.Monero.Services _moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key, pair => new JsonRpcClient(pair.Value.InternalWalletRpcUri, "", "", httpClientFactory.CreateClient($"{pair.Key}client"))); - if (environment.CheatMode) - { - CashCowWalletRpcClients = - _moneroLikeConfiguration.MoneroLikeConfigurationItems - .Where(i => i.Value.CashCowWalletRpcUri is not null).ToImmutableDictionary(pair => pair.Key, - pair => new JsonRpcClient(pair.Value.CashCowWalletRpcUri, "", "", - httpClientFactory.CreateClient($"{pair.Key}cashcow-client"))); - } } - public ImmutableDictionary CashCowWalletRpcClients { get; set; } - public bool IsConfigured(string cryptoCode) => WalletRpcClients.ContainsKey(cryptoCode) && DaemonRpcClients.ContainsKey(cryptoCode); public bool IsAvailable(string cryptoCode) { @@ -96,9 +77,6 @@ namespace BTCPayServer.Plugins.Monero.Services { summary.DaemonAvailable = false; } - - bool walletCreated = false; - retry: try { var walletResult = @@ -107,23 +85,11 @@ namespace BTCPayServer.Plugins.Monero.Services summary.WalletHeight = walletResult.Height; summary.WalletAvailable = true; } - catch when (environment.CheatMode && !walletCreated) - { - await CreateTestWallet(walletRpcClient); - walletCreated = true; - goto retry; - } catch { summary.WalletAvailable = false; } - if (environment.CheatMode && - CashCowWalletRpcClients.TryGetValue(cryptoCode.ToUpperInvariant(), out var cashCow)) - { - await MakeCashCowFat(cashCow, daemonRpcClient); - } - var changed = !_summaries.ContainsKey(cryptoCode) || IsAvailable(cryptoCode) != IsAvailable(summary); _summaries.AddOrReplace(cryptoCode, summary); @@ -135,68 +101,6 @@ namespace BTCPayServer.Plugins.Monero.Services return summary; } - private async Task MakeCashCowFat(JsonRpcClient cashcow, JsonRpcClient deamon) - { - try - { - var walletResult = - await cashcow.SendCommandAsync( - "get_height", JsonRpcClient.NoRequestModel.Instance); - } - catch - { - _logger.LogInformation("Creating XMR cashcow wallet..."); - await CreateTestWallet(cashcow); - } - - var balance = - (await cashcow.SendCommandAsync("get_balance", - JsonRpcClient.NoRequestModel.Instance)); - if (balance.UnlockedBalance != 0) - { - return; - } - _logger.LogInformation("Mining blocks for the cashcow..."); - var address = (await cashcow.SendCommandAsync("get_address", new() - { - AccountIndex = 0 - })).Address; - await deamon.SendCommandAsync("generateblocks", new GenerateBlocks() - { - WalletAddress = address, - AmountOfBlocks = 100 - }); - _logger.LogInformation("Mining succeed!"); - } - - private static async Task CreateTestWallet(JsonRpcClient walletRpcClient) - { - try - { - await walletRpcClient.SendCommandAsync( - "open_wallet", - new OpenWalletRequest() - { - Filename = "wallet", - Password = "password" - }); - return; - } - catch - { - // ignored - } - - await walletRpcClient.SendCommandAsync("create_wallet", - new() - { - Filename = "wallet", - Password = "password", - Language = "English" - }); - } - - public class MoneroDaemonStateChange { public string CryptoCode { get; set; } diff --git a/Plugins/Monero/Services/MoneroSyncSummaryProvider.cs b/Plugins/Monero/Services/MoneroSyncSummaryProvider.cs index 1bbcf1d..83cccc3 100644 --- a/Plugins/Monero/Services/MoneroSyncSummaryProvider.cs +++ b/Plugins/Monero/Services/MoneroSyncSummaryProvider.cs @@ -18,7 +18,7 @@ namespace BTCPayServer.Plugins.Monero.Services public bool AllAvailable() { - return _moneroRpcProvider.Summaries.All(pair => pair.Value.WalletAvailable); + return _moneroRpcProvider.Summaries.All(pair => pair.Value.DaemonAvailable); } public string Partial { get; } = "/Views/Monero/MoneroSyncSummary.cshtml"; diff --git a/Plugins/Monero/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml b/Plugins/Monero/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml index 6f660e0..81444ef 100644 --- a/Plugins/Monero/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml +++ b/Plugins/Monero/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml @@ -20,38 +20,45 @@
  • Node available: @Model.Summary.DaemonAvailable
  • +
  • Wallet RPC available: @Model.Summary.WalletAvailable
  • Last updated: @Model.Summary.UpdatedAt
  • Synced: @Model.Summary.Synced (@Model.Summary.CurrentHeight / @Model.Summary.TargetHeight)
} - @if (Model.SupportWalletExport && Model.Summary?.WalletHeight is null or 0) + @if (Model.Summary?.WalletHeight is null or 0) { -
+ class="mt-4">
-

Upload Wallet

+

Set View-Only Wallet Details

+
- - - + + +
- - - + + +
- - + + + +
+
+ +
diff --git a/README.md b/README.md index cba3912..9a22c38 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,13 @@ This plugin extends BTCPay Server to enable users to receive payments via Monero Configure this plugin using the following environment variables: -| Environment variable | Description | Example | -| --- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- | -**BTCPAY_XMR_DAEMON_URI** | **Required**. The URI of the [monerod](https://github.com/monero-project/monero) RPC interface. | http://127.0.0.1:18081 | -**BTCPAY_XMR_DAEMON_USERNAME** | **Optional**. The username for authenticating with the daemon. | john | -**BTCPAY_XMR_DAEMON_PASSWORD** | **Optional**. The password for authenticating with the daemon. | secret | -**BTCPAY_XMR_WALLET_DAEMON_URI** | **Required**. The URI of the [monero-wallet-rpc](https://getmonero.dev/interacting/monero-wallet-rpc.html) RPC interface. | http://127.0.0.1:18082 | -**BTCPAY_XMR_WALLET_DAEMON_WALLETDIR** | **Optional**. The directory where BTCPay Server saves wallet files uploaded via the UI ([See this blog post for more details](https://sethforprivacy.com/guides/accepting-monero-via-btcpay-server/#configure-the-bitcoin-wallet-of-choice)). | /home/cypherpunk/Monero/wallets/ | +| Environment variable | Description | Example | +| --- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- | +**BTCPAY_XMR_DAEMON_URI** | **Required**. The URI of the [monerod](https://github.com/monero-project/monero) RPC interface. | http://127.0.0.1:18081 | +**BTCPAY_XMR_DAEMON_USERNAME** | **Optional**. The username for authenticating with the daemon. | john | +**BTCPAY_XMR_DAEMON_PASSWORD** | **Optional**. The password for authenticating with the daemon. | secret | +**BTCPAY_XMR_WALLET_DAEMON_URI** | **Required**. The URI of the [monero-wallet-rpc](https://getmonero.dev/interacting/monero-wallet-rpc.html) RPC interface. | http://127.0.0.1:18082 | +**BTCPAY_XMR_WALLET_DAEMON_WALLETDIR** | **Optional**. The directory where BTCPay Server saves wallet files created via the UI ([See this blog post for more details](https://sethforprivacy.com/guides/accepting-monero-via-btcpay-server/#configure-the-bitcoin-wallet-of-choice)). | /home/cypherpunk/Monero/wallets/ | BTCPay Server's Docker deployment simplifies the setup by automatically configuring these variables. For further details, refer to this [blog post](https://sethforprivacy.com/guides/accepting-monero-via-btcpay-server). @@ -68,7 +68,7 @@ dotnet test BTCPayServer.Plugins.UnitTests --verbosity normal To run unit tests with coverage, install JetBrains dotCover CLI: ```bash -dotnet tool install --global JetBrains.dotCover.CommandLineTools +dotnet tool install --global JetBrains.dotCover.CommandLineTools --version 2025.1.6 ``` Then run the following command: @@ -83,10 +83,6 @@ dotnet build btcpay-monero-plugin.sln docker compose -f BTCPayServer.Plugins.IntegrationTests/docker-compose.yml run tests ``` -| Environment variable | Description | Example | -| --- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- | -**BTCPAY_XMR_CASHCOW_WALLET_DAEMON_URI** | **Optional**. | The URI of the [monero-wallet-rpc](https://getmonero.dev/interacting/monero-wallet-rpc.html) interface for the cashcow wallet. This is used to create a second wallet for testing purposes in regtest mode. - ## Code formatting We use the **unmodified** standardized `.editorconfig` from .NET SDK. Run `dotnet new editorconfig --force` to apply the latest version. @@ -105,8 +101,7 @@ Then create the `appsettings.dev.json` file in `btcpayserver/BTCPayServer`, with { "DEBUG_PLUGINS": "..\\..\\Plugins\\Monero\\bin\\Debug\\net8.0\\BTCPayServer.Plugins.Monero.dll", "XMR_DAEMON_URI": "http://127.0.0.1:18081", - "XMR_WALLET_DAEMON_URI": "http://127.0.0.1:18082", - "XMR_CASHCOW_WALLET_DAEMON_URI": "http://127.0.0.1:18092" + "XMR_WALLET_DAEMON_URI": "http://127.0.0.1:18082" } ``` This will ensure that BTCPay Server loads the plugin when it starts. @@ -133,9 +128,6 @@ We recommend using [Rider](https://www.jetbrains.com/rider/) for plugin developm Visual Studio does not support this feature. -When debugging in regtest, BTCPay Server will automatically create an configure two wallets. (cashcow and merchant) -You can trigger payments or mine blocks on the invoice's checkout page. - ## About docker-compose deployment BTCPay Server maintains its own [deployment stack project](https://github.com/btcpayserver/btcpayserver-docker) to enable users to easily update or deploy additional infrastructure (such as nodes).