Auto load wallet on start up if available and deprecate password

Co-authored-by: Deverick <5827364+deverickapollo@users.noreply.github.com>
This commit is contained in:
napoly 2026-01-13 18:53:53 +01:00
parent 50d3a23c7b
commit f30d55072e
25 changed files with 320 additions and 95 deletions

4
.gitattributes vendored
View file

@ -6,4 +6,6 @@
# Denote all files that are truly binary and should not be modified. # Denote all files that are truly binary and should not be modified.
*.png binary *.png binary
*.jpg binary *.jpg binary
*.keys binary
wallet binary

View file

@ -9,6 +9,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Docker.DotNet" Version="3.125.15" /> <PackageReference Include="Docker.DotNet" Version="3.125.15" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.Playwright" Version="1.52.0" /> <PackageReference Include="Microsoft.Playwright" Version="1.52.0" />
<PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit" Version="2.9.3" />
@ -32,6 +33,12 @@
<ProjectReference Include="..\Plugins\Monero\BTCPayServer.Plugins.Monero.csproj" /> <ProjectReference Include="..\Plugins\Monero\BTCPayServer.Plugins.Monero.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="Resources\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemDefinitionGroup> <ItemDefinitionGroup>
<ProjectReference> <ProjectReference>
<Properties>StaticWebAssetsEnabled=false</Properties> <Properties>StaticWebAssetsEnabled=false</Properties>

View file

@ -5,20 +5,26 @@ using BTCPayServer.Tests;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Mono.Unix.Native;
using Npgsql; using Npgsql;
using static Mono.Unix.Native.Syscall;
namespace BTCPayServer.Plugins.IntegrationTests.Monero; namespace BTCPayServer.Plugins.IntegrationTests.Monero;
public static class IntegrationTestUtils public static class IntegrationTestUtils
{ {
private static readonly ILogger Logger = LoggerFactory private static readonly ILogger Logger = LoggerFactory
.Create(builder => builder.AddConsole()) .Create(builder => builder.AddConsole())
.CreateLogger("IntegrationTestUtils"); .CreateLogger("IntegrationTestUtils");
private static readonly string ContainerWalletDir =
Environment.GetEnvironmentVariable("BTCPAY_XMR_WALLET_DAEMON_WALLETDIR") ?? "/wallet";
public static async Task CleanUpAsync(PlaywrightTester playwrightTester) public static async Task CleanUpAsync(PlaywrightTester playwrightTester)
{ {
MoneroRPCProvider moneroRpcProvider = playwrightTester.Server.PayTester.GetService<MoneroRPCProvider>(); MoneroRpcProvider moneroRpcProvider = playwrightTester.Server.PayTester.GetService<MoneroRpcProvider>();
if (moneroRpcProvider.IsAvailable("XMR")) if (moneroRpcProvider.IsAvailable("XMR"))
{ {
await moneroRpcProvider.CloseWallet("XMR"); await moneroRpcProvider.CloseWallet("XMR");
@ -26,7 +32,7 @@ public static class IntegrationTestUtils
if (playwrightTester.Server.PayTester.InContainer) if (playwrightTester.Server.PayTester.InContainer)
{ {
moneroRpcProvider.DeleteWallet(); DeleteWalletInContainer();
await DropDatabaseAsync( await DropDatabaseAsync(
"btcpayserver", "btcpayserver",
"Host=postgres;Port=5432;Username=postgres;Database=postgres"); "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() private static async Task RemoveWalletFromLocalDocker()
{ {
try try
@ -101,4 +202,33 @@ public static class IntegrationTestUtils
Logger.LogError(ex, "Wallet cleanup via Docker failed."); 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);
}
}
} }

View file

@ -44,7 +44,6 @@ public class MoneroPluginIntegrationTest(ITestOutputHelper helper) : MoneroAndBi
await s.Page.Locator("input#PrivateViewKey") await s.Page.Locator("input#PrivateViewKey")
.FillAsync("1bfa03b0c78aa6bc8292cf160ec9875657d61e889c41d0ebe5c54fd3a2c4b40e"); .FillAsync("1bfa03b0c78aa6bc8292cf160ec9875657d61e889c41d0ebe5c54fd3a2c4b40e");
await s.Page.Locator("input#RestoreHeight").FillAsync("0"); 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.ClickAsync("button[name='command'][value='set-wallet-details']");
await s.Page.CheckAsync("#Enabled"); await s.Page.CheckAsync("#Enabled");
await s.Page.SelectOptionAsync("#SettlementConfirmationThresholdChoice", "2"); await s.Page.SelectOptionAsync("#SettlementConfirmationThresholdChoice", "2");
@ -119,7 +118,6 @@ public class MoneroPluginIntegrationTest(ITestOutputHelper helper) : MoneroAndBi
await s.Page.Locator("input#PrivateViewKey") await s.Page.Locator("input#PrivateViewKey")
.FillAsync("1bfa03b0c78aa6bc8292cf160ec9875657d61e889c41d0ebe5c54fd3a2c4b40e"); .FillAsync("1bfa03b0c78aa6bc8292cf160ec9875657d61e889c41d0ebe5c54fd3a2c4b40e");
await s.Page.Locator("input#RestoreHeight").FillAsync("0"); 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.ClickAsync("button[name='command'][value='set-wallet-details']");
var errorText = await s.Page var errorText = await s.Page
.Locator("div.validation-summary-errors li") .Locator("div.validation-summary-errors li")
@ -136,12 +134,12 @@ public class MoneroPluginIntegrationTest(ITestOutputHelper helper) : MoneroAndBi
await using var s = CreatePlaywrightTester(); await using var s = CreatePlaywrightTester();
await s.StartAsync(); await s.StartAsync();
MoneroRPCProvider moneroRpcProvider = s.Server.PayTester.GetService<MoneroRPCProvider>(); MoneroRpcProvider moneroRpcProvider = s.Server.PayTester.GetService<MoneroRpcProvider>();
await moneroRpcProvider.WalletRpcClients["XMR"].SendCommandAsync<GenerateFromKeysRequest, GenerateFromKeysResponse>("generate_from_keys", new GenerateFromKeysRequest await moneroRpcProvider.WalletRpcClients["XMR"].SendCommandAsync<GenerateFromKeysRequest, GenerateFromKeysResponse>("generate_from_keys", new GenerateFromKeysRequest
{ {
PrimaryAddress = "43Pnj6ZKGFTJhaLhiecSFfLfr64KPJZw7MyGH73T6PTDekBBvsTAaWEUSM4bmJqDuYLizhA13jQkMRPpz9VXBCBqQQb6y5L", PrimaryAddress = "43Pnj6ZKGFTJhaLhiecSFfLfr64KPJZw7MyGH73T6PTDekBBvsTAaWEUSM4bmJqDuYLizhA13jQkMRPpz9VXBCBqQQb6y5L",
PrivateViewKey = "1bfa03b0c78aa6bc8292cf160ec9875657d61e889c41d0ebe5c54fd3a2c4b40e", PrivateViewKey = "1bfa03b0c78aa6bc8292cf160ec9875657d61e889c41d0ebe5c54fd3a2c4b40e",
WalletFileName = "view_wallet", WalletFileName = "wallet",
Password = "" Password = ""
}); });
await moneroRpcProvider.CloseWallet("XMR"); await moneroRpcProvider.CloseWallet("XMR");
@ -154,13 +152,49 @@ public class MoneroPluginIntegrationTest(ITestOutputHelper helper) : MoneroAndBi
await s.Page.Locator("input#PrivateViewKey") await s.Page.Locator("input#PrivateViewKey")
.FillAsync("1bfa03b0c78aa6bc8292cf160ec9875657d61e889c41d0ebe5c54fd3a2c4b40e"); .FillAsync("1bfa03b0c78aa6bc8292cf160ec9875657d61e889c41d0ebe5c54fd3a2c4b40e");
await s.Page.Locator("input#RestoreHeight").FillAsync("0"); 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.ClickAsync("button[name='command'][value='set-wallet-details']");
var errorText = await s.Page var errorText = await s.Page
.Locator("div.validation-summary-errors li") .Locator("div.validation-summary-errors li")
.InnerTextAsync(); .InnerTextAsync();
Assert.Equal("Could not generate view wallet from keys: Wallet already exists.", errorText); 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); await IntegrationTestUtils.CleanUpAsync(s);
} }

View file

@ -0,0 +1 @@
pass123

View file

@ -1,6 +1,6 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -1,7 +1,7 @@
{ {
"Identifier": "BTCPayServer.Plugins.Monero", "Identifier": "BTCPayServer.Plugins.Monero",
"Name": "BTCPay Server: Monero support plugin", "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.", "Description": "This plugin extends BTCPay Server to enable users to receive payments via Monero.",
"SystemPlugin": false, "SystemPlugin": false,
"Dependencies": [ "Dependencies": [

View file

@ -33,12 +33,12 @@ namespace BTCPayServer.Plugins.Monero.Controllers
{ {
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration; private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;
private readonly StoreRepository _StoreRepository; private readonly StoreRepository _StoreRepository;
private readonly MoneroRPCProvider _MoneroRpcProvider; private readonly MoneroRpcProvider _MoneroRpcProvider;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _handlers;
private IStringLocalizer StringLocalizer { get; } private IStringLocalizer StringLocalizer { get; }
public UIMoneroLikeStoreController(MoneroLikeConfiguration moneroLikeConfiguration, public UIMoneroLikeStoreController(MoneroLikeConfiguration moneroLikeConfiguration,
StoreRepository storeRepository, MoneroRPCProvider moneroRpcProvider, StoreRepository storeRepository, MoneroRpcProvider moneroRpcProvider,
PaymentMethodHandlerDictionary handlers, PaymentMethodHandlerDictionary handlers,
IStringLocalizer stringLocalizer) IStringLocalizer stringLocalizer)
{ {
@ -219,9 +219,8 @@ namespace BTCPayServer.Plugins.Monero.Controllers
{ {
PrimaryAddress = viewModel.PrimaryAddress, PrimaryAddress = viewModel.PrimaryAddress,
PrivateViewKey = viewModel.PrivateViewKey, PrivateViewKey = viewModel.PrivateViewKey,
WalletFileName = "view_wallet", WalletFileName = "wallet",
RestoreHeight = viewModel.RestoreHeight, RestoreHeight = viewModel.RestoreHeight
Password = viewModel.WalletPassword
}); });
if (response?.Error != null) if (response?.Error != null)
{ {
@ -286,7 +285,7 @@ namespace BTCPayServer.Plugins.Monero.Controllers
public class MoneroLikePaymentMethodViewModel : IValidatableObject public class MoneroLikePaymentMethodViewModel : IValidatableObject
{ {
public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; } public MoneroRpcProvider.MoneroLikeSummary Summary { get; set; }
public string CryptoCode { get; set; } public string CryptoCode { get; set; }
public string NewAccountLabel { get; set; } public string NewAccountLabel { get; set; }
public long AccountIndex { get; set; } public long AccountIndex { get; set; }
@ -300,8 +299,6 @@ namespace BTCPayServer.Plugins.Monero.Controllers
public string PrivateViewKey { get; set; } public string PrivateViewKey { get; set; }
[Display(Name = "Restore Height")] [Display(Name = "Restore Height")]
public int RestoreHeight { get; set; } public int RestoreHeight { get; set; }
[Display(Name = "Wallet Password")]
public string WalletPassword { get; set; }
[Display(Name = "Consider the invoice settled when the payment transaction …")] [Display(Name = "Consider the invoice settled when the payment transaction …")]
public MoneroLikeSettlementThresholdChoice SettlementConfirmationThresholdChoice { get; set; } public MoneroLikeSettlementThresholdChoice SettlementConfirmationThresholdChoice { get; set; }
[Display(Name = "Required Confirmations"), Range(0, 100)] [Display(Name = "Required Confirmations"), Range(0, 100)]

View file

@ -75,9 +75,10 @@ public class MoneroPlugin : BaseBTCPayServerPlugin
PreAuthenticate = true PreAuthenticate = true
}; };
}); });
services.AddSingleton<MoneroRPCProvider>(); services.AddSingleton<MoneroRpcProvider>();
services.AddHostedService<MoneroLikeSummaryUpdaterHostedService>(); services.AddHostedService<MoneroLikeSummaryUpdaterHostedService>();
services.AddHostedService<MoneroListener>(); services.AddHostedService<MoneroListener>();
services.AddHostedService<MoneroLoadUpService>();
services.AddSingleton(provider => services.AddSingleton(provider =>
(IPaymentMethodHandler)ActivatorUtilities.CreateInstance(provider, typeof(MoneroLikePaymentMethodHandler), network)); (IPaymentMethodHandler)ActivatorUtilities.CreateInstance(provider, typeof(MoneroLikePaymentMethodHandler), network));
services.AddSingleton(provider => services.AddSingleton(provider =>

View file

@ -17,11 +17,11 @@ namespace BTCPayServer.Plugins.Monero.Payments
private readonly MoneroLikeSpecificBtcPayNetwork _network; private readonly MoneroLikeSpecificBtcPayNetwork _network;
public MoneroLikeSpecificBtcPayNetwork Network => _network; public MoneroLikeSpecificBtcPayNetwork Network => _network;
public JsonSerializer Serializer { get; } public JsonSerializer Serializer { get; }
private readonly MoneroRPCProvider _moneroRpcProvider; private readonly MoneroRpcProvider _moneroRpcProvider;
public PaymentMethodId PaymentMethodId { get; } public PaymentMethodId PaymentMethodId { get; }
public MoneroLikePaymentMethodHandler(MoneroLikeSpecificBtcPayNetwork network, MoneroRPCProvider moneroRpcProvider) public MoneroLikePaymentMethodHandler(MoneroLikeSpecificBtcPayNetwork network, MoneroRpcProvider moneroRpcProvider)
{ {
PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode); PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
_network = network; _network = network;

View file

@ -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; }
}
}

View file

@ -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; }
}
}

View file

@ -12,13 +12,13 @@ namespace BTCPayServer.Plugins.Monero.Services
{ {
public class MoneroLikeSummaryUpdaterHostedService : IHostedService public class MoneroLikeSummaryUpdaterHostedService : IHostedService
{ {
private readonly MoneroRPCProvider _MoneroRpcProvider; private readonly MoneroRpcProvider _MoneroRpcProvider;
private readonly MoneroLikeConfiguration _moneroLikeConfiguration; private readonly MoneroLikeConfiguration _moneroLikeConfiguration;
public Logs Logs { get; } public Logs Logs { get; }
private CancellationTokenSource _Cts; private CancellationTokenSource _Cts;
public MoneroLikeSummaryUpdaterHostedService(MoneroRPCProvider moneroRpcProvider, MoneroLikeConfiguration moneroLikeConfiguration, Logs logs) public MoneroLikeSummaryUpdaterHostedService(MoneroRpcProvider moneroRpcProvider, MoneroLikeConfiguration moneroLikeConfiguration, Logs logs)
{ {
_MoneroRpcProvider = moneroRpcProvider; _MoneroRpcProvider = moneroRpcProvider;
_moneroLikeConfiguration = moneroLikeConfiguration; _moneroLikeConfiguration = moneroLikeConfiguration;

View file

@ -29,7 +29,7 @@ namespace BTCPayServer.Plugins.Monero.Services
{ {
private readonly InvoiceRepository _invoiceRepository; private readonly InvoiceRepository _invoiceRepository;
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly MoneroRPCProvider _moneroRpcProvider; private readonly MoneroRpcProvider _moneroRpcProvider;
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration; private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;
private readonly BTCPayNetworkProvider _networkProvider; private readonly BTCPayNetworkProvider _networkProvider;
private readonly ILogger<MoneroListener> _logger; private readonly ILogger<MoneroListener> _logger;
@ -39,7 +39,7 @@ namespace BTCPayServer.Plugins.Monero.Services
public MoneroListener(InvoiceRepository invoiceRepository, public MoneroListener(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator, EventAggregator eventAggregator,
MoneroRPCProvider moneroRpcProvider, MoneroRpcProvider moneroRpcProvider,
MoneroLikeConfiguration moneroLikeConfiguration, MoneroLikeConfiguration moneroLikeConfiguration,
BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider networkProvider,
ILogger<MoneroListener> logger, ILogger<MoneroListener> logger,
@ -62,12 +62,12 @@ namespace BTCPayServer.Plugins.Monero.Services
{ {
base.SubscribeToEvents(); base.SubscribeToEvents();
Subscribe<MoneroEvent>(); Subscribe<MoneroEvent>();
Subscribe<MoneroRPCProvider.MoneroDaemonStateChange>(); Subscribe<MoneroRpcProvider.MoneroDaemonStateChange>();
} }
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) 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)) if (_moneroRpcProvider.IsAvailable(stateChange.CryptoCode))
{ {

View file

@ -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<MoneroLoadUpService> _logger;
private readonly MoneroRpcProvider _moneroRpcProvider;
public MoneroLoadUpService(ILogger<MoneroLoadUpService> 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<OpenWalletRequest, OpenWalletResponse>("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<string> 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;
}
}

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -9,32 +8,25 @@ using BTCPayServer.Plugins.Monero.Configuration;
using BTCPayServer.Plugins.Monero.RPC; using BTCPayServer.Plugins.Monero.RPC;
using BTCPayServer.Plugins.Monero.RPC.Models; using BTCPayServer.Plugins.Monero.RPC.Models;
using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
namespace BTCPayServer.Plugins.Monero.Services namespace BTCPayServer.Plugins.Monero.Services
{ {
public class MoneroRPCProvider public class MoneroRpcProvider
{ {
private readonly MoneroLikeConfiguration _moneroLikeConfiguration; private readonly MoneroLikeConfiguration _moneroLikeConfiguration;
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
public ImmutableDictionary<string, JsonRpcClient> DaemonRpcClients; public ImmutableDictionary<string, JsonRpcClient> DaemonRpcClients;
public ImmutableDictionary<string, JsonRpcClient> WalletRpcClients; public ImmutableDictionary<string, JsonRpcClient> WalletRpcClients;
private readonly ILogger<MoneroRPCProvider> _logger;
private readonly ConcurrentDictionary<string, MoneroLikeSummary> _summaries = new(); public ConcurrentDictionary<string, MoneroLikeSummary> Summaries { get; } = new();
public ConcurrentDictionary<string, MoneroLikeSummary> Summaries => _summaries; public MoneroRpcProvider(MoneroLikeConfiguration moneroLikeConfiguration,
public MoneroRPCProvider(MoneroLikeConfiguration moneroLikeConfiguration,
EventAggregator eventAggregator, EventAggregator eventAggregator,
ILogger<MoneroRPCProvider> logger,
IHttpClientFactory httpClientFactory) IHttpClientFactory httpClientFactory)
{ {
_moneroLikeConfiguration = moneroLikeConfiguration; _moneroLikeConfiguration = moneroLikeConfiguration;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_logger = logger;
DaemonRpcClients = DaemonRpcClients =
_moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key, _moneroLikeConfiguration.MoneroLikeConfigurationItems.ToImmutableDictionary(pair => pair.Key,
pair => new JsonRpcClient(pair.Value.DaemonRpcUri, pair.Value.Username, pair.Value.Password, 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) public bool IsAvailable(string cryptoCode)
{ {
cryptoCode = cryptoCode.ToUpperInvariant(); cryptoCode = cryptoCode.ToUpperInvariant();
return _summaries.ContainsKey(cryptoCode) && IsAvailable(_summaries[cryptoCode]); return Summaries.ContainsKey(cryptoCode) && IsAvailable(Summaries[cryptoCode]);
} }
private bool IsAvailable(MoneroLikeSummary summary) private bool IsAvailable(MoneroLikeSummary summary)
@ -69,43 +61,12 @@ namespace BTCPayServer.Plugins.Monero.Services
"close_wallet", JsonRpcClient.NoRequestModel.Instance); "close_wallet", JsonRpcClient.NoRequestModel.Instance);
} }
public void DeleteWallet() public string GetWalletDirectory(string cryptoCode)
{ {
if (!_moneroLikeConfiguration.MoneroLikeConfigurationItems.TryGetValue("XMR", out var configItem)) cryptoCode = cryptoCode.ToUpperInvariant();
{ return !_moneroLikeConfiguration.MoneroLikeConfigurationItems.TryGetValue(cryptoCode, out var configItem)
_logger.LogWarning("DeleteWallet: No XMR configuration found."); ? null
return; : configItem.WalletDirectory;
}
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);
}
} }
public async Task<MoneroLikeSummary> UpdateSummary(string cryptoCode) public async Task<MoneroLikeSummary> UpdateSummary(string cryptoCode)
@ -146,9 +107,9 @@ namespace BTCPayServer.Plugins.Monero.Services
summary.WalletAvailable = false; 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) if (changed)
{ {
_eventAggregator.Publish(new MoneroDaemonStateChange() { Summary = summary, CryptoCode = cryptoCode }); _eventAggregator.Publish(new MoneroDaemonStateChange() { Summary = summary, CryptoCode = cryptoCode });

View file

@ -9,9 +9,9 @@ namespace BTCPayServer.Plugins.Monero.Services
{ {
public class MoneroSyncSummaryProvider : ISyncSummaryProvider public class MoneroSyncSummaryProvider : ISyncSummaryProvider
{ {
private readonly MoneroRPCProvider _moneroRpcProvider; private readonly MoneroRpcProvider _moneroRpcProvider;
public MoneroSyncSummaryProvider(MoneroRPCProvider moneroRpcProvider) public MoneroSyncSummaryProvider(MoneroRpcProvider moneroRpcProvider)
{ {
_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; }
} }
} }

View file

@ -52,11 +52,6 @@
<input type="number" class="form-control" asp-for="RestoreHeight" required> <input type="number" class="form-control" asp-for="RestoreHeight" required>
<span asp-validation-for="RestoreHeight" class="text-danger"></span> <span asp-validation-for="RestoreHeight" class="text-danger"></span>
</div> </div>
<div class="form-group p-2">
<label asp-for="WalletPassword" class="form-label">Wallet Password</label>
<input type="password" class="form-control" asp-for="WalletPassword" required>
<span asp-validation-for="WalletPassword" class="text-danger"></span>
</div>
<div class="card-footer text-right"> <div class="card-footer text-right">
<button name="command" value="set-wallet-details" class="btn btn-secondary" type="submit">Set Wallet Details</button> <button name="command" value="set-wallet-details" class="btn btn-secondary" type="submit">Set Wallet Details</button>
</div> </div>

View file

@ -1,8 +1,5 @@
@using BTCPayServer
@using BTCPayServer.Data
@using BTCPayServer.Plugins.Monero.Services @using BTCPayServer.Plugins.Monero.Services
@using Microsoft.AspNetCore.Identity @inject MoneroRpcProvider MoneroRpcProvider
@inject MoneroRPCProvider MoneroRpcProvider
@inject SignInManager<ApplicationUser> SignInManager; @inject SignInManager<ApplicationUser> SignInManager;
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroRpcProvider.Summaries.Any()) @if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroRpcProvider.Summaries.Any())

View file

@ -4,10 +4,10 @@
We currently support the following versions of the Monero plugin for BTCPayServer: We currently support the following versions of the Monero plugin for BTCPayServer:
| Version | Supported | | Version | Supported |
| ------- | ------------------ | |---------|-------------|
| 2.x | ✅ Yes | | 1.1.x | ✅ Yes |
| 1.x | ❌ No | | 1.0.x | ❌ No |
## Reporting a Vulnerability ## Reporting a Vulnerability

@ -1 +1 @@
Subproject commit 335c906b811b8f131a5973ef3e9cdcac99017654 Subproject commit 3f52aa7fd9faf8bb99343572b3dae305037db2ba