From a37fdde214c6e517edaec12fbe4c06aead34c287 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Thu, 21 Dec 2017 15:52:04 +0900 Subject: [PATCH] Big refactorying for supporting multi currencies --- BTCPayServer.Tests/UnitTest1.cs | 45 +- BTCPayServer/BTCPayNetwork.cs | 16 + BTCPayServer/BTCPayNetworkProvider.cs | 59 +++ .../Controllers/CallbackController.cs | 4 +- .../Controllers/InvoiceController.API.cs | 9 +- .../InvoiceController.PaymentProtocol.cs | 14 +- .../Controllers/InvoiceController.UI.cs | 71 ++- BTCPayServer/Controllers/InvoiceController.cs | 68 ++- BTCPayServer/Controllers/StoresController.cs | 10 +- .../Data/HistoricalAddressInvoiceData.cs | 11 + BTCPayServer/Data/StoreData.cs | 26 +- BTCPayServer/Hosting/BTCPayServerServices.cs | 13 +- .../20171221054550_AltcoinSupport.Designer.cs | 481 ++++++++++++++++++ .../20171221054550_AltcoinSupport.cs | 24 + .../ApplicationDbContextModelSnapshot.cs | 6 +- BTCPayServer/Models/InvoiceResponse.cs | 57 ++- .../InvoicingModels/InvoiceDetailsModel.cs | 2 +- .../Services/Fees/IFeeProviderFactory.cs | 12 + .../Services/Fees/NBxplorerFeeProvider.cs | 36 +- .../Services/Invoices/InvoiceEntity.cs | 305 ++++++++--- .../Invoices/InvoiceNotificationManager.cs | 26 +- .../Services/Invoices/InvoiceRepository.cs | 113 ++-- .../Services/Invoices/InvoiceWatcher.cs | 33 +- 23 files changed, 1201 insertions(+), 240 deletions(-) create mode 100644 BTCPayServer/BTCPayNetwork.cs create mode 100644 BTCPayServer/BTCPayNetworkProvider.cs create mode 100644 BTCPayServer/Migrations/20171221054550_AltcoinSupport.Designer.cs create mode 100644 BTCPayServer/Migrations/20171221054550_AltcoinSupport.cs create mode 100644 BTCPayServer/Services/Fees/IFeeProviderFactory.cs diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 333bf02ce..4b26d419c 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -38,33 +38,38 @@ namespace BTCPayServer.Tests public void CanCalculateCryptoDue() { var entity = new InvoiceEntity(); +#pragma warning disable CS0618 entity.TxFee = Money.Coins(0.1m); entity.Rate = 5000; + + var cryptoData = entity.GetCryptoData("BTC"); + Assert.NotNull(cryptoData); // Should use legacy data to build itself entity.Payments = new System.Collections.Generic.List(); entity.ProductInformation = new ProductInformation() { Price = 5000 }; - Assert.Equal(Money.Coins(1.1m), entity.GetCryptoDue()); - Assert.Equal(Money.Coins(1.1m), entity.GetTotalCryptoDue()); + Assert.Equal(Money.Coins(1.1m), cryptoData.GetCryptoDue()); + Assert.Equal(Money.Coins(1.1m), cryptoData.GetTotalCryptoDue()); entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true }); //Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1 - Assert.Equal(Money.Coins(0.7m), entity.GetCryptoDue()); - Assert.Equal(Money.Coins(1.2m), entity.GetTotalCryptoDue()); + Assert.Equal(Money.Coins(0.7m), cryptoData.GetCryptoDue()); + Assert.Equal(Money.Coins(1.2m), cryptoData.GetTotalCryptoDue()); entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true }); - Assert.Equal(Money.Coins(0.6m), entity.GetCryptoDue()); - Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue()); + Assert.Equal(Money.Coins(0.6m), cryptoData.GetCryptoDue()); + Assert.Equal(Money.Coins(1.3m), cryptoData.GetTotalCryptoDue()); entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true }); - Assert.Equal(Money.Zero, entity.GetCryptoDue()); - Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue()); + Assert.Equal(Money.Zero, cryptoData.GetCryptoDue()); + Assert.Equal(Money.Coins(1.3m), cryptoData.GetTotalCryptoDue()); entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true }); - Assert.Equal(Money.Zero, entity.GetCryptoDue()); - Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue()); + Assert.Equal(Money.Zero, cryptoData.GetCryptoDue()); + Assert.Equal(Money.Coins(1.3m), cryptoData.GetTotalCryptoDue()); +#pragma warning restore CS0618 } [Fact] @@ -312,26 +317,26 @@ namespace BTCPayServer.Tests StoreId = user.StoreId, TextSearch = invoice.OrderId }).GetAwaiter().GetResult(); - Assert.Equal(1, textSearchResult.Length); + Assert.Single(textSearchResult); textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery() { StoreId = user.StoreId, TextSearch = invoice.Id }).GetAwaiter().GetResult(); - Assert.Equal(1, textSearchResult.Length); + Assert.Single(textSearchResult); }); invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal(Money.Coins(0), invoice.BtcPaid); Assert.Equal("new", invoice.Status); - Assert.Equal(false, (bool)((JValue)invoice.ExceptionStatus).Value); + Assert.False((bool)((JValue)invoice.ExceptionStatus).Value); - Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime).Length); - Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)).Length); - Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)).Length); - Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime).Length); - Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)).Length); + Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime)); + Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1))); + Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5))); + Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime)); + Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1))); var firstPayment = Money.Coins(0.04m); @@ -348,7 +353,7 @@ namespace BTCPayServer.Tests cashCow.SendToAddress(invoiceAddress, firstPayment); var invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult(); - Assert.Equal(1, invoiceEntity.HistoricalAddresses.Length); + Assert.Single(invoiceEntity.HistoricalAddresses); Assert.Null(invoiceEntity.HistoricalAddresses[0].UnAssigned); Money secondPayment = Money.Zero; @@ -360,7 +365,7 @@ namespace BTCPayServer.Tests Assert.Equal("new", localInvoice.Status); Assert.Equal(firstPayment, localInvoice.BtcPaid); txFee = localInvoice.BtcDue - invoice.BtcDue; - Assert.Equal("paidPartial", localInvoice.ExceptionStatus); + Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString()); Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address Assert.True(IsMapped(invoice, ctx)); Assert.True(IsMapped(localInvoice, ctx)); diff --git a/BTCPayServer/BTCPayNetwork.cs b/BTCPayServer/BTCPayNetwork.cs new file mode 100644 index 000000000..d399438eb --- /dev/null +++ b/BTCPayServer/BTCPayNetwork.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NBitcoin; + +namespace BTCPayServer +{ + public class BTCPayNetwork + { + public Network NBitcoinNetwork { get; set; } + public string CryptoCode { get; internal set; } + public string BlockExplorerLink { get; internal set; } + public string UriScheme { get; internal set; } + } +} diff --git a/BTCPayServer/BTCPayNetworkProvider.cs b/BTCPayServer/BTCPayNetworkProvider.cs new file mode 100644 index 000000000..84e9178ec --- /dev/null +++ b/BTCPayServer/BTCPayNetworkProvider.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NBitcoin; + +namespace BTCPayServer +{ + public class BTCPayNetworkProvider + { + Dictionary _Networks = new Dictionary(); + public BTCPayNetworkProvider(Network network) + { + if(network == Network.Main) + { + Add(new BTCPayNetwork() + { + CryptoCode = "BTC", + BlockExplorerLink = "https://www.smartbit.com.au/tx/{0}", + NBitcoinNetwork = Network.Main, + UriScheme = "bitcoin" + }); + } + + if (network == Network.TestNet) + { + Add(new BTCPayNetwork() + { + CryptoCode = "BTC", + BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}", + NBitcoinNetwork = Network.TestNet, + UriScheme = "bitcoin" + }); + } + + if (network == Network.RegTest) + { + Add(new BTCPayNetwork() + { + CryptoCode = "BTC", + BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}", + NBitcoinNetwork = Network.RegTest, + UriScheme = "bitcoin" + }); + } + } + + public void Add(BTCPayNetwork network) + { + _Networks.Add(network.CryptoCode, network); + } + + public BTCPayNetwork GetNetwork(string cryptoCode) + { + _Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network); + return network; + } + } +} diff --git a/BTCPayServer/Controllers/CallbackController.cs b/BTCPayServer/Controllers/CallbackController.cs index 0c57d1795..34dc87540 100644 --- a/BTCPayServer/Controllers/CallbackController.cs +++ b/BTCPayServer/Controllers/CallbackController.cs @@ -43,10 +43,10 @@ namespace BTCPayServer.Controllers EventAggregator eventAggregator, BTCPayServerOptions options, IServer server, - Network network) + BTCPayNetworkProvider networkProvider) { _Settings = repo; - _Network = network; + _Network = networkProvider.GetNetwork("BTC").NBitcoinNetwork; _Explorer = explorer; _Options = options; _EventAggregator = eventAggregator; diff --git a/BTCPayServer/Controllers/InvoiceController.API.cs b/BTCPayServer/Controllers/InvoiceController.API.cs index 9da44a58f..dc9e42877 100644 --- a/BTCPayServer/Controllers/InvoiceController.API.cs +++ b/BTCPayServer/Controllers/InvoiceController.API.cs @@ -24,16 +24,19 @@ namespace BTCPayServer.Controllers private InvoiceRepository _InvoiceRepository; private TokenRepository _TokenRepository; private StoreRepository _StoreRepository; + private BTCPayNetworkProvider _NetworkProvider; public InvoiceControllerAPI(InvoiceController invoiceController, InvoiceRepository invoceRepository, TokenRepository tokenRepository, - StoreRepository storeRepository) + StoreRepository storeRepository, + BTCPayNetworkProvider networkProvider) { this._InvoiceController = invoiceController; this._InvoiceRepository = invoceRepository; this._TokenRepository = tokenRepository; this._StoreRepository = storeRepository; + this._NetworkProvider = networkProvider; } [HttpPost] @@ -56,7 +59,7 @@ namespace BTCPayServer.Controllers if (invoice == null) throw new BitpayHttpException(404, "Object not found"); - var resp = invoice.EntityToDTO(); + var resp = invoice.EntityToDTO(_NetworkProvider); return new DataWrapper(resp); } @@ -90,7 +93,7 @@ namespace BTCPayServer.Controllers var entities = (await _InvoiceRepository.GetInvoices(query)) - .Select((o) => o.EntityToDTO()).ToArray(); + .Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray(); return DataWrapper.Create(entities); } diff --git a/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs b/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs index e72934865..dfb382570 100644 --- a/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs +++ b/BTCPayServer/Controllers/InvoiceController.PaymentProtocol.cs @@ -17,21 +17,25 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("i/{invoiceId}")] [AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")] - public async Task GetInvoiceRequest(string invoiceId) + public async Task GetInvoiceRequest(string invoiceId, string cryptoCode = null) { + if (cryptoCode == null) + cryptoCode = "BTC"; var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); - if (invoice == null || invoice.IsExpired()) + var network = _NetworkProvider.GetNetwork(cryptoCode); + if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(network)) return NotFound(); - var dto = invoice.EntityToDTO(); + var dto = invoice.EntityToDTO(_NetworkProvider); + var cryptoData = dto.CryptoInfo.First(c => c.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase)); PaymentRequest request = new PaymentRequest { DetailsVersion = 1 }; request.Details.Expires = invoice.ExpirationTime; request.Details.Memo = invoice.ProductInformation.ItemDesc; - request.Details.Network = _Network; - request.Details.Outputs.Add(new PaymentOutput() { Amount = dto.BTCDue, Script = BitcoinAddress.Create(dto.BitcoinAddress, _Network).ScriptPubKey }); + request.Details.Network = network.NBitcoinNetwork; + request.Details.Outputs.Add(new PaymentOutput() { Amount = cryptoData.Due, Script = BitcoinAddress.Create(cryptoData.Address, network.NBitcoinNetwork).ScriptPubKey }); request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id); request.Details.Time = DateTimeOffset.UtcNow; request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute); diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index e87f93998..f9677d794 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -26,7 +26,7 @@ namespace BTCPayServer.Controllers { [HttpPost] [Route("invoices/{invoiceId}")] - public IActionResult Invoice(string invoiceId, string command) + public IActionResult Invoice(string invoiceId, string command, string cryptoCode = null) { if (command == "refresh") { @@ -41,8 +41,10 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("invoices/{invoiceId}")] - public async Task Invoice(string invoiceId) + public async Task Invoice(string invoiceId, string cryptoCode = null) { + if (cryptoCode == null) + cryptoCode = "BTC"; var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery() { UserId = GetUserId(), @@ -51,7 +53,14 @@ namespace BTCPayServer.Controllers if (invoice == null) return NotFound(); - var dto = invoice.EntityToDTO(); + var network = _NetworkProvider.GetNetwork(cryptoCode); + if (network == null || !invoice.Support(network)) + return NotFound(); + + var cryptoData = invoice.GetCryptoData(network); + + var dto = invoice.EntityToDTO(_NetworkProvider); + var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase)); var store = await _StoreRepository.FindStore(invoice.StoreId); InvoiceDetailsModel model = new InvoiceDetailsModel() { @@ -64,16 +73,16 @@ namespace BTCPayServer.Controllers ExpirationDate = invoice.ExpirationTime, OrderId = invoice.OrderId, BuyerInformation = invoice.BuyerInformation, - Rate = invoice.Rate, + Rate = cryptoData.Rate, Fiat = dto.Price + " " + dto.Currency, - BTC = invoice.GetTotalCryptoDue().ToString() + " BTC", - BTCDue = invoice.GetCryptoDue().ToString() + " BTC", - BTCPaid = invoice.GetTotalPaid().ToString() + " BTC", - NetworkFee = invoice.GetNetworkFee().ToString() + " BTC", + BTC = cryptoData.GetTotalCryptoDue().ToString() + $" {network.CryptoCode}", + BTCDue = cryptoData.GetCryptoDue().ToString() + $" {network.CryptoCode}", + BTCPaid = cryptoData.GetTotalPaid().ToString() + $" {network.CryptoCode}", + NetworkFee = cryptoData.GetNetworkFee().ToString() + $" {network.CryptoCode}", NotificationUrl = invoice.NotificationURL, ProductInformation = invoice.ProductInformation, - BitcoinAddress = invoice.DepositAddress, - PaymentUrl = dto.PaymentUrls.BIP72 + BitcoinAddress = BitcoinAddress.Create(cryptoInfo.Address, network.NBitcoinNetwork), + PaymentUrl = cryptoInfo.PaymentUrls.BIP72 }; var payments = invoice @@ -81,11 +90,11 @@ namespace BTCPayServer.Controllers .Select(async payment => { var m = new InvoiceDetailsModel.Payment(); - m.DepositAddress = payment.Output.ScriptPubKey.GetDestinationAddress(_Network); + m.DepositAddress = payment.GetScriptPubKey().GetDestinationAddress(network.NBitcoinNetwork); m.Confirmations = (await _Explorer.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0; m.TransactionId = payment.Outpoint.Hash.ToString(); m.ReceivedTime = payment.ReceivedTime; - m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/tx/{m.TransactionId}"; + m.TransactionLink = string.Format(network.BlockExplorerLink, m.TransactionId); return m; }) .ToArray(); @@ -100,48 +109,54 @@ namespace BTCPayServer.Controllers [Route("invoice")] [AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)] [XFrameOptionsAttribute(null)] - public async Task Checkout(string invoiceId, string id = null) + public async Task Checkout(string invoiceId, string id = null, string cryptoCode = null) { + if (cryptoCode == null) + cryptoCode = "BTC"; //Keep compatibility with Bitpay invoiceId = invoiceId ?? id; id = invoiceId; //// - var model = await GetInvoiceModel(invoiceId); + var model = await GetInvoiceModel(invoiceId, cryptoCode); if (model == null) return NotFound(); return View(nameof(Checkout), model); } - private async Task GetInvoiceModel(string invoiceId) + private async Task GetInvoiceModel(string invoiceId, string cryptoCode) { var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId); - if (invoice == null) + var network = _NetworkProvider.GetNetwork(cryptoCode); + if (invoice == null || network == null || !invoice.Support(network)) return null; + + var cryptoData = invoice.GetCryptoData(network); var store = await _StoreRepository.FindStore(invoice.StoreId); - var dto = invoice.EntityToDTO(); + var dto = invoice.EntityToDTO(_NetworkProvider); + var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode == network.CryptoCode); var currency = invoice.ProductInformation.Currency; var model = new PaymentModel() { ServerUrl = HttpContext.Request.GetAbsoluteRoot(), OrderId = invoice.OrderId, InvoiceId = invoice.Id, - BtcAddress = invoice.DepositAddress.ToString(), - BtcAmount = (invoice.GetTotalCryptoDue() - invoice.TxFee).ToString(), - BtcTotalDue = invoice.GetTotalCryptoDue().ToString(), - BtcDue = invoice.GetCryptoDue().ToString(), + BtcAddress = cryptoData.DepositAddress, + BtcAmount = (cryptoData.GetTotalCryptoDue() - cryptoData.TxFee).ToString(), + BtcTotalDue = cryptoData.GetTotalCryptoDue().ToString(), + BtcDue = cryptoData.GetCryptoDue().ToString(), CustomerEmail = invoice.RefundMail, ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds), MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds, ItemDesc = invoice.ProductInformation.ItemDesc, - Rate = invoice.Rate.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})", + Rate = cryptoData.Rate.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})", MerchantRefLink = invoice.RedirectURL ?? "/", StoreName = store.StoreName, - TxFees = invoice.TxFee.ToString(), - InvoiceBitcoinUrl = dto.PaymentUrls.BIP72, - TxCount = invoice.GetTxCount(), - BtcPaid = invoice.GetTotalPaid().ToString(), + TxFees = cryptoData.TxFee.ToString(), + InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP72, + TxCount = cryptoData.GetTxCount(), + BtcPaid = cryptoData.GetTotalPaid().ToString(), Status = invoice.Status }; @@ -163,9 +178,9 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("i/{invoiceId}/status")] - public async Task GetStatus(string invoiceId) + public async Task GetStatus(string invoiceId, string cryptoCode) { - var model = await GetInvoiceModel(invoiceId); + var model = await GetInvoiceModel(invoiceId, cryptoCode); if (model == null) return NotFound(); return Json(model); diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index b3ff2d5d2..0b18a743e 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -48,15 +48,13 @@ namespace BTCPayServer.Controllers IRateProvider _RateProvider; private InvoiceWatcher _Watcher; StoreRepository _StoreRepository; - Network _Network; UserManager _UserManager; - IFeeProvider _FeeProvider; + IFeeProviderFactory _FeeProviderFactory; private CurrencyNameTable _CurrencyNameTable; ExplorerClient _Explorer; EventAggregator _EventAggregator; - public InvoiceController( - Network network, - InvoiceRepository invoiceRepository, + BTCPayNetworkProvider _NetworkProvider; + public InvoiceController(InvoiceRepository invoiceRepository, CurrencyNameTable currencyNameTable, UserManager userManager, BTCPayWallet wallet, @@ -65,31 +63,32 @@ namespace BTCPayServer.Controllers EventAggregator eventAggregator, InvoiceWatcherAccessor watcher, ExplorerClient explorerClient, - IFeeProvider feeProvider) + BTCPayNetworkProvider networkProvider, + IFeeProviderFactory feeProviderFactory) { _CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); _Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient)); _StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository)); - _Network = network ?? throw new ArgumentNullException(nameof(network)); _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); _Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet)); _RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider)); _Watcher = (watcher ?? throw new ArgumentNullException(nameof(watcher))).Instance; _UserManager = userManager; - _FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider)); + _FeeProviderFactory = feeProviderFactory ?? throw new ArgumentNullException(nameof(feeProviderFactory)); _EventAggregator = eventAggregator; + _NetworkProvider = networkProvider; } + internal async Task> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15) { - //TODO: expiryMinutes (time before a new invoice can become paid) and monitoringMinutes (time before a paid invoice becomes invalid) should be configurable at store level var derivationStrategy = store.DerivationStrategy; var entity = new InvoiceEntity { InvoiceTime = DateTimeOffset.UtcNow, DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy") }; - var storeBlob = store.GetStoreBlob(_Network); + var storeBlob = store.GetStoreBlob(); Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null; if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ? notificationUri = null; @@ -114,16 +113,45 @@ namespace BTCPayServer.Controllers entity.Status = "new"; entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy); - var getFeeRate = _FeeProvider.GetFeeRateAsync(); - var getRate = _RateProvider.GetRateAsync(invoice.Currency); - var getAddress = _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy)); - entity.TxFee = storeBlob.NetworkFeeDisabled ? Money.Zero : (await getFeeRate).GetFee(100); // assume price for 100 bytes - entity.Rate = (double)await getRate; + var queries = storeBlob.GetSupportedCryptoCurrencies() + .Select(n => _NetworkProvider.GetNetwork(n)) + .Where(n => n != null) + .Select(network => + { + return new + { + network = network, + getFeeRate = _FeeProviderFactory.CreateFeeProvider(network).GetFeeRateAsync(), + getRate = _RateProvider.GetRateAsync(invoice.Currency), + getAddress = _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy, network)) + }; + }); + + var cryptoDatas = new Dictionary(); + foreach (var q in queries) + { + CryptoData cryptoData = new CryptoData(); + cryptoData.CryptoCode = q.network.CryptoCode; + cryptoData.FeeRate = (await q.getFeeRate); + cryptoData.TxFee = storeBlob.NetworkFeeDisabled ? Money.Zero : cryptoData.FeeRate.GetFee(100); // assume price for 100 bytes + cryptoData.Rate = await q.getRate; + cryptoData.DepositAddress = (await q.getAddress).ToString(); + +#pragma warning disable CS0618 + if (q.network.CryptoCode == "BTC") + { + entity.TxFee = cryptoData.TxFee; + entity.Rate = cryptoData.Rate; + entity.DepositAddress = cryptoData.DepositAddress; + } +#pragma warning restore CS0618 + cryptoDatas.Add(cryptoData.CryptoCode, cryptoData); + } + entity.SetCryptoData(cryptoDatas); entity.PosData = invoice.PosData; - entity.DepositAddress = await getAddress; - entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity); + entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider); _Watcher.Watch(entity.Id); - var resp = entity.EntityToDTO(); + var resp = entity.EntityToDTO(_NetworkProvider); return new DataWrapper(resp) { Facade = "pos/invoice" }; } @@ -155,9 +183,9 @@ namespace BTCPayServer.Controllers buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip; } - private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy) + private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy, BTCPayNetwork network) { - return new DerivationStrategyFactory(_Network).Parse(derivationStrategy); + return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy); } private TDest Map(TFrom data) diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 55fff5039..af2cd48ad 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -34,7 +34,7 @@ namespace BTCPayServer.Controllers UserManager userManager, AccessTokenController tokenController, BTCPayWallet wallet, - Network network, + BTCPayNetworkProvider networkProvider, IHostingEnvironment env) { _Repo = repo; @@ -43,7 +43,7 @@ namespace BTCPayServer.Controllers _TokenController = tokenController; _Wallet = wallet; _Env = env; - _Network = network; + _Network = networkProvider.GetNetwork("BTC").NBitcoinNetwork; _CallbackController = callbackController; } Network _Network; @@ -145,7 +145,7 @@ namespace BTCPayServer.Controllers if (store == null) return NotFound(); - var storeBlob = store.GetStoreBlob(_Network); + var storeBlob = store.GetStoreBlob(); var vm = new StoreViewModel(); vm.Id = store.Id; vm.StoreName = store.StoreName; @@ -210,11 +210,11 @@ namespace BTCPayServer.Controllers } } - var blob = store.GetStoreBlob(_Network); + var blob = store.GetStoreBlob(); blob.NetworkFeeDisabled = !model.NetworkFee; blob.MonitoringExpiration = model.MonitoringExpiration; - if (store.SetStoreBlob(blob, _Network)) + if (store.SetStoreBlob(blob)) { needUpdate = true; } diff --git a/BTCPayServer/Data/HistoricalAddressInvoiceData.cs b/BTCPayServer/Data/HistoricalAddressInvoiceData.cs index a00e5d32b..51ad08c03 100644 --- a/BTCPayServer/Data/HistoricalAddressInvoiceData.cs +++ b/BTCPayServer/Data/HistoricalAddressInvoiceData.cs @@ -17,6 +17,17 @@ namespace BTCPayServer.Data get; set; } + + [Obsolete("Use GetCryptoCode instead")] + public string CryptoCode { get; set; } + +#pragma warning disable CS0618 + public string GetCryptoCode() + { + return string.IsNullOrEmpty(CryptoCode) ? "BTC" : CryptoCode; + } +#pragma warning restore CS0618 + public DateTimeOffset Assigned { get; set; diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index 826fc7650..4e767275b 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -62,15 +62,17 @@ namespace BTCPayServer.Data set; } - public StoreBlob GetStoreBlob(Network network) + static Network Dummy = Network.Main; + + public StoreBlob GetStoreBlob() { - return StoreBlob == null ? new StoreBlob() : new Serializer(network).ToObject(Encoding.UTF8.GetString(StoreBlob)); + return StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject(Encoding.UTF8.GetString(StoreBlob)); } - public bool SetStoreBlob(StoreBlob storeBlob, Network network) + public bool SetStoreBlob(StoreBlob storeBlob) { - var original = new Serializer(network).ToString(GetStoreBlob(network)); - var newBlob = new Serializer(network).ToString(storeBlob); + var original = new Serializer(Dummy).ToString(GetStoreBlob()); + var newBlob = new Serializer(Dummy).ToString(storeBlob); if (original == newBlob) return false; StoreBlob = Encoding.UTF8.GetBytes(newBlob); @@ -95,5 +97,19 @@ namespace BTCPayServer.Data get; set; } + + [Obsolete("Use GetSupportedCryptoCurrencies() instead")] + public string[] SupportedCryptoCurrencies { get; set; } + + public string[] GetSupportedCryptoCurrencies() + { +#pragma warning disable CS0618 + if(SupportedCryptoCurrencies == null) + { + return new string[] { "BTC" }; + } + return SupportedCryptoCurrencies; +#pragma warning restore CS0618 + } } } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 89234f1dd..052985c00 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -107,7 +107,6 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(o => o.GetRequiredService().Network); services.TryAddSingleton(o => { var opts = o.GetRequiredService(); @@ -125,14 +124,20 @@ namespace BTCPayServer.Hosting } return dbContext; }); + + services.TryAddSingleton(o => + { + var opts = o.GetRequiredService(); + return new BTCPayNetworkProvider(opts.Network); + }); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(o => new NBXplorerFeeProvider() + services.TryAddSingleton(o => new NBXplorerFeeProviderFactory(o.GetRequiredService()) { Fallback = new FeeRate(100, 1), - BlockTarget = 20, - ExplorerClient = o.GetRequiredService() + BlockTarget = 20 }); services.TryAddSingleton(); diff --git a/BTCPayServer/Migrations/20171221054550_AltcoinSupport.Designer.cs b/BTCPayServer/Migrations/20171221054550_AltcoinSupport.Designer.cs new file mode 100644 index 000000000..e92483bf4 --- /dev/null +++ b/BTCPayServer/Migrations/20171221054550_AltcoinSupport.Designer.cs @@ -0,0 +1,481 @@ +// +using BTCPayServer.Data; +using BTCPayServer.Services.Invoices; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20171221054550_AltcoinSupport")] + partial class AltcoinSupport + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.1-rtm-125"); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.Property("Address") + .ValueGeneratedOnAdd(); + + b.Property("CreatedTime"); + + b.Property("InvoiceDataId"); + + b.HasKey("Address"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("AddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.Property("InvoiceDataId"); + + b.Property("Address"); + + b.Property("Assigned"); + + b.Property("CryptoCode"); + + b.Property("UnAssigned"); + + b.HasKey("InvoiceDataId", "Address"); + + b.ToTable("HistoricalAddressInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("Created"); + + b.Property("CustomerEmail"); + + b.Property("ExceptionStatus"); + + b.Property("ItemCode"); + + b.Property("OrderId"); + + b.Property("Status"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("StoreDataId"); + + b.ToTable("Invoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("PairingTime"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.HasKey("Id"); + + b.HasIndex("SIN"); + + b.HasIndex("StoreDataId"); + + b.ToTable("PairedSINData"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateCreated"); + + b.Property("Expiration"); + + b.Property("Facade"); + + b.Property("Label"); + + b.Property("SIN"); + + b.Property("StoreDataId"); + + b.Property("TokenValue"); + + b.HasKey("Id"); + + b.ToTable("PairingCodes"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Accounted"); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.HasKey("Id"); + + b.ToTable("PendingInvoices"); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Blob"); + + b.Property("InvoiceDataId"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceDataId"); + + b.ToTable("RefundAddresses"); + }); + + modelBuilder.Entity("BTCPayServer.Data.SettingData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Value"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("BTCPayServer.Data.StoreData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DerivationStrategy"); + + b.Property("SpeedPolicy"); + + b.Property("StoreBlob"); + + b.Property("StoreCertificate"); + + b.Property("StoreName"); + + b.Property("StoreWebsite"); + + b.HasKey("Id"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.Property("ApplicationUserId"); + + b.Property("StoreDataId"); + + b.Property("Role"); + + b.HasKey("ApplicationUserId", "StoreDataId"); + + b.HasIndex("StoreDataId"); + + b.ToTable("UserStore"); + }); + + modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("RequiresEmailConfirmation"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("AddressInvoices") + .HasForeignKey("InvoiceDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData") + .WithMany("HistoricalAddressInvoices") + .HasForeignKey("InvoiceDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b => + { + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany() + .HasForeignKey("StoreDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.PaymentData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("Payments") + .HasForeignKey("InvoiceDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b => + { + b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") + .WithMany("RefundAddresses") + .HasForeignKey("InvoiceDataId"); + }); + + modelBuilder.Entity("BTCPayServer.Data.UserStore", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser") + .WithMany("UserStores") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Data.StoreData", "StoreData") + .WithMany("UserStores") + .HasForeignKey("StoreDataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BTCPayServer.Models.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BTCPayServer/Migrations/20171221054550_AltcoinSupport.cs b/BTCPayServer/Migrations/20171221054550_AltcoinSupport.cs new file mode 100644 index 000000000..46e56da6c --- /dev/null +++ b/BTCPayServer/Migrations/20171221054550_AltcoinSupport.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace BTCPayServer.Migrations +{ + public partial class AltcoinSupport : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CryptoCode", + table: "HistoricalAddressInvoices", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CryptoCode", + table: "HistoricalAddressInvoices"); + } + } +} diff --git a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs index 02037b4a9..bb586afe1 100644 --- a/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer/Migrations/ApplicationDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace BTCPayServer.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + .HasAnnotation("ProductVersion", "2.0.1-rtm-125"); modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => { @@ -44,6 +44,8 @@ namespace BTCPayServer.Migrations b.Property("Assigned"); + b.Property("CryptoCode"); + b.Property("UnAssigned"); b.HasKey("InvoiceDataId", "Address"); @@ -382,7 +384,7 @@ namespace BTCPayServer.Migrations modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b => { b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") - .WithMany() + .WithMany("AddressInvoices") .HasForeignKey("InvoiceDataId"); }); diff --git a/BTCPayServer/Models/InvoiceResponse.cs b/BTCPayServer/Models/InvoiceResponse.cs index 08b9db6ef..a7b6cef5c 100644 --- a/BTCPayServer/Models/InvoiceResponse.cs +++ b/BTCPayServer/Models/InvoiceResponse.cs @@ -36,11 +36,56 @@ namespace BTCPayServer.Models } } + public class InvoiceCryptoInfo + { + [JsonProperty("cryptoCode")] + public string CryptoCode { get; set; } + + [JsonProperty("rate")] + public decimal Rate { get; set; } + + //"exRates":{"USD":4320.02} + [JsonProperty("exRates")] + public Dictionary ExRates + { + get; set; + } + + //"btcPaid":"0.000000" + [JsonProperty("paid")] + public string Paid + { + get; set; + } + + //"btcPrice":"0.001157" + [JsonProperty("price")] + public string Price + { + get; set; + } + + //"btcDue":"0.001160" + [JsonProperty("due")] + public string Due + { + get; set; + } + + public NBitpayClient.InvoicePaymentUrls PaymentUrls + { + get; set; + } + public string Address { get; set; } + public string Url { get; set; } + } + //{"facade":"pos/invoice","data":{,}} public class InvoiceResponse { //"url":"https://test.bitpay.com/invoice?id=9saCHtp1zyPcNoi3rDdBu8" [JsonProperty("url")] + [Obsolete("Use CryptoInfo.Url instead")] public string Url { get; set; @@ -59,6 +104,7 @@ namespace BTCPayServer.Models } //"btcPrice":"0.001157" [JsonProperty("btcPrice")] + [Obsolete("Use CryptoInfo.Price instead")] public string BTCPrice { get; set; @@ -66,11 +112,15 @@ namespace BTCPayServer.Models //"btcDue":"0.001160" [JsonProperty("btcDue")] + [Obsolete("Use CryptoInfo.Due instead")] public string BTCDue { get; set; } + [JsonProperty("cryptoInfo")] + public List CryptoInfo { get; set; } + //"price":5 [JsonProperty("price")] public double Price @@ -87,6 +137,7 @@ namespace BTCPayServer.Models //"exRates":{"USD":4320.02} [JsonProperty("exRates")] + [Obsolete("Use CryptoInfo.ExRates instead")] public Dictionary ExRates { get; set; @@ -156,6 +207,7 @@ namespace BTCPayServer.Models //"btcPaid":"0.000000" [JsonProperty("btcPaid")] + [Obsolete("Use CryptoInfo.Paid instead")] public string BTCPaid { get; set; @@ -163,7 +215,8 @@ namespace BTCPayServer.Models //"rate":4320.02 [JsonProperty("rate")] - public double Rate + [Obsolete("Use CryptoInfo.Rate instead")] + public decimal Rate { get; set; } @@ -178,6 +231,7 @@ namespace BTCPayServer.Models //"paymentUrls":{"BIP21":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160","BIP72":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160&r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP72b":"bitcoin:?r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP73":"https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8"} [JsonProperty("paymentUrls")] + [Obsolete("Use CryptoInfo.PaymentsUrls instead")] public NBitpayClient.InvoicePaymentUrls PaymentUrls { get; set; @@ -197,6 +251,7 @@ namespace BTCPayServer.Models //"bitcoinAddress":"muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv" [JsonProperty("bitcoinAddress")] + [Obsolete("Use CryptoInfo.Address instead")] public string BitcoinAddress { get; set; diff --git a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs index 72d2e16e5..25b04b9c3 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs @@ -92,7 +92,7 @@ namespace BTCPayServer.Models.InvoicingModels get; set; } - public double Rate + public decimal Rate { get; internal set; diff --git a/BTCPayServer/Services/Fees/IFeeProviderFactory.cs b/BTCPayServer/Services/Fees/IFeeProviderFactory.cs new file mode 100644 index 000000000..07d2af778 --- /dev/null +++ b/BTCPayServer/Services/Fees/IFeeProviderFactory.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Services +{ + public interface IFeeProviderFactory + { + IFeeProvider CreateFeeProvider(BTCPayNetwork network); + } +} diff --git a/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs b/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs index 48958dc44..d5386b570 100644 --- a/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs +++ b/BTCPayServer/Services/Fees/NBxplorerFeeProvider.cs @@ -8,29 +8,49 @@ using System.Threading.Tasks; namespace BTCPayServer.Services.Fees { - public class NBXplorerFeeProvider : IFeeProvider + public class NBXplorerFeeProviderFactory : IFeeProviderFactory { + public NBXplorerFeeProviderFactory(ExplorerClient explorerClient) + { + if (explorerClient == null) + throw new ArgumentNullException(nameof(explorerClient)); + _ExplorerClient = explorerClient; + } + + private readonly ExplorerClient _ExplorerClient; public ExplorerClient ExplorerClient { - get; set; + get + { + return _ExplorerClient; + } } - public FeeRate Fallback + + public FeeRate Fallback { get; set; } + public int BlockTarget { get; set; } + public IFeeProvider CreateFeeProvider(BTCPayNetwork network) { - get; set; + return new NBXplorerFeeProvider(this); } - public int BlockTarget + } + public class NBXplorerFeeProvider : IFeeProvider + { + public NBXplorerFeeProvider(NBXplorerFeeProviderFactory factory) { - get; set; + if (factory == null) + throw new ArgumentNullException(nameof(factory)); + _Factory = factory; } + private readonly NBXplorerFeeProviderFactory _Factory; public async Task GetFeeRateAsync() { try { - return (await ExplorerClient.GetFeeRateAsync(BlockTarget).ConfigureAwait(false)).FeeRate; + return (await _Factory.ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate; } catch (NBXplorerException ex) when (ex.Error.HttpCode == 400 && ex.Error.Code == "fee-estimation-unavailable") { - return Fallback; + return _Factory.Fallback; } } } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index f598f1efb..0a6f106a0 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json.Linq; using NBitcoin.DataEncoders; using BTCPayServer.Data; using NBXplorer.Models; +using NBXplorer; namespace BTCPayServer.Services.Invoices { @@ -82,7 +83,7 @@ namespace BTCPayServer.Services.Invoices } [JsonProperty(PropertyName = "price")] - public double Price + public decimal Price { get; set; } @@ -111,65 +112,17 @@ namespace BTCPayServer.Services.Invoices get; set; } - public int GetTxCount() - { - return Calculate().TxCount; - } - public string OrderId { get; set; } - public Money GetTotalCryptoDue() - { - return Calculate().TotalDue; - } - - private (Money TotalDue, Money Paid, int TxCount) Calculate() - { - var totalDue = Money.Coins((decimal)(ProductInformation.Price / Rate)) + TxFee; - var paid = Money.Zero; - int txCount = 1; - var payments = - Payments - .Where(p => p.Accounted) - .OrderByDescending(p => p.ReceivedTime) - .Select(_ => - { - paid += _.Output.Value; - return _; - }) - .TakeWhile(_ => - { - var paidEnough = totalDue <= paid; - if (!paidEnough) - { - txCount++; - totalDue += TxFee; - } - return !paidEnough; - }) - .ToArray(); - return (totalDue, paid, txCount); - } - - public Money GetTotalPaid() - { - return Calculate().Paid; - } - public Money GetCryptoDue() - { - var o = Calculate(); - var v = o.TotalDue - o.Paid; - return v < Money.Zero ? Money.Zero : v; - } - public SpeedPolicy SpeedPolicy { get; set; } - public double Rate + [Obsolete("Use GetCryptoData(network).Rate instead")] + public decimal Rate { get; set; } @@ -181,7 +134,9 @@ namespace BTCPayServer.Services.Invoices { get; set; } - public BitcoinAddress DepositAddress + + [Obsolete("Use GetCryptoData(network).DepositAddress instead")] + public string DepositAddress { get; set; } @@ -231,6 +186,8 @@ namespace BTCPayServer.Services.Invoices get; set; } + + [Obsolete("Use GetCryptoData(network).TxFee instead")] public Money TxFee { get; @@ -251,6 +208,10 @@ namespace BTCPayServer.Services.Invoices get; set; } + + [Obsolete("Use Set/GetCryptoData() instead")] + public JObject CryptoData { get; set; } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] public DateTimeOffset MonitoringExpiration { @@ -275,7 +236,7 @@ namespace BTCPayServer.Services.Invoices } - public InvoiceResponse EntityToDTO() + public InvoiceResponse EntityToDTO(BTCPayNetworkProvider networkProvider) { ServerUrl = ServerUrl ?? ""; InvoiceResponse dto = new InvoiceResponse @@ -286,34 +247,62 @@ namespace BTCPayServer.Services.Invoices CurrentTime = DateTimeOffset.UtcNow, InvoiceTime = InvoiceTime, ExpirationTime = ExpirationTime, - BTCPrice = Money.Coins((decimal)(ProductInformation.Price / Rate)).ToString(), Status = Status, - Url = ServerUrl.WithTrailingSlash() + "invoice?id=" + Id, Currency = ProductInformation.Currency, - Flags = new Flags() { Refundable = Refundable }, - Rate = Rate + Flags = new Flags() { Refundable = Refundable } }; + + dto.CryptoInfo = new List(); + foreach (var info in this.GetCryptoData().Values) + { + var cryptoInfo = new InvoiceCryptoInfo(); + cryptoInfo.CryptoCode = info.CryptoCode; + cryptoInfo.Rate = info.Rate; + cryptoInfo.Price = Money.Coins(ProductInformation.Price / cryptoInfo.Rate).ToString(); + cryptoInfo.Due = info.GetCryptoDue().ToString(); + var paid = Payments.Where(p => p.Accounted && p.GetCryptoCode() == info.CryptoCode).Select(p => p.GetValue()).Sum(); + cryptoInfo.Paid = paid.ToString(); + cryptoInfo.Address = info.DepositAddress; + cryptoInfo.ExRates = new Dictionary + { + { ProductInformation.Currency, (double)cryptoInfo.Rate } + }; + + var scheme = networkProvider.GetNetwork(info.CryptoCode)?.UriScheme ?? "BTC"; + var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode; + cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"invoice{cryptoSuffix}?id=" + Id; + + + cryptoInfo.PaymentUrls = new InvoicePaymentUrls() + { + BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}", + BIP72b = $"{scheme}:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}", + BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}"), + BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}", + }; +#pragma warning disable CS0618 + if (info.CryptoCode == "BTC") + { + dto.Url = cryptoInfo.Url; + dto.BTCPrice = cryptoInfo.Price; + dto.Rate = cryptoInfo.Rate; + dto.ExRates = cryptoInfo.ExRates; + dto.BitcoinAddress = cryptoInfo.Address; + dto.BTCPaid = cryptoInfo.Paid; + dto.BTCDue = cryptoInfo.Due; + dto.PaymentUrls = cryptoInfo.PaymentUrls; + } +#pragma warning restore CS0618 + + dto.CryptoInfo.Add(cryptoInfo); + } + Populate(ProductInformation, dto); Populate(BuyerInformation, dto); - dto.ExRates = new Dictionary - { - { ProductInformation.Currency, Rate } - }; - dto.PaymentUrls = new InvoicePaymentUrls() - { - BIP72 = $"bitcoin:{DepositAddress}?amount={GetCryptoDue()}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}")}", - BIP72b = $"bitcoin:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}")}", - BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}"), - BIP21 = $"bitcoin:{DepositAddress}?amount={GetCryptoDue()}", - }; - dto.BitcoinAddress = DepositAddress.ToString(); + dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for dto.Guid = Guid.NewGuid().ToString(); - var paid = Payments.Where(p => p.Accounted).Select(p => p.Output.Value).Sum(); - dto.BTCPaid = paid.ToString(); - dto.BTCDue = GetCryptoDue().ToString(); - dto.ExceptionStatus = ExceptionStatus == null ? new JValue(false) : new JValue(ExceptionStatus); return dto; } @@ -324,11 +313,135 @@ namespace BTCPayServer.Services.Invoices JsonConvert.PopulateObject(str, dest); } + internal bool Support(BTCPayNetwork network) + { + var rates = GetCryptoData(); + return rates.TryGetValue(network.CryptoCode, out var data); + } + + public CryptoData GetCryptoData(string cryptoCode) + { + GetCryptoData().TryGetValue(cryptoCode, out var data); + return data; + } + + public CryptoData GetCryptoData(BTCPayNetwork network) + { + GetCryptoData().TryGetValue(network.CryptoCode, out var data); + return data; + } + + public Dictionary GetCryptoData() + { + Dictionary rates = new Dictionary(); + var serializer = new Serializer(Dummy); +#pragma warning disable CS0618 + // Legacy + if (Rate != 0.0m) + { + rates.TryAdd("BTC", new CryptoData() { ParentEntity = this, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress }); + } + if (CryptoData != null) + { + foreach (var prop in CryptoData.Properties()) + { + var r = serializer.ToObject(prop.Value.ToString()); + r.CryptoCode = prop.Name; + r.ParentEntity = this; + rates.TryAdd(r.CryptoCode, r); + } + } +#pragma warning restore CS0618 + return rates; + } + + Network Dummy = Network.Main; + public void SetCryptoData(Dictionary 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 + CryptoData = obj; +#pragma warning restore CS0618 + } + } + + public class CryptoData + { + [JsonIgnore] + public InvoiceEntity ParentEntity { get; set; } + [JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string CryptoCode { get; set; } + [JsonProperty(PropertyName = "rate")] + public decimal Rate { get; set; } + [JsonProperty(PropertyName = "feeRate")] + public FeeRate FeeRate { get; set; } + [JsonProperty(PropertyName = "txFee")] + public Money TxFee { get; set; } + [JsonProperty(PropertyName = "depositAddress")] + public string DepositAddress { get; set; } + public Money GetNetworkFee() { var item = Calculate(); return TxFee * item.TxCount; } + + public int GetTxCount() + { + return Calculate().TxCount; + } + + public Money GetTotalCryptoDue() + { + return Calculate().TotalDue; + } + + private (Money TotalDue, Money Paid, int TxCount) Calculate() + { + var cryptoData = ParentEntity.GetCryptoData(); + var totalDue = Money.Coins(ParentEntity.ProductInformation.Price / Rate) + TxFee; + var paid = Money.Zero; + int txCount = 1; + var payments = + ParentEntity.Payments + .Where(p => p.Accounted) + .OrderByDescending(p => p.ReceivedTime) + .Select(_ => + { + paid += _.GetValue(cryptoData, CryptoCode); + return _; + }) + .TakeWhile(_ => + { + var paidEnough = totalDue <= paid; + if (!paidEnough && _.GetCryptoCode() == CryptoCode) + { + txCount++; + totalDue += TxFee; + } + return !paidEnough; + }) + .ToArray(); + return (totalDue, paid, txCount); + } + + public Money GetTotalPaid() + { + return Calculate().Paid; + } + public Money GetCryptoDue() + { + var o = Calculate(); + var v = o.TotalDue - o.Paid; + return v < Money.Zero ? Money.Zero : v; + } } public class AccountedPaymentEntity @@ -352,13 +465,59 @@ namespace BTCPayServer.Services.Invoices { get; set; } + + [Obsolete("Use GetValue() or GetScriptPubKey() instead")] public TxOut Output { get; set; } + + public Script GetScriptPubKey() + { +#pragma warning disable CS0618 + return Output.ScriptPubKey; +#pragma warning restore CS0618 + } + public bool Accounted { get; set; } + + [Obsolete("Use GetCryptoCode() instead")] + public string CryptoCode + { + get; + set; + } + public Money GetValue() + { +#pragma warning disable CS0618 + return Output.Value; +#pragma warning restore CS0618 + } + public Money GetValue(Dictionary cryptoData, string cryptoCode) + { +#pragma warning disable CS0618 + var to = cryptoCode; + var from = GetCryptoCode(); + if (to == from) + return Output.Value; + var fromRate = cryptoData[from].Rate; + var toRate = cryptoData[to].Rate; + + var fiatValue = fromRate * Output.Value.ToDecimal(MoneyUnit.BTC); + var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate; + return Money.Coins(otherCurrencyValue); +#pragma warning restore CS0618 + } + + public string GetCryptoCode() + { +#pragma warning disable CS0618 + return CryptoCode ?? "BTC"; +#pragma warning restore CS0618 + } + } } diff --git a/BTCPayServer/Services/Invoices/InvoiceNotificationManager.cs b/BTCPayServer/Services/Invoices/InvoiceNotificationManager.cs index bdb930b77..16c977452 100644 --- a/BTCPayServer/Services/Invoices/InvoiceNotificationManager.cs +++ b/BTCPayServer/Services/Invoices/InvoiceNotificationManager.cs @@ -45,17 +45,20 @@ namespace BTCPayServer.Services.Invoices IBackgroundJobClient _JobClient; EventAggregator _EventAggregator; InvoiceRepository _InvoiceRepository; + BTCPayNetworkProvider _NetworkProvider; public InvoiceNotificationManager( IBackgroundJobClient jobClient, EventAggregator eventAggregator, InvoiceRepository invoiceRepository, + BTCPayNetworkProvider networkProvider, ILogger logger) { Logger = logger as ILogger ?? NullLogger.Instance; _JobClient = jobClient; _EventAggregator = eventAggregator; _InvoiceRepository = invoiceRepository; + _NetworkProvider = networkProvider; } async Task Notify(InvoiceEntity invoice) @@ -110,19 +113,15 @@ namespace BTCPayServer.Services.Invoices } } - private static async Task SendNotification(InvoiceEntity invoice, CancellationToken cancellation) + private async Task SendNotification(InvoiceEntity invoice, CancellationToken cancellation) { var request = new HttpRequestMessage(); request.Method = HttpMethod.Post; - var dto = invoice.EntityToDTO(); + var dto = invoice.EntityToDTO(_NetworkProvider); InvoicePaymentNotification notification = new InvoicePaymentNotification() { Id = dto.Id, - Url = dto.Url, - BTCDue = dto.BTCDue, - BTCPaid = dto.BTCPaid, - BTCPrice = dto.BTCPrice, Currency = dto.Currency, CurrentTime = dto.CurrentTime, ExceptionStatus = dto.ExceptionStatus, @@ -130,10 +129,23 @@ namespace BTCPayServer.Services.Invoices InvoiceTime = dto.InvoiceTime, PosData = dto.PosData, Price = dto.Price, - Rate = dto.Rate, Status = dto.Status, BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) } }; + + // 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"); + if(btcCryptoInfo != null) + { +#pragma warning disable CS0618 + notification.Rate = (double)dto.Rate; + notification.Url = dto.Url; + notification.BTCDue = dto.BTCDue; + notification.BTCPaid = dto.BTCPaid; + notification.BTCPrice = dto.BTCPrice; +#pragma warning restore CS0618 + } request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute); request.Content = new StringContent(JsonConvert.SerializeObject(notification), Encoding.UTF8, "application/json"); var response = await _Client.SendAsync(request, cancellation); diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 8a3f40d39..c6a314fac 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -45,7 +45,7 @@ namespace BTCPayServer.Services.Invoices _Network = value; } } - + private ApplicationDbContextFactory _ContextFactory; private CustomThreadPool _IndexerThread; public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath, Network network) @@ -102,8 +102,9 @@ namespace BTCPayServer.Services.Invoices } } - public async Task CreateInvoiceAsync(string storeId, InvoiceEntity invoice) + public async Task CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider) { + List textSearch = new List(); invoice = Clone(invoice); invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); invoice.Payments = new List(); @@ -121,52 +122,77 @@ namespace BTCPayServer.Services.Invoices ItemCode = invoice.ProductInformation.ItemCode, CustomerEmail = invoice.RefundMail }); - context.AddressInvoices.Add(new AddressInvoiceData() + + foreach (var cryptoData in invoice.GetCryptoData().Values) { - Address = invoice.DepositAddress.ScriptPubKey.Hash.ToString(), - InvoiceDataId = invoice.Id, - CreatedTime = DateTimeOffset.UtcNow, - }); - context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData() - { - InvoiceDataId = invoice.Id, - Address = invoice.DepositAddress.ToString(), - Assigned = DateTimeOffset.UtcNow - }); + var network = networkProvider.GetNetwork(cryptoData.CryptoCode); + if (network == null) + throw new InvalidOperationException("CryptoCode unsupported"); + context.AddressInvoices.Add(new AddressInvoiceData() + { + Address = BitcoinAddress.Create(cryptoData.DepositAddress, network.NBitcoinNetwork).ScriptPubKey.Hash.ToString(), + InvoiceDataId = invoice.Id, + CreatedTime = DateTimeOffset.UtcNow, + }); + context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData() + { + InvoiceDataId = invoice.Id, + Address = cryptoData.DepositAddress, +#pragma warning disable CS0618 + CryptoCode = cryptoData.CryptoCode, +#pragma warning restore CS0618 + Assigned = DateTimeOffset.UtcNow + }); + textSearch.Add(cryptoData.DepositAddress); + textSearch.Add(cryptoData.GetTotalCryptoDue().ToString()); + } context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id }); await context.SaveChangesAsync().ConfigureAwait(false); } - AddToTextSearch(invoice.Id, - invoice.Id, - invoice.DepositAddress.ToString(), - invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture), - invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture), - invoice.GetTotalCryptoDue().ToString(), - invoice.OrderId, - ToString(invoice.BuyerInformation), - ToString(invoice.ProductInformation), - invoice.StoreId - ); + textSearch.Add(invoice.Id); + textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture)); + textSearch.Add(invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)); + textSearch.Add(invoice.OrderId); + textSearch.Add(ToString(invoice.BuyerInformation)); + textSearch.Add(ToString(invoice.ProductInformation)); + textSearch.Add(invoice.StoreId); + + AddToTextSearch(invoice.Id, textSearch.ToArray()); return invoice; } - public async Task NewAddress(string invoiceId, BitcoinAddress bitcoinAddress) + public async Task NewAddress(string invoiceId, BitcoinAddress bitcoinAddress, BTCPayNetwork network) { using (var context = _ContextFactory.CreateContext()) { var invoice = await context.Invoices.FirstOrDefaultAsync(i => i.Id == invoiceId); if (invoice == null) return false; + var invoiceEntity = ToObject(invoice.Blob); - var old = invoiceEntity.DepositAddress; - invoiceEntity.DepositAddress = bitcoinAddress; - invoice.Blob = ToBytes(invoiceEntity); - if (old != null) + var cryptoData = invoiceEntity.GetCryptoData(); + var currencyData = cryptoData.Where(c => c.Value.CryptoCode == network.CryptoCode).Select(f => f.Value).FirstOrDefault(); + if (currencyData == null) + return false; + + if (currencyData.DepositAddress != null) { - MarkUnassigned(invoiceId, old, context); + MarkUnassigned(invoiceId, invoiceEntity, context, network.CryptoCode); } + + currencyData.DepositAddress = bitcoinAddress.ToString(); + +#pragma warning disable CS0618 + if (network.CryptoCode == "BTC") + { + invoiceEntity.DepositAddress = currencyData.DepositAddress; + } +#pragma warning disable CS0618 + invoiceEntity.SetCryptoData(cryptoData); + invoice.Blob = ToBytes(invoiceEntity); + context.AddressInvoices.Add(new AddressInvoiceData() { Address = bitcoinAddress.ScriptPubKey.Hash.ToString(), InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow }); context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData() { @@ -181,14 +207,19 @@ namespace BTCPayServer.Services.Invoices } } - private static void MarkUnassigned(string invoiceId, BitcoinAddress old, ApplicationDbContext context) + private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, string cryptoCode) { - var historical = new HistoricalAddressInvoiceData(); - historical.InvoiceDataId = invoiceId; - historical.Address = old.ToString(); - historical.UnAssigned = DateTimeOffset.UtcNow; - context.Attach(historical); - context.Entry(historical).Property(o => o.UnAssigned).IsModified = true; + foreach (var address in entity.GetCryptoData()) + { + if (cryptoCode != null && cryptoCode != address.Value.CryptoCode) + continue; + var historical = new HistoricalAddressInvoiceData(); + historical.InvoiceDataId = invoiceId; + historical.Address = address.Value.DepositAddress; + historical.UnAssigned = DateTimeOffset.UtcNow; + context.Attach(historical); + context.Entry(historical).Property(o => o.UnAssigned).IsModified = true; + } } public async Task UnaffectAddress(string invoiceId) @@ -199,9 +230,7 @@ namespace BTCPayServer.Services.Invoices if (invoiceData == null) return; var invoiceEntity = ToObject(invoiceData.Blob); - if (invoiceEntity.DepositAddress == null) - return; - MarkUnassigned(invoiceId, invoiceEntity.DepositAddress, context); + MarkUnassigned(invoiceId, invoiceEntity, context, null); try { await context.SaveChangesAsync(); @@ -223,7 +252,7 @@ namespace BTCPayServer.Services.Invoices void AddToTextSearch(string invoiceId, params string[] terms) { - _IndexerThread.DoAsync(() => + _IndexerThread.DoAsync(() => { using (var tx = _Engine.GetTransaction()) { @@ -284,7 +313,7 @@ namespace BTCPayServer.Services.Invoices private InvoiceEntity ToEntity(InvoiceData invoice) { var entity = ToObject(invoice.Blob); - entity.Payments = invoice.Payments.Select(p => + entity.Payments = invoice.Payments.Select(p => { var paymentEntity = ToObject(p.Blob); paymentEntity.Accounted = p.Accounted; diff --git a/BTCPayServer/Services/Invoices/InvoiceWatcher.cs b/BTCPayServer/Services/Invoices/InvoiceWatcher.cs index 3d97e6a35..80df7e438 100644 --- a/BTCPayServer/Services/Invoices/InvoiceWatcher.cs +++ b/BTCPayServer/Services/Invoices/InvoiceWatcher.cs @@ -29,9 +29,10 @@ namespace BTCPayServer.Services.Invoices DerivationStrategyFactory _DerivationFactory; EventAggregator _EventAggregator; BTCPayWallet _Wallet; - + BTCPayNetworkProvider _NetworkProvider; public InvoiceWatcher(ExplorerClient explorerClient, + BTCPayNetworkProvider networkProvider, InvoiceRepository invoiceRepository, EventAggregator eventAggregator, BTCPayWallet wallet, @@ -44,6 +45,7 @@ namespace BTCPayServer.Services.Invoices _DerivationFactory = new DerivationStrategyFactory(_ExplorerClient.Network); _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); _EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); + _NetworkProvider = networkProvider; accessor.Instance = this; } CompositeDisposable leases = new CompositeDisposable(); @@ -126,6 +128,7 @@ namespace BTCPayServer.Services.Invoices var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy); changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false); + var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray(); List receivedCoins = new List(); foreach (var received in utxos) @@ -142,7 +145,9 @@ namespace BTCPayServer.Services.Invoices dirtyAddress = true; } ////// - + var network = _NetworkProvider.GetNetwork("BTC"); + var cryptoData = invoice.GetCryptoData(network); + var cryptoDataAll = invoice.GetCryptoData(); if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow) { needSave = true; @@ -154,8 +159,8 @@ namespace BTCPayServer.Services.Invoices if (invoice.Status == "new" || invoice.Status == "expired") { - var totalPaid = (await GetPaymentsWithTransaction(invoice)).Select(p => p.Payment.Output.Value).Sum(); - if (totalPaid >= invoice.GetTotalCryptoDue()) + var totalPaid = (await GetPaymentsWithTransaction(invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); + if (totalPaid >= cryptoData.GetTotalCryptoDue()) { if (invoice.Status == "new") { @@ -172,23 +177,23 @@ namespace BTCPayServer.Services.Invoices } } - if (totalPaid > invoice.GetTotalCryptoDue() && invoice.ExceptionStatus != "paidOver") + if (totalPaid > cryptoData.GetTotalCryptoDue() && invoice.ExceptionStatus != "paidOver") { invoice.ExceptionStatus = "paidOver"; await _InvoiceRepository.UnaffectAddress(invoice.Id); needSave = true; } - if (totalPaid < invoice.GetTotalCryptoDue() && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial") + if (totalPaid < cryptoData.GetTotalCryptoDue() && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial") { - Logs.PayServer.LogInformation("Paid to " + invoice.DepositAddress); + Logs.PayServer.LogInformation("Paid to " + cryptoData.DepositAddress); invoice.ExceptionStatus = "paidPartial"; needSave = true; if (dirtyAddress) { var address = await _Wallet.ReserveAddressAsync(_DerivationFactory.Parse(invoice.DerivationStrategy)); Logs.PayServer.LogInformation("Generate new " + address); - await _InvoiceRepository.NewAddress(invoice.Id, address); + await _InvoiceRepository.NewAddress(invoice.Id, address, network); } } } @@ -210,13 +215,13 @@ namespace BTCPayServer.Services.Invoices transactions = transactions.Where(t => t.Confirmations >= 6); } - var chainTotalConfirmed = chainConfirmedTransactions.Select(t => t.Payment.Output.Value).Sum(); + var chainTotalConfirmed = chainConfirmedTransactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (// Is after the monitoring deadline (invoice.MonitoringExpiration < DateTimeOffset.UtcNow) && // And not enough amount confirmed - (chainTotalConfirmed < invoice.GetTotalCryptoDue())) + (chainTotalConfirmed < cryptoData.GetTotalCryptoDue())) { await _InvoiceRepository.UnaffectAddress(invoice.Id); postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "invalid"))); @@ -225,8 +230,8 @@ namespace BTCPayServer.Services.Invoices } else { - var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum(); - if (totalConfirmed >= invoice.GetTotalCryptoDue()) + var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); + if (totalConfirmed >= cryptoData.GetTotalCryptoDue()) { await _InvoiceRepository.UnaffectAddress(invoice.Id); postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "confirmed"))); @@ -240,8 +245,8 @@ namespace BTCPayServer.Services.Invoices { var transactions = await GetPaymentsWithTransaction(invoice); transactions = transactions.Where(t => t.Confirmations >= 6); - var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum(); - if (totalConfirmed >= invoice.GetTotalCryptoDue()) + var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); + if (totalConfirmed >= cryptoData.GetTotalCryptoDue()) { postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "complete"))); invoice.Status = "complete";