btcpayserver/BTCPayServer/Controllers/WalletsController.cs

1175 lines
52 KiB
C#
Raw Normal View History

2018-07-26 15:32:24 +02:00
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
2020-03-19 11:11:15 +01:00
using BTCPayServer.Client;
2018-07-26 15:32:24 +02:00
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
2018-07-26 15:32:24 +02:00
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Labels;
2018-07-26 16:23:28 +02:00
using BTCPayServer.Services.Rates;
2018-07-26 15:32:24 +02:00
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.DataEncoders;
2020-01-21 09:33:12 +01:00
using NBXplorer;
2018-07-26 15:32:24 +02:00
using NBXplorer.DerivationStrategy;
2018-10-26 16:07:39 +02:00
using NBXplorer.Models;
2018-07-26 15:32:24 +02:00
using Newtonsoft.Json;
namespace BTCPayServer.Controllers
{
[Route("wallets")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2018-07-26 15:32:24 +02:00
[AutoValidateAntiforgeryToken]
public partial class WalletsController : Controller
2018-07-26 15:32:24 +02:00
{
public StoreRepository Repository { get; }
public WalletRepository WalletRepository { get; }
public BTCPayNetworkProvider NetworkProvider { get; }
public ExplorerClientProvider ExplorerClientProvider { get; }
2018-07-26 15:32:24 +02:00
private readonly UserManager<ApplicationUser> _userManager;
2019-10-03 10:06:49 +02:00
private readonly JsonSerializerSettings _serializerSettings;
2018-07-26 15:32:24 +02:00
private readonly NBXplorerDashboard _dashboard;
2019-10-12 13:35:30 +02:00
private readonly IAuthorizationService _authorizationService;
2018-07-26 15:32:24 +02:00
private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider;
private readonly WalletReceiveStateService _WalletReceiveStateService;
private readonly EventAggregator _EventAggregator;
2020-01-21 09:33:12 +01:00
private readonly SettingsRepository _settingsRepository;
2020-03-29 17:28:22 +02:00
private readonly DelayedTransactionBroadcaster _broadcaster;
private readonly PayjoinClient _payjoinClient;
private readonly LabelFactory _labelFactory;
public RateFetcher RateFetcher { get; }
2018-07-26 16:23:28 +02:00
CurrencyNameTable _currencyTable;
2018-07-26 15:32:24 +02:00
public WalletsController(StoreRepository repo,
WalletRepository walletRepository,
2018-07-26 16:23:28 +02:00
CurrencyNameTable currencyTable,
2018-07-26 15:32:24 +02:00
BTCPayNetworkProvider networkProvider,
UserManager<ApplicationUser> userManager,
2019-10-03 10:06:49 +02:00
MvcNewtonsoftJsonOptions mvcJsonOptions,
2018-07-26 15:32:24 +02:00
NBXplorerDashboard dashboard,
RateFetcher rateProvider,
2019-10-12 13:35:30 +02:00
IAuthorizationService authorizationService,
2018-07-26 15:32:24 +02:00
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
BTCPayWalletProvider walletProvider,
WalletReceiveStateService walletReceiveStateService,
2020-01-21 09:33:12 +01:00
EventAggregator eventAggregator,
2020-01-06 13:57:32 +01:00
SettingsRepository settingsRepository,
2020-03-29 17:28:22 +02:00
DelayedTransactionBroadcaster broadcaster,
PayjoinClient payjoinClient,
LabelFactory labelFactory)
2018-07-26 15:32:24 +02:00
{
2018-07-26 16:23:28 +02:00
_currencyTable = currencyTable;
Repository = repo;
WalletRepository = walletRepository;
RateFetcher = rateProvider;
2019-10-12 13:35:30 +02:00
_authorizationService = authorizationService;
NetworkProvider = networkProvider;
2018-07-26 15:32:24 +02:00
_userManager = userManager;
2019-10-03 10:06:49 +02:00
_serializerSettings = mvcJsonOptions.SerializerSettings;
2018-07-26 15:32:24 +02:00
_dashboard = dashboard;
ExplorerClientProvider = explorerProvider;
2018-07-26 15:32:24 +02:00
_feeRateProvider = feeRateProvider;
_walletProvider = walletProvider;
_WalletReceiveStateService = walletReceiveStateService;
_EventAggregator = eventAggregator;
2020-01-21 09:33:12 +01:00
_settingsRepository = settingsRepository;
2020-03-29 17:28:22 +02:00
_broadcaster = broadcaster;
_payjoinClient = payjoinClient;
_labelFactory = labelFactory;
2018-07-26 15:32:24 +02:00
}
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
string[] LabelColorScheme = new string[]
{
"#fbca04",
"#0e8a16",
"#ff7619",
"#84b6eb",
"#5319e7",
"#000000",
"#cc317c",
};
2019-08-03 15:03:49 +02:00
const int MaxLabelSize = 20;
const int MaxCommentSize = 200;
[HttpPost]
[Route("{walletId}")]
public async Task<IActionResult> ModifyTransaction(
// We need addlabel and addlabelclick. addlabel is the + button if the label does not exists,
// addlabelclick is if the user click on existing label. For some reason, reusing the same name attribute for both
// does not work
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string transactionId,
string addlabel = null,
2019-08-03 14:52:47 +02:00
string addlabelclick = null,
string addcomment = null,
2019-08-03 14:52:47 +02:00
string removelabel = null)
{
addlabel = addlabel ?? addlabelclick;
2019-08-03 05:41:12 +02:00
// Hack necessary when the user enter a empty comment and submit.
// For some reason asp.net consider addcomment null instead of empty string...
try
{
2019-08-03 16:02:15 +02:00
if (addcomment == null && Request?.Form?.TryGetValue(nameof(addcomment), out _) is true)
2019-08-03 05:41:12 +02:00
{
addcomment = string.Empty;
}
}
2019-08-03 05:41:12 +02:00
catch { }
/////////
2019-10-12 13:35:30 +02:00
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);
if (paymentMethod == null)
return NotFound();
var walletBlobInfoAsync = WalletRepository.GetWalletInfo(walletId);
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
var walletBlobInfo = await walletBlobInfoAsync;
var walletTransactionsInfo = await walletTransactionsInfoAsync;
if (addlabel != null)
{
addlabel = addlabel.Trim().TrimStart('{').ToLowerInvariant().Replace(',', ' ').Truncate(MaxLabelSize);
var labels = _labelFactory.GetLabels(walletBlobInfo, Request);
if (!walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
{
walletTransactionInfo = new WalletTransactionInfo();
}
if (!labels.Any(l => l.Value.Equals(addlabel, StringComparison.OrdinalIgnoreCase)))
{
List<string> allColors = new List<string>();
allColors.AddRange(LabelColorScheme);
allColors.AddRange(labels.Select(l => l.Color));
var chosenColor =
allColors
.GroupBy(k => k)
.OrderBy(k => k.Count())
.ThenBy(k => Array.IndexOf(LabelColorScheme, k.Key))
.First().Key;
walletBlobInfo.LabelColors.Add(addlabel, chosenColor);
await WalletRepository.SetWalletInfo(walletId, walletBlobInfo);
}
if (walletTransactionInfo.Labels.Add(addlabel))
{
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
}
}
else if (removelabel != null)
{
removelabel = removelabel.Trim();
if (walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
{
if (walletTransactionInfo.Labels.Remove(removelabel))
{
var canDelete = !walletTransactionsInfo.SelectMany(txi => txi.Value.Labels).Any(l => l == removelabel);
if (canDelete)
{
walletBlobInfo.LabelColors.Remove(removelabel);
await WalletRepository.SetWalletInfo(walletId, walletBlobInfo);
}
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
}
}
}
else if (addcomment != null)
{
2019-08-03 15:03:49 +02:00
addcomment = addcomment.Trim().Truncate(MaxCommentSize);
if (!walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
{
walletTransactionInfo = new WalletTransactionInfo();
}
walletTransactionInfo.Comment = addcomment;
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
}
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
}
2019-10-12 13:35:30 +02:00
[HttpGet]
[AllowAnonymous]
2018-07-26 15:32:24 +02:00
public async Task<IActionResult> ListWallets()
{
2019-10-12 13:35:30 +02:00
if (GetUserId() == null)
{
return Challenge(AuthenticationSchemes.Cookie);
}
2018-07-26 15:32:24 +02:00
var wallets = new ListWalletsViewModel();
var stores = await Repository.GetStoresByUserId(GetUserId());
2018-07-26 15:32:24 +02:00
var onChainWallets = stores
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationSchemeSettings>()
2018-07-26 15:32:24 +02:00
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
DerivationStrategy: d.AccountDerivation,
2018-07-26 15:32:24 +02:00
Network: d.Network)))
2019-12-24 08:20:44 +01:00
.Where(_ => _.Wallet != null && _.Network.WalletSupported)
2018-07-26 15:32:24 +02:00
.Select(_ => (Wallet: _.Wallet,
Store: s,
Balance: GetBalanceString(_.Wallet, _.DerivationStrategy),
DerivationStrategy: _.DerivationStrategy,
Network: _.Network)))
.ToList();
foreach (var wallet in onChainWallets)
{
ListWalletsViewModel.WalletViewModel walletVm = new ListWalletsViewModel.WalletViewModel();
wallets.Wallets.Add(walletVm);
walletVm.Balance = await wallet.Balance + " " + wallet.Wallet.Network.CryptoCode;
2019-10-12 13:35:30 +02:00
walletVm.IsOwner = wallet.Store.Role == StoreRoles.Owner;
if (!walletVm.IsOwner)
2018-07-26 15:32:24 +02:00
{
walletVm.Balance = "";
}
walletVm.CryptoCode = wallet.Network.CryptoCode;
walletVm.StoreId = wallet.Store.Id;
walletVm.Id = new WalletId(wallet.Store.Id, wallet.Network.CryptoCode);
walletVm.StoreName = wallet.Store.StoreName;
}
return View(wallets);
}
[HttpGet]
[Route("{walletId}")]
[Route("{walletId}/transactions")]
public async Task<IActionResult> WalletTransactions(
2018-07-26 15:32:24 +02:00
[ModelBinder(typeof(WalletIdModelBinder))]
2019-08-03 16:10:45 +02:00
WalletId walletId, string labelFilter = null)
2018-07-26 15:32:24 +02:00
{
2019-10-12 13:35:30 +02:00
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);
if (paymentMethod == null)
2018-07-26 15:32:24 +02:00
return NotFound();
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
var walletBlobAsync = WalletRepository.GetWalletInfo(walletId);
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
var transactions = await wallet.FetchTransactions(paymentMethod.AccountDerivation);
var walletBlob = await walletBlobAsync;
var walletTransactionsInfo = await walletTransactionsInfoAsync;
var model = new ListTransactionsViewModel();
2019-12-29 17:08:30 +01:00
if (transactions == null)
{
2019-12-29 17:08:30 +01:00
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message =
"There was an error retrieving the transactions list. Is NBXplorer configured correctly?"
});
model.Transactions = new List<ListTransactionsViewModel.TransactionViewModel>();
}
else
{
foreach (var tx in transactions.UnconfirmedTransactions.Transactions
.Concat(transactions.ConfirmedTransactions.Transactions).ToArray())
{
2019-12-29 17:08:30 +01:00
var vm = new ListTransactionsViewModel.TransactionViewModel();
vm.Id = tx.TransactionId.ToString();
vm.Link = string.Format(CultureInfo.InvariantCulture, paymentMethod.Network.BlockExplorerLink,
vm.Id);
vm.Timestamp = tx.Timestamp;
vm.Positive = tx.BalanceChange.GetValue(wallet.Network) >= 0;
vm.Balance = tx.BalanceChange.ShowMoney(wallet.Network);
2019-12-29 17:08:30 +01:00
vm.IsConfirmed = tx.Confirmations != 0;
if (walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo))
{
var labels = _labelFactory.GetLabels(walletBlob, transactionInfo, Request);
2019-12-29 17:08:30 +01:00
vm.Labels.AddRange(labels);
model.Labels.AddRange(labels);
vm.Comment = transactionInfo.Comment;
}
if (labelFilter == null ||
vm.Labels.Any(l => l.Value.Equals(labelFilter, StringComparison.OrdinalIgnoreCase)))
model.Transactions.Add(vm);
}
2019-08-03 16:10:45 +02:00
2019-12-29 17:08:30 +01:00
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).ToList();
}
2019-12-29 17:08:30 +01:00
return View(model);
}
2018-07-26 15:32:24 +02:00
private static string GetLabelTarget(WalletId walletId, uint256 txId)
{
return $"{walletId}:{txId}";
}
[HttpGet]
[Route("{walletId}/receive")]
public IActionResult WalletReceive([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
if (walletId?.StoreId == null)
return NotFound();
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);
if (paymentMethod == null)
return NotFound();
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
if (network == null)
return NotFound();
var address = _WalletReceiveStateService.Get(walletId)?.Address;
return View(new WalletReceiveViewModel()
{
CryptoCode = walletId.CryptoCode,
Address = address?.ToString(),
CryptoImage = GetImage(paymentMethod.PaymentId, network)
});
}
[HttpPost]
[Route("{walletId}/receive")]
public async Task<IActionResult> WalletReceive([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletReceiveViewModel viewModel, string command)
{
if (walletId?.StoreId == null)
return NotFound();
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
if (network == null)
return NotFound();
var wallet = _walletProvider.GetWallet(network);
switch (command)
{
case "unreserve-current-address":
KeyPathInformation cachedAddress = _WalletReceiveStateService.Get(walletId);
if (cachedAddress == null)
{
break;
}
var address = cachedAddress.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork);
ExplorerClientProvider.GetExplorerClient(network)
.CancelReservation(cachedAddress.DerivationStrategy, new[] { cachedAddress.KeyPath });
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
AllowDismiss = true,
Message = $"Address {address} was unreserved.",
Severity = StatusMessageModel.StatusSeverity.Success,
});
_WalletReceiveStateService.Remove(walletId);
break;
case "generate-new-address":
var reserve = (await wallet.ReserveAddressAsync(paymentMethod.AccountDerivation));
_WalletReceiveStateService.Set(walletId, reserve);
break;
}
return RedirectToAction(nameof(WalletReceive), new { walletId });
}
2020-01-21 09:33:12 +01:00
private async Task<bool> CanUseHotWallet()
{
2020-03-20 05:44:02 +01:00
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded;
2020-01-21 09:33:12 +01:00
if (isAdmin)
return true;
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
return policies?.AllowHotWalletForAll is true;
}
[HttpGet]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string defaultDestination = null, string defaultAmount = null, string bip21 = null)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
2019-10-12 13:35:30 +02:00
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);
2018-07-26 15:32:24 +02:00
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
2019-12-29 17:08:30 +01:00
if (network == null || network.ReadonlyWallet)
return NotFound();
2018-07-26 16:23:28 +02:00
var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);
rateRules.Spread = 0.0m;
2018-07-26 18:17:43 +02:00
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
double.TryParse(defaultAmount, out var amount);
var model = new WalletSendModel()
{
Outputs = new List<WalletSendModel.TransactionOutput>()
{
new WalletSendModel.TransactionOutput()
{
Amount = Convert.ToDecimal(amount),
DestinationAddress = defaultDestination
}
},
CryptoCode = walletId.CryptoCode
};
if (!string.IsNullOrEmpty(bip21))
{
LoadFromBIP21(model, bip21, network);
}
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
2020-05-07 22:34:39 +02:00
var recommendedFees =
new[]
{
TimeSpan.FromMinutes(10.0), TimeSpan.FromMinutes(60.0), TimeSpan.FromHours(6.0),
TimeSpan.FromHours(24.0),
}.Select(async time =>
{
try
{
var result = await feeProvider.GetFeeRateAsync(
(int)network.NBitcoinNetwork.Consensus.GetExpectedBlocksFor(time));
return new WalletSendModel.FeeRateOption() { Target = time, FeeRate = result.SatoshiPerByte };
2020-05-07 22:34:39 +02:00
}
catch (Exception)
{
return null;
}
})
.ToArray();
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);
2020-01-21 09:33:12 +01:00
model.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
WellknownMetadataKeys.MasterHDKey));
model.CurrentBalance = await balance;
2020-05-07 22:34:39 +02:00
await Task.WhenAll(recommendedFees);
model.RecommendedSatoshiPerByte =
recommendedFees.Select(tuple => tuple.Result).Where(option => option != null).ToList();
2020-05-07 22:34:39 +02:00
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte.LastOrDefault()?.FeeRate;
model.SupportRBF = network.SupportRBF;
2018-07-26 16:23:28 +02:00
using (CancellationTokenSource cts = new CancellationTokenSource())
{
try
{
cts.CancelAfter(TimeSpan.FromSeconds(5));
var result = await RateFetcher.FetchRate(currencyPair, rateRules, cts.Token).WithCancellation(cts.Token);
if (result.BidAsk != null)
2018-07-26 16:23:28 +02:00
{
model.Rate = result.BidAsk.Center;
2018-07-26 16:23:28 +02:00
model.Divisibility = _currencyTable.GetNumberFormatInfo(currencyPair.Right, true).CurrencyDecimalDigits;
model.Fiat = currencyPair.Right;
}
else
{
model.RateError = $"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
}
2018-07-26 16:23:28 +02:00
}
catch (Exception ex) { model.RateError = ex.Message; }
2018-07-26 16:23:28 +02:00
}
2018-07-26 15:32:24 +02:00
return View(model);
}
2018-07-26 15:32:24 +02:00
[HttpPost]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default, string bip21 = "")
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
if (store == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
2019-12-29 17:08:30 +01:00
if (network == null || network.ReadonlyWallet)
return NotFound();
vm.SupportRBF = network.SupportRBF;
2020-01-06 13:57:32 +01:00
vm.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
WellknownMetadataKeys.MasterHDKey, cancellation));
if (!string.IsNullOrEmpty(bip21))
{
LoadFromBIP21(vm, bip21, network);
}
decimal transactionAmountSum = 0;
if (command == "toggle-input-selection")
{
vm.InputSelection = !vm.InputSelection;
}
if (vm.InputSelection)
{
var schemeSettings = GetDerivationSchemeSettings(walletId);
var walletBlobAsync = await WalletRepository.GetWalletInfo(walletId);
var walletTransactionsInfoAsync = await WalletRepository.GetWalletTransactionsInfo(walletId);
var utxos = await _walletProvider.GetWallet(network).GetUnspentCoins(schemeSettings.AccountDerivation, cancellation);
vm.InputsAvailable = utxos.Select(coin =>
{
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
return new WalletSendModel.InputSelectionOption()
{
Outpoint = coin.OutPoint.ToString(),
Amount = coin.Value.GetValue(network),
Comment = info?.Comment,
Labels = info == null ? null : _labelFactory.GetLabels(walletBlobAsync, info, Request),
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, coin.OutPoint.Hash.ToString())
};
}).ToArray();
}
if (command == "toggle-input-selection")
{
ModelState.Clear();
return View(vm);
}
if (!string.IsNullOrEmpty(bip21))
{
return View(vm);
}
if (command == "add-output")
{
ModelState.Clear();
vm.Outputs.Add(new WalletSendModel.TransactionOutput());
return View(vm);
}
if (command.StartsWith("remove-output", StringComparison.InvariantCultureIgnoreCase))
{
ModelState.Clear();
var index = int.Parse(command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), CultureInfo.InvariantCulture);
vm.Outputs.RemoveAt(index);
return View(vm);
}
if (!vm.Outputs.Any())
{
ModelState.AddModelError(string.Empty,
"Please add at least one transaction output");
return View(vm);
}
var subtractFeesOutputsCount = new List<int>();
2019-05-21 12:04:39 +02:00
var substractFees = vm.Outputs.Any(o => o.SubtractFeesFromOutput);
for (var i = 0; i < vm.Outputs.Count; i++)
{
var transactionOutput = vm.Outputs[i];
if (transactionOutput.SubtractFeesFromOutput)
{
subtractFeesOutputsCount.Add(i);
}
transactionOutput.DestinationAddress = transactionOutput.DestinationAddress?.Trim() ?? string.Empty;
try
{
BitcoinAddress.Create(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
}
catch
{
var inputName =
string.Format(CultureInfo.InvariantCulture, "Outputs[{0}].", i.ToString(CultureInfo.InvariantCulture)) +
nameof(transactionOutput.DestinationAddress);
ModelState.AddModelError(inputName, "Invalid address");
}
if (transactionOutput.Amount.HasValue)
{
transactionAmountSum += transactionOutput.Amount.Value;
if (vm.CurrentBalance == transactionOutput.Amount.Value &&
!transactionOutput.SubtractFeesFromOutput)
vm.AddModelError(model => model.Outputs[i].SubtractFeesFromOutput,
"You are sending your entire balance to the same destination, you should subtract the fees",
2019-10-03 11:00:07 +02:00
this);
}
}
if (subtractFeesOutputsCount.Count > 1)
{
foreach (var subtractFeesOutput in subtractFeesOutputsCount)
{
vm.AddModelError(model => model.Outputs[subtractFeesOutput].SubtractFeesFromOutput,
2019-10-03 11:00:07 +02:00
"You can only subtract fees from one output", this);
}
}
else if (vm.CurrentBalance == transactionAmountSum && !substractFees)
{
ModelState.AddModelError(string.Empty,
"You are sending your entire balance, you should subtract the fees from an output");
}
if (vm.CurrentBalance < transactionAmountSum)
{
for (var i = 0; i < vm.Outputs.Count; i++)
{
vm.AddModelError(model => model.Outputs[i].Amount,
2019-10-03 11:00:07 +02:00
"You are sending more than what you own", this);
}
}
if (vm.FeeSatoshiPerByte is decimal fee)
{
if (fee < 0)
{
vm.AddModelError(model => model.FeeSatoshiPerByte,
"The fee rate should be above 0", this);
}
if (fee > 5_000m)
{
vm.AddModelError(model => model.FeeSatoshiPerByte,
"The fee rate is absurdly high", this);
}
if (_dashboard.Get(network.CryptoCode).Status?.BitcoinStatus?.MinRelayTxFee?.SatoshiPerByte is decimal minFee)
{
if (vm.FeeSatoshiPerByte < minFee)
vm.AddModelError(model => model.FeeSatoshiPerByte,
$"The fee rate is lower than the minimum relay fee ({vm.FeeSatoshiPerByte} < {minFee})", this);
}
}
if (!ModelState.IsValid)
return View(vm);
2019-10-12 13:35:30 +02:00
DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings(walletId);
2019-05-13 01:55:26 +02:00
CreatePSBTResponse psbt = null;
try
{
psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
}
catch (NBXplorerException ex)
{
ModelState.AddModelError(string.Empty, ex.Error.Message);
2019-05-13 01:55:26 +02:00
return View(vm);
}
catch (NotSupportedException)
{
ModelState.AddModelError(string.Empty, "You need to update your version of NBXplorer");
2019-05-13 01:55:26 +02:00
return View(vm);
}
derivationScheme.RebaseKeyPaths(psbt.PSBT);
var signingContext = new SigningContextModel()
{
PayJoinEndpointUrl = vm.PayJoinEndpointUrl,
EnforceLowR = psbt.Suggestions?.ShouldEnforceLowR,
ChangeAddress = psbt.ChangeAddress?.ToString()
};
var res = await TryHandleSigningCommands(walletId, psbt.PSBT, command, signingContext);
if (res != null)
{
return res;
}
switch (command)
2019-05-08 07:39:37 +02:00
{
case "analyze-psbt":
var name =
$"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt";
return RedirectToWalletPSBT(new WalletPSBTViewModel()
{
PSBT = psbt.PSBT.ToBase64(),
FileName = name
});
default:
return View(vm);
2019-05-08 07:39:37 +02:00
}
2019-05-08 07:39:37 +02:00
}
private void LoadFromBIP21(WalletSendModel vm, string bip21, BTCPayNetwork network)
{
try
{
if (bip21.StartsWith(network.UriScheme, StringComparison.InvariantCultureIgnoreCase))
{
bip21 = $"bitcoin{bip21.Substring(network.UriScheme.Length)}";
}
var uriBuilder = new NBitcoin.Payment.BitcoinUrlBuilder(bip21, network.NBitcoinNetwork);
vm.Outputs = new List<WalletSendModel.TransactionOutput>()
{
new WalletSendModel.TransactionOutput()
{
Amount = uriBuilder.Amount.ToDecimal(MoneyUnit.BTC),
DestinationAddress = uriBuilder.Address.ToString(),
SubtractFeesFromOutput = false
}
};
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Info,
Html =
$"Payment {(string.IsNullOrEmpty(uriBuilder.Label) ? string.Empty : $" to {uriBuilder.Label}")} {(string.IsNullOrEmpty(uriBuilder.Message) ? string.Empty : $" for {uriBuilder.Message}")}"
});
}
2020-04-09 14:07:26 +02:00
uriBuilder.UnknowParameters.TryGetValue(PayjoinClient.BIP21EndpointKey, out var vmPayJoinEndpointUrl);
2020-01-06 13:57:32 +01:00
vm.PayJoinEndpointUrl = vmPayJoinEndpointUrl;
}
2020-03-26 11:59:28 +01:00
catch
{
2020-03-26 11:59:28 +01:00
try
{
2020-03-26 11:59:28 +01:00
vm.Outputs = new List<WalletSendModel.TransactionOutput>()
{
new WalletSendModel.TransactionOutput()
{
DestinationAddress = BitcoinAddress.Create(bip21, network.NBitcoinNetwork).ToString()
}
};
}
catch
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "The provided BIP21 payment URI was malformed"
});
}
}
ModelState.Clear();
}
private IActionResult ViewVault(WalletId walletId, SigningContextModel signingContext)
{
2020-03-26 12:39:19 +01:00
return View(nameof(WalletSendVault), new WalletSendVaultModel()
{
SigningContext = signingContext,
WalletId = walletId.ToString(),
WebsocketPath = this.Url.Action(nameof(VaultController.VaultBridgeConnection), "Vault", new { walletId = walletId.ToString() })
});
}
[HttpPost]
[Route("{walletId}/vault")]
public IActionResult WalletSendVault([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendVaultModel model)
{
return RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel()
{
SigningContext = model.SigningContext
});
}
private IActionResult RedirectToWalletPSBTReady(WalletPSBTReadyViewModel vm)
{
var redirectVm = new PostRedirectViewModel()
{
AspController = "Wallets",
AspAction = nameof(WalletPSBTReady),
Parameters =
{
new KeyValuePair<string, string>("SigningKey", vm.SigningKey),
new KeyValuePair<string, string>("SigningKeyPath", vm.SigningKeyPath)
}
};
AddSigningContext(redirectVm, vm.SigningContext);
return View("PostRedirect", redirectVm);
}
private void AddSigningContext(PostRedirectViewModel redirectVm, SigningContextModel signingContext)
{
if (signingContext is null)
return;
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.PSBT", signingContext.PSBT));
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.OriginalPSBT", signingContext.OriginalPSBT));
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.PayJoinEndpointUrl", signingContext.PayJoinEndpointUrl));
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.EnforceLowR", signingContext.EnforceLowR?.ToString(CultureInfo.InvariantCulture)));
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.ChangeAddress", signingContext.ChangeAddress));
}
private IActionResult RedirectToWalletPSBT(WalletPSBTViewModel vm)
{
var redirectVm = new PostRedirectViewModel()
{
AspController = "Wallets",
AspAction = nameof(WalletPSBT),
Parameters =
{
new KeyValuePair<string, string>("psbt", vm.PSBT),
new KeyValuePair<string, string>("fileName", vm.FileName)
}
};
return View("PostRedirect", redirectVm);
}
[HttpGet("{walletId}/psbt/seed")]
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, SigningContextModel signingContext)
{
return View(nameof(SignWithSeed), new SignWithSeedViewModel()
{
SigningContext = signingContext,
});
}
[HttpPost("{walletId}/psbt/seed")]
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, SignWithSeedViewModel viewModel)
{
if (!ModelState.IsValid)
{
2020-01-21 09:33:12 +01:00
return View("SignWithSeed", viewModel);
}
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
ExtKey extKey = viewModel.GetExtKey(network.NBitcoinNetwork);
if (extKey == null)
{
ModelState.AddModelError(nameof(viewModel.SeedOrKey),
"Seed or Key was not in a valid format. It is either the 12/24 words or starts with xprv");
}
var psbt = PSBT.Parse(viewModel.SigningContext.PSBT, network.NBitcoinNetwork);
if (!psbt.IsReadyToSign())
{
ModelState.AddModelError(nameof(viewModel.SigningContext.PSBT), "PSBT is not ready to be signed");
}
if (!ModelState.IsValid)
{
2020-01-21 09:33:12 +01:00
return View("SignWithSeed", viewModel);
}
ExtKey signingKey = null;
2019-10-12 13:35:30 +02:00
var settings = GetDerivationSchemeSettings(walletId);
var signingKeySettings = settings.GetSigningAccountKeySettings();
if (signingKeySettings.RootFingerprint is null)
signingKeySettings.RootFingerprint = extKey.GetPublicKey().GetHDFingerPrint();
RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath();
if (rootedKeyPath == null)
{
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint and/or account key path of your seed are not set in the wallet settings.");
return View(nameof(SignWithSeed), viewModel);
}
// The user gave the root key, let's try to rebase the PSBT, and derive the account private key
if (rootedKeyPath.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint())
{
psbt.RebaseKeyPaths(signingKeySettings.AccountKey, rootedKeyPath);
signingKey = extKey.Derive(rootedKeyPath.KeyPath);
}
else
{
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint does not match the one set in your wallet settings. Probable cause are: wrong seed, wrong passphrase or wrong fingerprint in your wallet settings.");
return View(viewModel);
}
var changed = PSBTChanged(psbt, () => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath, new SigningOptions()
{
EnforceLowR = !(viewModel.SigningContext?.EnforceLowR is false)
}));
if (!changed)
{
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Impossible to sign the transaction. Probable cause: Incorrect account key path in wallet settings, PSBT already signed.");
return View(viewModel);
}
ModelState.Remove(nameof(viewModel.SigningContext.PSBT));
viewModel.SigningContext.PSBT = psbt.ToBase64();
return RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel()
{
SigningKey = signingKey.GetWif(network.NBitcoinNetwork).ToString(),
SigningKeyPath = rootedKeyPath?.ToString(),
SigningContext = viewModel.SigningContext
});
}
private bool PSBTChanged(PSBT psbt, Action act)
{
var before = psbt.ToBase64();
act();
var after = psbt.ToBase64();
return before != after;
}
private string ValueToString(Money v, BTCPayNetworkBase network)
{
return v.ToString() + " " + network.CryptoCode;
}
2019-05-11 13:26:31 +02:00
2019-10-12 13:35:30 +02:00
private IActionResult RedirectToWalletTransaction(WalletId walletId, Transaction transaction)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
if (transaction != null)
{
var wallet = _walletProvider.GetWallet(network);
2019-10-12 13:35:30 +02:00
var derivationSettings = GetDerivationSchemeSettings(walletId);
wallet.InvalidateCache(derivationSettings.AccountDerivation);
}
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
}
2018-10-26 16:07:39 +02:00
[HttpGet]
[Route("{walletId}/rescan")]
public async Task<IActionResult> WalletRescan(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
if (walletId?.StoreId == null)
return NotFound();
2019-10-12 13:35:30 +02:00
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);
2018-10-26 16:07:39 +02:00
if (paymentMethod == null)
return NotFound();
var vm = new RescanWalletModel();
vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
vm.IsServerAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded;
2018-10-26 16:07:39 +02:00
vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.AccountDerivation);
if (scanProgress != null)
2018-10-26 16:07:39 +02:00
{
vm.PreviousError = scanProgress.Error;
if (scanProgress.Status == ScanUTXOStatus.Queued || scanProgress.Status == ScanUTXOStatus.Pending)
{
if (scanProgress.Progress == null)
{
vm.Progress = 0;
}
else
{
vm.Progress = scanProgress.Progress.OverallProgress;
vm.RemainingTime = TimeSpan.FromSeconds(scanProgress.Progress.RemainingSeconds).PrettyPrint();
}
}
if (scanProgress.Status == ScanUTXOStatus.Complete)
{
vm.LastSuccess = scanProgress.Progress;
vm.TimeOfScan = (scanProgress.Progress.CompletedAt.Value - scanProgress.Progress.StartedAt).PrettyPrint();
}
}
return View(vm);
}
[HttpPost]
[Route("{walletId}/rescan")]
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2018-10-26 16:07:39 +02:00
public async Task<IActionResult> WalletRescan(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, RescanWalletModel vm)
{
if (walletId?.StoreId == null)
return NotFound();
2019-10-12 13:35:30 +02:00
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);
2018-10-26 16:07:39 +02:00
if (paymentMethod == null)
return NotFound();
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
try
{
await explorer.ScanUTXOSetAsync(paymentMethod.AccountDerivation, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
2018-10-26 16:07:39 +02:00
}
catch (NBXplorerException ex) when (ex.Error.Code == "scanutxoset-in-progress")
{
}
return RedirectToAction();
}
2018-07-26 18:17:43 +02:00
private string GetCurrencyCode(string defaultLang)
{
if (defaultLang == null)
return null;
try
{
var ri = new RegionInfo(defaultLang);
return ri.ISOCurrencySymbol;
}
catch (ArgumentException) { }
2018-07-26 18:17:43 +02:00
return null;
}
2019-10-12 13:35:30 +02:00
public StoreData CurrentStore
{
2019-10-12 13:35:30 +02:00
get
{
return HttpContext.GetStoreData();
}
}
internal DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId)
2019-10-12 13:35:30 +02:00
{
var paymentMethod = CurrentStore
.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
return paymentMethod;
}
2018-07-26 15:32:24 +02:00
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
try
{
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ShowMoney(wallet.Network
.Divisibility);
2018-07-26 15:32:24 +02:00
}
catch
{
return "--";
}
}
}
private string GetUserId()
{
return _userManager.GetUserId(User);
}
[Route("{walletId}/settings")]
public async Task<IActionResult> WalletSettings(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
2019-10-12 13:35:30 +02:00
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
2019-12-29 17:08:30 +01:00
if (derivationSchemeSettings == null || derivationSchemeSettings.Network.ReadonlyWallet)
return NotFound();
var store = (await Repository.FindStore(walletId.StoreId, GetUserId()));
var vm = new WalletSettingsViewModel()
{
StoreName = store.StoreName,
UriScheme = derivationSchemeSettings.Network.UriScheme,
Label = derivationSchemeSettings.Label,
DerivationScheme = derivationSchemeSettings.AccountDerivation.ToString(),
DerivationSchemeInput = derivationSchemeSettings.AccountOriginal,
2020-04-29 08:28:13 +02:00
SelectedSigningKey = derivationSchemeSettings.SigningKey.ToString(),
NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
WellknownMetadataKeys.MasterHDKey))
};
vm.AccountKeys = derivationSchemeSettings.AccountKeySettings
.Select(e => new WalletSettingsAccountKeyViewModel()
{
AccountKey = e.AccountKey.ToString(),
MasterFingerprint = e.RootFingerprint is HDFingerprint fp ? fp.ToString() : null,
AccountKeyPath = e.AccountKeyPath == null ? "" : $"m/{e.AccountKeyPath}"
}).ToList();
return View(vm);
}
[Route("{walletId}/settings")]
[HttpPost]
public async Task<IActionResult> WalletSettings(
2020-04-29 08:28:13 +02:00
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSettingsViewModel vm, string command = "save",
CancellationToken cancellationToken = default)
{
if (!ModelState.IsValid)
return View(vm);
2019-10-12 13:35:30 +02:00
var derivationScheme = GetDerivationSchemeSettings(walletId);
2019-12-29 17:08:30 +01:00
if (derivationScheme == null || derivationScheme.Network.ReadonlyWallet)
return NotFound();
2019-11-17 09:13:09 +01:00
if (command == "save")
{
2019-11-17 09:13:09 +01:00
derivationScheme.Label = vm.Label;
2020-04-29 08:28:13 +02:00
derivationScheme.SigningKey = string.IsNullOrEmpty(vm.SelectedSigningKey)
? null
: new BitcoinExtPubKey(vm.SelectedSigningKey, derivationScheme.Network.NBitcoinNetwork);
2019-11-17 09:13:09 +01:00
for (int i = 0; i < derivationScheme.AccountKeySettings.Length; i++)
{
2020-04-29 08:28:13 +02:00
derivationScheme.AccountKeySettings[i].AccountKeyPath =
string.IsNullOrWhiteSpace(vm.AccountKeys[i].AccountKeyPath)
? null
: new KeyPath(vm.AccountKeys[i].AccountKeyPath);
derivationScheme.AccountKeySettings[i].RootFingerprint =
string.IsNullOrWhiteSpace(vm.AccountKeys[i].MasterFingerprint)
? (HDFingerprint?)null
: new HDFingerprint(Encoders.Hex.DecodeData(vm.AccountKeys[i].MasterFingerprint));
2019-11-17 09:13:09 +01:00
}
2020-04-29 08:28:13 +02:00
2019-11-17 09:13:09 +01:00
var store = (await Repository.FindStore(walletId.StoreId, GetUserId()));
store.SetSupportedPaymentMethod(derivationScheme);
await Repository.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = "Wallet settings updated";
return RedirectToAction(nameof(WalletSettings));
}
else if (command == "prune")
{
2020-04-29 08:28:13 +02:00
var result = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
.PruneAsync(derivationScheme.AccountDerivation, new PruneRequest(), cancellationToken);
2019-11-17 09:13:09 +01:00
if (result.TotalPruned == 0)
{
TempData[WellKnownTempData.SuccessMessage] = $"The wallet is already pruned";
}
else
{
2020-04-29 08:28:13 +02:00
TempData[WellKnownTempData.SuccessMessage] =
$"The wallet has been successfully pruned ({result.TotalPruned} transactions have been removed from the history)";
2019-11-17 09:13:09 +01:00
}
2020-04-29 08:28:13 +02:00
return RedirectToAction(nameof(WalletSettings));
}
else if (command == "view-seed" && await CanUseHotWallet())
{
var seed = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
2020-04-29 08:28:13 +02:00
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
WellknownMetadataKeys.Mnemonic, cancellationToken);
if (string.IsNullOrEmpty(seed))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "The seed was not found"
2020-04-29 08:28:13 +02:00
});
}
else
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"Please store your seed securely! <br/><code class=\"alert-link\">{seed}</code>"
});
}
2019-11-17 09:13:09 +01:00
return RedirectToAction(nameof(WalletSettings));
}
else
{
return NotFound();
}
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
? Url.Content(network.CryptoImagePath)
: Url.Content(network.LightningImagePath);
return "/" + res;
}
}
public class WalletReceiveViewModel
{
public string CryptoImage { get; set; }
public string CryptoCode { get; set; }
public string Address { get; set; }
2018-07-26 15:32:24 +02:00
}
public class GetInfoResult
{
}
public class SendToAddressResult
{
[JsonProperty("psbt")]
public string PSBT { get; set; }
2018-07-26 15:32:24 +02:00
}
}