new feature: Wallet Receive Page (#1065)

* new feature: Wallet Receive Page

closes #965

* Conserve addresses by waiting till address is spent before generating each run

* fix tests

* Filter by cryptocode before matching outpoint

* fix build

* fix edge case issue

* use address in keypathinfo directly

* rebase fixes

* rebase fixes

* remove duplicate code

* fix messy condition

* fixes

* fix e2e

* fix
This commit is contained in:
Andrew Camilleri
2020-01-18 06:12:27 +01:00
committed by Nicolas Dorier
parent 4ac79a7ea3
commit 025da0261d
15 changed files with 398 additions and 11 deletions

View File

@@ -415,7 +415,7 @@ namespace BTCPayServer.Tests
s.Driver.Quit(); s.Driver.Quit();
} }
} }
[Fact(Timeout = TestTimeout)] [Fact(Timeout = TestTimeout)]
public async Task CanManageWallet() public async Task CanManageWallet()
{ {
@@ -427,16 +427,55 @@ namespace BTCPayServer.Tests
// In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', then try to use the seed // In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', then try to use the seed
// to sign the transaction // to sign the transaction
var mnemonic = s.GenerateWallet("BTC", "", true, false); s.GenerateWallet("BTC", "", true, false);
//let's test quickly the receive wallet page
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletReceive")).Click();
//generate a receiving address
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed);
var receiveAddr = s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value");
//unreserve
s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click();
//generate it again, should be the same one as before as nothign got used in the meantime
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed);
Assert.Equal( receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
//send money to addr and ensure it changed
var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync();
sess.ListenAllTrackedSource();
var nextEvent = sess.NextEventAsync();
s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(receiveAddr, Network.RegTest),
Money.Parse("0.1"));
await nextEvent;
await Task.Delay(200);
s.Driver.Navigate().Refresh();
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual( receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
receiveAddr = s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value");
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
s.GoToStore(storeId.storeId);
s.GenerateWallet("BTC", "", true, false);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletReceive")).Click();
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual( receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
var invoiceId = s.CreateInvoice(storeId.storeId); var invoiceId = s.CreateInvoice(storeId.storeId);
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
var address = invoice.EntityToDTO().Addresses["BTC"]; var address = invoice.EntityToDTO().Addresses["BTC"];
var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest)); var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
Assert.True(result.IsWatchOnly); Assert.True(result.IsWatchOnly);
s.GoToStore(storeId.storeId); s.GoToStore(storeId.storeId);
mnemonic = s.GenerateWallet("BTC", "", true, true); var mnemonic = s.GenerateWallet("BTC", "", true, true);
var root = new Mnemonic(mnemonic).DeriveExtKey(); var root = new Mnemonic(mnemonic).DeriveExtKey();
invoiceId = s.CreateInvoice(storeId.storeId); invoiceId = s.CreateInvoice(storeId.storeId);

View File

@@ -9,6 +9,7 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
@@ -267,6 +268,11 @@ namespace BTCPayServer.Controllers
} }
await _Repo.UpdateStore(store); await _Repo.UpdateStore(store);
_EventAggregator.Publish(new WalletChangedEvent()
{
WalletId = new WalletId(storeId, cryptoCode)
});
if (willBeExcluded != wasExcluded) if (willBeExcluded != wasExcluded)
{ {
var label = willBeExcluded ? "disabled" : "enabled"; var label = willBeExcluded ? "disabled" : "enabled";

View File

@@ -58,6 +58,7 @@ namespace BTCPayServer.Controllers
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
SettingsRepository settingsRepository, SettingsRepository settingsRepository,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
EventAggregator eventAggregator,
CssThemeManager cssThemeManager) CssThemeManager cssThemeManager)
{ {
_RateFactory = rateFactory; _RateFactory = rateFactory;
@@ -74,6 +75,7 @@ namespace BTCPayServer.Controllers
_settingsRepository = settingsRepository; _settingsRepository = settingsRepository;
_authorizationService = authorizationService; _authorizationService = authorizationService;
_CssThemeManager = cssThemeManager; _CssThemeManager = cssThemeManager;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider; _NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider; _ExplorerProvider = explorerProvider;
_FeeRateProvider = feeRateProvider; _FeeRateProvider = feeRateProvider;
@@ -100,6 +102,7 @@ namespace BTCPayServer.Controllers
private readonly SettingsRepository _settingsRepository; private readonly SettingsRepository _settingsRepository;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly CssThemeManager _CssThemeManager; private readonly CssThemeManager _CssThemeManager;
private readonly EventAggregator _EventAggregator;
[TempData] [TempData]
public bool StoreNotConfigured public bool StoreNotConfigured

View File

@@ -8,10 +8,12 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders; using BTCPayServer.ModelBinders;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels; using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Security; using BTCPayServer.Security;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
@@ -23,6 +25,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NBitcoin; using NBitcoin;
using NBitcoin.DataEncoders; using NBitcoin.DataEncoders;
@@ -49,6 +52,8 @@ namespace BTCPayServer.Controllers
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly IFeeProviderFactory _feeRateProvider; private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider; private readonly BTCPayWalletProvider _walletProvider;
private readonly WalletReceiveStateService _WalletReceiveStateService;
private readonly EventAggregator _EventAggregator;
public RateFetcher RateFetcher { get; } public RateFetcher RateFetcher { get; }
CurrencyNameTable _currencyTable; CurrencyNameTable _currencyTable;
@@ -63,7 +68,9 @@ namespace BTCPayServer.Controllers
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
ExplorerClientProvider explorerProvider, ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider, IFeeProviderFactory feeRateProvider,
BTCPayWalletProvider walletProvider) BTCPayWalletProvider walletProvider,
WalletReceiveStateService walletReceiveStateService,
EventAggregator eventAggregator)
{ {
_currencyTable = currencyTable; _currencyTable = currencyTable;
Repository = repo; Repository = repo;
@@ -77,6 +84,8 @@ namespace BTCPayServer.Controllers
ExplorerClientProvider = explorerProvider; ExplorerClientProvider = explorerProvider;
_feeRateProvider = feeRateProvider; _feeRateProvider = feeRateProvider;
_walletProvider = walletProvider; _walletProvider = walletProvider;
_WalletReceiveStateService = walletReceiveStateService;
_EventAggregator = eventAggregator;
} }
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md // Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
@@ -231,6 +240,7 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("{walletId}")] [Route("{walletId}")]
[Route("{walletId}/transactions")]
public async Task<IActionResult> WalletTransactions( public async Task<IActionResult> WalletTransactions(
[ModelBinder(typeof(WalletIdModelBinder))] [ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string labelFilter = null) WalletId walletId, string labelFilter = null)
@@ -294,6 +304,76 @@ namespace BTCPayServer.Controllers
return $"{walletId}:{txId}"; return $"{walletId}:{txId}";
} }
[HttpGet]
[Route("{walletId}/receive")]
public IActionResult WalletReceive([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string statusMessage = null)
{
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;
if (!string.IsNullOrEmpty(statusMessage))
{
TempData[WellKnownTempData.SuccessMessage] = statusMessage;
}
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 statusMessage = string.Empty;
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});
statusMessage = new StatusMessageModel()
{
AllowDismiss =true,
Message = $"Address {address} was unreserved.",
Severity = StatusMessageModel.StatusSeverity.Success,
}.ToString();
_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, statusMessage});
}
[HttpGet] [HttpGet]
[Route("{walletId}/send")] [Route("{walletId}/send")]
public async Task<IActionResult> WalletSend( public async Task<IActionResult> WalletSend(
@@ -957,6 +1037,23 @@ namespace BTCPayServer.Controllers
return NotFound(); 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; }
} }

View File

@@ -0,0 +1,10 @@
using NBXplorer.Models;
namespace BTCPayServer.Events
{
public class NewOnChainTransactionEvent
{
public NewTransactionEvent NewTransactionEvent { get; set; }
public string CryptoCode { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace BTCPayServer.Events
{
public class WalletChangedEvent
{
public WalletId WalletId { get; set; }
}
}

View File

@@ -0,0 +1,51 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Events;
using BTCPayServer.Services.Wallets;
using Microsoft.Extensions.Hosting;
using NBXplorer;
namespace BTCPayServer.HostedServices
{
public class WalletReceiveCacheUpdater : IHostedService
{
private readonly EventAggregator _EventAggregator;
private readonly WalletReceiveStateService _WalletReceiveStateService;
private readonly CompositeDisposable _Leases = new CompositeDisposable();
public WalletReceiveCacheUpdater(EventAggregator eventAggregator,
WalletReceiveStateService walletReceiveStateService)
{
_EventAggregator = eventAggregator;
_WalletReceiveStateService = walletReceiveStateService;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_Leases.Add(_EventAggregator.Subscribe<WalletChangedEvent>(evt =>
_WalletReceiveStateService.Remove(evt.WalletId)));
_Leases.Add(_EventAggregator.Subscribe<NewOnChainTransactionEvent>(evt =>
{
var matching = _WalletReceiveStateService
.GetByDerivation(evt.CryptoCode, evt.NewTransactionEvent.DerivationStrategy).Where(pair =>
evt.NewTransactionEvent.Outputs.Any(output => output.ScriptPubKey == pair.Value.ScriptPubKey));
foreach (var keyValuePair in matching)
{
_WalletReceiveStateService.Remove(keyValuePair.Key);
}
}));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_Leases.Dispose();
return Task.CompletedTask;
}
}
}

View File

@@ -177,6 +177,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<StoreRepository>(); services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<PaymentRequestRepository>(); services.TryAddSingleton<PaymentRequestRepository>();
services.TryAddSingleton<BTCPayWalletProvider>(); services.TryAddSingleton<BTCPayWalletProvider>();
services.TryAddSingleton<WalletReceiveStateService>();
services.TryAddSingleton<CurrencyNameTable>(); services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>()) services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
{ {
@@ -216,6 +217,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, DynamicDnsHostedService>(); services.AddSingleton<IHostedService, DynamicDnsHostedService>();
services.AddSingleton<IHostedService, TorServicesHostedService>(); services.AddSingleton<IHostedService, TorServicesHostedService>();
services.AddSingleton<IHostedService, PaymentRequestStreamer>(); services.AddSingleton<IHostedService, PaymentRequestStreamer>();
services.AddSingleton<IHostedService, WalletReceiveCacheUpdater>();
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>(); services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>(); services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, OpenIdAuthorizationHandler>(); services.AddScoped<IAuthorizationHandler, OpenIdAuthorizationHandler>();

View File

@@ -10,6 +10,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using NBitcoin; using NBitcoin;
using NBXplorer.Models;
namespace BTCPayServer.Payments.Bitcoin namespace BTCPayServer.Payments.Bitcoin
{ {
@@ -35,7 +36,7 @@ namespace BTCPayServer.Payments.Bitcoin
{ {
public Task<FeeRate> GetFeeRate; public Task<FeeRate> GetFeeRate;
public Task<FeeRate> GetNetworkFeeRate; public Task<FeeRate> GetNetworkFeeRate;
public Task<BitcoinAddress> ReserveAddress; public Task<KeyPathInformation> ReserveAddress;
} }
public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse, public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse,
@@ -141,7 +142,7 @@ namespace BTCPayServer.Payments.Bitcoin
onchainMethod.NextNetworkFee = Money.Zero; onchainMethod.NextNetworkFee = Money.Zero;
break; break;
} }
onchainMethod.DepositAddress = (await prepare.ReserveAddress).ToString(); onchainMethod.DepositAddress = (await prepare.ReserveAddress).Address.ToString();
return onchainMethod; return onchainMethod;
} }
} }

View File

@@ -145,6 +145,11 @@ namespace BTCPayServer.Payments.Bitcoin
break; break;
case NBXplorer.Models.NewTransactionEvent evt: case NBXplorer.Models.NewTransactionEvent evt:
wallet.InvalidateCache(evt.DerivationStrategy); wallet.InvalidateCache(evt.DerivationStrategy);
_Aggregator.Publish(new NewOnChainTransactionEvent()
{
CryptoCode = wallet.Network.CryptoCode,
NewTransactionEvent = evt
});
foreach (var output in network.GetValidOutputs(evt)) foreach (var output in network.GetValidOutputs(evt))
{ {
var key = output.Item1.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); var key = output.Item1.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant();
@@ -373,7 +378,7 @@ namespace BTCPayServer.Payments.Bitcoin
paymentMethod.Calculate().Due > Money.Zero) paymentMethod.Calculate().Due > Money.Zero)
{ {
var address = await wallet.ReserveAddressAsync(strategy); var address = await wallet.ReserveAddressAsync(strategy);
btc.DepositAddress = address.ToString(); btc.DepositAddress = address.Address.ToString();
await _InvoiceRepository.NewAddress(invoice.Id, btc, wallet.Network); await _InvoiceRepository.NewAddress(invoice.Id, btc, wallet.Network);
_Aggregator.Publish(new InvoiceNewAddressEvent(invoice.Id, address.ToString(), wallet.Network)); _Aggregator.Publish(new InvoiceNewAddressEvent(invoice.Id, address.ToString(), wallet.Network));
paymentMethod.SetPaymentMethodDetails(btc); paymentMethod.SetPaymentMethodDetails(btc);

View File

@@ -63,7 +63,7 @@ namespace BTCPayServer.Services.Wallets
public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(5); public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(5);
public async Task<BitcoinAddress> ReserveAddressAsync(DerivationStrategyBase derivationStrategy) public async Task<KeyPathInformation> ReserveAddressAsync(DerivationStrategyBase derivationStrategy)
{ {
if (derivationStrategy == null) if (derivationStrategy == null)
throw new ArgumentNullException(nameof(derivationStrategy)); throw new ArgumentNullException(nameof(derivationStrategy));
@@ -74,8 +74,7 @@ namespace BTCPayServer.Services.Wallets
await _Client.TrackAsync(derivationStrategy).ConfigureAwait(false); await _Client.TrackAsync(derivationStrategy).ConfigureAwait(false);
pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false); pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
} }
return pathInfo;
return pathInfo.Address;
} }
public async Task<(BitcoinAddress, KeyPath)> GetChangeAddressAsync(DerivationStrategyBase derivationStrategy) public async Task<(BitcoinAddress, KeyPath)> GetChangeAddressAsync(DerivationStrategyBase derivationStrategy)

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
namespace BTCPayServer.Services.Wallets
{
public class WalletReceiveStateService
{
private readonly ConcurrentDictionary<WalletId, KeyPathInformation> _walletReceiveState =
new ConcurrentDictionary<WalletId, KeyPathInformation>();
public void Remove(WalletId walletId)
{
_walletReceiveState.TryRemove(walletId, out _);
}
public KeyPathInformation Get(WalletId walletId)
{
if (_walletReceiveState.ContainsKey(walletId))
{
return _walletReceiveState[walletId];
}
return null;
}
public void Set(WalletId walletId, KeyPathInformation information)
{
_walletReceiveState.AddOrReplace(walletId, information);
}
public IEnumerable<KeyValuePair<WalletId, KeyPathInformation>> GetByDerivation(string cryptoCode,
DerivationStrategyBase derivationStrategyBase)
{
return _walletReceiveState.Where(pair =>
pair.Key.CryptoCode.Equals(cryptoCode, StringComparison.InvariantCulture) &&
pair.Value.DerivationStrategy == derivationStrategyBase);
}
}
}

View File

@@ -0,0 +1,121 @@
@using Microsoft.AspNetCore.Mvc.ModelBinding
@model BTCPayServer.Controllers.WalletReceiveViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage wallet";
ViewData.SetActivePageAndTitle(WalletsNavPages.Receive);
}
<div class="row no-gutters">
<div class="col-lg-6 mx-auto my-auto ">
<form method="post" asp-action="WalletReceive" class="card text-center">
@if (string.IsNullOrEmpty(Model.Address))
{
<h2 class="card-title">Receive @Model.CryptoCode</h2>
<button class="btn btn-lg btn-primary m-2" type="submit" name="command" value="generate-new-address">Generate @Model.CryptoCode address</button>
}
else
{
<h2 class="card-title">Next available @Model.CryptoCode address</h2>
<noscript>
<div class="card-body m-sm-0 p-sm-0">
<div class="input-group">
<input type="text" class="form-control " readonly="readonly" asp-for="Address" id="address"/>
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
</div>
</div>
<button type="submit" name="command" value="unreserve-current-address" class="btn btn-link">Unreserve this address</button>
</div>
</noscript>
<div class="only-for-js card-body m-sm-0 p-sm-0" id="app">
<div class="qr-container mb-2">
<img v-bind:src="srvModel.cryptoImage" class="qr-icon" />
<qrcode v-bind:value="srvModel.address" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#fff'} }" tag="svg">
</qrcode>
</div>
<div class="input-group copy" data-clipboard-target="#vue-address">
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.address" id="vue-address"/>
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
</div>
</div>
<button type="submit" name="command" value="unreserve-current-address" class="btn btn-link">Unreserve this address</button>
</div>
}
</form>
</div>
</div>
@section HeadScripts
{
<script src="~/bundles/lightning-node-info-bundle.min.js" type="text/javascript"></script>
<script type="text/javascript">
var srvModel = @Safe.Json(Model);
window.onload = function() {
if($("#app").length <1){
return;
}
Vue.use(Toasted);
var app = new Vue({
el: '#app',
components: {
qrcode: VueQrcode
},
data: {
srvModel: srvModel
},
mounted: function() {
this.$nextTick(function() {
var copyInput = new Clipboard('.copy');
copyInput.on("success",
function(e) {
Vue.toasted.show('Copied',
{
iconPack: "fontawesome",
icon: "copy",
duration: 5000
});
});
});
}
});
}
</script>
<style>
.qr-icon {
height: 64px;
width: 64px;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
.qr-container {
position: relative;
text-align: center;
}
.qr-container svg {
width: 256px;
height: 256px;
}
.copy {
cursor: copy;
}
</style>
}

View File

@@ -11,6 +11,7 @@ namespace BTCPayServer.Views.Wallets
Transactions, Transactions,
Rescan, Rescan,
PSBT, PSBT,
Settings Settings,
Receive
} }
} }

View File

@@ -10,6 +10,7 @@
{ {
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletSend">Send</a> <a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletSend">Send</a>
} }
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Receive)" asp-action="WalletReceive" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletReceive">Receive</a>
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Rescan)" asp-action="WalletRescan" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletRescan">Rescan</a> <a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Rescan)" asp-action="WalletRescan" asp-route-walletId="@this.Context.GetRouteValue("walletId")" id="WalletRescan">Rescan</a>
@if (!network.ReadonlyWallet) @if (!network.ReadonlyWallet)
{ {