mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
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:
committed by
Nicolas Dorier
parent
4ac79a7ea3
commit
025da0261d
@@ -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);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
10
BTCPayServer/Events/NewOnChainTransactionEvent.cs
Normal file
10
BTCPayServer/Events/NewOnChainTransactionEvent.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using NBXplorer.Models;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Events
|
||||||
|
{
|
||||||
|
public class NewOnChainTransactionEvent
|
||||||
|
{
|
||||||
|
public NewTransactionEvent NewTransactionEvent { get; set; }
|
||||||
|
public string CryptoCode { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
7
BTCPayServer/Events/WalletChangedEvent.cs
Normal file
7
BTCPayServer/Events/WalletChangedEvent.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace BTCPayServer.Events
|
||||||
|
{
|
||||||
|
public class WalletChangedEvent
|
||||||
|
{
|
||||||
|
public WalletId WalletId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
51
BTCPayServer/HostedServices/WalletReceiveCacheUpdater.cs
Normal file
51
BTCPayServer/HostedServices/WalletReceiveCacheUpdater.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
44
BTCPayServer/Services/Wallets/WalletReceiveStateService.cs
Normal file
44
BTCPayServer/Services/Wallets/WalletReceiveStateService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
BTCPayServer/Views/Wallets/WalletReceive.cshtml
Normal file
121
BTCPayServer/Views/Wallets/WalletReceive.cshtml
Normal 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>
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ namespace BTCPayServer.Views.Wallets
|
|||||||
Transactions,
|
Transactions,
|
||||||
Rescan,
|
Rescan,
|
||||||
PSBT,
|
PSBT,
|
||||||
Settings
|
Settings,
|
||||||
|
Receive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user