From c56c6401d6f1de4ec79f965f94f69d27f12867dc Mon Sep 17 00:00:00 2001 From: Henry Hollingworth Date: Thu, 14 Mar 2024 17:31:27 +0800 Subject: [PATCH] (feat) monero settlement thresholds (#5807) * (bug) treat xmr wallet directory as required The wallet directory configuration setting is required because the `UIMoneroLikeStoreController`'s `GetMoneroLikePaymentMethodViewModel` method checks if the wallet file exists, and to do that in needs the directory. * (feat) xmr settlement thresholds Adds the ability to select zero, 1, 10, or a custom number of confirmations as the payment settlement threshold. * (review) fix validation message not showing --------- Co-authored-by: Henry Hollingworth --- .../Altcoins/Monero/MoneroLikeExtensions.cs | 2 +- .../MoneroLikeOnChainPaymentMethodDetails.cs | 1 + .../Monero/Payments/MoneroLikePaymentData.cs | 7 +++ .../MoneroLikePaymentMethodHandler.cs | 1 + .../Payments/MoneroSupportedPaymentMethod.cs | 1 + .../Monero/Services/MoneroListener.cs | 8 ++- .../Monero/UI/MoneroLikeStoreController.cs | 59 ++++++++++++++++++- .../GetStoreMoneroLikePaymentMethod.cshtml | 40 ++++++++++++- 8 files changed, 112 insertions(+), 7 deletions(-) diff --git a/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs b/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs index 1e17e1538..772667d98 100644 --- a/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs +++ b/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs @@ -73,7 +73,7 @@ namespace BTCPayServer.Services.Altcoins.Monero var daemonPassword = configuration.GetOrDefault( $"{moneroLikeSpecificBtcPayNetwork.CryptoCode}_daemon_password", null); - if (daemonUri == null || walletDaemonUri == null) + if (daemonUri == null || walletDaemonUri == null || walletDaemonWalletDirectory == null) { throw new ConfigException($"{moneroLikeSpecificBtcPayNetwork.CryptoCode} is misconfigured"); } diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs index 41227780c..da9257bcf 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs @@ -29,6 +29,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments public long AddressIndex { get; set; } public string DepositAddress { get; set; } public decimal NextNetworkFee { get; set; } + public long? InvoiceSettledConfirmationThreshold { get; set; } } } #endif diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentData.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentData.cs index 01960d8f9..e01e7e7f4 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentData.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentData.cs @@ -16,6 +16,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments public long BlockHeight { get; set; } public long ConfirmationCount { get; set; } public string TransactionId { get; set; } + public long? InvoiceSettledConfirmationThreshold { get; set; } public BTCPayNetworkBase Network { get; set; } public long LockTime { get; set; } = 0; @@ -48,6 +49,12 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments { return false; } + + if (InvoiceSettledConfirmationThreshold.HasValue) + { + return ConfirmationCount >= InvoiceSettledConfirmationThreshold; + } + switch (speedPolicy) { case SpeedPolicy.HighSpeed: diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs index b59b1ef0c..0f7fd62e2 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikePaymentMethodHandler.cs @@ -59,6 +59,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments AccountIndex = supportedPaymentMethod.AccountIndex, AddressIndex = address.AddressIndex, DepositAddress = address.Address, + InvoiceSettledConfirmationThreshold = supportedPaymentMethod.InvoiceSettledConfirmationThreshold, Activated = true }; diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroSupportedPaymentMethod.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroSupportedPaymentMethod.cs index 38d4d5a20..fefa6d067 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroSupportedPaymentMethod.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroSupportedPaymentMethod.cs @@ -9,6 +9,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments public string CryptoCode { get; set; } public long AccountIndex { get; set; } + public long? InvoiceSettledConfirmationThreshold { get; set; } [JsonIgnore] public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, MoneroPaymentType.Instance); } diff --git a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs index 0b85806eb..093d3e472 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs @@ -324,6 +324,11 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services string txId, long confirmations, long blockHeight, long locktime, InvoiceEntity invoice, BlockingCollection<(PaymentEntity Payment, InvoiceEntity invoice)> paymentsToUpdate) { + var network = _networkProvider.GetNetwork(cryptoCode); + var moneroPaymentMethodDetails = invoice + .GetPaymentMethod(network, MoneroPaymentType.Instance) + .GetPaymentMethodDetails() as MoneroLikeOnChainPaymentMethodDetails; + //construct the payment data var paymentData = new MoneroLikePaymentData() { @@ -335,7 +340,8 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services Amount = totalAmount, BlockHeight = blockHeight, Network = _networkProvider.GetNetwork(cryptoCode), - LockTime = locktime + LockTime = locktime, + InvoiceSettledConfirmationThreshold = moneroPaymentMethodDetails.InvoiceSettledConfirmationThreshold }; //check if this tx exists as a payment to this invoice already diff --git a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs b/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs index 73aa6061e..50c48c135 100644 --- a/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs +++ b/BTCPayServer/Services/Altcoins/Monero/UI/MoneroLikeStoreController.cs @@ -104,6 +104,14 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI new SelectListItem( $"{account.AccountIndex} - {(string.IsNullOrEmpty(account.Label) ? "No label" : account.Label)}", account.AccountIndex.ToString(CultureInfo.InvariantCulture))); + var settlementThresholdChoice = settings.InvoiceSettledConfirmationThreshold switch + { + null => MoneroLikeSettlementThresholdChoice.StoreSpeedPolicy, + 0 => MoneroLikeSettlementThresholdChoice.ZeroConfirmation, + 1 => MoneroLikeSettlementThresholdChoice.AtLeastOne, + 10 => MoneroLikeSettlementThresholdChoice.AtLeastTen, + _ => MoneroLikeSettlementThresholdChoice.Custom + }; return new MoneroLikePaymentMethodViewModel() { WalletFileFound = System.IO.File.Exists(fileAddress), @@ -114,7 +122,11 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI CryptoCode = cryptoCode, AccountIndex = settings?.AccountIndex ?? accountsResponse?.SubaddressAccounts?.FirstOrDefault()?.AccountIndex ?? 0, Accounts = accounts == null ? null : new SelectList(accounts, nameof(SelectListItem.Value), - nameof(SelectListItem.Text)) + nameof(SelectListItem.Text)), + SettlementConfirmationThresholdChoice = settlementThresholdChoice, + CustomSettlementConfirmationThreshold = settlementThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom + ? settings.InvoiceSettledConfirmationThreshold + : null }; } @@ -250,6 +262,8 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI vm.Enabled = viewModel.Enabled; vm.NewAccountLabel = viewModel.NewAccountLabel; vm.AccountIndex = viewModel.AccountIndex; + vm.SettlementConfirmationThresholdChoice = viewModel.SettlementConfirmationThresholdChoice; + vm.CustomSettlementConfirmationThreshold = viewModel.CustomSettlementConfirmationThreshold; return View(vm); } @@ -258,7 +272,15 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI storeData.SetSupportedPaymentMethod(new MoneroSupportedPaymentMethod() { AccountIndex = viewModel.AccountIndex, - CryptoCode = viewModel.CryptoCode + CryptoCode = viewModel.CryptoCode, + InvoiceSettledConfirmationThreshold = viewModel.SettlementConfirmationThresholdChoice switch + { + MoneroLikeSettlementThresholdChoice.ZeroConfirmation => 0, + MoneroLikeSettlementThresholdChoice.AtLeastOne => 1, + MoneroLikeSettlementThresholdChoice.AtLeastTen => 10, + MoneroLikeSettlementThresholdChoice.Custom when viewModel.CustomSettlementConfirmationThreshold is { } custom => custom, + _ => null + } }); blob.SetExcluded(new PaymentMethodId(viewModel.CryptoCode, MoneroPaymentType.Instance), !viewModel.Enabled); @@ -297,7 +319,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI public IEnumerable Items { get; set; } } - public class MoneroLikePaymentMethodViewModel + public class MoneroLikePaymentMethodViewModel : IValidatableObject { public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; } public string CryptoCode { get; set; } @@ -309,8 +331,39 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI 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 = "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)] + public long? CustomSettlementConfirmationThreshold { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (SettlementConfirmationThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom + && CustomSettlementConfirmationThreshold is null) + { + yield return new ValidationResult( + "You must specify the number of required confirmations when using a custom threshold.", + new[] { nameof(CustomSettlementConfirmationThreshold) }); + } + } + } + + public enum MoneroLikeSettlementThresholdChoice + { + [Display(Name = "Store Speed Policy", Description = "Use the store's speed policy")] + StoreSpeedPolicy, + [Display(Name = "Zero Confirmation", Description = "Is unconfirmed")] + ZeroConfirmation, + [Display(Name = "At Least One", Description = "Has at least 1 confirmation")] + AtLeastOne, + [Display(Name = "At Least Ten", Description = "Has at least 10 confirmations")] + AtLeastTen, + [Display(Name = "Custom", Description = "Custom")] + Custom } } } diff --git a/BTCPayServer/Views/UIMoneroLikeStore/GetStoreMoneroLikePaymentMethod.cshtml b/BTCPayServer/Views/UIMoneroLikeStore/GetStoreMoneroLikePaymentMethod.cshtml index 9924fe376..f0fbf8365 100644 --- a/BTCPayServer/Views/UIMoneroLikeStore/GetStoreMoneroLikePaymentMethod.cshtml +++ b/BTCPayServer/Views/UIMoneroLikeStore/GetStoreMoneroLikePaymentMethod.cshtml @@ -1,6 +1,8 @@ @using BTCPayServer.Views.Stores @using BTCPayServer.Abstractions.Extensions -@model BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikePaymentMethodViewModel +@using MoneroLikePaymentMethodViewModel = BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikePaymentMethodViewModel +@using MoneroLikeSettlementThresholdChoice = BTCPayServer.Services.Altcoins.Monero.UI.UIMoneroLikeStoreController.MoneroLikeSettlementThresholdChoice; +@model MoneroLikePaymentMethodViewModel @{ Layout = "../Shared/_NavLayout.cshtml"; @@ -10,7 +12,7 @@
-
+
@if (Model.Summary != null) {
@@ -92,6 +94,40 @@
+
+ + + + + + + +
+ + +