Generate a wallet object for all scripts, save source in generatedBy rather than receive label (#4413)

This commit is contained in:
Nicolas Dorier 2022-12-08 13:16:18 +09:00 committed by GitHub
parent f5c5178f95
commit 9a4dec57d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 178 additions and 78 deletions

View file

@ -2,8 +2,11 @@ using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using OpenQA.Selenium;
@ -15,6 +18,10 @@ namespace BTCPayServer.Tests
{
public static class Extensions
{
public static Task<KeyPathInformation> ReserveAddressAsync(this BTCPayWallet wallet, DerivationStrategyBase derivationStrategyBase)
{
return wallet.ReserveAddressAsync(null, derivationStrategyBase, "test");
}
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
public static string ToJson(this object o) => JsonConvert.SerializeObject(o, Formatting.None, JsonSettings);

View file

@ -1935,6 +1935,9 @@ namespace BTCPayServer.Tests
RedirectURL = "http://toto.com/lol"
}
});
var invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", newInvoice.Id), false);
Assert.Contains(invoiceObject.Links.Select(l => l.Type), t => t == "script");
Assert.EndsWith($"/i/{newInvoice.Id}", newInvoice.CheckoutLink);
var controller = tester.PayTester.GetController<UIInvoiceController>(user.UserId, user.StoreId);
var model = (PaymentModel)((ViewResult)await controller.Checkout(newInvoice.Id)).Model;
@ -1968,11 +1971,18 @@ namespace BTCPayServer.Tests
Assert.True(store.LazyPaymentMethods);
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = 1, Currency = "USD" });
invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false);
Assert.DoesNotContain(invoiceObject.Links.Select(l => l.Type), t => t == "script");
paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id);
Assert.Single(paymentMethods);
Assert.False(paymentMethods.First().Activated);
await client.ActivateInvoicePaymentMethod(user.StoreId, invoice.Id,
paymentMethods.First().PaymentMethod);
invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false);
Assert.Contains(invoiceObject.Links.Select(l => l.Type), t => t == "script");
paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id);
Assert.Single(paymentMethods);
Assert.True(paymentMethods.First().Activated);
@ -2030,12 +2040,16 @@ namespace BTCPayServer.Tests
BitcoinAddress.Create(pm.Destination, tester.ExplorerClient.Network.NBitcoinNetwork),
new Money(0.0002m, MoneyUnit.BTC));
});
await TestUtils.EventuallyAsync(async () =>
{
var pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
Assert.Single(pm.Payments);
Assert.Equal(-0.0001m, pm.Due);
});
invoiceObject = await client.GetOnChainWalletObject(user.StoreId, "BTC", new OnChainWalletObjectId("invoice", invoice.Id), false);
Assert.Contains(invoiceObject.Links.Select(l => l.Type), t => t == "tx");
}
[Fact(Timeout = 60 * 20 * 1000)]

View file

@ -34,32 +34,28 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly UIInvoiceController _invoiceController;
private readonly InvoiceRepository _invoiceRepository;
private readonly LinkGenerator _linkGenerator;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly EventAggregator _eventAggregator;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
private readonly CurrencyNameTable _currencyNameTable;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly PullPaymentHostedService _pullPaymentService;
private readonly RateFetcher _rateProvider;
private readonly InvoiceActivator _invoiceActivator;
private readonly ApplicationDbContextFactory _dbContextFactory;
public LanguageService LanguageService { get; }
public GreenfieldInvoiceController(UIInvoiceController invoiceController, InvoiceRepository invoiceRepository,
LinkGenerator linkGenerator, LanguageService languageService, BTCPayNetworkProvider btcPayNetworkProvider,
EventAggregator eventAggregator, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
CurrencyNameTable currencyNameTable, BTCPayNetworkProvider networkProvider, RateFetcher rateProvider,
CurrencyNameTable currencyNameTable, RateFetcher rateProvider,
InvoiceActivator invoiceActivator,
PullPaymentHostedService pullPaymentService, ApplicationDbContextFactory dbContextFactory)
{
_invoiceController = invoiceController;
_invoiceRepository = invoiceRepository;
_linkGenerator = linkGenerator;
_btcPayNetworkProvider = btcPayNetworkProvider;
_eventAggregator = eventAggregator;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
_currencyNameTable = currencyNameTable;
_networkProvider = networkProvider;
_networkProvider = btcPayNetworkProvider;
_rateProvider = rateProvider;
_invoiceActivator = invoiceActivator;
_pullPaymentService = pullPaymentService;
_dbContextFactory = dbContextFactory;
LanguageService = languageService;
@ -342,8 +338,7 @@ namespace BTCPayServer.Controllers.Greenfield
if (PaymentMethodId.TryParse(paymentMethod, out var paymentMethodId))
{
await _invoiceRepository.ActivateInvoicePaymentMethod(_eventAggregator, _btcPayNetworkProvider,
_paymentMethodHandlerDictionary, store, invoice, paymentMethodId);
await _invoiceActivator.ActivateInvoicePaymentMethod(paymentMethodId, invoice, store);
return Ok();
}
ModelState.AddModelError(nameof(paymentMethod), "Invalid payment method");

View file

@ -711,8 +711,7 @@ namespace BTCPayServer.Controllers
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
if (!paymentMethodDetails.Activated)
{
if (await _InvoiceRepository.ActivateInvoicePaymentMethod(_EventAggregator, _NetworkProvider,
_paymentMethodHandlerDictionary, store, invoice, paymentMethod.GetId()))
if (await _invoiceActivator.ActivateInvoicePaymentMethod(paymentMethod.GetId(), invoice, store))
{
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
}

View file

@ -14,6 +14,7 @@ using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services;
@ -27,6 +28,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using NBitcoin;
using NBitpayClient;
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
using StoreData = BTCPayServer.Data.StoreData;
@ -37,6 +39,7 @@ namespace BTCPayServer.Controllers
public partial class UIInvoiceController : Controller
{
readonly InvoiceRepository _InvoiceRepository;
private readonly WalletRepository _walletRepository;
readonly RateFetcher _RateProvider;
readonly StoreRepository _StoreRepository;
readonly UserManager<ApplicationUser> _UserManager;
@ -49,12 +52,14 @@ namespace BTCPayServer.Controllers
private readonly LanguageService _languageService;
private readonly ExplorerClientProvider _ExplorerClients;
private readonly UIWalletsController _walletsController;
private readonly InvoiceActivator _invoiceActivator;
private readonly LinkGenerator _linkGenerator;
public WebhookSender WebhookNotificationManager { get; }
public UIInvoiceController(
InvoiceRepository invoiceRepository,
WalletRepository walletRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
RateFetcher rateProvider,
@ -69,11 +74,13 @@ namespace BTCPayServer.Controllers
LanguageService languageService,
ExplorerClientProvider explorerClients,
UIWalletsController walletsController,
InvoiceActivator invoiceActivator,
LinkGenerator linkGenerator)
{
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_walletRepository = walletRepository;
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_UserManager = userManager;
_EventAggregator = eventAggregator;
@ -85,6 +92,7 @@ namespace BTCPayServer.Controllers
_languageService = languageService;
this._ExplorerClients = explorerClients;
_walletsController = walletsController;
_invoiceActivator = invoiceActivator;
_linkGenerator = linkGenerator;
}
@ -368,6 +376,30 @@ namespace BTCPayServer.Controllers
using (logs.Measure("Saving invoice"))
{
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, additionalSearchTerms);
foreach (var method in paymentMethods)
{
if (method.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod bp)
{
var walletId = new WalletId(store.Id, method.GetId().CryptoCode);
await _walletRepository.EnsureWalletObject(new WalletObjectId(
walletId,
WalletObjectData.Types.Invoice,
entity.Id
));
if (bp.GetDepositAddress(((BTCPayNetwork)method.Network).NBitcoinNetwork) is BitcoinAddress address)
{
await _walletRepository.EnsureWalletObjectLink(
new WalletObjectId(
walletId,
WalletObjectData.Types.Script,
address.ScriptPubKey.ToHex()),
new WalletObjectId(
walletId,
WalletObjectData.Types.Invoice,
entity.Id));
}
}
}
}
_ = Task.Run(async () =>
{

View file

@ -432,6 +432,7 @@ namespace BTCPayServer.Hosting
services.AddTransient<UIPaymentRequestController>();
// Add application services.
services.AddSingleton<EmailSenderFactory>();
services.AddSingleton<InvoiceActivator>();
//create a simple client which hooks up to the http scope
services.AddScoped<BTCPayServerClient, LocalBTCPayServerClient>();

View file

@ -146,7 +146,7 @@ namespace BTCPayServer.Payments.Bitcoin
? null
: _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync(),
ReserveAddress = _WalletProvider.GetWallet(network)
.ReserveAddressAsync(supportedPaymentMethod.AccountDerivation)
.ReserveAddressAsync(store.Id, supportedPaymentMethod.AccountDerivation, "invoice")
};
}

View file

@ -420,7 +420,7 @@ namespace BTCPayServer.Payments.Bitcoin
btc.GetDepositAddress(wallet.Network.NBitcoinNetwork).ScriptPubKey == paymentData.ScriptPubKey &&
paymentMethod.Calculate().Due > Money.Zero)
{
var address = await wallet.ReserveAddressAsync(strategy);
var address = await wallet.ReserveAddressAsync(invoice.StoreId, strategy, "invoice");
btc.DepositAddress = address.Address.ToString();
btc.KeyPath = address.KeyPath;
await _InvoiceRepository.NewPaymentDetails(invoice.Id, btc, wallet.Network);

View file

@ -0,0 +1,96 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using AngleSharp.Dom;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Invoices;
using NBitcoin;
namespace BTCPayServer.Services
{
public class InvoiceActivator
{
private readonly InvoiceRepository _invoiceRepository;
private readonly EventAggregator _eventAggregator;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
private readonly WalletRepository _walletRepository;
public InvoiceActivator(
InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
BTCPayNetworkProvider btcPayNetworkProvider,
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
WalletRepository walletRepository)
{
_invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
_btcPayNetworkProvider = btcPayNetworkProvider;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
_walletRepository = walletRepository;
}
public async Task<bool> ActivateInvoicePaymentMethod(PaymentMethodId paymentMethodId, InvoiceEntity invoice, StoreData store)
{
if (invoice.GetInvoiceState().Status != InvoiceStatusLegacy.New)
return false;
bool success = false;
var eligibleMethodToActivate = invoice.GetPaymentMethod(paymentMethodId);
if (!eligibleMethodToActivate.GetPaymentMethodDetails().Activated)
{
var payHandler = _paymentMethodHandlerDictionary[paymentMethodId];
var supportPayMethod = invoice.GetSupportedPaymentMethod()
.Single(method => method.PaymentId == paymentMethodId);
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var network = _btcPayNetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
var prepare = payHandler.PreparePayment(supportPayMethod, store, network);
InvoiceLogs logs = new InvoiceLogs();
try
{
var pmis = invoice.GetPaymentMethods().Select(method => method.GetId()).ToHashSet();
logs.Write($"{paymentMethodId}: Activating", InvoiceEventData.EventSeverity.Info);
var newDetails = await
payHandler.CreatePaymentMethodDetails(logs, supportPayMethod, paymentMethod, store, network,
prepare, pmis);
eligibleMethodToActivate.SetPaymentMethodDetails(newDetails);
await _invoiceRepository.UpdateInvoicePaymentMethod(invoice.Id, eligibleMethodToActivate);
if (newDetails is BitcoinLikeOnChainPaymentMethod bp)
{
var walletId = new WalletId(store.Id, paymentMethodId.CryptoCode);
if (bp.GetDepositAddress(((BTCPayNetwork)_btcPayNetworkProvider.GetNetwork(paymentMethodId.CryptoCode)).NBitcoinNetwork) is BitcoinAddress address)
{
await _walletRepository.EnsureWalletObjectLink(
new WalletObjectId(
walletId,
WalletObjectData.Types.Script,
address.ScriptPubKey.ToHex()),
new WalletObjectId(
walletId,
WalletObjectData.Types.Invoice,
invoice.Id));
}
}
_eventAggregator.Publish(new InvoicePaymentMethodActivated(paymentMethodId, invoice));
_eventAggregator.Publish(new InvoiceNeedUpdateEvent(invoice.Id));
success = true;
}
catch (PaymentMethodUnavailableException ex)
{
logs.Write($"{paymentMethodId}: Payment method unavailable ({ex.Message})", InvoiceEventData.EventSeverity.Error);
}
catch (Exception ex)
{
logs.Write($"{paymentMethodId}: Unexpected exception ({ex})", InvoiceEventData.EventSeverity.Error);
}
await _invoiceRepository.AddInvoiceLogs(invoice.Id, logs);
}
return success;
}
}
}

View file

@ -1,58 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
namespace BTCPayServer.Services.Invoices
{
public static class InvoiceExtensions
{
public static async Task<bool> ActivateInvoicePaymentMethod(this InvoiceRepository invoiceRepository,
EventAggregator eventAggregator, BTCPayNetworkProvider btcPayNetworkProvider, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
StoreData store, InvoiceEntity invoice, PaymentMethodId paymentMethodId)
{
if (invoice.GetInvoiceState().Status != InvoiceStatusLegacy.New)
return false;
bool success = false;
var eligibleMethodToActivate = invoice.GetPaymentMethod(paymentMethodId);
if (!eligibleMethodToActivate.GetPaymentMethodDetails().Activated)
{
var payHandler = paymentMethodHandlerDictionary[paymentMethodId];
var supportPayMethod = invoice.GetSupportedPaymentMethod()
.Single(method => method.PaymentId == paymentMethodId);
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var network = btcPayNetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
var prepare = payHandler.PreparePayment(supportPayMethod, store, network);
InvoiceLogs logs = new InvoiceLogs();
try
{
var pmis = invoice.GetPaymentMethods().Select(method => method.GetId()).ToHashSet();
logs.Write($"{paymentMethodId}: Activating", InvoiceEventData.EventSeverity.Info);
var newDetails = await
payHandler.CreatePaymentMethodDetails(logs, supportPayMethod, paymentMethod, store, network,
prepare, pmis);
eligibleMethodToActivate.SetPaymentMethodDetails(newDetails);
await invoiceRepository.UpdateInvoicePaymentMethod(invoice.Id, eligibleMethodToActivate);
eventAggregator.Publish(new InvoicePaymentMethodActivated(paymentMethodId, invoice));
eventAggregator.Publish(new InvoiceNeedUpdateEvent(invoice.Id));
success = true;
}
catch (PaymentMethodUnavailableException ex)
{
logs.Write($"{paymentMethodId}: Payment method unavailable ({ex.Message})", InvoiceEventData.EventSeverity.Error);
}
catch (Exception ex)
{
logs.Write($"{paymentMethodId}: Unexpected exception ({ex})", InvoiceEventData.EventSeverity.Error);
}
await invoiceRepository.AddInvoiceLogs(invoice.Id, logs);
}
return success;
}
}
}

View file

@ -14,6 +14,7 @@ using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Wallets
{
@ -40,12 +41,14 @@ namespace BTCPayServer.Services.Wallets
}
public class BTCPayWallet
{
public WalletRepository WalletRepository { get; }
public NBXplorerConnectionFactory NbxplorerConnectionFactory { get; }
public Logs Logs { get; }
private readonly ExplorerClient _Client;
private readonly IMemoryCache _MemoryCache;
public BTCPayWallet(ExplorerClient client, IMemoryCache memoryCache, BTCPayNetwork network,
WalletRepository walletRepository,
ApplicationDbContextFactory dbContextFactory, NBXplorerConnectionFactory nbxplorerConnectionFactory, Logs logs)
{
ArgumentNullException.ThrowIfNull(client);
@ -53,6 +56,7 @@ namespace BTCPayServer.Services.Wallets
Logs = logs;
_Client = client;
_Network = network;
WalletRepository = walletRepository;
_dbContextFactory = dbContextFactory;
NbxplorerConnectionFactory = nbxplorerConnectionFactory;
_MemoryCache = memoryCache;
@ -72,8 +76,10 @@ namespace BTCPayServer.Services.Wallets
public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(5);
public async Task<KeyPathInformation> ReserveAddressAsync(DerivationStrategyBase derivationStrategy)
public async Task<KeyPathInformation> ReserveAddressAsync(string storeId, DerivationStrategyBase derivationStrategy, string generatedBy)
{
if (storeId != null)
ArgumentNullException.ThrowIfNull(generatedBy);
ArgumentNullException.ThrowIfNull(derivationStrategy);
var pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
// Might happen on some broken install
@ -82,6 +88,12 @@ namespace BTCPayServer.Services.Wallets
await _Client.TrackAsync(derivationStrategy).ConfigureAwait(false);
pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
}
if (storeId != null)
{
await WalletRepository.EnsureWalletObject(
new WalletObjectId(new WalletId(storeId, Network.CryptoCode), WalletObjectData.Types.Script, pathInfo.ScriptPubKey.ToHex()),
new JObject() { ["generatedBy"] = generatedBy });
}
return pathInfo;
}

View file

@ -9,6 +9,7 @@ namespace BTCPayServer.Services.Wallets
{
public class BTCPayWalletProvider
{
public WalletRepository WalletRepository { get; }
public Logs Logs { get; }
private readonly ExplorerClientProvider _Client;
@ -19,12 +20,14 @@ namespace BTCPayServer.Services.Wallets
Data.ApplicationDbContextFactory dbContextFactory,
BTCPayNetworkProvider networkProvider,
NBXplorerConnectionFactory nbxplorerConnectionFactory,
WalletRepository walletRepository,
Logs logs)
{
ArgumentNullException.ThrowIfNull(client);
this.Logs = logs;
_Client = client;
_NetworkProvider = networkProvider;
WalletRepository = walletRepository;
_Options = memoryCacheOption;
foreach (var network in networkProvider.GetAll().OfType<BTCPayNetwork>())
@ -32,7 +35,7 @@ namespace BTCPayServer.Services.Wallets
var explorerClient = _Client.GetExplorerClient(network.CryptoCode);
if (explorerClient == null)
continue;
_Wallets.Add(network.CryptoCode.ToUpperInvariant(), new BTCPayWallet(explorerClient, new MemoryCache(_Options), network, dbContextFactory, nbxplorerConnectionFactory, Logs));
_Wallets.Add(network.CryptoCode.ToUpperInvariant(), new BTCPayWallet(explorerClient, new MemoryCache(_Options), network, WalletRepository, dbContextFactory, nbxplorerConnectionFactory, Logs));
}
}

View file

@ -12,6 +12,7 @@ using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Wallets
{
@ -75,9 +76,7 @@ namespace BTCPayServer.Services.Wallets
return null;
}
var reserve = (await wallet.ReserveAddressAsync(derivationScheme.AccountDerivation));
await _walletRepository.AddWalletTransactionAttachment(walletId, reserve.ScriptPubKey.ToHex(), new []{new Attachment("receive")},
WalletObjectData.Types.Script);
var reserve = (await wallet.ReserveAddressAsync(walletId.StoreId, derivationScheme.AccountDerivation, "receive"));
Set(walletId, reserve);
return reserve;
}