#if ALTCOINS 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; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Filters; using BTCPayServer.Models; using BTCPayServer.Payments; using BTCPayServer.Security; using BTCPayServer.Services.Altcoins.Monero.Configuration; using BTCPayServer.Services.Altcoins.Monero.Payments; using BTCPayServer.Services.Altcoins.Monero.RPC.Models; using BTCPayServer.Services.Altcoins.Monero.Services; 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; namespace BTCPayServer.Services.Altcoins.Monero.UI { [Route("stores/{storeId}/monerolike")] [OnlyIfSupportAttribute("XMR")] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] public class UIMoneroLikeStoreController : Controller { private readonly MoneroLikeConfiguration _MoneroLikeConfiguration; private readonly StoreRepository _StoreRepository; private readonly MoneroRPCProvider _MoneroRpcProvider; private readonly PaymentMethodHandlerDictionary _handlers; private readonly BTCPayNetworkProvider _BtcPayNetworkProvider; public UIMoneroLikeStoreController(MoneroLikeConfiguration moneroLikeConfiguration, StoreRepository storeRepository, MoneroRPCProvider moneroRpcProvider, PaymentMethodHandlerDictionary handlers, BTCPayNetworkProvider btcPayNetworkProvider) { _MoneroLikeConfiguration = moneroLikeConfiguration; _StoreRepository = storeRepository; _MoneroRpcProvider = moneroRpcProvider; _handlers = handlers; _BtcPayNetworkProvider = btcPayNetworkProvider; } public StoreData StoreData => HttpContext.GetStoreData(); [HttpGet()] public async Task GetStoreMoneroLikePaymentMethods() { return View(await GetVM(StoreData)); } [NonAction] public async Task GetVM(StoreData storeData) { var excludeFilters = storeData.GetStoreBlob().GetExcludedPaymentMethods(); var accountsList = _MoneroLikeConfiguration.MoneroLikeConfigurationItems.ToDictionary(pair => pair.Key, pair => GetAccounts(pair.Key)); await Task.WhenAll(accountsList.Values); return new MoneroLikePaymentMethodListViewModel() { Items = _MoneroLikeConfiguration.MoneroLikeConfigurationItems.Select(pair => GetMoneroLikePaymentMethodViewModel(storeData, pair.Key, excludeFilters, accountsList[pair.Key].Result)) }; } private Task GetAccounts(string cryptoCode) { try { if (_MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary) && summary.WalletAvailable) { return _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync("get_accounts", new GetAccountsRequest()); } } catch { } return Task.FromResult(null); } private MoneroLikePaymentMethodViewModel GetMoneroLikePaymentMethodViewModel( StoreData storeData, string cryptoCode, IPaymentFilter excludeFilters, GetAccountsResponse accountsResponse) { var monero = storeData.GetPaymentMethodConfigs(_handlers) .Where(s => s.Value is MoneroPaymentPromptDetails) .Select(s => (PaymentMethodId: s.Key, Details: (MoneroPaymentPromptDetails)s.Value)); var pmi = MoneroPaymentType.Instance.GetPaymentMethodId(cryptoCode); var settings = monero.Where(method => method.PaymentMethodId == pmi).Select(m => m.Details).SingleOrDefault(); _MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary); _MoneroLikeConfiguration.MoneroLikeConfigurationItems.TryGetValue(cryptoCode, out var configurationItem); var fileAddress = Path.Combine(configurationItem.WalletDirectory, "wallet"); var accounts = accountsResponse?.SubaddressAccounts?.Select(account => 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), Enabled = settings != null && !excludeFilters.Match(MoneroPaymentType.Instance.GetPaymentMethodId(cryptoCode)), Summary = summary, CryptoCode = cryptoCode, AccountIndex = settings?.AccountIndex ?? accountsResponse?.SubaddressAccounts?.FirstOrDefault()?.AccountIndex ?? 0, Accounts = accounts == null ? null : new SelectList(accounts, nameof(SelectListItem.Value), nameof(SelectListItem.Text)), SettlementConfirmationThresholdChoice = settlementThresholdChoice, CustomSettlementConfirmationThreshold = settlementThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom ? settings.InvoiceSettledConfirmationThreshold : null }; } [HttpGet("{cryptoCode}")] public async Task GetStoreMoneroLikePaymentMethod(string cryptoCode) { cryptoCode = cryptoCode.ToUpperInvariant(); if (!_MoneroLikeConfiguration.MoneroLikeConfigurationItems.ContainsKey(cryptoCode)) { return NotFound(); } var vm = GetMoneroLikePaymentMethodViewModel(StoreData, cryptoCode, StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode)); return View(nameof(GetStoreMoneroLikePaymentMethod), vm); } [HttpPost("{cryptoCode}")] [DisableRequestSizeLimit] public async Task GetStoreMoneroLikePaymentMethod(MoneroLikePaymentMethodViewModel viewModel, string command, string cryptoCode) { cryptoCode = cryptoCode.ToUpperInvariant(); if (!_MoneroLikeConfiguration.MoneroLikeConfigurationItems.TryGetValue(cryptoCode, out var configurationItem)) { return NotFound(); } if (command == "add-account") { try { var newAccount = await _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync("create_account", new CreateAccountRequest() { Label = viewModel.NewAccountLabel }); viewModel.AccountIndex = newAccount.AccountIndex; } catch (Exception) { ModelState.AddModelError(nameof(viewModel.AccountIndex), "Could not create a new account."); } } else if (command == "upload-wallet") { var valid = true; if (viewModel.WalletFile == null) { ModelState.AddModelError(nameof(viewModel.WalletFile), "Please select the view-only wallet file"); valid = false; } if (viewModel.WalletKeysFile == null) { ModelState.AddModelError(nameof(viewModel.WalletKeysFile), "Please select the view-only wallet keys file"); valid = false; } if (valid) { if (_MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary)) { if (summary.WalletAvailable) { TempData.SetStatusMessageModel(new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Error, Message = $"There is already an active wallet configured for {cryptoCode}. Replacing it would break any existing invoices!" }); return RedirectToAction(nameof(GetStoreMoneroLikePaymentMethod), 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 { } } 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 { } } fileAddress = Path.Combine(configurationItem.WalletDirectory, "password"); using (var fileStream = new StreamWriter(fileAddress, false)) { await fileStream.WriteAsync(viewModel.WalletPassword); try { Exec($"chmod 666 {fileAddress}"); } catch { } } return RedirectToAction(nameof(GetStoreMoneroLikePaymentMethod), new { cryptoCode, StatusMessage = "View-only wallet files uploaded. If they are valid the wallet will soon become available." }); } } if (!ModelState.IsValid) { var vm = GetMoneroLikePaymentMethodViewModel(StoreData, cryptoCode, StoreData.GetStoreBlob().GetExcludedPaymentMethods(), await GetAccounts(cryptoCode)); vm.Enabled = viewModel.Enabled; vm.NewAccountLabel = viewModel.NewAccountLabel; vm.AccountIndex = viewModel.AccountIndex; vm.SettlementConfirmationThresholdChoice = viewModel.SettlementConfirmationThresholdChoice; vm.CustomSettlementConfirmationThreshold = viewModel.CustomSettlementConfirmationThreshold; return View(vm); } var storeData = StoreData; var blob = storeData.GetStoreBlob(); storeData.SetPaymentMethodConfig(_handlers[MoneroPaymentType.Instance.GetPaymentMethodId(cryptoCode)], new MoneroPaymentPromptDetails() { AccountIndex = viewModel.AccountIndex, InvoiceSettledConfirmationThreshold = viewModel.SettlementConfirmationThresholdChoice switch { MoneroLikeSettlementThresholdChoice.ZeroConfirmation => 0, MoneroLikeSettlementThresholdChoice.AtLeastOne => 1, MoneroLikeSettlementThresholdChoice.AtLeastTen => 10, MoneroLikeSettlementThresholdChoice.Custom when viewModel.CustomSettlementConfirmationThreshold is { } custom => custom, _ => null } }); blob.SetExcluded(MoneroPaymentType.Instance.GetPaymentMethodId(viewModel.CryptoCode), !viewModel.Enabled); storeData.SetStoreBlob(blob); await _StoreRepository.UpdateStore(storeData); return RedirectToAction("GetStoreMoneroLikePaymentMethods", 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 Items { get; set; } } public class MoneroLikePaymentMethodViewModel : IValidatableObject { public MoneroRPCProvider.MoneroLikeSummary Summary { get; set; } public string CryptoCode { get; set; } public string NewAccountLabel { get; set; } public long AccountIndex { get; set; } public bool Enabled { get; set; } public IEnumerable 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 = "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 } } } #endif