Skip to content

Commit 6f85edd

Browse files
Add wallet management
1 parent 1ae5455 commit 6f85edd

16 files changed

Lines changed: 1910 additions & 291 deletions

BTCPayServer.Plugins.IntegrationTests/Monero/MoneroPluginIntegrationTest.cs

Lines changed: 212 additions & 66 deletions
Large diffs are not rendered by default.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
3+
namespace BTCPayServer.Plugins.Monero.Configuration
4+
{
5+
public class MoneroWalletState
6+
{
7+
public string ActiveWalletName { get; set; }
8+
9+
public string ActiveWalletPassword { get; set; }
10+
11+
public DateTimeOffset? LastActivatedAt { get; set; }
12+
13+
public string LastActivatedByStoreId { get; set; }
14+
15+
public bool IsInitialized => !string.IsNullOrEmpty(ActiveWalletName);
16+
17+
public bool IsConnected { get; set; }
18+
}
19+
}

Plugins/Monero/Controllers/MoneroLikeStoreController.cs

Lines changed: 382 additions & 84 deletions
Large diffs are not rendered by default.

Plugins/Monero/MoneroPlugin.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using BTCPayServer.Abstractions.Models;
99
using BTCPayServer.Configuration;
1010
using BTCPayServer.Hosting;
11+
using BTCPayServer.Logging;
1112
using BTCPayServer.Payments;
1213
using BTCPayServer.Plugins.Monero.Configuration;
1314
using BTCPayServer.Plugins.Monero.Payments;
@@ -59,7 +60,7 @@ public override void Execute(IServiceCollection services)
5960
.AddTransactionLinkProvider(pmi, new SimpleTransactionLinkProvider(blockExplorerLink));
6061

6162

62-
services.AddSingleton(provider =>
63+
services.AddSingleton<MoneroLikeConfiguration>(provider =>
6364
ConfigureMoneroLikeConfiguration(provider));
6465
services.AddHttpClient("XMRclient")
6566
.ConfigurePrimaryHttpMessageHandler(provider =>
@@ -78,6 +79,9 @@ public override void Execute(IServiceCollection services)
7879
services.AddSingleton<MoneroRPCProvider>();
7980
services.AddHostedService<MoneroLikeSummaryUpdaterHostedService>();
8081
services.AddHostedService<MoneroListener>();
82+
services.AddHostedService<MoneroConfigurationMigrationService>();
83+
services.AddSingleton<MoneroWalletService>();
84+
services.AddHostedService(provider => provider.GetRequiredService<MoneroWalletService>());
8185
services.AddSingleton(provider =>
8286
(IPaymentMethodHandler)ActivatorUtilities.CreateInstance(provider, typeof(MoneroLikePaymentMethodHandler), network));
8387
services.AddSingleton(provider =>
@@ -134,17 +138,17 @@ private static MoneroLikeConfiguration ConfigureMoneroLikeConfiguration(IService
134138
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_password", null);
135139
if (daemonUri == null || walletDaemonUri == null)
136140
{
137-
var logger = serviceProvider.GetRequiredService<ILogger<MoneroPlugin>>();
141+
var logs = serviceProvider.GetRequiredService<Logs>();
138142
var cryptoCode = moneroLikeSpecificBtcPayNetwork.CryptoCode.ToUpperInvariant();
139143
if (daemonUri is null)
140144
{
141-
logger.LogWarning($"BTCPAY_{cryptoCode}_DAEMON_URI is not configured");
145+
logs.Configuration.LogWarning($"BTCPAY_{cryptoCode}_DAEMON_URI is not configured");
142146
}
143147
if (walletDaemonUri is null)
144148
{
145-
logger.LogWarning($"BTCPAY_{cryptoCode}_WALLET_DAEMON_URI is not configured");
149+
logs.Configuration.LogWarning($"BTCPAY_{cryptoCode}_WALLET_DAEMON_URI is not configured");
146150
}
147-
logger.LogWarning($"{cryptoCode} got disabled as it is not fully configured.");
151+
logs.Configuration.LogWarning($"{cryptoCode} got disabled as it is not fully configured.");
148152
}
149153
else
150154
{

Plugins/Monero/Payments/MoneroLikePaymentMethodHandler.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@ public class MoneroLikePaymentMethodHandler : IPaymentMethodHandler
1818
public MoneroLikeSpecificBtcPayNetwork Network => _network;
1919
public JsonSerializer Serializer { get; }
2020
private readonly MoneroRPCProvider _moneroRpcProvider;
21+
private readonly MoneroWalletService _walletService;
2122

2223
public PaymentMethodId PaymentMethodId { get; }
2324

24-
public MoneroLikePaymentMethodHandler(MoneroLikeSpecificBtcPayNetwork network, MoneroRPCProvider moneroRpcProvider)
25+
public MoneroLikePaymentMethodHandler(MoneroLikeSpecificBtcPayNetwork network, MoneroRPCProvider moneroRpcProvider, MoneroWalletService walletService)
2526
{
2627
PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
2728
_network = network;
2829
Serializer = BlobSerializer.CreateSerializer().Serializer;
2930
_moneroRpcProvider = moneroRpcProvider;
31+
_walletService = walletService;
3032
}
3133
bool IsReady() => _moneroRpcProvider.IsConfigured(_network.CryptoCode) && _moneroRpcProvider.IsAvailable(_network.CryptoCode);
3234

@@ -41,11 +43,14 @@ public Task BeforeFetchingRates(PaymentMethodContext context)
4143
var daemonClient = _moneroRpcProvider.DaemonRpcClients[_network.CryptoCode];
4244
try
4345
{
46+
var currentWallet = _walletService.GetWalletState()?.ActiveWalletName;
47+
var accountIndex = supportedPaymentMethod.GetAccountIndexForWallet(currentWallet);
48+
4449
context.State = new Prepare()
4550
{
4651
GetFeeRate = daemonClient.SendCommandAsync<GetFeeEstimateRequest, GetFeeEstimateResponse>("get_fee_estimate", new GetFeeEstimateRequest()),
47-
ReserveAddress = s => walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>("create_address", new CreateAddressRequest() { Label = $"btcpay invoice #{s}", AccountIndex = supportedPaymentMethod.AccountIndex }),
48-
AccountIndex = supportedPaymentMethod.AccountIndex
52+
ReserveAddress = s => walletClient.SendCommandAsync<CreateAddressRequest, CreateAddressResponse>("create_address", new CreateAddressRequest() { Label = $"btcpay invoice #{s}", AccountIndex = accountIndex }),
53+
AccountIndex = accountIndex
4954
};
5055
}
5156
catch (Exception ex)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,43 @@
1+
using System.Collections.Generic;
2+
13
namespace BTCPayServer.Plugins.Monero.Payments
24
{
35
public class MoneroPaymentPromptDetails
46
{
7+
public Dictionary<string, long> WalletAccountIndexes { get; set; } = [];
8+
59
public long AccountIndex { get; set; }
10+
611
public long? InvoiceSettledConfirmationThreshold { get; set; }
12+
13+
public long GetAccountIndexForWallet(string walletName)
14+
{
15+
if (string.IsNullOrEmpty(walletName))
16+
{
17+
return 0;
18+
}
19+
20+
return WalletAccountIndexes.TryGetValue(walletName, out var index) ? index : 0;
21+
}
22+
23+
public void SetAccountIndexForWallet(string walletName, long accountIndex)
24+
{
25+
if (string.IsNullOrEmpty(walletName))
26+
{
27+
return;
28+
}
29+
30+
WalletAccountIndexes[walletName] = accountIndex;
31+
}
32+
33+
public bool RemoveWallet(string walletName)
34+
{
35+
if (string.IsNullOrEmpty(walletName))
36+
{
37+
return false;
38+
}
39+
40+
return WalletAccountIndexes.Remove(walletName);
41+
}
742
}
843
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Newtonsoft.Json;
2+
3+
namespace BTCPayServer.Plugins.Monero.RPC.Models
4+
{
5+
public class OpenWalletRequest
6+
{
7+
[JsonProperty("filename")]
8+
public string Filename { get; set; }
9+
10+
[JsonProperty("password")]
11+
public string Password { get; set; }
12+
}
13+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
using BTCPayServer.Abstractions.Contracts;
8+
using BTCPayServer.Logging;
9+
using BTCPayServer.Plugins.Monero.Configuration;
10+
11+
using Microsoft.Extensions.Hosting;
12+
using Microsoft.Extensions.Logging;
13+
14+
15+
namespace BTCPayServer.Plugins.Monero.Services
16+
{
17+
public class MoneroConfigurationMigrationService : IHostedService
18+
{
19+
private const string CryptoCode = "XMR";
20+
private const string WalletStateMigrationKey = "MoneroWalletStateMigration";
21+
22+
private readonly ISettingsRepository _settingsRepository;
23+
private readonly MoneroRPCProvider _rpcProvider;
24+
private readonly Logs _logs;
25+
26+
public MoneroConfigurationMigrationService(
27+
ISettingsRepository settingsRepository,
28+
MoneroRPCProvider rpcProvider,
29+
Logs logs)
30+
{
31+
_settingsRepository = settingsRepository;
32+
_rpcProvider = rpcProvider;
33+
_logs = logs;
34+
}
35+
36+
public async Task StartAsync(CancellationToken cancellationToken)
37+
{
38+
await MigrateWalletState();
39+
}
40+
41+
public Task StopAsync(CancellationToken cancellationToken)
42+
{
43+
return Task.CompletedTask;
44+
}
45+
46+
private async Task MigrateWalletState()
47+
{
48+
try
49+
{
50+
var migrationStatus = await _settingsRepository.GetSettingAsync<MigrationStatus>(WalletStateMigrationKey);
51+
if (migrationStatus?.Complete == true)
52+
{
53+
_logs.PayServer.LogDebug("Wallet migration already completed");
54+
return;
55+
}
56+
57+
_logs.PayServer.LogInformation("Starting wallet migration");
58+
59+
string walletDir = _rpcProvider.GetWalletDirectory(CryptoCode);
60+
if (string.IsNullOrEmpty(walletDir))
61+
{
62+
_logs.PayServer.LogInformation("No wallet directory configured, skipping wallet migration");
63+
await _settingsRepository.UpdateSetting(new MigrationStatus { Complete = true }, WalletStateMigrationKey);
64+
return;
65+
}
66+
67+
string passwordFile = Path.Combine(walletDir, "password");
68+
if (!File.Exists(passwordFile))
69+
{
70+
_logs.PayServer.LogInformation("No password file found, skipping wallet migration");
71+
await _settingsRepository.UpdateSetting(new MigrationStatus { Complete = true }, WalletStateMigrationKey);
72+
return;
73+
}
74+
75+
string[] availableWallets = _rpcProvider.GetWalletList(CryptoCode);
76+
if (availableWallets is null or { Length: 0 })
77+
{
78+
_logs.PayServer.LogWarning("Password file found but no wallet files exist");
79+
await _settingsRepository.UpdateSetting(new MigrationStatus { Complete = true }, WalletStateMigrationKey);
80+
return;
81+
}
82+
83+
string password = await File.ReadAllTextAsync(passwordFile);
84+
password = password.Trim();
85+
string walletName = availableWallets.First();
86+
87+
bool opened = await _rpcProvider.OpenWallet(CryptoCode, walletName, password);
88+
if (!opened)
89+
{
90+
_logs.PayServer.LogWarning($"Failed to open wallet '{walletName}' during migration - password may be incorrect");
91+
await _settingsRepository.UpdateSetting(new MigrationStatus { Complete = true }, WalletStateMigrationKey);
92+
return;
93+
}
94+
95+
await _rpcProvider.CloseWallet(CryptoCode);
96+
97+
var walletState = new MoneroWalletState
98+
{
99+
ActiveWalletName = walletName,
100+
ActiveWalletPassword = password,
101+
LastActivatedAt = DateTimeOffset.UtcNow,
102+
LastActivatedByStoreId = "migration",
103+
IsConnected = false
104+
};
105+
await _settingsRepository.UpdateSetting(walletState);
106+
107+
_logs.PayServer.LogInformation($"Successfully migrated legacy wallet '{walletName}'");
108+
109+
await _settingsRepository.UpdateSetting(new MigrationStatus { Complete = true }, WalletStateMigrationKey);
110+
}
111+
catch (Exception ex)
112+
{
113+
_logs.PayServer.LogError(ex, "Error during wallet migration");
114+
await _settingsRepository.UpdateSetting(new MigrationStatus { Complete = true }, WalletStateMigrationKey);
115+
}
116+
}
117+
118+
private class MigrationStatus
119+
{
120+
public bool Complete { get; set; }
121+
}
122+
}
123+
}

Plugins/Monero/Services/MoneroListener.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using BTCPayServer.Data;
99
using BTCPayServer.Events;
1010
using BTCPayServer.HostedServices;
11+
using BTCPayServer.Logging;
1112
using BTCPayServer.Payments;
1213
using BTCPayServer.Plugins.Monero.Configuration;
1314
using BTCPayServer.Plugins.Monero.Payments;
@@ -32,7 +33,7 @@ public class MoneroListener : EventHostedServiceBase
3233
private readonly MoneroRPCProvider _moneroRpcProvider;
3334
private readonly MoneroLikeConfiguration _MoneroLikeConfiguration;
3435
private readonly BTCPayNetworkProvider _networkProvider;
35-
private readonly ILogger<MoneroListener> _logger;
36+
private readonly Logs _logs;
3637
private readonly PaymentMethodHandlerDictionary _handlers;
3738
private readonly InvoiceActivator _invoiceActivator;
3839
private readonly PaymentService _paymentService;
@@ -42,17 +43,17 @@ public MoneroListener(InvoiceRepository invoiceRepository,
4243
MoneroRPCProvider moneroRpcProvider,
4344
MoneroLikeConfiguration moneroLikeConfiguration,
4445
BTCPayNetworkProvider networkProvider,
45-
ILogger<MoneroListener> logger,
46+
Logs logs,
4647
PaymentMethodHandlerDictionary handlers,
4748
InvoiceActivator invoiceActivator,
48-
PaymentService paymentService) : base(eventAggregator, logger)
49+
PaymentService paymentService) : base(eventAggregator, logs)
4950
{
5051
_invoiceRepository = invoiceRepository;
5152
_eventAggregator = eventAggregator;
5253
_moneroRpcProvider = moneroRpcProvider;
5354
_MoneroLikeConfiguration = moneroLikeConfiguration;
5455
_networkProvider = networkProvider;
55-
_logger = logger;
56+
_logs = logs;
5657
_handlers = handlers;
5758
_invoiceActivator = invoiceActivator;
5859
_paymentService = paymentService;
@@ -71,12 +72,12 @@ protected override async Task ProcessEvent(object evt, CancellationToken cancell
7172
{
7273
if (_moneroRpcProvider.IsAvailable(stateChange.CryptoCode))
7374
{
74-
_logger.LogInformation($"{stateChange.CryptoCode} just became available");
75+
_logs.PayServer.LogInformation($"{stateChange.CryptoCode} just became available");
7576
_ = UpdateAnyPendingMoneroLikePayment(stateChange.CryptoCode);
7677
}
7778
else
7879
{
79-
_logger.LogInformation($"{stateChange.CryptoCode} just became unavailable");
80+
_logs.PayServer.LogInformation($"{stateChange.CryptoCode} just became unavailable");
8081
}
8182
}
8283
else if (evt is MoneroEvent moneroEvent)
@@ -100,7 +101,7 @@ protected override async Task ProcessEvent(object evt, CancellationToken cancell
100101

101102
private async Task ReceivedPayment(InvoiceEntity invoice, PaymentEntity payment)
102103
{
103-
_logger.LogInformation(
104+
_logs.PayServer.LogInformation(
104105
$"Invoice {invoice.Id} received payment {payment.Value} {payment.Currency} {payment.Id}");
105106

106107
var prompt = invoice.GetPaymentPrompt(payment.PaymentMethodId);

0 commit comments

Comments
 (0)