mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-28 00:52:52 +01:00
371 lines
17 KiB
C#
371 lines
17 KiB
C#
#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<IActionResult> GetStoreMoneroLikePaymentMethods()
|
||
{
|
||
return View(await GetVM(StoreData));
|
||
}
|
||
[NonAction]
|
||
public async Task<MoneroLikePaymentMethodListViewModel> 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<GetAccountsResponse> GetAccounts(string cryptoCode)
|
||
{
|
||
try
|
||
{
|
||
if (_MoneroRpcProvider.Summaries.TryGetValue(cryptoCode, out var summary) && summary.WalletAvailable)
|
||
{
|
||
|
||
return _MoneroRpcProvider.WalletRpcClients[cryptoCode].SendCommandAsync<GetAccountsRequest, GetAccountsResponse>("get_accounts", new GetAccountsRequest());
|
||
}
|
||
}
|
||
catch { }
|
||
return Task.FromResult<GetAccountsResponse>(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<IActionResult> 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<IActionResult> 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<CreateAccountRequest, CreateAccountResponse>("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<MoneroLikePaymentMethodViewModel> 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<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 = "Wallet Password")]
|
||
public string WalletPassword { get; set; }
|
||
[Display(Name = "Consider the invoice settled when the payment transaction <20>")]
|
||
public MoneroLikeSettlementThresholdChoice SettlementConfirmationThresholdChoice { get; set; }
|
||
[Display(Name = "Required Confirmations"), Range(0, 100)]
|
||
public long? CustomSettlementConfirmationThreshold { get; set; }
|
||
|
||
public IEnumerable<ValidationResult> 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
|