From aa4519ac308a3ffebad5e8582b1e22f0e4aeb570 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Mon, 19 Feb 2018 02:38:03 +0900 Subject: [PATCH] Big refactoring for supporting new type of payment --- BTCPayServer.Tests/UnitTest1.cs | 51 +-- BTCPayServer/BTCPayServer.csproj | 2 +- .../InvoiceController.PaymentProtocol.cs | 10 +- .../Controllers/InvoiceController.UI.cs | 67 ++-- BTCPayServer/Controllers/InvoiceController.cs | 16 +- BTCPayServer/Extensions.cs | 7 + .../InvoiceNotificationManager.cs | 2 +- BTCPayServer/HostedServices/InvoiceWatcher.cs | 17 +- .../HostedServices/NBXplorerListener.cs | 15 +- .../Models/InvoicingModels/PaymentModel.cs | 4 +- BTCPayServer/Payments/BitcoinPayment.cs | 110 ++++++ BTCPayServer/Payments/PaymentTypes.cs | 12 + .../Services/Invoices/CryptoDataDictionary.cs | 74 ++++ .../Services/Invoices/InvoiceEntity.cs | 317 +++++++++++------- .../Services/Invoices/InvoiceRepository.cs | 46 ++- BTCPayServer/Views/Invoice/Checkout.cshtml | 2 +- BTCPayServer/wwwroot/js/core.js | 2 +- 17 files changed, 540 insertions(+), 214 deletions(-) create mode 100644 BTCPayServer/Payments/BitcoinPayment.cs create mode 100644 BTCPayServer/Payments/PaymentTypes.cs create mode 100644 BTCPayServer/Services/Invoices/CryptoDataDictionary.cs diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 1cce41a0f..cccd168e2 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -27,6 +27,7 @@ using System.Collections.Generic; using BTCPayServer.Models.StoreViewModels; using System.Threading.Tasks; using System.Globalization; +using BTCPayServer.Payments; namespace BTCPayServer.Tests { @@ -50,13 +51,13 @@ namespace BTCPayServer.Tests entity.ProductInformation = new ProductInformation() { Price = 5000 }; // Some check that handling legacy stuff does not break things - var cryptoData = entity.GetCryptoData("BTC", null, true); + var cryptoData = entity.GetCryptoData(null, true).TryGet("BTC", PaymentTypes.BTCLike); cryptoData.Calculate(); Assert.NotNull(cryptoData); - Assert.Null(entity.GetCryptoData("BTC", null, false)); + Assert.Null(entity.GetCryptoData(null, false).TryGet("BTC", PaymentTypes.BTCLike)); entity.SetCryptoData(new CryptoData() { ParentEntity = entity, Rate = entity.Rate, CryptoCode = "BTC", TxFee = entity.TxFee }); - Assert.NotNull(entity.GetCryptoData("BTC", null, false)); - Assert.NotNull(entity.GetCryptoData("BTC", null, true)); + Assert.NotNull(entity.GetCryptoData(null, false).TryGet("BTC", PaymentTypes.BTCLike)); + Assert.NotNull(entity.GetCryptoData(null, true).TryGet("BTC", PaymentTypes.BTCLike)); //////////////////// var accounting = cryptoData.Calculate(); @@ -90,30 +91,32 @@ namespace BTCPayServer.Tests entity = new InvoiceEntity(); entity.ProductInformation = new ProductInformation() { Price = 5000 }; - entity.SetCryptoData(new System.Collections.Generic.Dictionary(new KeyValuePair[] { - new KeyValuePair("BTC", new CryptoData() - { - Rate = 1000, - TxFee = Money.Coins(0.1m) - }), - new KeyValuePair("LTC", new CryptoData() - { - Rate = 500, - TxFee = Money.Coins(0.01m) - }) - })); + CryptoDataDictionary cryptoDatas = new CryptoDataDictionary(); + cryptoDatas.Add(new CryptoData() + { + CryptoCode = "BTC", + Rate = 1000, + TxFee = Money.Coins(0.1m) + }); + cryptoDatas.Add(new CryptoData() + { + CryptoCode = "LTC", + Rate = 500, + TxFee = Money.Coins(0.01m) + }); + entity.SetCryptoData(cryptoDatas); entity.Payments = new List(); - cryptoData = entity.GetCryptoData("BTC", null); + cryptoData = entity.GetCryptoData(new CryptoDataId("BTC", PaymentTypes.BTCLike), null); accounting = cryptoData.Calculate(); Assert.Equal(Money.Coins(5.1m), accounting.Due); - cryptoData = entity.GetCryptoData("LTC", null); + cryptoData = entity.GetCryptoData(new CryptoDataId("LTC", PaymentTypes.BTCLike), null); accounting = cryptoData.Calculate(); Assert.Equal(Money.Coins(10.01m), accounting.TotalDue); entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true }); - cryptoData = entity.GetCryptoData("BTC", null); + cryptoData = entity.GetCryptoData(new CryptoDataId("BTC", PaymentTypes.BTCLike), null); accounting = cryptoData.Calculate(); Assert.Equal(Money.Coins(4.2m), accounting.Due); Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); @@ -121,7 +124,7 @@ namespace BTCPayServer.Tests Assert.Equal(Money.Coins(5.2m), accounting.TotalDue); Assert.Equal(2, accounting.TxCount); - cryptoData = entity.GetCryptoData("LTC", null); + cryptoData = entity.GetCryptoData(new CryptoDataId("LTC", PaymentTypes.BTCLike), null); accounting = cryptoData.Calculate(); Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due); Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid); @@ -131,7 +134,7 @@ namespace BTCPayServer.Tests entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true }); - cryptoData = entity.GetCryptoData("BTC", null); + cryptoData = entity.GetCryptoData(new CryptoDataId("BTC", PaymentTypes.BTCLike), null); accounting = cryptoData.Calculate(); Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due); Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); @@ -139,7 +142,7 @@ namespace BTCPayServer.Tests Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added Assert.Equal(2, accounting.TxCount); - cryptoData = entity.GetCryptoData("LTC", null); + cryptoData = entity.GetCryptoData(new CryptoDataId("LTC", PaymentTypes.BTCLike), null); accounting = cryptoData.Calculate(); Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due); Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); @@ -150,7 +153,7 @@ namespace BTCPayServer.Tests var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2); entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true }); - cryptoData = entity.GetCryptoData("BTC", null); + cryptoData = entity.GetCryptoData(new CryptoDataId("BTC", PaymentTypes.BTCLike), null); accounting = cryptoData.Calculate(); Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid); @@ -159,7 +162,7 @@ namespace BTCPayServer.Tests Assert.Equal(accounting.Paid, accounting.TotalDue); Assert.Equal(2, accounting.TxCount); - cryptoData = entity.GetCryptoData("LTC", null); + cryptoData = entity.GetCryptoData(new CryptoDataId("LTC", PaymentTypes.BTCLike), null); accounting = cryptoData.Calculate(); Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 96b2b3ddc..e0b4c23e3 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -26,7 +26,7 @@ - + diff --git a/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs b/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs index f084c9a13..7c80e1c5c 100644 --- a/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs +++ b/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs @@ -9,13 +9,14 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using BTCPayServer.Services.Invoices; namespace BTCPayServer.Controllers { public partial class InvoiceController { [HttpGet] - [Route("i/{invoiceId}")] + [Route("i/{invoiceId}/{cryptoCode?}")] [AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")] public async Task GetInvoiceRequest(string invoiceId, string cryptoCode = null) { @@ -23,11 +24,12 @@ namespace BTCPayServer.Controllers cryptoCode = "BTC"; var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); var network = _NetworkProvider.GetNetwork(cryptoCode); - if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(network)) + var cryptoId = new CryptoDataId(cryptoCode, Payments.PaymentTypes.BTCLike); + if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(cryptoId)) return NotFound(); var dto = invoice.EntityToDTO(_NetworkProvider); - var cryptoData = dto.CryptoInfo.First(c => c.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase)); + var cryptoData = dto.CryptoInfo.First(c => c.GetCryptoDataId() == cryptoId); PaymentRequest request = new PaymentRequest { DetailsVersion = 1 @@ -69,7 +71,7 @@ namespace BTCPayServer.Controllers if (cryptoCode == null) cryptoCode = "BTC"; var network = _NetworkProvider.GetNetwork(cryptoCode); - if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(network)) + if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(new Services.Invoices.CryptoDataId(cryptoCode, Payments.PaymentTypes.BTCLike))) return NotFound(); var wallet = _WalletProvider.GetWallet(network); diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 3a93917e6..f8e953862 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -20,6 +20,7 @@ using System.Net.WebSockets; using System.Threading; using BTCPayServer.Events; using NBXplorer; +using BTCPayServer.Payments; namespace BTCPayServer.Controllers { @@ -65,22 +66,27 @@ namespace BTCPayServer.Controllers foreach (var data in invoice.GetCryptoData(null)) { - var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode.Equals(data.Key, StringComparison.OrdinalIgnoreCase)); - var accounting = data.Value.Calculate(); - var paymentNetwork = _NetworkProvider.GetNetwork(data.Key); + var cryptoInfo = dto.CryptoInfo.First(o => o.GetCryptoDataId() == data.GetId()); + var accounting = data.Calculate(); + var paymentNetwork = _NetworkProvider.GetNetwork(data.GetId().CryptoCode); var cryptoPayment = new InvoiceDetailsModel.CryptoPayment(); cryptoPayment.CryptoCode = paymentNetwork.CryptoCode; cryptoPayment.Due = accounting.Due.ToString() + $" {paymentNetwork.CryptoCode}"; cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentNetwork.CryptoCode}"; - cryptoPayment.Address = data.Value.DepositAddress; - cryptoPayment.Rate = FormatCurrency(data.Value); + + var onchainMethod = data.GetPaymentMethod() as BitcoinLikeOnChainPaymentMethod; + if(onchainMethod != null) + { + cryptoPayment.Address = onchainMethod.DepositAddress.ToString(); + } + cryptoPayment.Rate = FormatCurrency(data); cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21; model.CryptoPayments.Add(cryptoPayment); } var payments = invoice .GetPayments() - .Where(p => p.GetCryptoPaymentDataType() == BitcoinLikePaymentData.OnchainBitcoinType) + .Where(p => p.GetCryptoDataId().PaymentType == PaymentTypes.BTCLike) .Select(async payment => { var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData(); @@ -123,60 +129,65 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("i/{invoiceId}")] - [Route("i/{invoiceId}/{cryptoCode}")] + [Route("i/{invoiceId}/{cryptoDataId}")] [Route("invoice")] [AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)] [XFrameOptionsAttribute(null)] - public async Task Checkout(string invoiceId, string id = null, string cryptoCode = null) + public async Task Checkout(string invoiceId, string id = null, string cryptoDataId = null) { //Keep compatibility with Bitpay invoiceId = invoiceId ?? id; id = invoiceId; //// - var model = await GetInvoiceModel(invoiceId, cryptoCode); + var model = await GetInvoiceModel(invoiceId, cryptoDataId); if (model == null) return NotFound(); return View(nameof(Checkout), model); } - private async Task GetInvoiceModel(string invoiceId, string cryptoCode) + private async Task GetInvoiceModel(string invoiceId, string cryptoDataId) { var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); if (invoice == null) return null; var store = await _StoreRepository.FindStore(invoice.StoreId); bool isDefaultCrypto = false; - if (cryptoCode == null) - { - cryptoCode = store.GetDefaultCrypto(); + if (cryptoDataId == null) + { + cryptoDataId = store.GetDefaultCrypto(); isDefaultCrypto = true; } - var network = _NetworkProvider.GetNetwork(cryptoCode); + + var cryptoId = CryptoDataId.Parse(cryptoDataId); + var network = _NetworkProvider.GetNetwork(cryptoId.CryptoCode); if (invoice == null || network == null) return null; - - if(!invoice.Support(network)) + if (!invoice.Support(cryptoId)) { if(!isDefaultCrypto) return null; - network = invoice.GetCryptoData(_NetworkProvider).First().Value.Network; + var firstCryptoData = invoice.GetCryptoData(_NetworkProvider).First(); + network = firstCryptoData.Network; + cryptoId = firstCryptoData.GetId(); } - var cryptoData = invoice.GetCryptoData(network, _NetworkProvider); + var cryptoData = invoice.GetCryptoData(cryptoId, _NetworkProvider); + var paymentMethod = cryptoData.GetPaymentMethod(); var dto = invoice.EntityToDTO(_NetworkProvider); - var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode == network.CryptoCode); + var cryptoInfo = dto.CryptoInfo.First(o => o.GetCryptoDataId() == cryptoId); var currency = invoice.ProductInformation.Currency; var accounting = cryptoData.Calculate(); var model = new PaymentModel() { CryptoCode = network.CryptoCode, + CryptoDataId = cryptoId.ToString(), ServerUrl = HttpContext.Request.GetAbsoluteRoot(), OrderId = invoice.OrderId, InvoiceId = invoice.Id, - BtcAddress = cryptoData.DepositAddress, + BtcAddress = paymentMethod.GetPaymentDestination(), OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(), BtcDue = accounting.Due.ToString(), CustomerEmail = invoice.RefundMail, @@ -192,14 +203,14 @@ namespace BTCPayServer.Controllers BtcPaid = accounting.Paid.ToString(), Status = invoice.Status, CryptoImage = "/" + Url.Content(network.CryptoImagePath), - NetworkFeeDescription = $"{accounting.TxCount} transaction{(accounting.TxCount > 1 ? "s" : "")} x {cryptoData.TxFee} {network.CryptoCode}", + NetworkFeeDescription = $"{accounting.TxCount} transaction{(accounting.TxCount > 1 ? "s" : "")} x {paymentMethod.GetTxFee()} {network.CryptoCode}", AvailableCryptos = invoice.GetCryptoData(_NetworkProvider) - .Where(i => i.Value.Network != null) + .Where(i => i.Network != null) .Select(kv=> new PaymentModel.AvailableCrypto() { - CryptoCode = kv.Key, - CryptoImage = "/" + kv.Value.Network.CryptoImagePath, - Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, cryptoCode = kv.Key }) + CryptoDataId = kv.GetId().ToString(), + CryptoImage = "/" + kv.Network.CryptoImagePath, + Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, cryptoDataId = kv.GetId().ToString() }) }).Where(c => c.CryptoImage != "/") .ToList() }; @@ -236,10 +247,10 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("i/{invoiceId}/status")] - [Route("i/{invoiceId}/{cryptoCode}/status")] - public async Task GetStatus(string invoiceId, string cryptoCode) + [Route("i/{invoiceId}/{cryptoDataId}/status")] + public async Task GetStatus(string invoiceId, string cryptoDataId = null) { - var model = await GetInvoiceModel(invoiceId, cryptoCode); + var model = await GetInvoiceModel(invoiceId, cryptoDataId); if (model == null) return NotFound(); return Json(model); diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 573f3de9b..24506240f 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -39,6 +39,7 @@ using Microsoft.AspNetCore.Mvc.Routing; using NBXplorer.DerivationStrategy; using NBXplorer; using BTCPayServer.HostedServices; +using BTCPayServer.Payments; namespace BTCPayServer.Controllers { @@ -136,16 +137,17 @@ namespace BTCPayServer.Controllers }); bool legacyBTCisSet = false; - var cryptoDatas = new Dictionary(); + var cryptoDatas = new CryptoDataDictionary(); foreach (var q in queries) { CryptoData cryptoData = new CryptoData(); - cryptoData.CryptoCode = q.network.CryptoCode; - cryptoData.FeeRate = (await q.getFeeRate); - cryptoData.TxFee = GetTxFee(storeBlob, cryptoData.FeeRate); // assume price for 100 bytes + cryptoData.SetId(new CryptoDataId(q.network.CryptoCode, PaymentTypes.BTCLike)); + BitcoinLikeOnChainPaymentMethod onchainMethod = new BitcoinLikeOnChainPaymentMethod(); + onchainMethod.FeeRate = (await q.getFeeRate); + onchainMethod.TxFee = GetTxFee(storeBlob, onchainMethod.FeeRate); // assume price for 100 bytes cryptoData.Rate = await q.getRate; - cryptoData.DepositAddress = (await q.getAddress).ToString(); - + onchainMethod.DepositAddress = (await q.getAddress); + cryptoData.SetPaymentMethod(onchainMethod); #pragma warning disable CS0618 if (q.network.IsBTC) { @@ -155,7 +157,7 @@ namespace BTCPayServer.Controllers entity.DepositAddress = cryptoData.DepositAddress; } #pragma warning restore CS0618 - cryptoDatas.Add(cryptoData.CryptoCode, cryptoData); + cryptoDatas.Add(cryptoData); } if (!legacyBTCisSet) diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 2df1457a4..19d3cbd0b 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -22,11 +22,18 @@ using System.IO; using BTCPayServer.Logging; using Microsoft.Extensions.Logging; using System.Net.WebSockets; +using BTCPayServer.Services.Invoices; +using NBitpayClient; +using BTCPayServer.Payments; namespace BTCPayServer { public static class Extensions { + public static CryptoDataId GetCryptoDataId(this InvoiceCryptoInfo info) + { + return new CryptoDataId(info.CryptoCode, Enum.Parse(info.PaymentType)); + } public static async Task CloseSocket(this WebSocket webSocket) { try diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 83500089b..449e91341 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -201,7 +201,7 @@ namespace BTCPayServer.HostedServices // We keep backward compatibility with bitpay by passing BTC info to the notification // we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked) - var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "BTC"); + var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetCryptoDataId() == new CryptoDataId("BTC", Payments.PaymentTypes.BTCLike)); if (btcCryptoInfo != null) { #pragma warning disable CS0618 diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index 22b03991d..ad7038e5e 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -72,17 +72,16 @@ namespace BTCPayServer.HostedServices var derivationStrategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray(); var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray(); - foreach (BTCPayNetwork network in _NetworkProvider.GetAll()) + var cryptoDataAll = invoice.GetCryptoData(_NetworkProvider); + foreach (var cryptoData in cryptoDataAll.Select(c => c)) { - var cryptoData = invoice.GetCryptoData(network, _NetworkProvider); - if (cryptoData == null) // Altcoin not supported - continue; - var cryptoDataAll = invoice.GetCryptoData(_NetworkProvider); var accounting = cryptoData.Calculate(); - + var network = _NetworkProvider.GetNetwork(cryptoData.GetId().CryptoCode); + if (network == null) + continue; if (invoice.Status == "new" || invoice.Status == "expired") { - var totalPaid = payments.Select(p => p.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); + var totalPaid = payments.Select(p => p.GetValue(cryptoDataAll, cryptoData.GetId())).Sum(); if (totalPaid >= accounting.TotalDue) { if (invoice.Status == "new") @@ -112,7 +111,7 @@ namespace BTCPayServer.HostedServices { var transactions = payments.Where(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network)); - var totalConfirmed = transactions.Select(t => t.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); + var totalConfirmed = transactions.Select(t => t.GetValue(cryptoDataAll, cryptoData.GetId())).Sum(); if (// Is after the monitoring deadline (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) @@ -137,7 +136,7 @@ namespace BTCPayServer.HostedServices if (invoice.Status == "confirmed") { var transactions = payments.Where(p => p.GetCryptoPaymentData().PaymentCompleted(p, network)); - var totalConfirmed = transactions.Select(t => t.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); + var totalConfirmed = transactions.Select(t => t.GetValue(cryptoDataAll, cryptoData.GetId())).Sum(); if (totalConfirmed >= accounting.TotalDue) { context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed")); diff --git a/BTCPayServer/HostedServices/NBXplorerListener.cs b/BTCPayServer/HostedServices/NBXplorerListener.cs index 14e04d3c6..1a68a307c 100644 --- a/BTCPayServer/HostedServices/NBXplorerListener.cs +++ b/BTCPayServer/HostedServices/NBXplorerListener.cs @@ -16,6 +16,7 @@ using BTCPayServer.Services; using BTCPayServer.Services.Wallets; using NBitcoin; using NBXplorer.Models; +using BTCPayServer.Payments; namespace BTCPayServer.HostedServices { @@ -209,7 +210,7 @@ namespace BTCPayServer.HostedServices IEnumerable GetAllBitcoinPaymentData(InvoiceEntity invoice) { return invoice.GetPayments() - .Where(p => p.GetCryptoPaymentDataType() == BitcoinLikePaymentData.OnchainBitcoinType) + .Where(p => p.GetCryptoDataId().PaymentType == PaymentTypes.BTCLike) .Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData()); } @@ -223,7 +224,7 @@ namespace BTCPayServer.HostedServices var conflicts = GetConflicts(transactions.Select(t => t.Value)); foreach (var payment in invoice.GetPayments(wallet.Network)) { - if (payment.GetCryptoPaymentDataType() != BitcoinLikePaymentData.OnchainBitcoinType) + if (payment.GetCryptoDataId().PaymentType != PaymentTypes.BTCLike) continue; var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData(); if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx)) @@ -348,13 +349,15 @@ namespace BTCPayServer.HostedServices { var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData(); var invoice = (await UpdatePaymentStates(wallet, invoiceId)); - var cryptoData = invoice.GetCryptoData(wallet.Network, _ExplorerClients.NetworkProviders); - if (cryptoData.GetDepositAddress().ScriptPubKey == paymentData.Output.ScriptPubKey && cryptoData.Calculate().Due > Money.Zero) + var cryptoData = invoice.GetCryptoData(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders); + var method = cryptoData.GetPaymentMethod() as Payments.BitcoinLikeOnChainPaymentMethod; + if (method.DepositAddress.ScriptPubKey == paymentData.Output.ScriptPubKey && cryptoData.Calculate().Due > Money.Zero) { var address = await wallet.ReserveAddressAsync(strategy); - await _InvoiceRepository.NewAddress(invoiceId, address, wallet.Network); + method.DepositAddress = address; + await _InvoiceRepository.NewAddress(invoiceId, method, wallet.Network); _Aggregator.Publish(new InvoiceNewAddressEvent(invoiceId, address.ToString(), wallet.Network)); - cryptoData.DepositAddress = address.ToString(); + cryptoData.SetPaymentMethod(method); invoice.SetCryptoData(cryptoData); } wallet.InvalidateCache(strategy); diff --git a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs index 04c70d858..253d69fa6 100644 --- a/BTCPayServer/Models/InvoicingModels/PaymentModel.cs +++ b/BTCPayServer/Models/InvoicingModels/PaymentModel.cs @@ -9,7 +9,7 @@ namespace BTCPayServer.Models.InvoicingModels { public class AvailableCrypto { - public string CryptoCode { get; set; } + public string CryptoDataId { get; set; } public string CryptoImage { get; set; } public string Link { get; set; } } @@ -40,5 +40,7 @@ namespace BTCPayServer.Models.InvoicingModels public string CryptoImage { get; set; } public string NetworkFeeDescription { get; internal set; } public int MaxTimeMinutes { get; internal set; } + public string PaymentType { get; internal set; } + public string CryptoDataId { get; internal set; } } } diff --git a/BTCPayServer/Payments/BitcoinPayment.cs b/BTCPayServer/Payments/BitcoinPayment.cs new file mode 100644 index 000000000..8d2bdff42 --- /dev/null +++ b/BTCPayServer/Payments/BitcoinPayment.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Services.Invoices; +using NBitcoin; +using Newtonsoft.Json; + +namespace BTCPayServer.Payments +{ + public class BitcoinLikeOnChainPaymentMethod : IPaymentMethod + { + public PaymentTypes GetPaymentType() + { + return PaymentTypes.BTCLike; + } + + public string GetPaymentDestination() + { + return DepositAddress?.ToString(); + } + + public Money GetTxFee() + { + return TxFee; + } + + public void SetPaymentDestination(string newPaymentDestination) + { + if (newPaymentDestination == null) + DepositAddress = null; + else + DepositAddress = BitcoinAddress.Create(newPaymentDestination, DepositAddress.Network); + } + + [JsonIgnore] + public FeeRate FeeRate { get; set; } + [JsonIgnore] + public Money TxFee { get; set; } + [JsonIgnore] + public BitcoinAddress DepositAddress { get; set; } + } + + public class BitcoinLikePaymentData : CryptoPaymentData + { + public PaymentTypes GetPaymentType() + { + return PaymentTypes.BTCLike; + } + public BitcoinLikePaymentData() + { + + } + public BitcoinLikePaymentData(Coin coin, bool rbf) + { + Outpoint = coin.Outpoint; + Output = coin.TxOut; + ConfirmationCount = 0; + RBF = rbf; + } + [JsonIgnore] + public OutPoint Outpoint { get; set; } + [JsonIgnore] + public TxOut Output { get; set; } + public int ConfirmationCount { get; set; } + public bool RBF { get; set; } + + /// + /// This is set to true if the payment was created before CryptoPaymentData existed in BTCPayServer + /// + public bool Legacy { get; set; } + + public string GetPaymentId() + { + return Outpoint.ToString(); + } + + public string[] GetSearchTerms() + { + return new[] { Outpoint.Hash.ToString() }; + } + + public Money GetValue() + { + return Output.Value; + } + + public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network) + { + return ConfirmationCount >= network.MaxTrackedConfirmation; + } + + public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network) + { + if (speedPolicy == SpeedPolicy.HighSpeed) + { + return ConfirmationCount >= 1 || !RBF; + } + else if (speedPolicy == SpeedPolicy.MediumSpeed) + { + return ConfirmationCount >= 1; + } + else if (speedPolicy == SpeedPolicy.LowSpeed) + { + return ConfirmationCount >= 6; + } + return false; + } + } +} diff --git a/BTCPayServer/Payments/PaymentTypes.cs b/BTCPayServer/Payments/PaymentTypes.cs new file mode 100644 index 000000000..343152fba --- /dev/null +++ b/BTCPayServer/Payments/PaymentTypes.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Payments +{ + public enum PaymentTypes + { + BTCLike + } +} diff --git a/BTCPayServer/Services/Invoices/CryptoDataDictionary.cs b/BTCPayServer/Services/Invoices/CryptoDataDictionary.cs new file mode 100644 index 000000000..2b8e4dd34 --- /dev/null +++ b/BTCPayServer/Services/Invoices/CryptoDataDictionary.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Payments; + +namespace BTCPayServer.Services.Invoices +{ + public class CryptoDataDictionary : IEnumerable + { + Dictionary _Inner = new Dictionary(); + public CryptoDataDictionary() + { + + } + + public CryptoData this[CryptoDataId index] + { + get + { + return _Inner[index]; + } + } + + public void Add(CryptoData cryptoData) + { + _Inner.Add(cryptoData.GetId(), cryptoData); + } + + public void Remove(CryptoData cryptoData) + { + _Inner.Remove(cryptoData.GetId()); + } + public bool TryGetValue(CryptoDataId cryptoDataId, out CryptoData data) + { + if (cryptoDataId == null) + throw new ArgumentNullException(nameof(cryptoDataId)); + return _Inner.TryGetValue(cryptoDataId, out data); + } + + public void AddOrReplace(CryptoData cryptoData) + { + var key = cryptoData.GetId(); + _Inner.Remove(key); + _Inner.Add(key, cryptoData); + } + + public IEnumerator GetEnumerator() + { + return _Inner.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public CryptoData TryGet(CryptoDataId cryptoDataId) + { + if (cryptoDataId == null) + throw new ArgumentNullException(nameof(cryptoDataId)); + _Inner.TryGetValue(cryptoDataId, out var value); + return value; + } + public CryptoData TryGet(string network, PaymentTypes paymentType) + { + if (network == null) + throw new ArgumentNullException(nameof(network)); + var id = new CryptoDataId(network, paymentType); + return TryGet(id); + } + } +} diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 35f993570..ec9a97f21 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -11,6 +11,7 @@ using BTCPayServer.Data; using NBXplorer.Models; using NBXplorer; using NBXplorer.DerivationStrategy; +using BTCPayServer.Payments; namespace BTCPayServer.Services.Invoices { @@ -347,11 +348,12 @@ namespace BTCPayServer.Services.Invoices }; dto.CryptoInfo = new List(); - foreach (var info in this.GetCryptoData(networkProvider, true).Values) + foreach (var info in this.GetCryptoData(networkProvider, true)) { var accounting = info.Calculate(); var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo(); - cryptoInfo.CryptoCode = info.CryptoCode; + cryptoInfo.CryptoCode = info.GetId().CryptoCode; + cryptoInfo.PaymentType = info.GetId().PaymentType.ToString(); cryptoInfo.Rate = info.Rate; cryptoInfo.Price = Money.Coins(ProductInformation.Price / cryptoInfo.Rate).ToString(); @@ -362,7 +364,8 @@ namespace BTCPayServer.Services.Invoices cryptoInfo.TxCount = accounting.TxCount; cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString(); - cryptoInfo.Address = info.DepositAddress; + if (info.GetPaymentMethod() is BitcoinLikeOnChainPaymentMethod onchainMethod) + cryptoInfo.Address = onchainMethod.DepositAddress?.ToString(); cryptoInfo.ExRates = new Dictionary { { ProductInformation.Currency, (double)cryptoInfo.Rate } @@ -413,27 +416,25 @@ namespace BTCPayServer.Services.Invoices JsonConvert.PopulateObject(str, dest); } - internal bool Support(BTCPayNetwork network) + internal bool Support(CryptoDataId cryptoDataId) { var rates = GetCryptoData(null); - return rates.TryGetValue(network.CryptoCode, out var data); + return rates.TryGet(cryptoDataId) != null; } - public CryptoData GetCryptoData(string cryptoCode, BTCPayNetworkProvider networkProvider, bool alwaysIncludeBTC = false) + public CryptoData GetCryptoData(CryptoDataId cryptoDataId, BTCPayNetworkProvider networkProvider) { - GetCryptoData(networkProvider, alwaysIncludeBTC).TryGetValue(cryptoCode, out var data); + GetCryptoData(networkProvider).TryGetValue(cryptoDataId, out var data); return data; } - - public CryptoData GetCryptoData(BTCPayNetwork network, BTCPayNetworkProvider networkProvider) + public CryptoData GetCryptoData(BTCPayNetwork network, PaymentTypes paymentType, BTCPayNetworkProvider networkProvider) { - GetCryptoData(networkProvider).TryGetValue(network.CryptoCode, out var data); - return data; + return GetCryptoData(new CryptoDataId(network.CryptoCode, paymentType), networkProvider); } - public Dictionary GetCryptoData(BTCPayNetworkProvider networkProvider, bool alwaysIncludeBTC = false) + public CryptoDataDictionary GetCryptoData(BTCPayNetworkProvider networkProvider, bool alwaysIncludeBTC = false) { - Dictionary rates = new Dictionary(); + CryptoDataDictionary rates = new CryptoDataDictionary(); var serializer = new Serializer(Dummy); CryptoData phantom = null; #pragma warning disable CS0618 @@ -442,19 +443,21 @@ namespace BTCPayServer.Services.Invoices { var btcNetwork = networkProvider?.GetNetwork("BTC"); phantom = new CryptoData() { ParentEntity = this, IsPhantomBTC = true, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress, Network = btcNetwork }; - rates.Add("BTC", phantom); + rates.Add(phantom); } if (CryptoData != null) { foreach (var prop in CryptoData.Properties()) { if (prop.Name == "BTC" && phantom != null) - rates.Remove("BTC"); + rates.Remove(phantom); var r = serializer.ToObject(prop.Value.ToString()); - r.CryptoCode = prop.Name; + var cryptoDataId = CryptoDataId.Parse(prop.Name); + r.CryptoCode = cryptoDataId.CryptoCode; + r.PaymentType = cryptoDataId.PaymentType.ToString(); r.ParentEntity = this; r.Network = networkProvider?.GetNetwork(r.CryptoCode); - rates.Add(r.CryptoCode, r); + rates.Add(r); } } #pragma warning restore CS0618 @@ -466,21 +469,22 @@ namespace BTCPayServer.Services.Invoices public void SetCryptoData(CryptoData cryptoData) { var dict = GetCryptoData(null); - dict.AddOrReplace(cryptoData.CryptoCode, cryptoData); + dict.AddOrReplace(cryptoData); SetCryptoData(dict); } - public void SetCryptoData(Dictionary cryptoData) + public void SetCryptoData(CryptoDataDictionary cryptoData) { var obj = new JObject(); var serializer = new Serializer(Dummy); - foreach (var kv in cryptoData) - { - var clone = serializer.ToObject(serializer.ToString(kv.Value)); - clone.CryptoCode = null; - obj.Add(new JProperty(kv.Key, JObject.Parse(serializer.ToString(clone)))); - } #pragma warning disable CS0618 + foreach (var v in cryptoData) + { + var clone = serializer.ToObject(serializer.ToString(v)); + clone.CryptoCode = null; + clone.PaymentType = null; + obj.Add(new JProperty(v.GetId().ToString(), JObject.Parse(serializer.ToString(clone)))); + } CryptoData = obj; #pragma warning restore CS0618 } @@ -518,6 +522,69 @@ namespace BTCPayServer.Services.Invoices public Money NetworkFee { get; set; } } + public interface IPaymentMethod + { + string GetPaymentDestination(); + PaymentTypes GetPaymentType(); + Money GetTxFee(); + void SetPaymentDestination(string newPaymentDestination); + } + + public class CryptoDataId + { + public CryptoDataId(string cryptoCode, PaymentTypes paymentType) + { + if (cryptoCode == null) + throw new ArgumentNullException(nameof(cryptoCode)); + PaymentType = paymentType; + CryptoCode = cryptoCode; + } + public string CryptoCode { get; private set; } + public PaymentTypes PaymentType { get; private set; } + + + public override bool Equals(object obj) + { + CryptoDataId item = obj as CryptoDataId; + if (item == null) + return false; + return ToString().Equals(item.ToString(), StringComparison.InvariantCulture); + } + public static bool operator ==(CryptoDataId a, CryptoDataId b) + { + if (System.Object.ReferenceEquals(a, b)) + return true; + if (((object)a == null) || ((object)b == null)) + return false; + return a.ToString() == b.ToString(); + } + + public static bool operator !=(CryptoDataId a, CryptoDataId b) + { + return !(a == b); + } + + public override int GetHashCode() + { +#pragma warning disable CA1307 // Specify StringComparison + return ToString().GetHashCode(); +#pragma warning restore CA1307 // Specify StringComparison + } + + public override string ToString() + { + if (PaymentType == PaymentTypes.BTCLike) + return CryptoCode; + return CryptoCode + "_" + PaymentType.ToString(); + } + + public static CryptoDataId Parse(string str) + { + var parts = str.Split('_'); + return new CryptoDataId(parts[0], parts.Length == 1 ? PaymentTypes.BTCLike : Enum.Parse(parts[1])); + } + } + public class CryptoData { [JsonIgnore] @@ -525,25 +592,99 @@ namespace BTCPayServer.Services.Invoices [JsonIgnore] public BTCPayNetwork Network { get; set; } [JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)] + [Obsolete("Use GetId().CryptoCode instead")] public string CryptoCode { get; set; } + [JsonProperty(PropertyName = "paymentType", DefaultValueHandling = DefaultValueHandling.Ignore)] + [Obsolete("Use GetId().PaymentType instead")] + public string PaymentType { get; set; } + + + public CryptoDataId GetId() + { +#pragma warning disable CS0618 // Type or member is obsolete + return new CryptoDataId(CryptoCode, string.IsNullOrEmpty(PaymentType) ? PaymentTypes.BTCLike : Enum.Parse(PaymentType)); +#pragma warning restore CS0618 // Type or member is obsolete + } + + public void SetId(CryptoDataId id) + { +#pragma warning disable CS0618 // Type or member is obsolete + CryptoCode = id.CryptoCode; + PaymentType = id.PaymentType.ToString(); +#pragma warning restore CS0618 // Type or member is obsolete + } + [JsonProperty(PropertyName = "rate")] public decimal Rate { get; set; } + + [Obsolete("Use GetPaymentMethod() instead")] + public JObject PaymentMethod { get; set; } + public IPaymentMethod GetPaymentMethod() + { +#pragma warning disable CS0618 // Type or member is obsolete + // Legacy, old code does not have PaymentMethods + if (string.IsNullOrEmpty(PaymentType) || PaymentMethod == null) + { + return new BitcoinLikeOnChainPaymentMethod() + { + FeeRate = FeeRate, + DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork), + TxFee = TxFee + }; + } + else + { + + if (GetId().PaymentType == PaymentTypes.BTCLike) + { + var method = DeserializePaymentMethod(PaymentMethod); + method.TxFee = TxFee; + method.DepositAddress = BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork); + method.FeeRate = FeeRate; + return method; + } + } + throw new NotSupportedException(PaymentType); +#pragma warning restore CS0618 // Type or member is obsolete + } + + private T DeserializePaymentMethod(JObject jobj) where T : class, IPaymentMethod + { + return JsonConvert.DeserializeObject(jobj.ToString()); + } + + public void SetPaymentMethod(IPaymentMethod paymentMethod) + { +#pragma warning disable CS0618 // Type or member is obsolete + // Legacy, need to fill the old fields + + if (PaymentType == null) + PaymentType = paymentMethod.GetPaymentType().ToString(); + else if (PaymentType != paymentMethod.GetPaymentType().ToString()) + throw new InvalidOperationException("Invalid payment method affected"); + + if (paymentMethod is BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod) + { + TxFee = bitcoinPaymentMethod.TxFee; + FeeRate = bitcoinPaymentMethod.FeeRate; + DepositAddress = bitcoinPaymentMethod.DepositAddress.ToString(); + } + var jobj = JObject.Parse(JsonConvert.SerializeObject(paymentMethod)); + PaymentMethod = jobj; + +#pragma warning restore CS0618 // Type or member is obsolete + } + [JsonProperty(PropertyName = "feeRate")] + [Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).FeeRate")] public FeeRate FeeRate { get; set; } [JsonProperty(PropertyName = "txFee")] + [Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).TxFee")] public Money TxFee { get; set; } [JsonProperty(PropertyName = "depositAddress")] + [Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")] public string DepositAddress { get; set; } - public BitcoinAddress GetDepositAddress() - { - if (string.IsNullOrEmpty(DepositAddress)) - { - return null; - } - return BitcoinAddress.Create(DepositAddress, Network.NBitcoinNetwork); - } - [JsonIgnore] public bool IsPhantomBTC { get; set; } @@ -563,15 +704,15 @@ namespace BTCPayServer.Services.Invoices .OrderBy(p => p.ReceivedTime) .Select(_ => { - var txFee = _.GetValue(cryptoData, CryptoCode, cryptoData[_.GetCryptoCode()].TxFee); - paid += _.GetValue(cryptoData, CryptoCode); + var txFee = _.GetValue(cryptoData, GetId(), cryptoData[_.GetCryptoDataId()].GetTxFee()); + paid += _.GetValue(cryptoData, GetId()); if (!paidEnough) { totalDue += txFee; paidTxFee += txFee; } paidEnough |= totalDue <= paid; - if (CryptoCode == _.GetCryptoCode()) + if (GetId() == _.GetCryptoDataId()) { cryptoPaid += _.GetCryptoPaymentData().GetValue(); txCount++; @@ -583,8 +724,8 @@ namespace BTCPayServer.Services.Invoices if (!paidEnough) { txCount++; - totalDue += TxFee; - paidTxFee += TxFee; + totalDue += GetTxFee(); + paidTxFee += GetTxFee(); } var accounting = new CryptoDataAccounting(); accounting.TotalDue = totalDue; @@ -596,6 +737,13 @@ namespace BTCPayServer.Services.Invoices return accounting; } + private Money GetTxFee() + { + var method = GetPaymentMethod(); + if (method == null) + return Money.Zero; + return method.GetTxFee(); + } } public class PaymentEntity @@ -623,7 +771,7 @@ namespace BTCPayServer.Services.Invoices } - [Obsolete("Use GetCryptoCode() instead")] + [Obsolete("Use GetCryptoDataId().CryptoCode instead")] public string CryptoCode { get; @@ -632,15 +780,9 @@ namespace BTCPayServer.Services.Invoices [Obsolete("Use GetCryptoPaymentData() instead")] public string CryptoPaymentData { get; set; } - [Obsolete("Use GetCryptoPaymentDataType() instead")] + [Obsolete("Use GetCryptoDataId().PaymentType instead")] public string CryptoPaymentDataType { get; set; } - public string GetCryptoPaymentDataType() - { -#pragma warning disable CS0618 // Type or member is obsolete - return String.IsNullOrEmpty(CryptoPaymentDataType) ? BitcoinLikePaymentData.OnchainBitcoinType : CryptoPaymentDataType; -#pragma warning restore CS0618 // Type or member is obsolete - } public CryptoPaymentData GetCryptoPaymentData() { @@ -656,7 +798,7 @@ namespace BTCPayServer.Services.Invoices paymentData.Legacy = true; return paymentData; } - if (CryptoPaymentDataType == BitcoinLikePaymentData.OnchainBitcoinType) + if (GetCryptoDataId().PaymentType == PaymentTypes.BTCLike) { var paymentData = JsonConvert.DeserializeObject(CryptoPaymentData); // legacy @@ -674,7 +816,6 @@ namespace BTCPayServer.Services.Invoices #pragma warning disable CS0618 if (cryptoPaymentData is BitcoinLikePaymentData paymentData) { - CryptoPaymentDataType = BitcoinLikePaymentData.OnchainBitcoinType; // Legacy Outpoint = paymentData.Outpoint; Output = paymentData.Output; @@ -682,16 +823,17 @@ namespace BTCPayServer.Services.Invoices } else throw new NotSupportedException(cryptoPaymentData.ToString()); + CryptoPaymentDataType = paymentData.GetPaymentType().ToString(); CryptoPaymentData = JsonConvert.SerializeObject(cryptoPaymentData); #pragma warning restore CS0618 } - public Money GetValue(Dictionary cryptoData, string cryptoCode, Money value = null) + public Money GetValue(CryptoDataDictionary cryptoData, CryptoDataId cryptoDataId, Money value = null) { #pragma warning disable CS0618 value = value ?? Output.Value; #pragma warning restore CS0618 - var to = cryptoCode; - var from = GetCryptoCode(); + var to = cryptoDataId; + var from = this.GetCryptoDataId(); if (to == from) return value; var fromRate = cryptoData[from].Rate; @@ -702,6 +844,13 @@ namespace BTCPayServer.Services.Invoices return Money.Coins(otherCurrencyValue); } + public CryptoDataId GetCryptoDataId() + { +#pragma warning disable CS0618 // Type or member is obsolete + return new CryptoDataId(CryptoCode ?? "BTC", string.IsNullOrEmpty(CryptoPaymentDataType) ? PaymentTypes.BTCLike : Enum.Parse(CryptoPaymentDataType)); +#pragma warning restore CS0618 // Type or member is obsolete + } + public string GetCryptoCode() { #pragma warning disable CS0618 @@ -732,68 +881,4 @@ namespace BTCPayServer.Services.Invoices bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network); } - - public class BitcoinLikePaymentData : CryptoPaymentData - { - public readonly static string OnchainBitcoinType = "BTCLike"; - public BitcoinLikePaymentData() - { - - } - public BitcoinLikePaymentData(Coin coin, bool rbf) - { - Outpoint = coin.Outpoint; - Output = coin.TxOut; - ConfirmationCount = 0; - RBF = rbf; - } - [JsonIgnore] - public OutPoint Outpoint { get; set; } - [JsonIgnore] - public TxOut Output { get; set; } - public int ConfirmationCount { get; set; } - public bool RBF { get; set; } - - /// - /// This is set to true if the payment was created before CryptoPaymentData existed in BTCPayServer - /// - public bool Legacy { get; set; } - - public string GetPaymentId() - { - return Outpoint.ToString(); - } - - public string[] GetSearchTerms() - { - return new[] { Outpoint.Hash.ToString() }; - } - - public Money GetValue() - { - return Output.Value; - } - - public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network) - { - return ConfirmationCount >= network.MaxTrackedConfirmation; - } - - public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network) - { - if (speedPolicy == SpeedPolicy.HighSpeed) - { - return ConfirmationCount >= 1 || !RBF; - } - else if (speedPolicy == SpeedPolicy.MediumSpeed) - { - return ConfirmationCount >= 1; - } - else if (speedPolicy == SpeedPolicy.LowSpeed) - { - return ConfirmationCount >= 6; - } - return false; - } - } } diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index a245721cd..ef3616b7e 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -123,21 +123,25 @@ namespace BTCPayServer.Services.Invoices CustomerEmail = invoice.RefundMail }); - foreach (var cryptoData in invoice.GetCryptoData(networkProvider).Values) + foreach (var cryptoData in invoice.GetCryptoData(networkProvider)) { if (cryptoData.Network == null) throw new InvalidOperationException("CryptoCode unsupported"); + var paymentDestination = cryptoData.GetPaymentMethod().GetPaymentDestination(); + + ScriptId hash = GetAddressInvoiceHash(cryptoData); context.AddressInvoices.Add(new AddressInvoiceData() { InvoiceDataId = invoice.Id, CreatedTime = DateTimeOffset.UtcNow, - }.SetHash(cryptoData.GetDepositAddress().ScriptPubKey.Hash, cryptoData.CryptoCode)); + }.SetHash(hash, cryptoData.GetId().ToString())); + context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData() { InvoiceDataId = invoice.Id, Assigned = DateTimeOffset.UtcNow - }.SetAddress(cryptoData.DepositAddress, cryptoData.CryptoCode)); - textSearch.Add(cryptoData.DepositAddress); + }.SetAddress(paymentDestination, cryptoData.GetId().ToString())); + textSearch.Add(paymentDestination); textSearch.Add(cryptoData.Calculate().TotalDue.ToString()); } context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id }); @@ -157,7 +161,17 @@ namespace BTCPayServer.Services.Invoices return invoice; } - public async Task NewAddress(string invoiceId, BitcoinAddress bitcoinAddress, BTCPayNetwork network) + private static ScriptId GetAddressInvoiceHash(CryptoData cryptoData) + { + ScriptId hash = null; + if (cryptoData.GetId().PaymentType == Payments.PaymentTypes.BTCLike) + { + hash = ((Payments.BitcoinLikeOnChainPaymentMethod)cryptoData.GetPaymentMethod()).DepositAddress.ScriptPubKey.Hash; + } + return hash; + } + + public async Task NewAddress(string invoiceId, IPaymentMethod paymentMethod, BTCPayNetwork network) { using (var context = _ContextFactory.CreateContext()) { @@ -166,17 +180,19 @@ namespace BTCPayServer.Services.Invoices return false; var invoiceEntity = ToObject(invoice.Blob, network.NBitcoinNetwork); - var currencyData = invoiceEntity.GetCryptoData(network, null); + var currencyData = invoiceEntity.GetCryptoData(network, paymentMethod.GetPaymentType(), null); if (currencyData == null) return false; - if (currencyData.DepositAddress != null) + var existingPaymentMethod = currencyData.GetPaymentMethod(); + if (existingPaymentMethod.GetPaymentDestination() != null) { - MarkUnassigned(invoiceId, invoiceEntity, context, network.CryptoCode); + MarkUnassigned(invoiceId, invoiceEntity, context, currencyData.GetId()); } - currencyData.DepositAddress = bitcoinAddress.ToString(); + existingPaymentMethod.SetPaymentDestination(paymentMethod.GetPaymentDestination()); + currencyData.SetPaymentMethod(existingPaymentMethod); #pragma warning disable CS0618 if (network.IsBTC) { @@ -191,15 +207,15 @@ namespace BTCPayServer.Services.Invoices InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow } - .SetHash(bitcoinAddress.ScriptPubKey.Hash, network.CryptoCode)); + .SetHash(GetAddressInvoiceHash(currencyData), network.CryptoCode)); context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData() { InvoiceDataId = invoiceId, Assigned = DateTimeOffset.UtcNow - }.SetAddress(bitcoinAddress.ToString(), network.CryptoCode)); + }.SetAddress(paymentMethod.GetPaymentDestination(), network.CryptoCode)); await context.SaveChangesAsync(); - AddToTextSearch(invoice.Id, bitcoinAddress.ToString()); + AddToTextSearch(invoice.Id, paymentMethod.GetPaymentDestination()); return true; } } @@ -219,15 +235,15 @@ namespace BTCPayServer.Services.Invoices } } - private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, string cryptoCode) + private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, CryptoDataId cryptoDataId) { foreach (var address in entity.GetCryptoData(null)) { - if (cryptoCode != null && cryptoCode != address.Value.CryptoCode) + if (cryptoDataId != null && cryptoDataId != address.GetId()) continue; var historical = new HistoricalAddressInvoiceData(); historical.InvoiceDataId = invoiceId; - historical.SetAddress(address.Value.DepositAddress, address.Value.CryptoCode); + historical.SetAddress(address.GetPaymentMethod().GetPaymentDestination(), address.GetId().ToString()); historical.UnAssigned = DateTimeOffset.UtcNow; context.Attach(historical); context.Entry(historical).Property(o => o.UnAssigned).IsModified = true; diff --git a/BTCPayServer/Views/Invoice/Checkout.cshtml b/BTCPayServer/Views/Invoice/Checkout.cshtml index f63c1d3f8..0535c88b1 100644 --- a/BTCPayServer/Views/Invoice/Checkout.cshtml +++ b/BTCPayServer/Views/Invoice/Checkout.cshtml @@ -613,7 +613,7 @@
@foreach(var crypto in Model.AvailableCryptos) { - @crypto.CryptoCode + @crypto.CryptoDataId }
} diff --git a/BTCPayServer/wwwroot/js/core.js b/BTCPayServer/wwwroot/js/core.js index 92508c9f8..858c3938d 100644 --- a/BTCPayServer/wwwroot/js/core.js +++ b/BTCPayServer/wwwroot/js/core.js @@ -198,7 +198,7 @@ function onDataCallback(jsonData) { } function fetchStatus() { - var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/" + srvModel.cryptoCode + "/status"; + var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/" + srvModel.cryptoDataId + "/status"; $.ajax({ url: path, type: "GET"