diff --git a/.gitattributes b/.gitattributes index 0b8e159..8793de5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,4 +6,6 @@ # Denote all files that are truly binary and should not be modified. *.png binary -*.jpg binary \ No newline at end of file +*.jpg binary +*.keys binary +wallet binary \ No newline at end of file diff --git a/BTCPayServer.Plugins.IntegrationTests/BTCPayServer.Plugins.IntegrationTests.csproj b/BTCPayServer.Plugins.IntegrationTests/BTCPayServer.Plugins.IntegrationTests.csproj index b0e1029..2d3a71c 100644 --- a/BTCPayServer.Plugins.IntegrationTests/BTCPayServer.Plugins.IntegrationTests.csproj +++ b/BTCPayServer.Plugins.IntegrationTests/BTCPayServer.Plugins.IntegrationTests.csproj @@ -9,6 +9,7 @@ + @@ -32,6 +33,12 @@ + + + PreserveNewest + + + StaticWebAssetsEnabled=false diff --git a/BTCPayServer.Plugins.IntegrationTests/Monero/IntegrationTestUtils.cs b/BTCPayServer.Plugins.IntegrationTests/Monero/IntegrationTestUtils.cs index d51aab2..27e67d2 100644 --- a/BTCPayServer.Plugins.IntegrationTests/Monero/IntegrationTestUtils.cs +++ b/BTCPayServer.Plugins.IntegrationTests/Monero/IntegrationTestUtils.cs @@ -5,20 +5,26 @@ using BTCPayServer.Tests; using Microsoft.Extensions.Logging; +using Mono.Unix.Native; + using Npgsql; +using static Mono.Unix.Native.Syscall; + namespace BTCPayServer.Plugins.IntegrationTests.Monero; public static class IntegrationTestUtils { - private static readonly ILogger Logger = LoggerFactory .Create(builder => builder.AddConsole()) .CreateLogger("IntegrationTestUtils"); + private static readonly string ContainerWalletDir = + Environment.GetEnvironmentVariable("BTCPAY_XMR_WALLET_DAEMON_WALLETDIR") ?? "/wallet"; + public static async Task CleanUpAsync(PlaywrightTester playwrightTester) { - MoneroRPCProvider moneroRpcProvider = playwrightTester.Server.PayTester.GetService(); + MoneroRpcProvider moneroRpcProvider = playwrightTester.Server.PayTester.GetService(); if (moneroRpcProvider.IsAvailable("XMR")) { await moneroRpcProvider.CloseWallet("XMR"); @@ -26,7 +32,7 @@ public static class IntegrationTestUtils if (playwrightTester.Server.PayTester.InContainer) { - moneroRpcProvider.DeleteWallet(); + DeleteWalletInContainer(); await DropDatabaseAsync( "btcpayserver", "Host=postgres;Port=5432;Username=postgres;Database=postgres"); @@ -62,6 +68,101 @@ public static class IntegrationTestUtils } } + public static async Task CopyWalletFilesToMoneroRpcDirAsync(PlaywrightTester playwrightTester, String walletDir) + { + Logger.LogInformation("Starting to copy wallet files"); + if (playwrightTester.Server.PayTester.InContainer) + { + CopyWalletFilesInContainer(walletDir); + } + else + { + await CopyWalletFilesToLocalDocker(walletDir); + } + } + + private static void CopyWalletFilesInContainer(String walletDir) + { + try + { + CopyWalletFile("wallet", walletDir); + CopyWalletFile("wallet.keys", walletDir); + CopyWalletFile("password", walletDir); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to copy wallet files to the Monero directory."); + } + } + + private static void CopyWalletFile(string name, string walletDir) + { + var resourceWalletDir = Path.Combine(AppContext.BaseDirectory, "Resources", walletDir); + + var src = Path.Combine(resourceWalletDir, name); + var dst = Path.Combine(ContainerWalletDir, name); + + if (!File.Exists(src)) + { + return; + } + + File.Copy(src, dst, overwrite: true); + + // monero ownership + if (chown(dst, 980, 980) == 0) + { + return; + } + + Logger.LogError("chown failed for {File}. errno={Errno}", dst, Stdlib.GetLastError()); + } + + + private static async Task CopyWalletFilesToLocalDocker(String walletDir) + { + try + { + var fullWalletDir = Path.Combine(AppContext.BaseDirectory, "Resources", walletDir); + + await RunProcessAsync("docker", + $"cp \"{Path.Combine(fullWalletDir, "wallet")}\" xmr_wallet:/wallet/wallet"); + + await RunProcessAsync("docker", + $"cp \"{Path.Combine(fullWalletDir, "wallet.keys")}\" xmr_wallet:/wallet/wallet.keys"); + + await RunProcessAsync("docker", + $"cp \"{Path.Combine(fullWalletDir, "password")}\" xmr_wallet:/wallet/password"); + + await RunProcessAsync("docker", + "exec xmr_wallet chown monero:monero /wallet/wallet /wallet/wallet.keys /wallet/password"); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to copy wallet files to the Monero directory."); + } + } + + static async Task RunProcessAsync(string fileName, string args) + { + var psi = new ProcessStartInfo + { + FileName = fileName, + Arguments = args, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }; + + using var process = Process.Start(psi)!; + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + { + throw new Exception(await process.StandardError.ReadToEndAsync()); + } + } + private static async Task RemoveWalletFromLocalDocker() { try @@ -101,4 +202,33 @@ public static class IntegrationTestUtils Logger.LogError(ex, "Wallet cleanup via Docker failed."); } } + + private static void DeleteWalletInContainer() + { + try + { + var walletFile = Path.Combine(ContainerWalletDir, "wallet"); + var keysFile = walletFile + ".keys"; + var passwordFile = Path.Combine(ContainerWalletDir, "password"); + + if (File.Exists(walletFile)) + { + File.Delete(walletFile); + } + + if (File.Exists(keysFile)) + { + File.Delete(keysFile); + } + + if (File.Exists(passwordFile)) + { + File.Delete(passwordFile); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to delete wallet files in directory {Dir}", ContainerWalletDir); + } + } } \ No newline at end of file diff --git a/BTCPayServer.Plugins.IntegrationTests/Monero/MoneroPluginIntegrationTest.cs b/BTCPayServer.Plugins.IntegrationTests/Monero/MoneroPluginIntegrationTest.cs index c42b022..b0c4476 100644 --- a/BTCPayServer.Plugins.IntegrationTests/Monero/MoneroPluginIntegrationTest.cs +++ b/BTCPayServer.Plugins.IntegrationTests/Monero/MoneroPluginIntegrationTest.cs @@ -44,7 +44,6 @@ public class MoneroPluginIntegrationTest(ITestOutputHelper helper) : MoneroAndBi 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"); @@ -119,7 +118,6 @@ public class MoneroPluginIntegrationTest(ITestOutputHelper helper) : MoneroAndBi 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']"); var errorText = await s.Page .Locator("div.validation-summary-errors li") @@ -136,12 +134,12 @@ public class MoneroPluginIntegrationTest(ITestOutputHelper helper) : MoneroAndBi await using var s = CreatePlaywrightTester(); await s.StartAsync(); - MoneroRPCProvider moneroRpcProvider = s.Server.PayTester.GetService(); + MoneroRpcProvider moneroRpcProvider = s.Server.PayTester.GetService(); await moneroRpcProvider.WalletRpcClients["XMR"].SendCommandAsync("generate_from_keys", new GenerateFromKeysRequest { PrimaryAddress = "43Pnj6ZKGFTJhaLhiecSFfLfr64KPJZw7MyGH73T6PTDekBBvsTAaWEUSM4bmJqDuYLizhA13jQkMRPpz9VXBCBqQQb6y5L", PrivateViewKey = "1bfa03b0c78aa6bc8292cf160ec9875657d61e889c41d0ebe5c54fd3a2c4b40e", - WalletFileName = "view_wallet", + WalletFileName = "wallet", Password = "" }); await moneroRpcProvider.CloseWallet("XMR"); @@ -154,13 +152,49 @@ public class MoneroPluginIntegrationTest(ITestOutputHelper helper) : MoneroAndBi 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']"); var errorText = await s.Page .Locator("div.validation-summary-errors li") .InnerTextAsync(); Assert.Equal("Could not generate view wallet from keys: Wallet already exists.", errorText); + await IntegrationTestUtils.CleanUpAsync(s); + } + + [Fact] + public async Task ShouldLoadViewWalletOnStartUpIfExists() + { + await using var s = CreatePlaywrightTester(); + await IntegrationTestUtils.CopyWalletFilesToMoneroRpcDirAsync(s, "wallet"); + await s.StartAsync(); + await s.RegisterNewUser(true); + await s.CreateNewStore(); + await s.Page.Locator("a.nav-link[href*='monerolike/XMR']").ClickAsync(); + + var walletRpcIsAvailable = await s.Page + .Locator("li.list-group-item:text('Wallet RPC available: True')") + .InnerTextAsync(); + + Assert.Contains("Wallet RPC available: True", walletRpcIsAvailable); + + await IntegrationTestUtils.CleanUpAsync(s); + } + + [Fact] + public async Task ShouldLoadViewWalletWithPasswordOnStartUpIfExists() + { + await using var s = CreatePlaywrightTester(); + await IntegrationTestUtils.CopyWalletFilesToMoneroRpcDirAsync(s, "wallet_password"); + await s.StartAsync(); + await s.RegisterNewUser(true); + await s.CreateNewStore(); + await s.Page.Locator("a.nav-link[href*='monerolike/XMR']").ClickAsync(); + + var walletRpcIsAvailable = await s.Page + .Locator("li.list-group-item:text('Wallet RPC available: True')") + .InnerTextAsync(); + + Assert.Contains("Wallet RPC available: True", walletRpcIsAvailable); await IntegrationTestUtils.CleanUpAsync(s); } diff --git a/BTCPayServer.Plugins.IntegrationTests/Resources/wallet/wallet b/BTCPayServer.Plugins.IntegrationTests/Resources/wallet/wallet new file mode 100644 index 0000000..95acb9a Binary files /dev/null and b/BTCPayServer.Plugins.IntegrationTests/Resources/wallet/wallet differ diff --git a/BTCPayServer.Plugins.IntegrationTests/Resources/wallet/wallet.keys b/BTCPayServer.Plugins.IntegrationTests/Resources/wallet/wallet.keys new file mode 100644 index 0000000..968b124 Binary files /dev/null and b/BTCPayServer.Plugins.IntegrationTests/Resources/wallet/wallet.keys differ diff --git a/BTCPayServer.Plugins.IntegrationTests/Resources/wallet_password/password b/BTCPayServer.Plugins.IntegrationTests/Resources/wallet_password/password new file mode 100644 index 0000000..f8f0db2 --- /dev/null +++ b/BTCPayServer.Plugins.IntegrationTests/Resources/wallet_password/password @@ -0,0 +1 @@ +pass123 \ No newline at end of file diff --git a/BTCPayServer.Plugins.IntegrationTests/Resources/wallet_password/wallet b/BTCPayServer.Plugins.IntegrationTests/Resources/wallet_password/wallet new file mode 100644 index 0000000..acac115 Binary files /dev/null and b/BTCPayServer.Plugins.IntegrationTests/Resources/wallet_password/wallet differ diff --git a/BTCPayServer.Plugins.IntegrationTests/Resources/wallet_password/wallet.keys b/BTCPayServer.Plugins.IntegrationTests/Resources/wallet_password/wallet.keys new file mode 100644 index 0000000..c02fb97 Binary files /dev/null and b/BTCPayServer.Plugins.IntegrationTests/Resources/wallet_password/wallet.keys differ diff --git a/LICENSE.md b/LICENSE.md index 9fecbd2..5966ff3 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2025 btcpayserver +Copyright (c) 2017-2026 btcpayserver Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Plugins/Monero/BTCPayServer.Plugins.Monero.json b/Plugins/Monero/BTCPayServer.Plugins.Monero.json index 7f9e2a5..324f633 100644 --- a/Plugins/Monero/BTCPayServer.Plugins.Monero.json +++ b/Plugins/Monero/BTCPayServer.Plugins.Monero.json @@ -1,7 +1,7 @@ { "Identifier": "BTCPayServer.Plugins.Monero", "Name": "BTCPay Server: Monero support plugin", - "Version": "1.0.1.0", + "Version": "1.1.0", "Description": "This plugin extends BTCPay Server to enable users to receive payments via Monero.", "SystemPlugin": false, "Dependencies": [ diff --git a/Plugins/Monero/Controllers/MoneroLikeStoreController.cs b/Plugins/Monero/Controllers/MoneroLikeStoreController.cs index ffdd1ae..ab99996 100644 --- a/Plugins/Monero/Controllers/MoneroLikeStoreController.cs +++ b/Plugins/Monero/Controllers/MoneroLikeStoreController.cs @@ -33,12 +33,12 @@ namespace BTCPayServer.Plugins.Monero.Controllers { private readonly MoneroLikeConfiguration _MoneroLikeConfiguration; private readonly StoreRepository _StoreRepository; - private readonly MoneroRPCProvider _MoneroRpcProvider; + private readonly MoneroRpcProvider _MoneroRpcProvider; private readonly PaymentMethodHandlerDictionary _handlers; private IStringLocalizer StringLocalizer { get; } public UIMoneroLikeStoreController(MoneroLikeConfiguration moneroLikeConfiguration, - StoreRepository storeRepository, MoneroRPCProvider moneroRpcProvider, + StoreRepository storeRepository, MoneroRpcProvider moneroRpcProvider, PaymentMethodHandlerDictionary handlers, IStringLocalizer stringLocalizer) { @@ -219,9 +219,8 @@ namespace BTCPayServer.Plugins.Monero.Controllers { PrimaryAddress = viewModel.PrimaryAddress, PrivateViewKey = viewModel.PrivateViewKey, - WalletFileName = "view_wallet", - RestoreHeight = viewModel.RestoreHeight, - Password = viewModel.WalletPassword + WalletFileName = "wallet", + RestoreHeight = viewModel.RestoreHeight }); if (response?.Error != null) { @@ -286,7 +285,7 @@ namespace BTCPayServer.Plugins.Monero.Controllers public class MoneroLikePaymentMethodViewModel : IValidatableObject { - public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; } + public MoneroRpcProvider.MoneroLikeSummary Summary { get; set; } public string CryptoCode { get; set; } public string NewAccountLabel { get; set; } public long AccountIndex { get; set; } @@ -300,8 +299,6 @@ namespace BTCPayServer.Plugins.Monero.Controllers 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 …")] public MoneroLikeSettlementThresholdChoice SettlementConfirmationThresholdChoice { get; set; } [Display(Name = "Required Confirmations"), Range(0, 100)] diff --git a/Plugins/Monero/MoneroPlugin.cs b/Plugins/Monero/MoneroPlugin.cs index cabf09d..73b38ec 100644 --- a/Plugins/Monero/MoneroPlugin.cs +++ b/Plugins/Monero/MoneroPlugin.cs @@ -75,9 +75,10 @@ public class MoneroPlugin : BaseBTCPayServerPlugin PreAuthenticate = true }; }); - services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(); services.AddHostedService(); + services.AddHostedService(); services.AddSingleton(provider => (IPaymentMethodHandler)ActivatorUtilities.CreateInstance(provider, typeof(MoneroLikePaymentMethodHandler), network)); services.AddSingleton(provider => diff --git a/Plugins/Monero/Payments/MoneroLikePaymentMethodHandler.cs b/Plugins/Monero/Payments/MoneroLikePaymentMethodHandler.cs index 19d7af6..774894d 100644 --- a/Plugins/Monero/Payments/MoneroLikePaymentMethodHandler.cs +++ b/Plugins/Monero/Payments/MoneroLikePaymentMethodHandler.cs @@ -17,11 +17,11 @@ namespace BTCPayServer.Plugins.Monero.Payments private readonly MoneroLikeSpecificBtcPayNetwork _network; public MoneroLikeSpecificBtcPayNetwork Network => _network; public JsonSerializer Serializer { get; } - private readonly MoneroRPCProvider _moneroRpcProvider; + private readonly MoneroRpcProvider _moneroRpcProvider; public PaymentMethodId PaymentMethodId { get; } - public MoneroLikePaymentMethodHandler(MoneroLikeSpecificBtcPayNetwork network, MoneroRPCProvider moneroRpcProvider) + public MoneroLikePaymentMethodHandler(MoneroLikeSpecificBtcPayNetwork network, MoneroRpcProvider moneroRpcProvider) { PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode); _network = network; diff --git a/Plugins/Monero/RPC/Models/OpenWalletRequest.cs b/Plugins/Monero/RPC/Models/OpenWalletRequest.cs new file mode 100644 index 0000000..15f598b --- /dev/null +++ b/Plugins/Monero/RPC/Models/OpenWalletRequest.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace BTCPayServer.Plugins.Monero.RPC.Models +{ + public 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 new file mode 100644 index 0000000..e04578b --- /dev/null +++ b/Plugins/Monero/RPC/Models/OpenWalletResponse.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace BTCPayServer.Plugins.Monero.RPC.Models +{ + public 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 ErrorResponse Error { get; set; } + } +} \ No newline at end of file diff --git a/Plugins/Monero/Services/MoneroLikeSummaryUpdaterHostedService.cs b/Plugins/Monero/Services/MoneroLikeSummaryUpdaterHostedService.cs index 899c029..19ddb25 100644 --- a/Plugins/Monero/Services/MoneroLikeSummaryUpdaterHostedService.cs +++ b/Plugins/Monero/Services/MoneroLikeSummaryUpdaterHostedService.cs @@ -12,13 +12,13 @@ namespace BTCPayServer.Plugins.Monero.Services { public class MoneroLikeSummaryUpdaterHostedService : IHostedService { - private readonly MoneroRPCProvider _MoneroRpcProvider; + private readonly MoneroRpcProvider _MoneroRpcProvider; private readonly MoneroLikeConfiguration _moneroLikeConfiguration; public Logs Logs { get; } private CancellationTokenSource _Cts; - public MoneroLikeSummaryUpdaterHostedService(MoneroRPCProvider moneroRpcProvider, MoneroLikeConfiguration moneroLikeConfiguration, Logs logs) + public MoneroLikeSummaryUpdaterHostedService(MoneroRpcProvider moneroRpcProvider, MoneroLikeConfiguration moneroLikeConfiguration, Logs logs) { _MoneroRpcProvider = moneroRpcProvider; _moneroLikeConfiguration = moneroLikeConfiguration; diff --git a/Plugins/Monero/Services/MoneroListener.cs b/Plugins/Monero/Services/MoneroListener.cs index 3380f7b..545ea25 100644 --- a/Plugins/Monero/Services/MoneroListener.cs +++ b/Plugins/Monero/Services/MoneroListener.cs @@ -29,7 +29,7 @@ namespace BTCPayServer.Plugins.Monero.Services { private readonly InvoiceRepository _invoiceRepository; private readonly EventAggregator _eventAggregator; - private readonly MoneroRPCProvider _moneroRpcProvider; + private readonly MoneroRpcProvider _moneroRpcProvider; private readonly MoneroLikeConfiguration _MoneroLikeConfiguration; private readonly BTCPayNetworkProvider _networkProvider; private readonly ILogger _logger; @@ -39,7 +39,7 @@ namespace BTCPayServer.Plugins.Monero.Services public MoneroListener(InvoiceRepository invoiceRepository, EventAggregator eventAggregator, - MoneroRPCProvider moneroRpcProvider, + MoneroRpcProvider moneroRpcProvider, MoneroLikeConfiguration moneroLikeConfiguration, BTCPayNetworkProvider networkProvider, ILogger logger, @@ -62,12 +62,12 @@ namespace BTCPayServer.Plugins.Monero.Services { base.SubscribeToEvents(); Subscribe(); - Subscribe(); + Subscribe(); } protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) { - if (evt is MoneroRPCProvider.MoneroDaemonStateChange stateChange) + if (evt is MoneroRpcProvider.MoneroDaemonStateChange stateChange) { if (_moneroRpcProvider.IsAvailable(stateChange.CryptoCode)) { diff --git a/Plugins/Monero/Services/MoneroLoadUpService.cs b/Plugins/Monero/Services/MoneroLoadUpService.cs new file mode 100644 index 0000000..1327cf9 --- /dev/null +++ b/Plugins/Monero/Services/MoneroLoadUpService.cs @@ -0,0 +1,78 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using BTCPayServer.Plugins.Monero.RPC.Models; + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace BTCPayServer.Plugins.Monero.Services; + +public class MoneroLoadUpService : IHostedService +{ + private const string CryptoCode = "XMR"; + private readonly ILogger _logger; + private readonly MoneroRpcProvider _moneroRpcProvider; + + public MoneroLoadUpService(ILogger logger, MoneroRpcProvider moneroRpcProvider) + { + _moneroRpcProvider = moneroRpcProvider; + _logger = logger; + } + + [Obsolete("Remove optional password parameter")] + public async Task StartAsync(CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("Attempt to load existing wallet"); + + string walletDir = _moneroRpcProvider.GetWalletDirectory(CryptoCode); + if (!string.IsNullOrEmpty(walletDir)) + { + string password = await TryToGetPassword(walletDir, cancellationToken); + + await _moneroRpcProvider.WalletRpcClients[CryptoCode] + .SendCommandAsync("open_wallet", + new OpenWalletRequest { Filename = "wallet", Password = password }, cancellationToken); + + await _moneroRpcProvider.UpdateSummary(CryptoCode); + _logger.LogInformation("Existing wallet successfully loaded"); + } + else + { + _logger.LogInformation("No wallet directory configured, skipping wallet migration"); + } + } + catch (Exception ex) + { + _logger.LogError("Failed to load {CryptoCode} wallet. Error Message: {ErrorMessage}", CryptoCode, + ex.Message); + } + } + + [Obsolete("Password is obsolete due to the inability to fully separate the password file from the wallet file.")] + private async Task TryToGetPassword(string walletDir, CancellationToken cancellationToken) + { + string password = ""; + string passwordFile = Path.Combine(walletDir, "password"); + if (File.Exists(passwordFile)) + { + password = await File.ReadAllTextAsync(passwordFile, cancellationToken); + password = password.Trim(); + } + else + { + _logger.LogInformation("No password file found - ignoring"); + } + + return password; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Plugins/Monero/Services/MoneroRPCProvider.cs b/Plugins/Monero/Services/MoneroRpcProvider.cs similarity index 70% rename from Plugins/Monero/Services/MoneroRPCProvider.cs rename to Plugins/Monero/Services/MoneroRpcProvider.cs index 3d19381..4ccdad8 100644 --- a/Plugins/Monero/Services/MoneroRPCProvider.cs +++ b/Plugins/Monero/Services/MoneroRpcProvider.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Immutable; -using System.IO; using System.Net.Http; using System.Threading.Tasks; @@ -9,32 +8,25 @@ using BTCPayServer.Plugins.Monero.Configuration; using BTCPayServer.Plugins.Monero.RPC; using BTCPayServer.Plugins.Monero.RPC.Models; -using Microsoft.Extensions.Logging; - using NBitcoin; namespace BTCPayServer.Plugins.Monero.Services { - public class MoneroRPCProvider + public class MoneroRpcProvider { private readonly MoneroLikeConfiguration _moneroLikeConfiguration; private readonly EventAggregator _eventAggregator; public ImmutableDictionary DaemonRpcClients; public ImmutableDictionary WalletRpcClients; - private readonly ILogger _logger; - private readonly ConcurrentDictionary _summaries = new(); + public ConcurrentDictionary Summaries { get; } = new(); - public ConcurrentDictionary Summaries => _summaries; - - public MoneroRPCProvider(MoneroLikeConfiguration moneroLikeConfiguration, + public MoneroRpcProvider(MoneroLikeConfiguration moneroLikeConfiguration, EventAggregator eventAggregator, - ILogger logger, IHttpClientFactory httpClientFactory) { _moneroLikeConfiguration = moneroLikeConfiguration; _eventAggregator = eventAggregator; - _logger = logger; DaemonRpcClients = _moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key, pair => new JsonRpcClient(pair.Value.DaemonRpcUri, pair.Value.Username, pair.Value.Password, @@ -49,7 +41,7 @@ namespace BTCPayServer.Plugins.Monero.Services public bool IsAvailable(string cryptoCode) { cryptoCode = cryptoCode.ToUpperInvariant(); - return _summaries.ContainsKey(cryptoCode) && IsAvailable(_summaries[cryptoCode]); + return Summaries.ContainsKey(cryptoCode) && IsAvailable(Summaries[cryptoCode]); } private bool IsAvailable(MoneroLikeSummary summary) @@ -69,43 +61,12 @@ namespace BTCPayServer.Plugins.Monero.Services "close_wallet", JsonRpcClient.NoRequestModel.Instance); } - public void DeleteWallet() + public string GetWalletDirectory(string cryptoCode) { - if (!_moneroLikeConfiguration.MoneroLikeConfigurationItems.TryGetValue("XMR", out var configItem)) - { - _logger.LogWarning("DeleteWallet: No XMR configuration found."); - return; - } - - if (string.IsNullOrEmpty(configItem.WalletDirectory)) - { - _logger.LogWarning("DeleteWallet: WalletDirectory is null or empty for XMR configuration."); - return; - } - try - { - var walletFile = Path.Combine(configItem.WalletDirectory, "view_wallet"); - var keysFile = walletFile + ".keys"; - var passwordFile = Path.Combine(configItem.WalletDirectory, "password"); - - if (File.Exists(walletFile)) - { - File.Delete(walletFile); - } - if (File.Exists(keysFile)) - { - File.Delete(keysFile); - } - if (File.Exists(passwordFile)) - { - File.Delete(passwordFile); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to delete wallet files in directory {Dir}", - configItem.WalletDirectory); - } + cryptoCode = cryptoCode.ToUpperInvariant(); + return !_moneroLikeConfiguration.MoneroLikeConfigurationItems.TryGetValue(cryptoCode, out var configItem) + ? null + : configItem.WalletDirectory; } public async Task UpdateSummary(string cryptoCode) @@ -146,9 +107,9 @@ namespace BTCPayServer.Plugins.Monero.Services summary.WalletAvailable = false; } - var changed = !_summaries.ContainsKey(cryptoCode) || IsAvailable(cryptoCode) != IsAvailable(summary); + var changed = !Summaries.ContainsKey(cryptoCode) || IsAvailable(cryptoCode) != IsAvailable(summary); - _summaries.AddOrReplace(cryptoCode, summary); + Summaries.AddOrReplace(cryptoCode, summary); if (changed) { _eventAggregator.Publish(new MoneroDaemonStateChange() { Summary = summary, CryptoCode = cryptoCode }); diff --git a/Plugins/Monero/Services/MoneroSyncSummaryProvider.cs b/Plugins/Monero/Services/MoneroSyncSummaryProvider.cs index 83cccc3..7748706 100644 --- a/Plugins/Monero/Services/MoneroSyncSummaryProvider.cs +++ b/Plugins/Monero/Services/MoneroSyncSummaryProvider.cs @@ -9,9 +9,9 @@ namespace BTCPayServer.Plugins.Monero.Services { public class MoneroSyncSummaryProvider : ISyncSummaryProvider { - private readonly MoneroRPCProvider _moneroRpcProvider; + private readonly MoneroRpcProvider _moneroRpcProvider; - public MoneroSyncSummaryProvider(MoneroRPCProvider moneroRpcProvider) + public MoneroSyncSummaryProvider(MoneroRpcProvider moneroRpcProvider) { _moneroRpcProvider = moneroRpcProvider; } @@ -42,6 +42,6 @@ namespace BTCPayServer.Plugins.Monero.Services } } - public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; } + public MoneroRpcProvider.MoneroLikeSummary Summary { get; set; } } } \ No newline at end of file diff --git a/Plugins/Monero/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml b/Plugins/Monero/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml index 81444ef..b3ee472 100644 --- a/Plugins/Monero/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml +++ b/Plugins/Monero/Views/Monero/GetStoreMoneroLikePaymentMethod.cshtml @@ -52,11 +52,6 @@ -
- - - -
diff --git a/Plugins/Monero/Views/Monero/MoneroSyncSummary.cshtml b/Plugins/Monero/Views/Monero/MoneroSyncSummary.cshtml index 37052ef..1a8769c 100644 --- a/Plugins/Monero/Views/Monero/MoneroSyncSummary.cshtml +++ b/Plugins/Monero/Views/Monero/MoneroSyncSummary.cshtml @@ -1,8 +1,5 @@ -@using BTCPayServer -@using BTCPayServer.Data @using BTCPayServer.Plugins.Monero.Services -@using Microsoft.AspNetCore.Identity -@inject MoneroRPCProvider MoneroRpcProvider +@inject MoneroRpcProvider MoneroRpcProvider @inject SignInManager SignInManager; @if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroRpcProvider.Summaries.Any()) diff --git a/SECURITY.md b/SECURITY.md index b06b5c7..377c596 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,10 +4,10 @@ We currently support the following versions of the Monero plugin for BTCPayServer: -| Version | Supported | -| ------- | ------------------ | -| 2.x | ✅ Yes | -| 1.x | ❌ No | +| Version | Supported | +|---------|-------------| +| 1.1.x | ✅ Yes | +| 1.0.x | ❌ No | ## Reporting a Vulnerability diff --git a/submodules/btcpayserver b/submodules/btcpayserver index 335c906..3f52aa7 160000 --- a/submodules/btcpayserver +++ b/submodules/btcpayserver @@ -1 +1 @@ -Subproject commit 335c906b811b8f131a5973ef3e9cdcac99017654 +Subproject commit 3f52aa7fd9faf8bb99343572b3dae305037db2ba