diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 36c1b7921..8396318cb 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1508,6 +1508,35 @@ namespace BTCPayServer.Tests ); } } + + // [Fact(Timeout = TestTimeout)] + [Fact()] + [Trait("Integration", "Integration")] + public async Task CanSaveKeyPathForOnChainPayments() + { + using var tester = ServerTester.Create(); + await tester.StartAsync(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(); + await user.RegisterDerivationSchemeAsync("BTC"); + + var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.01m, "BTC")); + await tester.WaitForEvent(async () => + { + var tx = await tester.ExplorerNode.SendToAddressAsync( + BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), + Money.Coins(0.01m)); + }); + + + + var payments = Assert.IsType( + Assert.IsType(await user.GetController().Invoice(invoice.Id)).Model) + .Payments; + Assert.Single(payments); + var paymentData = payments.First().GetCryptoPaymentData() as BitcoinLikePaymentData; + Assert.NotNull(paymentData.KeyPath); + } [Fact(Timeout = TestTimeout)] [Trait("Fast", "Fast")] diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 79c09e90d..986e56df0 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -600,7 +600,7 @@ namespace BTCPayServer.Controllers try { leases.Add(_EventAggregator.Subscribe(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId))); - leases.Add(_EventAggregator.Subscribe(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId))); + leases.Add(_EventAggregator.Subscribe(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId))); leases.Add(_EventAggregator.Subscribe(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId))); while (true) { diff --git a/BTCPayServer/Events/InvoiceNewAddressEvent.cs b/BTCPayServer/Events/InvoiceNewAddressEvent.cs deleted file mode 100644 index 48e9161b3..000000000 --- a/BTCPayServer/Events/InvoiceNewAddressEvent.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace BTCPayServer.Events -{ - public class InvoiceNewAddressEvent - { - public InvoiceNewAddressEvent(string invoiceId, string address, BTCPayNetworkBase network) - { - Address = address; - InvoiceId = invoiceId; - Network = network; - } - - public string Address { get; set; } - public string InvoiceId { get; set; } - public BTCPayNetworkBase Network { get; set; } - public override string ToString() - { - return $"{Network.CryptoCode}: New address {Address} for invoice {InvoiceId}"; - } - } -} diff --git a/BTCPayServer/Events/InvoiceNewPaymentDetailsEvent.cs b/BTCPayServer/Events/InvoiceNewPaymentDetailsEvent.cs new file mode 100644 index 000000000..1bef4c50b --- /dev/null +++ b/BTCPayServer/Events/InvoiceNewPaymentDetailsEvent.cs @@ -0,0 +1,24 @@ +using BTCPayServer.Payments; + +namespace BTCPayServer.Events +{ + public class InvoiceNewPaymentDetailsEvent + { + + public InvoiceNewPaymentDetailsEvent(string invoiceId, IPaymentMethodDetails details, PaymentMethodId paymentMethodId) + { + InvoiceId = invoiceId; + Details = details; + PaymentMethodId = paymentMethodId; + } + + public string Address { get; set; } + public string InvoiceId { get; set; } + public IPaymentMethodDetails Details { get; } + public PaymentMethodId PaymentMethodId { get; } + public override string ToString() + { + return $"{PaymentMethodId.ToPrettyString()}: New payment details {Details.GetPaymentDestination()} for invoice {InvoiceId}"; + } + } +} diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs index 8b4b2f271..ee10290ce 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using BTCPayServer.Client.Models; using NBitcoin; using Newtonsoft.Json; @@ -24,9 +25,10 @@ namespace BTCPayServer.Payments.Bitcoin return FeeRate.SatoshiPerByte; } - public void SetPaymentDestination(string newPaymentDestination) + public void SetPaymentDetails(IPaymentMethodDetails newPaymentMethodDetails) { - DepositAddress = newPaymentDestination; + DepositAddress = newPaymentMethodDetails.GetPaymentDestination(); + KeyPath = (newPaymentMethodDetails as BitcoinLikeOnChainPaymentMethod)?.KeyPath; } public NetworkFeeMode NetworkFeeMode { get; set; } @@ -51,7 +53,9 @@ namespace BTCPayServer.Payments.Bitcoin [JsonIgnore] public Money NextNetworkFee { get; set; } [JsonIgnore] - public String DepositAddress { get; set; } + public String DepositAddress { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))] + public KeyPath KeyPath { get; set; } public BitcoinAddress GetDepositAddress(Network network) { diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs index 8557aa99d..0f7a1c7f8 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs @@ -17,13 +17,14 @@ namespace BTCPayServer.Payments.Bitcoin } - public BitcoinLikePaymentData(BitcoinAddress address, IMoney value, OutPoint outpoint, bool rbf) + public BitcoinLikePaymentData(BitcoinAddress address, IMoney value, OutPoint outpoint, bool rbf, KeyPath keyPath) { Address = address; Value = value; Outpoint = outpoint; ConfirmationCount = 0; RBF = rbf; + KeyPath = keyPath; } [JsonIgnore] public BTCPayNetworkBase Network { get; set; } @@ -34,6 +35,8 @@ namespace BTCPayServer.Payments.Bitcoin public int ConfirmationCount { get; set; } public bool RBF { get; set; } public BitcoinAddress Address { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))] + public KeyPath KeyPath { get; set; } public IMoney Value { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs index ba1d36711..a7fa5f88a 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs @@ -159,7 +159,9 @@ namespace BTCPayServer.Payments.Bitcoin break; } - onchainMethod.DepositAddress = (await prepare.ReserveAddress).Address.ToString(); + var reserved = await prepare.ReserveAddress; + onchainMethod.DepositAddress = reserved.Address.ToString(); + onchainMethod.KeyPath = reserved.KeyPath; onchainMethod.PayjoinEnabled = blob.PayJoinEnabled && PayjoinClient.SupportedFormats.Contains(supportedPaymentMethod .AccountDerivation.ScriptPubKeyType()) && diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index 0f61f54f5..c192683e0 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -157,7 +157,7 @@ namespace BTCPayServer.Payments.Bitcoin var paymentData = new BitcoinLikePaymentData(address, output.matchedOutput.Value, output.outPoint, - evt.TransactionData.Transaction.RBF); + evt.TransactionData.Transaction.RBF, output.Item1.KeyPath); var alreadyExist = invoice.GetAllBitcoinPaymentData().Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any(); if (!alreadyExist) @@ -363,7 +363,7 @@ namespace BTCPayServer.Payments.Bitcoin var address = network.NBXplorerNetwork.CreateAddress(strategy, coin.KeyPath, coin.ScriptPubKey); var paymentData = new BitcoinLikePaymentData(address, coin.Value, coin.OutPoint, - transaction.Transaction.RBF); + transaction.Transaction.RBF, coin.KeyPath); var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network).ConfigureAwait(false); alreadyAccounted.Add(coin.OutPoint); @@ -400,8 +400,9 @@ namespace BTCPayServer.Payments.Bitcoin { var address = await wallet.ReserveAddressAsync(strategy); btc.DepositAddress = address.Address.ToString(); - await _InvoiceRepository.NewAddress(invoice.Id, btc, wallet.Network); - _Aggregator.Publish(new InvoiceNewAddressEvent(invoice.Id, btc.DepositAddress, wallet.Network)); + btc.KeyPath = address.KeyPath; + await _InvoiceRepository.NewPaymentDetails(invoice.Id, btc, wallet.Network); + _Aggregator.Publish(new InvoiceNewPaymentDetailsEvent(invoice.Id, btc, paymentMethod.GetId())); paymentMethod.SetPaymentMethodDetails(btc); invoice.SetPaymentMethod(paymentMethod); } diff --git a/BTCPayServer/Payments/IPaymentMethodDetails.cs b/BTCPayServer/Payments/IPaymentMethodDetails.cs index 80d8a24b0..6b8d4974f 100644 --- a/BTCPayServer/Payments/IPaymentMethodDetails.cs +++ b/BTCPayServer/Payments/IPaymentMethodDetails.cs @@ -21,10 +21,5 @@ namespace BTCPayServer.Payments /// /// decimal GetFeeRate(); - /// - /// Change the payment destination (internal plumbing) - /// - /// - void SetPaymentDestination(string newPaymentDestination); } } diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs index 793e98ec6..da19050d8 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs @@ -26,9 +26,9 @@ namespace BTCPayServer.Payments.Lightning return 0.0m; } - public void SetPaymentDestination(string newPaymentDestination) + public void SetPaymentDetails(IPaymentMethodDetails newPaymentMethodDetails) { - BOLT11 = newPaymentDestination; + BOLT11 = newPaymentMethodDetails.GetPaymentDestination(); } } } diff --git a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs index 0560e5aa6..f35d90d45 100644 --- a/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs +++ b/BTCPayServer/Payments/PayJoin/PayJoinEndpointController.cs @@ -238,6 +238,7 @@ namespace BTCPayServer.Payments.PayJoin Dictionary selectedUTXOs = new Dictionary(); PSBTOutput originalPaymentOutput = null; BitcoinAddress paymentAddress = null; + KeyPath paymentAddressIndex = null; InvoiceEntity invoice = null; DerivationSchemeSettings derivationSchemeSettings = null; foreach (var output in psbt.Outputs) @@ -300,6 +301,7 @@ namespace BTCPayServer.Payments.PayJoin ctx.LockedUTXOs = selectedUTXOs.Select(u => u.Key).ToArray(); originalPaymentOutput = output; paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork); + paymentAddressIndex = paymentDetails.KeyPath; break; } @@ -440,7 +442,7 @@ namespace BTCPayServer.Payments.PayJoin var originalPaymentData = new BitcoinLikePaymentData(paymentAddress, originalPaymentOutput.Value, new OutPoint(ctx.OriginalTransaction.GetHash(), originalPaymentOutput.Index), - ctx.OriginalTransaction.RBF); + ctx.OriginalTransaction.RBF, paymentAddressIndex); originalPaymentData.ConfirmationCount = -1; originalPaymentData.PayjoinInformation = new PayjoinInformation() { diff --git a/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumLikeOnChainPaymentMethodDetails.cs b/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumLikeOnChainPaymentMethodDetails.cs index 24b76e382..3a7bb0b2d 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumLikeOnChainPaymentMethodDetails.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumLikeOnChainPaymentMethodDetails.cs @@ -25,6 +25,11 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Payments return 0; } + public void SetPaymentDetails(IPaymentMethodDetails newPaymentMethodDetails) + { + throw new System.NotImplementedException(); + } + public void SetPaymentDestination(string newPaymentDestination) { DepositAddress = newPaymentDestination; diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs index d71173b34..c70ec910b 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroLikeOnChainPaymentMethodDetails.cs @@ -25,10 +25,6 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments return 0.0m; } - public void SetPaymentDestination(string newPaymentDestination) - { - DepositAddress = newPaymentDestination; - } public long AccountIndex { get; set; } public long AddressIndex { get; set; } public string DepositAddress { get; set; } diff --git a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs index ee17ce646..2fa9a5de0 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Services/MoneroListener.cs @@ -135,9 +135,9 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services }); monero.DepositAddress = address.Address; monero.AddressIndex = address.AddressIndex; - await _invoiceRepository.NewAddress(invoice.Id, monero, payment.Network); + await _invoiceRepository.NewPaymentDetails(invoice.Id, monero, payment.Network); _eventAggregator.Publish( - new InvoiceNewAddressEvent(invoice.Id, address.Address, payment.Network)); + new InvoiceNewPaymentDetailsEvent(invoice.Id, monero, payment.GetPaymentMethodId())); paymentMethod.SetPaymentMethodDetails(monero); invoice.SetPaymentMethod(paymentMethod); } diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 8c40e40f7..87f906420 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -245,67 +245,48 @@ retry: return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination(); } - public async Task NewAddress(string invoiceId, IPaymentMethodDetails paymentMethod, BTCPayNetworkBase network) + public async Task NewPaymentDetails(string invoiceId, IPaymentMethodDetails paymentMethodDetails, BTCPayNetworkBase network) { - using (var context = _ContextFactory.CreateContext()) + await using var context = _ContextFactory.CreateContext(); + var invoice = (await context.Invoices.Where(i => i.Id == invoiceId).ToListAsync()).FirstOrDefault(); + if (invoice == null) + return false; + + var invoiceEntity = invoice.GetBlob(_Networks); + var paymentMethod = invoiceEntity.GetPaymentMethod(network, paymentMethodDetails.GetPaymentType()); + if (paymentMethod == null) + return false; + + var existingPaymentMethod = paymentMethod.GetPaymentMethodDetails(); + if (existingPaymentMethod.GetPaymentDestination() != null) { - var invoice = (await context.Invoices.Where(i => i.Id == invoiceId).ToListAsync()).FirstOrDefault(); - if (invoice == null) - return false; - - var invoiceEntity = invoice.GetBlob(_Networks); - var currencyData = invoiceEntity.GetPaymentMethod(network, paymentMethod.GetPaymentType()); - if (currencyData == null) - return false; - - var existingPaymentMethod = currencyData.GetPaymentMethodDetails(); - if (existingPaymentMethod.GetPaymentDestination() != null) - { - MarkUnassigned(invoiceId, invoiceEntity, context, currencyData.GetId()); - } - - existingPaymentMethod.SetPaymentDestination(paymentMethod.GetPaymentDestination()); - currencyData.SetPaymentMethodDetails(existingPaymentMethod); + MarkUnassigned(invoiceId, invoiceEntity, context, paymentMethod.GetId()); + } + paymentMethod.SetPaymentMethodDetails(paymentMethodDetails); #pragma warning disable CS0618 - if (network.IsBTC) - { - invoiceEntity.DepositAddress = currencyData.DepositAddress; - } + if (network.IsBTC) + { + invoiceEntity.DepositAddress = paymentMethod.DepositAddress; + } #pragma warning restore CS0618 - invoiceEntity.SetPaymentMethod(currencyData); - invoice.Blob = ToBytes(invoiceEntity, network); + invoiceEntity.SetPaymentMethod(paymentMethod); + invoice.Blob = ToBytes(invoiceEntity, network); - context.AddressInvoices.Add(new AddressInvoiceData() + await context.AddressInvoices.AddAsync(new AddressInvoiceData() { InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow } - .Set(GetDestination(currencyData), currencyData.GetId())); - context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData() - { - InvoiceDataId = invoiceId, - Assigned = DateTimeOffset.UtcNow - }.SetAddress(paymentMethod.GetPaymentDestination(), network.CryptoCode)); - - await context.SaveChangesAsync(); - AddToTextSearch(invoice.Id, paymentMethod.GetPaymentDestination()); - return true; - } - } - - public async Task UpdateInvoicePaymentMethod(string invoiceId, PaymentMethod paymentMethod) - { - using (var context = _ContextFactory.CreateContext()) + .Set(GetDestination(paymentMethod), paymentMethod.GetId())); + await context.HistoricalAddressInvoices.AddAsync(new HistoricalAddressInvoiceData() { - var invoice = await context.Invoices.FindAsync(invoiceId); - if (invoice == null) - return; - var network = paymentMethod.Network; - var invoiceEntity = invoice.GetBlob(_Networks); - invoiceEntity.SetPaymentMethod(paymentMethod); - invoice.Blob = ToBytes(invoiceEntity, network); - await context.SaveChangesAsync(); - } + InvoiceDataId = invoiceId, + Assigned = DateTimeOffset.UtcNow + }.SetAddress(paymentMethodDetails.GetPaymentDestination(), network.CryptoCode)); + + await context.SaveChangesAsync(); + AddToTextSearch(invoice.Id, paymentMethodDetails.GetPaymentDestination()); + return true; } public async Task AddPendingInvoiceIfNotPresent(string invoiceId) diff --git a/BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml b/BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml index b4230f815..285eceba0 100644 --- a/BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml +++ b/BTCPayServer/Views/Shared/ViewBitcoinLikePaymentData.cshtml @@ -54,6 +54,7 @@ Crypto + Index Deposit address Amount Transaction Id @@ -65,9 +66,10 @@ { @payment.Crypto - @payment.DepositAddress + @(payment.CryptoPaymentData.KeyPath?.ToString()?? "Unknown") + @payment.DepositAddress @payment.CryptoPaymentData.GetValue() @Safe.Raw(payment.AdditionalInformation is string i ? $"
({i})" : string.Empty) - +