Remove cash cow with cheat code and replace upload wallet with private view key

This commit is contained in:
napoly 2025-06-22 00:36:28 +02:00
parent 73e8905166
commit 36a274fcfa
22 changed files with 112 additions and 349 deletions

View file

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

View file

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

View file

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

View file

@ -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");

View file

@ -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:

View file

@ -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")]

View file

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

View file

@ -0,0 +1,5 @@
using System;
namespace BTCPayServer.Plugins.Monero.Controllers;
public class GenerateFromKeysException(string message) : Exception(message);

View file

@ -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<OpenWalletRequest, OpenWalletResponse>("open_wallet", new OpenWalletRequest
var response = await _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync<GenerateFromKeysRequest, GenerateFromKeysResponse>("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<MoneroLikePaymentMethodViewModel> 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<SelectListItem> 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 …")]

View file

@ -1,5 +0,0 @@
using System;
namespace BTCPayServer.Plugins.Monero.Controllers;
public class WalletOpenException(string message) : Exception(message);

View file

@ -78,15 +78,12 @@ public class MoneroPlugin : BaseBTCPayServerPlugin
services.AddSingleton<MoneroRPCProvider>();
services.AddHostedService<MoneroLikeSummaryUpdaterHostedService>();
services.AddHostedService<MoneroListener>();
services.AddSingleton<IPaymentMethodHandler>(provider =>
(IPaymentMethodHandler)ActivatorUtilities.CreateInstance(provider, typeof(MoneroLikePaymentMethodHandler), new object[] { network }));
services.AddSingleton<IPaymentLinkExtension>(provider =>
(IPaymentLinkExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroPaymentLinkExtension), new object[] { network, pmi }));
services.AddSingleton<ICheckoutModelExtension>(provider =>
(ICheckoutModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(MoneroCheckoutModelExtension), new object[] { network, pmi }));
services.AddSingleton<ICheckoutCheatModeExtension>(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<Uri>(
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_wallet_daemon_uri", null);
var cashCowWalletDaemonUri =
configuration.GetOrDefault<Uri>(
$"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_cashcow_wallet_daemon_uri", null);
var walletDaemonWalletDirectory =
configuration.GetOrDefault<string>(
$"{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
});
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<ICheckoutCheatModeExtension.PayInvoiceResult> 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<TransferRequest, TransferResponse>("transfer",
new TransferRequest()
{
Destinations = new[] { new TransferDestination()
{
Amount = (long)amount,
Address = payInvoiceContext.PaymentPrompt.Destination
}
}
});
return new ICheckoutCheatModeExtension.PayInvoiceResult(result.TransactionHash);
}
public async Task<ICheckoutCheatModeExtension.MineBlockResult> MineBlock(ICheckoutCheatModeExtension.MineBlockContext mineBlockContext)
{
var cashcow = _rpcProvider.CashCowWalletRpcClients[_network.CryptoCode];
var deamon = _rpcProvider.DaemonRpcClients[_network.CryptoCode];
var address = (await cashcow.SendCommandAsync<GetAddressRequest, GetAddressResponse>("get_address", new()
{
AccountIndex = 0
})).Address;
await deamon.SendCommandAsync<GenerateBlocks, JsonRpcClient.NoRequestModel>("generateblocks", new GenerateBlocks()
{
WalletAddress = address,
AmountOfBlocks = mineBlockContext.BlockCount
});
return new ICheckoutCheatModeExtension.MineBlockResult();
}
}

View file

@ -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<MoneroRPCProvider> _logger;
private readonly EventAggregator _eventAggregator;
private readonly BTCPayServerEnvironment environment;
public ImmutableDictionary<string, JsonRpcClient> DaemonRpcClients;
public ImmutableDictionary<string, JsonRpcClient> WalletRpcClients;
@ -30,14 +24,11 @@ namespace BTCPayServer.Plugins.Monero.Services
public ConcurrentDictionary<string, MoneroLikeSummary> Summaries => _summaries;
public MoneroRPCProvider(MoneroLikeConfiguration moneroLikeConfiguration,
ILogger<MoneroRPCProvider> 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<string, JsonRpcClient> 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<JsonRpcClient.NoRequestModel, GetHeightResponse>(
"get_height", JsonRpcClient.NoRequestModel.Instance);
}
catch
{
_logger.LogInformation("Creating XMR cashcow wallet...");
await CreateTestWallet(cashcow);
}
var balance =
(await cashcow.SendCommandAsync<JsonRpcClient.NoRequestModel, GetBalanceResponse>("get_balance",
JsonRpcClient.NoRequestModel.Instance));
if (balance.UnlockedBalance != 0)
{
return;
}
_logger.LogInformation("Mining blocks for the cashcow...");
var address = (await cashcow.SendCommandAsync<GetAddressRequest, GetAddressResponse>("get_address", new()
{
AccountIndex = 0
})).Address;
await deamon.SendCommandAsync<GenerateBlocks, JsonRpcClient.NoRequestModel>("generateblocks", new GenerateBlocks()
{
WalletAddress = address,
AmountOfBlocks = 100
});
_logger.LogInformation("Mining succeed!");
}
private static async Task CreateTestWallet(JsonRpcClient walletRpcClient)
{
try
{
await walletRpcClient.SendCommandAsync<OpenWalletRequest, JsonRpcClient.NoRequestModel>(
"open_wallet",
new OpenWalletRequest()
{
Filename = "wallet",
Password = "password"
});
return;
}
catch
{
// ignored
}
await walletRpcClient.SendCommandAsync<CreateWalletRequest, JsonRpcClient.NoRequestModel>("create_wallet",
new()
{
Filename = "wallet",
Password = "password",
Language = "English"
});
}
public class MoneroDaemonStateChange
{
public string CryptoCode { get; set; }

View file

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

View file

@ -20,38 +20,45 @@
<div class="card">
<ul class="list-group list-group-flush">
<li class="list-group-item">Node available: @Model.Summary.DaemonAvailable</li>
<li class="list-group-item">Wallet RPC available: @Model.Summary.WalletAvailable</li>
<li class="list-group-item">Last updated: @Model.Summary.UpdatedAt</li>
<li class="list-group-item">Synced: @Model.Summary.Synced (@Model.Summary.CurrentHeight / @Model.Summary.TargetHeight)</li>
</ul>
</div>
}
@if (Model.SupportWalletExport && Model.Summary?.WalletHeight is null or 0)
@if (Model.Summary?.WalletHeight is null or 0)
{
<form method="post" asp-action="GetStoreMoneroLikePaymentMethod"
<form method="post" asp-action="GetStoreMoneroLikePaymentMethod"
asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-cryptoCode="@Context.GetRouteValue("cryptoCode")"
class="mt-4" enctype="multipart/form-data">
class="mt-4">
<div class="card my-2">
<h3 class="card-title p-2">Upload Wallet</h3>
<h3 class="card-title p-2">Set View-Only Wallet Details</h3>
<div class="form-group p-2">
<label asp-for="WalletFile" class="form-label"></label>
<input class="form-control" asp-for="WalletFile" required>
<span asp-validation-for="WalletFile" class="text-danger"></span>
<label asp-for="PrimaryAddress" class="form-label">Wallet Primary Address</label>
<input type="text" class="form-control" asp-for="PrimaryAddress" required>
<span asp-validation-for="PrimaryAddress" class="text-danger"></span>
</div>
<div class="form-group p-2">
<label asp-for="WalletKeysFile" class="form-label"></label>
<input class="form-control" asp-for="WalletKeysFile" required>
<span asp-validation-for="WalletKeysFile" class="text-danger"></span>
<label asp-for="PrivateViewKey" class="form-label">Wallet Private View Key</label>
<input type="text" class="form-control" asp-for="PrivateViewKey" required>
<span asp-validation-for="PrivateViewKey" class="text-danger"></span>
</div>
<div class="form-group p-2">
<label asp-for="WalletPassword" class="form-label"></label>
<input class="form-control" asp-for="WalletPassword">
<label asp-for="RestoreHeight" class="form-label">Restore Height (Block Height)</label>
<input type="number" class="form-control" asp-for="RestoreHeight" required>
<span asp-validation-for="RestoreHeight" class="text-danger"></span>
</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">
<button name="command" value="upload-wallet" class="btn btn-secondary" type="submit">Upload</button>
<button name="command" value="set-wallet-details" class="btn btn-secondary" type="submit">Set Wallet Details</button>
</div>
</div>
</form>

View file

@ -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).