diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 317e0682c..237bd882c 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -43,6 +43,8 @@ namespace BTCPayServer.Tests { get; set; } + + public Uri LTCNBXplorerUri { get; set; } public string CookieFile { get; set; @@ -79,7 +81,9 @@ namespace BTCPayServer.Tests config.AppendLine($"regtest=1"); config.AppendLine($"port={Port}"); config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}"); + config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}"); config.AppendLine($"explorer.cookiefile={CookieFile}"); + config.AppendLine($"ltc.explorer.cookiefile={CookieFile}"); config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}"); if (Postgres != null) config.AppendLine($"postgres=" + Postgres); diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index daa679139..0345b3fa1 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -47,11 +47,17 @@ namespace BTCPayServer.Tests Directory.CreateDirectory(_Directory); - ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_RPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), Network); + var network = new BTCPayNetworkProvider(Network); + ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_RPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), network.GetNetwork("BTC").NBitcoinNetwork); + LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), network.GetNetwork("LTC").NBitcoinNetwork); + ExplorerClient = new ExplorerClient(Network, new Uri(GetEnvironment("TESTS_NBXPLORERURL", "http://127.0.0.1:32838/"))); + LTCExplorerClient = new ExplorerClient(Network, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32839/"))); + PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay")) { NBXplorerUri = ExplorerClient.Address, + LTCNBXplorerUri = LTCExplorerClient.Address, Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver") }; PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString())); @@ -107,10 +113,16 @@ namespace BTCPayServer.Tests get; set; } + public RPCClient LTCExplorerNode + { + get; set; + } + public ExplorerClient ExplorerClient { get; set; } + public ExplorerClient LTCExplorerClient { get; set; } HttpClient _Http = new HttpClient(); diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index ca786d773..a0d07d671 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -71,6 +71,23 @@ namespace BTCPayServer.Tests return store; } + public void RegisterDerivationScheme(string crytoCode) + { + RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult(); + } + public async Task RegisterDerivationSchemeAsync(string crytoCode) + { + var store = parent.PayTester.GetController(UserId); + var networkProvider = parent.PayTester.GetService(); + var derivation = new DerivationStrategyFactory(networkProvider.GetNetwork(crytoCode).NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]"); + await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel() + { + CryptoCurrency = crytoCode, + DerivationSchemeFormat = crytoCode, + DerivationScheme = derivation.ToString(), + }, "Save"); + } + public DerivationStrategyBase DerivationScheme { get; set; } private async Task RegisterAsync() diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 343a17513..3aa84efa2 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -43,7 +43,7 @@ namespace BTCPayServer.Tests entity.TxFee = Money.Coins(0.1m); entity.Rate = 5000; - var cryptoData = entity.GetCryptoData("BTC"); + var cryptoData = entity.GetCryptoData("BTC", null); Assert.NotNull(cryptoData); // Should use legacy data to build itself entity.Payments = new System.Collections.Generic.List(); entity.ProductInformation = new ProductInformation() { Price = 5000 }; @@ -92,17 +92,17 @@ namespace BTCPayServer.Tests }) })); entity.Payments = new List(); - cryptoData = entity.GetCryptoData("BTC"); + cryptoData = entity.GetCryptoData("BTC", null); accounting = cryptoData.Calculate(); Assert.Equal(Money.Coins(5.1m), accounting.Due); - cryptoData = entity.GetCryptoData("LTC"); + cryptoData = entity.GetCryptoData("LTC", 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"); + cryptoData = entity.GetCryptoData("BTC", null); accounting = cryptoData.Calculate(); Assert.Equal(Money.Coins(4.2m), accounting.Due); Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); @@ -110,7 +110,7 @@ namespace BTCPayServer.Tests Assert.Equal(Money.Coins(5.2m), accounting.TotalDue); Assert.Equal(2, accounting.TxCount); - cryptoData = entity.GetCryptoData("LTC"); + cryptoData = entity.GetCryptoData("LTC", 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); @@ -120,7 +120,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"); + cryptoData = entity.GetCryptoData("BTC", 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); @@ -128,7 +128,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"); + cryptoData = entity.GetCryptoData("LTC", null); accounting = cryptoData.Calculate(); Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due); Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); @@ -139,7 +139,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"); + cryptoData = entity.GetCryptoData("BTC", null); accounting = cryptoData.Calculate(); Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid); @@ -148,7 +148,7 @@ namespace BTCPayServer.Tests Assert.Equal(accounting.Paid, accounting.TotalDue); Assert.Equal(2, accounting.TxCount); - cryptoData = entity.GetCryptoData("LTC"); + cryptoData = entity.GetCryptoData("LTC", null); accounting = cryptoData.Calculate(); Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid); @@ -384,6 +384,93 @@ namespace BTCPayServer.Tests } } + [Fact] + public void CanPayWithTwoCurrencies() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + + // First we try payment with a merchant having only BTC + var invoice = user.BitPay.CreateInvoice(new Invoice() + { + Price = 5000.0, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + var cashCow = tester.ExplorerNode; + var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); + var firstPayment = Money.Coins(0.04m); + cashCow.SendToAddress(invoiceAddress, firstPayment); + Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.True(invoice.BtcPaid == firstPayment); + }); + + Assert.Single(invoice.CryptoInfo); // Only BTC should be presented + + var controller = tester.PayTester.GetController(null); + var checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value; + Assert.Single(checkout.AvailableCryptos); + Assert.Equal("BTC", checkout.CryptoCode); + + ////////////////////// + + // Retry now with LTC enabled + user.RegisterDerivationScheme("LTC"); + invoice = user.BitPay.CreateInvoice(new Invoice() + { + Price = 5000.0, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + cashCow = tester.ExplorerNode; + invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); + firstPayment = Money.Coins(0.04m); + cashCow.SendToAddress(invoiceAddress, firstPayment); + Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.True(invoice.BtcPaid == firstPayment); + }); + + cashCow = tester.LTCExplorerNode; + var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC"); + Assert.NotNull(ltcCryptoInfo); + invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network); + var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due)); + cashCow.Generate(2); // LTC is not worth a lot, so just to make sure we have money... + cashCow.SendToAddress(invoiceAddress, secondPayment); + + Eventually(() => + { + invoice = user.BitPay.GetInvoice(invoice.Id); + Assert.Equal(Money.Zero, invoice.BtcDue); + var ltcPaid = invoice.CryptoInfo.First(c => c.CryptoCode == "LTC"); + Assert.Equal(Money.Zero, ltcPaid.Due); + Assert.Equal(secondPayment, ltcPaid.CryptoPaid); + Assert.Equal("paid", invoice.Status); + Assert.False((bool)((JValue)invoice.ExceptionStatus).Value); + }); + + controller = tester.PayTester.GetController(null); + checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC").GetAwaiter().GetResult()).Value; + Assert.Equal(2, checkout.AvailableCryptos.Count); + Assert.Equal("LTC", checkout.CryptoCode); + } + } + [Fact] public void InvoiceFlowThroughDifferentStatesCorrectly() { @@ -398,8 +485,6 @@ namespace BTCPayServer.Tests Currency = "USD", PosData = "posData", OrderId = "orderId", - //RedirectURL = redirect + "redirect", - //NotificationURL = CallbackUri + "/notification", ItemDesc = "Some description", FullNotifications = true }, Facade.Merchant); diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 551f1fb9c..033943367 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3" +version: "3" # Run `docker-compose up dev` for bootstrapping your development environment # Doing so will expose eclair API, NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run, @@ -11,7 +11,8 @@ services: dockerfile: BTCPayServer.Tests/Dockerfile environment: TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3 - TESTS_BTCNBXPLORERURL: http://bitcoin-nbxplorer:32838/ + TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3 + TESTS_NBXPLORERURL: http://bitcoin-nbxplorer:32838/ TESTS_LTCNBXPLORERURL: http://litecoin-nbxplorer:32839/ TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver TESTS_PORT: 80 diff --git a/BTCPayServer/BTCPayNetwork.cs b/BTCPayServer/BTCPayNetwork.cs index a42480334..af5a1cb32 100644 --- a/BTCPayServer/BTCPayNetwork.cs +++ b/BTCPayServer/BTCPayNetwork.cs @@ -25,5 +25,9 @@ namespace BTCPayServer } public string CryptoImagePath { get; set; } + public override string ToString() + { + return CryptoCode; + } } } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 52c80e8f3..134efd480 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.0 - 1.0.0.64 + 1.0.0.65 @@ -22,7 +22,7 @@ - + diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index 801b102e6..506632006 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -46,7 +46,7 @@ namespace BTCPayServer.Configuration Logs.Configuration.LogInformation("Network: " + Network); - + bool btcHandled = false; foreach (var net in new BTCPayNetworkProvider(Network).GetAll()) { var nbxplorer = NBXplorer.Configuration.NetworkInformation.GetNetworkByName(net.NBitcoinNetwork.Name); @@ -54,12 +54,16 @@ namespace BTCPayServer.Configuration var cookieFile = conf.GetOrDefault($"{net.CryptoCode}.explorer.cookiefile", nbxplorer.GetDefaultCookieFile()); if (explorer != null) { +#pragma warning disable CS0618 + if (net.IsBTC) + btcHandled = true; +#pragma warning restore CS0618 ExplorerFactories.Add(net.CryptoCode, (n) => CreateExplorerClient(n, explorer, cookieFile)); } } // Handle legacy explorer.url and explorer.cookiefile - if (ExplorerFactories.Count == 0) + if (!btcHandled) { var nbxplorer = NBXplorer.Configuration.NetworkInformation.GetNetworkByName(Network.Name); // Will get BTC info var explorer = conf.GetOrDefault($"explorer.url", new Uri(nbxplorer.GetDefaultExplorerUrl(), UriKind.Absolute)); diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index c156e6746..8ba021d4e 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -59,7 +59,7 @@ namespace BTCPayServer.Controllers StatusException = invoice.ExceptionStatus }; - foreach (var data in invoice.GetCryptoData()) + 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(); @@ -128,7 +128,7 @@ namespace BTCPayServer.Controllers var network = _NetworkProvider.GetNetwork(cryptoCode); if (invoice == null || network == null || !invoice.Support(network)) return null; - var cryptoData = invoice.GetCryptoData(network); + var cryptoData = invoice.GetCryptoData(network, _NetworkProvider); var dto = invoice.EntityToDTO(_NetworkProvider); var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode == network.CryptoCode); @@ -157,12 +157,15 @@ namespace BTCPayServer.Controllers Status = invoice.Status, CryptoImage = "/" + Url.Content(network.CryptoImagePath), NetworkFeeDescription = $"{accounting.TxCount} transaction{(accounting.TxCount > 1 ? "s" : "")} x {cryptoData.TxFee} {network.CryptoCode}", - AvailableCryptos = invoice.GetCryptoData().Select(kv=> new PaymentModel.AvailableCrypto() - { - CryptoCode = kv.Key, - CryptoImage = "/" + _NetworkProvider.GetNetwork(kv.Key).CryptoImagePath, - Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, cryptoCode = kv.Key }) - }).ToList() + AvailableCryptos = invoice.GetCryptoData(_NetworkProvider) + .Where(i => i.Value.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 }) + }).Where(c => c.CryptoImage != "/") + .ToList() }; var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetCryptoCode()).Concat(new[] { network.CryptoCode }).Distinct().Count() > 1; diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 5083eb509..dbd915071 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -121,7 +121,9 @@ namespace BTCPayServer.Controllers Network: derivationStrategy.Network, RateProvider: _RateProviders.GetRateProvider(derivationStrategy.Network), FeeRateProvider: _FeeProviderFactory.CreateFeeProvider(derivationStrategy.Network))) - .Where(_ => _.Wallet != null && _.FeeRateProvider != null && _.RateProvider != null) + .Where(_ => _.Wallet != null && + _.FeeRateProvider != null && + _.RateProvider != null) .Select(_ => { return new diff --git a/BTCPayServer/Events/TxOutReceivedEvent.cs b/BTCPayServer/Events/TxOutReceivedEvent.cs index 2a614776d..d1b748a48 100644 --- a/BTCPayServer/Events/TxOutReceivedEvent.cs +++ b/BTCPayServer/Events/TxOutReceivedEvent.cs @@ -14,7 +14,7 @@ namespace BTCPayServer.Events public override string ToString() { String address = ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork)?.ToString() ?? ScriptPubKey.ToString(); - return $"{address} received a transaction"; + return $"{address} received a transaction ({Network.CryptoCode})"; } } } diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index a532b839a..5e2026e58 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -37,11 +37,11 @@ namespace BTCPayServer return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite"; } - public static async Task> GetTransactions(this BTCPayWallet client, BTCPayNetwork network, uint256[] hashes, CancellationToken cts = default(CancellationToken)) + public static async Task> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken)) { hashes = hashes.Distinct().ToArray(); var transactions = hashes - .Select(async o => await client.GetTransactionAsync(network, o, cts)) + .Select(async o => await client.GetTransactionAsync(o, cts)) .ToArray(); await Task.WhenAll(transactions).ConfigureAwait(false); return transactions.Select(t => t.Result).Where(t => t != null).ToDictionary(o => o.Transaction.GetHash()); diff --git a/BTCPayServer/HostedServices/InvoiceWatcher.cs b/BTCPayServer/HostedServices/InvoiceWatcher.cs index ac45f50da..4095a9ded 100644 --- a/BTCPayServer/HostedServices/InvoiceWatcher.cs +++ b/BTCPayServer/HostedServices/InvoiceWatcher.cs @@ -150,7 +150,7 @@ namespace BTCPayServer.HostedServices } var derivationStrategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray(); - var payments = await GetPaymentsWithTransaction(derivationStrategies, invoice); + var payments = await GetPaymentsWithTransaction(null, derivationStrategies, invoice); foreach (Task coinsAsync in GetCoinsPerNetwork(context, invoice, derivationStrategies)) { var coins = await coinsAsync; @@ -173,11 +173,11 @@ namespace BTCPayServer.HostedServices } if (dirtyAddress) { - payments = await GetPaymentsWithTransaction(derivationStrategies, invoice); + payments = await GetPaymentsWithTransaction(payments, derivationStrategies, invoice); } var network = coins.Wallet.Network; - var cryptoData = invoice.GetCryptoData(network); - var cryptoDataAll = invoice.GetCryptoData(); + var cryptoData = invoice.GetCryptoData(network, _NetworkProvider); + var cryptoDataAll = invoice.GetCryptoData(_NetworkProvider); var accounting = cryptoData.Calculate(); if (invoice.Status == "new" || invoice.Status == "expired") @@ -222,7 +222,7 @@ namespace BTCPayServer.HostedServices if (invoice.Status == "paid") { - var transactions = payments; + IEnumerable transactions = payments; if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed) { transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF); @@ -260,7 +260,7 @@ namespace BTCPayServer.HostedServices if (invoice.Status == "confirmed") { - var transactions = payments; + IEnumerable transactions = payments; transactions = transactions.Where(t => t.Confirmations >= 6); var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); if (totalConfirmed >= accounting.TotalDue) @@ -292,21 +292,59 @@ namespace BTCPayServer.HostedServices .ToArray(); } - private async Task> GetPaymentsWithTransaction(DerivationStrategy[] derivations, InvoiceEntity invoice) + + class AccountedPaymentEntities : List + { + public AccountedPaymentEntities(AccountedPaymentEntities existing) + { + if (existing != null) + _Transactions = existing._Transactions; + } + + Dictionary _Transactions = new Dictionary(); + + public void AddToCache(IEnumerable transactions) + { + foreach (var tx in transactions) + _Transactions.TryAdd(tx.Transaction.GetHash(), tx); + } + public TransactionResult GetTransaction(uint256 txId) + { + _Transactions.TryGetValue(txId, out TransactionResult result); + return result; + } + + internal IEnumerable GetTransactions() + { + return _Transactions.Values; + } + } + private async Task GetPaymentsWithTransaction(AccountedPaymentEntities previous, DerivationStrategy[] derivations, InvoiceEntity invoice) { List updatedPaymentEntities = new List(); - List accountedPayments = new List(); + AccountedPaymentEntities accountedPayments = new AccountedPaymentEntities(previous); foreach (var network in derivations.Select(d => d.Network)) { var wallet = _WalletProvider.GetWallet(network); if (wallet == null) continue; - var transactions = await wallet.GetTransactions(network, invoice.GetPayments(network).Select(t => t.Outpoint.Hash).ToArray()); - var conflicts = GetConflicts(transactions.Select(t => t.Value)); + + var hashesToFetch = new HashSet(invoice + .GetPayments(network) + .Select(t => t.Outpoint.Hash) + .Where(h => accountedPayments?.GetTransaction(h) == null) + .ToList()); + + + if (hashesToFetch.Count > 0) + { + accountedPayments.AddToCache((await wallet.GetTransactions(hashesToFetch.ToArray())).Select(t => t.Value)); + } + var conflicts = GetConflicts(accountedPayments.GetTransactions()); foreach (var payment in invoice.GetPayments(network)) { - TransactionResult tx; - if (!transactions.TryGetValue(payment.Outpoint.Hash, out tx)) + TransactionResult tx = accountedPayments.GetTransaction(payment.Outpoint.Hash); + if (tx == null) continue; AccountedPaymentEntity accountedPayment = new AccountedPaymentEntity() diff --git a/BTCPayServer/Models/InvoiceResponse.cs b/BTCPayServer/Models/InvoiceResponse.cs index 208f875d8..dd646b66e 100644 --- a/BTCPayServer/Models/InvoiceResponse.cs +++ b/BTCPayServer/Models/InvoiceResponse.cs @@ -37,81 +37,6 @@ 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" - /// - /// Amount of crypto remaining to pay this invoice - /// - [JsonProperty("due")] - public string Due - { - get; set; - } - - [JsonProperty("paymentUrls")] - public NBitpayClient.InvoicePaymentUrls PaymentUrls - { - get; set; - } - - [JsonProperty("address")] - public string Address { get; set; } - [JsonProperty("url")] - public string Url { get; set; } - - /// - /// Total amount of this invoice - /// - [JsonProperty("totalDue")] - public string TotalDue { get; set; } - - /// - /// Total amount of network fee to pay to the invoice - /// - [JsonProperty("networkFee")] - public string NetworkFee { get; set; } - - /// - /// Number of transactions required to pay - /// - [JsonProperty("txCount")] - public int TxCount { get; set; } - - /// - /// Total amount of the invoice paid in this crypto - /// - [JsonProperty("cryptoPaid")] - public Money CryptoPaid { get; set; } - } - //{"facade":"pos/invoice","data":{,}} public class InvoiceResponse { @@ -151,7 +76,7 @@ namespace BTCPayServer.Models } [JsonProperty("cryptoInfo")] - public List CryptoInfo { get; set; } + public List CryptoInfo { get; set; } //"price":5 [JsonProperty("price")] diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 82d096b9d..2970b63a8 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Text; using BTCPayServer.Models; -using NBitpayClient; using Newtonsoft.Json.Linq; using NBitcoin.DataEncoders; using BTCPayServer.Data; @@ -234,7 +233,7 @@ namespace BTCPayServer.Services.Invoices } public List GetPayments(string cryptoCode) { - return Payments.Where(p=>p.CryptoCode == cryptoCode).ToList(); + return Payments.Where(p => p.CryptoCode == cryptoCode).ToList(); } public List GetPayments(BTCPayNetwork network) { @@ -323,11 +322,11 @@ namespace BTCPayServer.Services.Invoices Flags = new Flags() { Refundable = Refundable } }; - dto.CryptoInfo = new List(); - foreach (var info in this.GetCryptoData().Values) + dto.CryptoInfo = new List(); + foreach (var info in this.GetCryptoData(networkProvider).Values) { var accounting = info.Calculate(); - var cryptoInfo = new InvoiceCryptoInfo(); + var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo(); cryptoInfo.CryptoCode = info.CryptoCode; cryptoInfo.Rate = info.Rate; cryptoInfo.Price = Money.Coins(ProductInformation.Price / cryptoInfo.Rate).ToString(); @@ -337,7 +336,7 @@ namespace BTCPayServer.Services.Invoices cryptoInfo.TotalDue = accounting.TotalDue.ToString(); cryptoInfo.NetworkFee = accounting.NetworkFee.ToString(); cryptoInfo.TxCount = accounting.TxCount; - cryptoInfo.CryptoPaid = accounting.CryptoPaid; + cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString(); cryptoInfo.Address = info.DepositAddress; cryptoInfo.ExRates = new Dictionary @@ -345,12 +344,12 @@ namespace BTCPayServer.Services.Invoices { ProductInformation.Currency, (double)cryptoInfo.Rate } }; - var scheme = networkProvider.GetNetwork(info.CryptoCode)?.UriScheme ?? "BTC"; + var scheme = info.Network.UriScheme; var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode; cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"invoice{cryptoSuffix}?id=" + Id; - cryptoInfo.PaymentUrls = new InvoicePaymentUrls() + cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls() { BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}", BIP72b = $"{scheme}:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}", @@ -392,23 +391,23 @@ namespace BTCPayServer.Services.Invoices internal bool Support(BTCPayNetwork network) { - var rates = GetCryptoData(); + var rates = GetCryptoData(null); return rates.TryGetValue(network.CryptoCode, out var data); } - public CryptoData GetCryptoData(string cryptoCode) + public CryptoData GetCryptoData(string cryptoCode, BTCPayNetworkProvider networkProvider) { - GetCryptoData().TryGetValue(cryptoCode, out var data); + GetCryptoData(networkProvider).TryGetValue(cryptoCode, out var data); return data; } - public CryptoData GetCryptoData(BTCPayNetwork network) + public CryptoData GetCryptoData(BTCPayNetwork network, BTCPayNetworkProvider networkProvider) { - GetCryptoData().TryGetValue(network.CryptoCode, out var data); + GetCryptoData(networkProvider).TryGetValue(network.CryptoCode, out var data); return data; } - public Dictionary GetCryptoData() + public Dictionary GetCryptoData(BTCPayNetworkProvider networkProvider) { Dictionary rates = new Dictionary(); var serializer = new Serializer(Dummy); @@ -416,7 +415,8 @@ namespace BTCPayServer.Services.Invoices // 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 }); + var btcNetwork = networkProvider?.GetNetwork("BTC"); + rates.TryAdd("BTC", new CryptoData() { ParentEntity = this, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress, Network = btcNetwork }); } if (CryptoData != null) { @@ -425,6 +425,7 @@ namespace BTCPayServer.Services.Invoices var r = serializer.ToObject(prop.Value.ToString()); r.CryptoCode = prop.Name; r.ParentEntity = this; + r.Network = networkProvider?.GetNetwork(r.CryptoCode); rates.TryAdd(r.CryptoCode, r); } } @@ -433,6 +434,14 @@ namespace BTCPayServer.Services.Invoices } Network Dummy = Network.Main; + + public void SetCryptoData(CryptoData cryptoData) + { + var dict = GetCryptoData(null); + dict.AddOrReplace(cryptoData.CryptoCode, cryptoData); + SetCryptoData(dict); + } + public void SetCryptoData(Dictionary cryptoData) { var obj = new JObject(); @@ -485,6 +494,8 @@ namespace BTCPayServer.Services.Invoices { [JsonIgnore] public InvoiceEntity ParentEntity { get; set; } + [JsonIgnore] + public BTCPayNetwork Network { get; set; } [JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)] public string CryptoCode { get; set; } [JsonProperty(PropertyName = "rate")] @@ -498,7 +509,7 @@ namespace BTCPayServer.Services.Invoices public CryptoDataAccounting Calculate() { - var cryptoData = ParentEntity.GetCryptoData(); + var cryptoData = ParentEntity.GetCryptoData(null); var totalDue = Money.Coins(ParentEntity.ProductInformation.Price / Rate); var paid = Money.Zero; var cryptoPaid = Money.Zero; diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index c5cce5c2e..917e237d0 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -125,16 +125,15 @@ namespace BTCPayServer.Services.Invoices CustomerEmail = invoice.RefundMail }); - foreach (var cryptoData in invoice.GetCryptoData().Values) + foreach (var cryptoData in invoice.GetCryptoData(networkProvider).Values) { - var network = networkProvider.GetNetwork(cryptoData.CryptoCode); - if (network == null) + if (cryptoData.Network == null) throw new InvalidOperationException("CryptoCode unsupported"); context.AddressInvoices.Add(new AddressInvoiceData() { InvoiceDataId = invoice.Id, CreatedTime = DateTimeOffset.UtcNow, - }.SetHash(BitcoinAddress.Create(cryptoData.DepositAddress, network.NBitcoinNetwork).ScriptPubKey.Hash, network.CryptoCode)); + }.SetHash(BitcoinAddress.Create(cryptoData.DepositAddress, cryptoData.Network.NBitcoinNetwork).ScriptPubKey.Hash, cryptoData.CryptoCode)); context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData() { InvoiceDataId = invoice.Id, @@ -169,8 +168,7 @@ namespace BTCPayServer.Services.Invoices return false; var invoiceEntity = ToObject(invoice.Blob); - var cryptoData = invoiceEntity.GetCryptoData(); - var currencyData = cryptoData.Where(c => c.Value.CryptoCode == network.CryptoCode).Select(f => f.Value).FirstOrDefault(); + var currencyData = invoiceEntity.GetCryptoData(network, null); if (currencyData == null) return false; @@ -187,7 +185,7 @@ namespace BTCPayServer.Services.Invoices invoiceEntity.DepositAddress = currencyData.DepositAddress; } #pragma warning restore CS0618 - invoiceEntity.SetCryptoData(cryptoData); + invoiceEntity.SetCryptoData(currencyData); invoice.Blob = ToBytes(invoiceEntity); context.AddressInvoices.Add(new AddressInvoiceData() { @@ -207,7 +205,7 @@ namespace BTCPayServer.Services.Invoices private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, string cryptoCode) { - foreach (var address in entity.GetCryptoData()) + foreach (var address in entity.GetCryptoData(null)) { if (cryptoCode != null && cryptoCode != address.Value.CryptoCode) continue; diff --git a/BTCPayServer/Services/Rates/CachedRateProvider.cs b/BTCPayServer/Services/Rates/CachedRateProvider.cs index 865b331fc..b64466b69 100644 --- a/BTCPayServer/Services/Rates/CachedRateProvider.cs +++ b/BTCPayServer/Services/Rates/CachedRateProvider.cs @@ -37,13 +37,7 @@ namespace BTCPayServer.Services.Rates return _Inner.GetRateAsync(currency); }); } - - private bool TryGetFromCache(string key, out object obj) - { - obj = _MemoryCache.Get(key); - return obj != null; - } - + public Task> GetRatesAsync() { return _MemoryCache.GetOrCreateAsync("GLOBAL_RATES", (ICacheEntry entry) => diff --git a/BTCPayServer/Services/Wallets/BTCPayWallet.cs b/BTCPayServer/Services/Wallets/BTCPayWallet.cs index d7ea6a1d6..f8808587c 100644 --- a/BTCPayServer/Services/Wallets/BTCPayWallet.cs +++ b/BTCPayServer/Services/Wallets/BTCPayWallet.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using BTCPayServer.Data; using System.Threading; using NBXplorer.Models; +using Microsoft.Extensions.Caching.Memory; namespace BTCPayServer.Services.Wallets { @@ -51,6 +52,8 @@ namespace BTCPayServer.Services.Wallets } } + public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(60); + public async Task ReserveAddressAsync(DerivationStrategyBase derivationStrategy) { var pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false); @@ -62,10 +65,8 @@ namespace BTCPayServer.Services.Wallets await _Client.TrackAsync(derivationStrategy); } - public Task GetTransactionAsync(BTCPayNetwork network, uint256 txId, CancellationToken cancellation = default(CancellationToken)) + public Task GetTransactionAsync(uint256 txId, CancellationToken cancellation = default(CancellationToken)) { - if (network == null) - throw new ArgumentNullException(nameof(network)); if (txId == null) throw new ArgumentNullException(nameof(txId)); return _Client.GetTransactionAsync(txId, cancellation); diff --git a/BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs b/BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs index 320971ca7..7b8e46756 100644 --- a/BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs +++ b/BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; namespace BTCPayServer.Services.Wallets { @@ -29,7 +30,7 @@ namespace BTCPayServer.Services.Wallets throw new ArgumentNullException(nameof(cryptoCode)); var network = _NetworkProvider.GetNetwork(cryptoCode); var client = _Client.GetExplorerClient(cryptoCode); - if (network == null && client == null) + if (network == null || client == null) return null; return new BTCPayWallet(client, network); }