From 04726b3ee408972e8a1cb535636e2eefe09eeebe Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Fri, 16 Jul 2021 09:57:37 +0200 Subject: [PATCH] Payouts: Detect External OnChain Payouts (#2462) * Refactor and decouple Payout logic So that we can support lightning + external payout payments Fixes & refactoring almost there final Remove uneeded payment method checks Refactor payouts to handle custom payment method specific actions External onchain payments to approved payouts will now require "confirmation" from the merchant that it was sent by them. add pill tabs for payout status * Improve some UX around feature * add test and some fixes * Only listen to address tracked source and determine based on wallet get tx call from nbx * Simplify isInternal for Payout detection * fix test * Fix Noreferrer test * Make EnsureNewLightningInvoiceOnPartialPayment more resilient * Make notifications section test more resilient in CanUsePullPaymentsViaUI --- BTCPayServer.Tests/ApiKeysTests.cs | 7 +- BTCPayServer.Tests/SeleniumTests.cs | 45 ++++ BTCPayServer.Tests/UnitTest1.cs | 17 +- .../NotificationsDropdown/Default.cshtml | 2 +- .../WalletsController.PullPayments.cs | 36 ++- .../BitcoinLike/BitcoinLikePayoutHandler.cs | 213 ++++++++++++------ BTCPayServer/Data/Payouts/IPayoutHandler.cs | 7 + .../PullPaymentHostedService.cs | 16 +- BTCPayServer/Hosting/BTCPayServerServices.cs | 2 +- .../Payments/Bitcoin/NBXplorerListener.cs | 57 +++-- .../ExternalPayoutTransactionNotification.cs | 55 +++++ BTCPayServer/Views/Invoice/Invoice.cshtml | 2 +- BTCPayServer/Views/Wallets/Payouts.cshtml | 2 + 13 files changed, 356 insertions(+), 105 deletions(-) create mode 100644 BTCPayServer/Services/Notifications/Blobs/ExternalPayoutTransactionNotification.cs diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index 9d89a4a28..d5d812e1f 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -89,7 +89,12 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click(); //there should be a store already by default in the dropdown - var dropdown = s.Driver.FindElement(By.Name("PermissionValues[4].SpecificStores[0]")); + var src = s.Driver.PageSource; + var getPermissionValueIndex = + s.Driver.FindElement(By.CssSelector("input[value='btcpay.store.canmodifystoresettings']")) + .GetAttribute("name") + .Replace(".Permission", ".SpecificStores[0]"); + var dropdown = s.Driver.FindElement(By.Name(getPermissionValueIndex)); var option = dropdown.FindElement(By.TagName("option")); var storeId = option.GetAttribute("value"); option.Click(); diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 9441ca945..a1c1924cf 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -1019,6 +1019,51 @@ namespace BTCPayServer.Tests var payoutsData = await ctx.Payouts.Where(p => p.PullPaymentDataId == pullPaymentId).ToListAsync(); Assert.True(payoutsData.All(p => p.State == PayoutState.Completed)); }); + s.GoToHome(); + //offline/external payout test + s.Driver.FindElement(By.Id("NotificationsDropdownToggle")).Click(); + s.Driver.FindElement(By.CssSelector("#notificationsForm button")).Click(); + + + var newStore = s.CreateNewStore(); + s.GenerateWallet("BTC", "", true, true); + var newWalletId = new WalletId(newStore.storeId, "BTC"); + s.GoToWallet(newWalletId, WalletsNavPages.PullPayments); + + s.Driver.FindElement(By.Id("NewPullPayment")).Click(); + s.Driver.FindElement(By.Id("Name")).SendKeys("External Test"); + s.Driver.FindElement(By.Id("Amount")).Clear(); + s.Driver.FindElement(By.Id("Amount")).SendKeys("0.001"); + s.Driver.FindElement(By.Id("Currency")).Clear(); + s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC"); + s.Driver.FindElement(By.Id("Create")).Click(); + s.Driver.FindElement(By.LinkText("View")).Click(); + + address = await s.Server.ExplorerNode.GetNewAddressAsync(); + s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString()); + s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter); + s.FindAlertMessage(); + + Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource); + s.GoToWallet(newWalletId, WalletsNavPages.Payouts); + s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-view")).Click(); + s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click(); + s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click(); + s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve")).Click(); + s.FindAlertMessage(); + var tx =await s.Server.ExplorerNode.SendToAddressAsync(address, Money.FromUnit(0.001m, MoneyUnit.BTC)); + + s.GoToWallet(newWalletId, WalletsNavPages.Payouts); + + s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-view")).Click(); + Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource); + s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-selectAllCheckbox")).Click(); + s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-actions")).Click(); + s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-confirm-payment")).Click(); + s.FindAlertMessage(); + + s.Driver.FindElement(By.Id("InProgress-view")).Click(); + Assert.Contains(tx.ToString(), s.Driver.PageSource); } private static void CanBrowseContent(SeleniumTester s) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 0d0647db4..d84fe02da 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1018,13 +1018,18 @@ namespace BTCPayServer.Tests BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.00005m)); }, e => e.InvoiceId == invoice.Id && e.PaymentMethodId.PaymentType == LightningPaymentType.Instance ); await tester.ExplorerNode.GenerateAsync(1); + Invoice newInvoice = null; await Task.Delay(100); // wait a bit for payment to process before fetching new invoice - var newInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id); - var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; - var oldBolt11 = invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; - Assert.NotEqual(newBolt11, oldBolt11); - Assert.Equal(newInvoice.BtcDue.GetValue(), BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC)); - + await TestUtils.EventuallyAsync(async () => + { + newInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id); + var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; + var oldBolt11 = invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; + Assert.NotEqual(newBolt11, oldBolt11); + Assert.Equal(newInvoice.BtcDue.GetValue(), + BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC)); + }); + Logs.Tester.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue()} via lightning"); var evt = await tester.WaitForEvent(async () => { diff --git a/BTCPayServer/Components/NotificationsDropdown/Default.cshtml b/BTCPayServer/Components/NotificationsDropdown/Default.cshtml index 518ac4ecf..8131ecd3a 100644 --- a/BTCPayServer/Components/NotificationsDropdown/Default.cshtml +++ b/BTCPayServer/Components/NotificationsDropdown/Default.cshtml @@ -17,7 +17,7 @@