diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index eab823615..048c16dc5 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1429,6 +1429,7 @@ namespace BTCPayServer.Tests vmpos.ButtonText = "{0} Purchase"; vmpos.CustomButtonText = "Nicolas Sexy Hair"; vmpos.CustomTipText = "Wanna tip?"; + vmpos.CustomTipPercentages = "15,18,20"; vmpos.Template = @" apple: price: 5.0 @@ -1454,6 +1455,7 @@ donation: Assert.Equal("{0} Purchase", vmview.ButtonText); Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText); Assert.Equal("Wanna tip?", vmview.CustomTipText); + Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages)); Assert.IsType(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result); // @@ -1598,7 +1600,7 @@ donation: var invoice = user.BitPay.CreateInvoice(new Invoice() { - Price = 500, + Price = 10, Currency = "USD", PosData = "posData", OrderId = "orderId", @@ -1606,6 +1608,8 @@ donation: FullNotifications = true }, Facade.Merchant); + var networkFee = Money.Satoshis(10000); + // ensure 0 invoices exported because there are no payments yet var jsonResult = user.GetController().Export("json").GetAwaiter().GetResult(); var result = Assert.IsType(jsonResult); @@ -1614,46 +1618,42 @@ donation: var cashCow = tester.ExplorerNode; var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); - var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10); + // + var firstPayment = invoice.CryptoInfo[0].TotalDue - 3*networkFee; cashCow.SendToAddress(invoiceAddress, firstPayment); + Thread.Sleep(1000); // prevent race conditions, ordering payments + // look if you can reduce thread sleep, this was min value for me + + // should reduce invoice due by 0 USD because payment = network fee + cashCow.SendToAddress(invoiceAddress, networkFee); + Thread.Sleep(1000); + + // pay remaining amount + cashCow.SendToAddress(invoiceAddress, 4*networkFee); + Thread.Sleep(1000); Eventually(() => { var jsonResultPaid = user.GetController().Export("json").GetAwaiter().GetResult(); var paidresult = Assert.IsType(jsonResultPaid); Assert.Equal("application/json", paidresult.ContentType); - Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", paidresult.Content); - Assert.Contains("\"InvoicePrice\": 500.0", paidresult.Content); - Assert.Contains("\"ConversionRate\": 5000.0", paidresult.Content); - Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", paidresult.Content); - }); - /* -[ - { - "ReceivedDate": "2018-11-30T10:27:13Z", - "StoreId": "FKaSZrXLJ2tcLfCyeiYYfmZp1UM5nZ1LDecQqbwBRuHi", - "OrderId": "orderId", - "InvoiceId": "4XUkgPMaTBzwJGV9P84kPC", - "CreatedDate": "2018-11-30T10:27:06Z", - "ExpirationDate": "2018-11-30T10:42:06Z", - "MonitoringDate": "2018-11-30T11:42:06Z", - "PaymentId": "6e5755c3357b20fd66f5fc478778d81371eab341e7112ab66ed6122c0ec0d9e5-1", - "CryptoCode": "BTC", - "Destination": "mhhSEQuoM993o6vwnBeufJ4TaWov2ZUsPQ", - "PaymentType": "OnChain", - "PaymentDue": "0.10020000 BTC", - "PaymentPaid": "0.10009990 BTC", - "PaymentOverpaid": "0.00000000 BTC", - "ConversionRate": 5000.0, - "FiatPrice": 500.0, - "FiatCurrency": "USD", - "ItemCode": null, - "ItemDesc": "Some \", description", - "Status": "new" - } -] - */ + var parsedJson = JsonConvert.DeserializeObject(paidresult.Content); + Assert.Equal(3, parsedJson.Length); + + var pay1str = parsedJson[0].ToString(); + Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str); + Assert.Contains("\"InvoiceDue\": 1.5", pay1str); + Assert.Contains("\"InvoicePrice\": 10.0", pay1str); + Assert.Contains("\"ConversionRate\": 5000.0", pay1str); + Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str); + + var pay2str = parsedJson[1].ToString(); + Assert.Contains("\"InvoiceDue\": 1.5", pay2str); + + var pay3str = parsedJson[2].ToString(); + Assert.Contains("\"InvoiceDue\": 0", pay3str); + }); } } @@ -1689,8 +1689,8 @@ donation: var paidresult = Assert.IsType(exportResultPaid); Assert.Equal("application/csv", paidresult.ContentType); Assert.Contains($",\"orderId\",\"{invoice.Id}\",", paidresult.Content); - Assert.Contains($",\"OnChain\",\"0.1000999\",\"BTC\",\"5000.0\",\"500.0\"", paidresult.Content); - Assert.Contains($",\"USD\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content); + Assert.Contains($",\"OnChain\",\"BTC\",\"0.1000999\",\"0.0001\",\"5000.0\"", paidresult.Content); + Assert.Contains($",\"USD\",\"0.00050000\",\"500.0\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content); }); } } diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 7212c1379..9eaa07b61 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.1 - 1.0.3.34 + 1.0.3.36 NU1701,CA1816,CA1308,CA1810,CA2208 diff --git a/BTCPayServer/Controllers/AppsController.PointOfSale.cs b/BTCPayServer/Controllers/AppsController.PointOfSale.cs index c0245f7ac..22f4814e4 100644 --- a/BTCPayServer/Controllers/AppsController.PointOfSale.cs +++ b/BTCPayServer/Controllers/AppsController.PointOfSale.cs @@ -1,5 +1,8 @@ -using System.Text; +using System; +using System.Linq; +using System.Text; using System.Text.Encodings.Web; +using System.Text.RegularExpressions; using System.Threading.Tasks; using BTCPayServer.Data; using BTCPayServer.Models.AppViewModels; @@ -65,6 +68,9 @@ namespace BTCPayServer.Controllers public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF; public const string CUSTOM_TIP_TEXT_DEF = "Do you want to leave a tip?"; public string CustomTipText { get; set; } = CUSTOM_TIP_TEXT_DEF; + public static readonly int[] CUSTOM_TIP_PERCENTAGES_DEF = new int[] { 15, 18, 20 }; + public int[] CustomTipPercentages { get; set; } = CUSTOM_TIP_PERCENTAGES_DEF; + public string CustomCSSLink { get; set; } } @@ -87,6 +93,7 @@ namespace BTCPayServer.Controllers ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF, CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF, CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF, + CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF), CustomCSSLink = settings.CustomCSSLink }; if (HttpContext?.Request != null) @@ -157,6 +164,7 @@ namespace BTCPayServer.Controllers ButtonText = vm.ButtonText, CustomButtonText = vm.CustomButtonText, CustomTipText = vm.CustomTipText, + CustomTipPercentages = ListSplit(vm.CustomTipPercentages), CustomCSSLink = vm.CustomCSSLink }); await UpdateAppSettings(app); @@ -174,5 +182,21 @@ namespace BTCPayServer.Controllers await ctx.SaveChangesAsync(); } } + + private int[] ListSplit(string list, string separator = ",") + { + if (string.IsNullOrEmpty(list)) + { + return Array.Empty(); + } + else + { + // Remove all characters except numeric and comma + Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]"); + list = charsToDestroy.Replace(list, ""); + + return list.Split(separator, System.StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray(); + } + } } } diff --git a/BTCPayServer/Controllers/AppsPublicController.cs b/BTCPayServer/Controllers/AppsPublicController.cs index 9acbc4f4d..f2ea33c40 100644 --- a/BTCPayServer/Controllers/AppsPublicController.cs +++ b/BTCPayServer/Controllers/AppsPublicController.cs @@ -73,7 +73,9 @@ namespace BTCPayServer.Controllers ButtonText = settings.ButtonText, CustomButtonText = settings.CustomButtonText, CustomTipText = settings.CustomTipText, - CustomCSSLink = settings.CustomCSSLink + CustomTipPercentages = settings.CustomTipPercentages, + CustomCSSLink = settings.CustomCSSLink, + AppId = appId }); } diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index 58e609b06..05eea079c 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -46,7 +46,7 @@ namespace BTCPayServer.Controllers string storeId, string cryptoCode, string command, - int account = 0) + string keyPath = "") { if (!HttpContext.WebSockets.IsWebSocketRequest) return NotFound(); @@ -67,7 +67,10 @@ namespace BTCPayServer.Controllers } if (command == "getxpub") { - var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token); + var k = KeyPath.Parse(keyPath); + if (k.Indexes.Length == 0) + throw new FormatException("Invalid key path"); + var getxpubResult = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token); result = getxpubResult; } } @@ -171,7 +174,8 @@ namespace BTCPayServer.Controllers if (strategy != null) await wallet.TrackAsync(strategy.DerivationStrategyBase); store.SetSupportedPaymentMethod(paymentMethodId, strategy); - storeBlob.SetExcluded(paymentMethodId, willBeExcluded); + storeBlob.SetExcluded(paymentMethodId, willBeExcluded); + storeBlob.SetWalletKeyPathRoot(paymentMethodId, vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath)); store.SetStoreBlob(storeBlob); } catch diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index aa0290cd8..bda65045b 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -410,8 +410,8 @@ namespace BTCPayServer.Controllers return NotFound(); var cryptoCode = walletId.CryptoCode; - var storeBlob = (await Repository.FindStore(walletId.StoreId, GetUserId())); - var derivationScheme = GetPaymentMethod(walletId, storeBlob).DerivationStrategyBase; + var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId())); + var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase; var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); @@ -476,15 +476,6 @@ namespace BTCPayServer.Controllers } catch { throw new FormatException("Invalid value for subtract fees"); } } - if (command == "getinfo") - { - var strategy = GetDirectDerivationStrategy(derivationScheme); - if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null) - { - throw new Exception($"This store is not configured to use this ledger"); - } - result = new GetInfoResult(); - } if (command == "test") { result = await hw.Test(normalOperationTimeout.Token); @@ -514,10 +505,20 @@ namespace BTCPayServer.Controllers throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero"); } - var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token); - if (foundKeyPath == null) + var storeBlob = storeData.GetStoreBlob(); + var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike); + var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId); + // Some deployment have the wallet root key path saved in the store blob + // If it does, we only have to make 1 call to the hw to check if it can sign the given strategy, + if (foundKeyPath == null || !await hw.CanSign(network, strategy, foundKeyPath, normalOperationTimeout.Token)) { - throw new HardwareWalletException($"This store is not configured to use this ledger"); + // If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy + foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token); + if (foundKeyPath == null) + throw new HardwareWalletException($"This store is not configured to use this ledger"); + storeBlob.SetWalletKeyPathRoot(paymentId, foundKeyPath); + storeData.SetStoreBlob(storeBlob); + await Repository.UpdateStore(storeData); } TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder(); diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index ab2955047..a45c34111 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -366,6 +366,23 @@ namespace BTCPayServer.Data [Obsolete("Use GetExcludedPaymentMethods instead")] public string[] ExcludedPaymentMethods { get; set; } +#pragma warning disable CS0618 // Type or member is obsolete + public void SetWalletKeyPathRoot(PaymentMethodId paymentMethodId, KeyPath keyPath) + { + if (keyPath == null) + WalletKeyPathRoots.Remove(paymentMethodId.ToString()); + else + WalletKeyPathRoots.AddOrReplace(paymentMethodId.ToString().ToLowerInvariant(), keyPath.ToString()); + } + public KeyPath GetWalletKeyPathRoot(PaymentMethodId paymentMethodId) + { + if (WalletKeyPathRoots.TryGetValue(paymentMethodId.ToString().ToLowerInvariant(), out var k)) + return KeyPath.Parse(k); + return null; + } +#pragma warning restore CS0618 // Type or member is obsolete + [Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")] + public Dictionary WalletKeyPathRoots { get; set; } = new Dictionary(); public IPaymentFilter GetExcludedPaymentMethods() { diff --git a/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs b/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs index a0bfc35e3..0722c4cac 100644 --- a/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/UpdatePointOfSaleViewModel.cs @@ -32,8 +32,11 @@ namespace BTCPayServer.Models.AppViewModels public string CustomButtonText { get; set; } [Required] [MaxLength(30)] - [Display(Name = "Do you want to leave a tip?")] + [Display(Name = "Text to display in the tip input")] public string CustomTipText { get; set; } + [MaxLength(30)] + [Display(Name = "Tip percentage amounts (comma separated)")] + public string CustomTipPercentages { get; set; } [MaxLength(500)] [Display(Name = "Custom bootstrap CSS file")] diff --git a/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs b/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs index 47fc3630d..802d74a9c 100644 --- a/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs @@ -45,6 +45,7 @@ namespace BTCPayServer.Models.AppViewModels public string ButtonText { get; set; } public string CustomButtonText { get; set; } public string CustomTipText { get; set; } + public int[] CustomTipPercentages { get; set; } public string CustomCSSLink { get; set; } } diff --git a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs index ffa1c8448..0353c3601 100644 --- a/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/DerivationSchemeViewModel.cs @@ -24,6 +24,7 @@ namespace BTCPayServer.Models.StoreViewModels } = new List<(string KeyPath, string Address)>(); public string CryptoCode { get; set; } + public string KeyPath { get; set; } [Display(Name = "Hint address")] public string HintAddress { get; set; } public bool Confirmation { get; set; } diff --git a/BTCPayServer/Services/HardwareWalletService.cs b/BTCPayServer/Services/HardwareWalletService.cs index 315e8ad90..cc1a9a7e2 100644 --- a/BTCPayServer/Services/HardwareWalletService.cs +++ b/BTCPayServer/Services/HardwareWalletService.cs @@ -89,20 +89,19 @@ namespace BTCPayServer.Services return new LedgerTestResult() { Success = true }; } - public async Task GetExtPubKey(BTCPayNetwork network, int account, CancellationToken cancellation) + public async Task GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation) { if (network == null) throw new ArgumentNullException(nameof(network)); var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit; - var path = network.GetRootKeyPath().Derive(account, true); - var pubkey = await GetExtPubKey(Ledger, network, path, false, cancellation); + var pubkey = await GetExtPubKey(Ledger, network, keyPath, false, cancellation); var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions() { P2SH = segwit, Legacy = !segwit }); - return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = path }; + return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = keyPath }; } private static async Task GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation) @@ -129,7 +128,13 @@ namespace BTCPayServer.Services } } - public async Task GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation) + public async Task CanSign(BTCPayNetwork network, DirectDerivationStrategy strategy, KeyPath keyPath, CancellationToken cancellation) + { + var hwKey = await GetExtPubKey(Ledger, network, keyPath, true, cancellation); + return hwKey.ExtPubKey.PubKey == strategy.Root.PubKey; + } + + public async Task FindKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation) { List derivations = new List(); if (network.NBitcoinNetwork.Consensus.SupportSegwit) @@ -164,7 +169,17 @@ namespace BTCPayServer.Services KeyPath changeKeyPath, CancellationToken cancellationToken) { - return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken); + try + { + var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken); + if (signedTransaction == null) + throw new Exception("The ledger failed to sign the transaction"); + return signedTransaction; + } + catch (Exception ex) + { + throw new Exception("The ledger failed to sign the transaction", ex); + } } public void Dispose() diff --git a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs index 543759ee3..94938a457 100644 --- a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs +++ b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs @@ -52,6 +52,8 @@ namespace BTCPayServer.Services.Invoices.Export private IEnumerable convertFromDb(InvoiceEntity invoice) { var exportList = new List(); + + var invoiceDue = invoice.ProductInformation.Price; // in this first version we are only exporting invoices that were paid foreach (var payment in invoice.GetPayments()) { @@ -63,6 +65,9 @@ namespace BTCPayServer.Services.Invoices.Export var pdata = payment.GetCryptoPaymentData(); var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId(), Networks); + var paymentFee = pmethod.GetPaymentMethodDetails().GetTxFee(); + var paidAfterNetworkFees = pdata.GetValue() - paymentFee; + invoiceDue -= paidAfterNetworkFees * pmethod.Rate; var target = new ExportInvoiceHolder { @@ -73,6 +78,13 @@ namespace BTCPayServer.Services.Invoices.Export PaymentType = payment.GetPaymentMethodId().PaymentType == Payments.PaymentTypes.BTCLike ? "OnChain" : "OffChain", Destination = payment.GetCryptoPaymentData().GetDestination(Networks.GetNetwork(cryptoCode)), Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture), + PaidCurrency = (pdata.GetValue() * pmethod.Rate).ToString(CultureInfo.InvariantCulture), + // Adding NetworkFee because Paid doesn't take into account network fees + // so if fee is 10000 satoshis, customer can essentially send infinite number of tx + // and merchant effectivelly would receive 0 BTC, invoice won't be paid + // while looking just at export you could sum Paid and assume merchant "received payments" + NetworkFee = paymentFee.ToString(CultureInfo.InvariantCulture), + InvoiceDue = invoiceDue, OrderId = invoice.OrderId, StoreId = invoice.StoreId, InvoiceId = invoice.Id, @@ -112,12 +124,14 @@ namespace BTCPayServer.Services.Invoices.Export public string PaymentId { get; set; } public string Destination { get; set; } public string PaymentType { get; set; } - public string Paid { get; set; } public string CryptoCode { get; set; } + public string Paid { get; set; } + public string NetworkFee { get; set; } public decimal ConversionRate { get; set; } - - public decimal InvoicePrice { get; set; } + public string PaidCurrency { get; set; } public string InvoiceCurrency { get; set; } + public decimal InvoiceDue { get; set; } + public decimal InvoicePrice { get; set; } public string InvoiceItemCode { get; set; } public string InvoiceItemDesc { get; set; } public string InvoiceFullStatus { get; set; } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index d586d27b3..9c958d9e0 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -681,7 +681,7 @@ namespace BTCPayServer.Services.Invoices /// public Money NetworkFee { get; set; } /// - /// Minimum required to be paid in order to accept invocie as paid + /// Minimum required to be paid in order to accept invoice as paid /// public Money MinimumTotalDue { get; set; } } diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 5bbaccb6e..47d4210ff 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -41,7 +41,7 @@ namespace BTCPayServer.Services.Invoices public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath) { int retryCount = 0; - retry: +retry: try { _Engine = new DBreezeEngine(dbreezePath); @@ -385,7 +385,8 @@ namespace BTCPayServer.Services.Invoices var paymentEntity = ToObject(p.Blob, null); paymentEntity.Accounted = p.Accounted; return paymentEntity; - }).ToList(); + }) + .OrderBy(a => a.ReceivedTime).ToList(); #pragma warning restore CS0618 var state = invoice.GetInvoiceState(); entity.ExceptionStatus = state.ExceptionStatus; diff --git a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml index be9f81fda..654935726 100644 --- a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml @@ -72,6 +72,11 @@ +
+ + + +
diff --git a/BTCPayServer/Views/AppsPublic/ViewPointOfSale.cshtml b/BTCPayServer/Views/AppsPublic/ViewPointOfSale.cshtml index 12735bc3f..6dd637d08 100644 --- a/BTCPayServer/Views/AppsPublic/ViewPointOfSale.cshtml +++ b/BTCPayServer/Views/AppsPublic/ViewPointOfSale.cshtml @@ -5,6 +5,7 @@ @{ ViewData["Title"] = Model.Title; Layout = null; + int[] CustomTipPercentages = Model.CustomTipPercentages; } @@ -14,6 +15,11 @@ + + + + + @if (Model.CustomCSSLink != null) { @@ -23,125 +29,330 @@ @if (Model.EnableShoppingCart) { + } + + + + + + + + + + + + + @if (Model.EnableShoppingCart) { -