Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decouple PaymentMethodId from PayoutMethodId #5944

Merged
merged 1 commit into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 5 additions & 3 deletions BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,10 @@ public async Task CanCreateRefunds()
if (multiCurrency)
user.RegisterDerivationScheme("LTC");
foreach (var rateSelection in new[] { "FiatOption", "CurrentRateOption", "RateThenOption", "CustomOption" })
{
TestLogs.LogInformation((multiCurrency, rateSelection).ToString());
await CanCreateRefundsCore(s, user, multiCurrency, rateSelection);
}
}
}
}
Expand Down Expand Up @@ -399,11 +402,10 @@ private static async Task CanCreateRefundsCore(SeleniumTester s, TestAccount use
if (multiCurrency)
{
s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1));
s.Driver.WaitUntilAvailable(By.Id("SelectedPaymentMethod"), TimeSpan.FromSeconds(1));
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).SendKeys("BTC" + Keys.Enter);
s.Driver.WaitUntilAvailable(By.Id("SelectedPayoutMethod"), TimeSpan.FromSeconds(1));
s.Driver.FindElement(By.Id("SelectedPayoutMethod")).SendKeys("BTC" + Keys.Enter);
s.Driver.FindElement(By.Id("ok")).Click();
}

s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1));
Assert.Contains("5,500.00 USD", s.Driver.PageSource); // Should propose reimburse in fiat
Assert.Contains("1.10000000 BTC", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before
Expand Down
4 changes: 2 additions & 2 deletions BTCPayServer.Tests/GreenfieldAPITests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3450,7 +3450,7 @@ void VerifyOnChain(GenericPaymentMethodData[] dictionary)
Assert.Equal(connStr, methods.FirstOrDefault(m => m.PaymentMethodId == "BTC-LN")?.Config["connectionString"].Value<string>());
methods = await adminClient.GetStorePaymentMethods(store.Id);
Assert.Null(methods.FirstOrDefault(m => m.PaymentMethodId == "BTC-LN").Config);
await this.AssertValidationError(["paymentMethodId"], () => adminClient.RemoveStorePaymentMethod(store.Id, "LOL"));
await this.AssertAPIError("paymentmethod-not-found", () => adminClient.RemoveStorePaymentMethod(store.Id, "LOL"));
await adminClient.RemoveStorePaymentMethod(store.Id, "BTC-LN");

// Alternative way of setting the connection string
Expand Down Expand Up @@ -3948,7 +3948,7 @@ await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreReques
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, PaymentMethodId.Parse("BTC")));
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, Payouts.PayoutMethodId.Parse("BTC")));
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(4, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());
Expand Down
7 changes: 4 additions & 3 deletions BTCPayServer.Tests/SeleniumTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2094,6 +2094,7 @@ public async Task CanEditPullPaymentUI()
public async Task CanUsePullPaymentsViaUI()
{
using var s = CreateSeleniumTester();
s.Server.DeleteStore = false;
s.Server.ActivateLightning(LightningConnectionType.LndREST);
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
Expand Down Expand Up @@ -2274,7 +2275,7 @@ public async Task CanUsePullPaymentsViaUI()
s.GoToStore(newStore.storeId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();

var paymentMethodOptions = s.Driver.FindElements(By.CssSelector("input[name='PaymentMethods']"));
var paymentMethodOptions = s.Driver.FindElements(By.CssSelector("input[name='PayoutMethods']"));
Assert.Equal(2, paymentMethodOptions.Count);

s.Driver.FindElement(By.Id("Name")).SendKeys("Lightning Test");
Expand All @@ -2287,7 +2288,7 @@ public async Task CanUsePullPaymentsViaUI()
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());

// Bitcoin-only, SelectedPaymentMethod should not be displayed
s.Driver.ElementDoesNotExist(By.Id("SelectedPaymentMethod"));
s.Driver.ElementDoesNotExist(By.Id("SelectedPayoutMethod"));

var bolt = (await s.Server.CustomerLightningD.CreateInvoice(
payoutAmount,
Expand Down Expand Up @@ -3072,7 +3073,7 @@ public async Task CanUseLNURL()
// Check that pull payment has lightning option
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), PaymentMethodId.Parse(Assert.Single(s.Driver.FindElements(By.CssSelector("input[name='PaymentMethods']"))).GetAttribute("value")));
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), PaymentMethodId.Parse(Assert.Single(s.Driver.FindElements(By.CssSelector("input[name='PayoutMethods']"))).GetAttribute("value")));
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");
Expand Down
29 changes: 22 additions & 7 deletions BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payouts;
using BTCPayServer.Rating;
using BTCPayServer.Security.Greenfield;
using BTCPayServer.Services;
Expand Down Expand Up @@ -46,6 +47,7 @@ public class GreenfieldInvoiceController : Controller
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly IAuthorizationService _authorizationService;
private readonly Dictionary<PaymentMethodId, IPaymentLinkExtension> _paymentLinkExtensions;
private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly PaymentMethodHandlerDictionary _handlers;

public LanguageService LanguageService { get; }
Expand All @@ -58,6 +60,7 @@ public class GreenfieldInvoiceController : Controller
ApplicationDbContextFactory dbContextFactory,
IAuthorizationService authorizationService,
Dictionary<PaymentMethodId, IPaymentLinkExtension> paymentLinkExtensions,
PayoutMethodHandlerDictionary payoutHandlers,
PaymentMethodHandlerDictionary handlers)
{
_invoiceController = invoiceController;
Expand All @@ -71,6 +74,7 @@ public class GreenfieldInvoiceController : Controller
_dbContextFactory = dbContextFactory;
_authorizationService = authorizationService;
_paymentLinkExtensions = paymentLinkExtensions;
_payoutHandlers = payoutHandlers;
_handlers = handlers;
LanguageService = languageService;
}
Expand Down Expand Up @@ -206,10 +210,13 @@ public async Task<IActionResult> CreateInvoice(string storeId, CreateInvoiceRequ
{
for (int i = 0; i < request.Checkout.PaymentMethods.Length; i++)
{
if (!PaymentMethodId.TryParse(request.Checkout.PaymentMethods[i], out _))
if (
request.Checkout.PaymentMethods[i] is not { } pm ||
!PaymentMethodId.TryParse(pm, out var pm1) ||
_handlers.TryGet(pm1) is null)
{
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.PaymentMethods[i],
"Invalid payment method", this);
"Invalid PaymentMethodId", this);
}
}
}
Expand Down Expand Up @@ -394,18 +401,26 @@ public async Task<IActionResult> ActivateInvoicePaymentMethod(string storeId, st
return this.CreateAPIError("non-refundable", "Cannot refund this invoice");
}
PaymentPrompt? paymentPrompt = null;
PaymentMethodId? paymentMethodId = null;
if (request.PaymentMethod is not null && PaymentMethodId.TryParse(request.PaymentMethod, out paymentMethodId))
PayoutMethodId? payoutMethodId = null;
if (request.PaymentMethod is not null && PayoutMethodId.TryParse(request.PaymentMethod, out payoutMethodId))
{
paymentPrompt = invoice.GetPaymentPrompt(paymentMethodId);
var supported = _payoutHandlers.GetSupportedPayoutMethods(store);
if (supported.Contains(payoutMethodId))
{
var paymentMethodId = PaymentMethodId.GetSimilarities([payoutMethodId], invoice.GetPayments(false).Select(p => p.PaymentMethodId))
.OrderByDescending(o => o.similarity)
.Select(o => o.b)
.FirstOrDefault();
paymentPrompt = paymentMethodId is null ? null : invoice.GetPaymentPrompt(paymentMethodId);
}
}
if (paymentPrompt is null)
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
}
if (request.RefundVariant is null)
ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
if (!ModelState.IsValid || paymentPrompt is null || paymentMethodId is null)
if (!ModelState.IsValid || paymentPrompt is null || payoutMethodId is null)
return this.CreateValidationError(ModelState);

var accounting = paymentPrompt.Calculate();
Expand All @@ -425,7 +440,7 @@ public async Task<IActionResult> ActivateInvoicePaymentMethod(string storeId, st
Name = request.Name ?? $"Refund {invoice.Id}",
Description = request.Description,
StoreId = storeId,
PaymentMethodIds = new[] { paymentMethodId },
PayoutMethodIds = new[] { payoutMethodId },
};

if (request.RefundVariant != RefundVariant.Custom)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public IActionResult GetPayoutProcessors()
{
Name = factory.Processor,
FriendlyName = factory.FriendlyName,
PaymentMethods = factory.GetSupportedPaymentMethods().Select(id => id.ToString())
PaymentMethods = factory.GetSupportedPayoutMethods().Select(id => id.ToString())
.ToArray()
}));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using BTCPayServer.HostedServices;
using BTCPayServer.NTag424;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
Expand All @@ -42,7 +43,7 @@ public class GreenfieldPullPaymentController : ControllerBase
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly CurrencyNameTable _currencyNameTable;
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly IAuthorizationService _authorizationService;
private readonly SettingsRepository _settingsRepository;
Expand All @@ -53,7 +54,7 @@ public class GreenfieldPullPaymentController : ControllerBase
ApplicationDbContextFactory dbContextFactory,
CurrencyNameTable currencyNameTable,
Services.BTCPayNetworkJsonSerializerSettings serializerSettings,
IEnumerable<IPayoutHandler> payoutHandlers,
PayoutMethodHandlerDictionary payoutHandlers,
BTCPayNetworkProvider btcPayNetworkProvider,
IAuthorizationService authorizationService,
SettingsRepository settingsRepository,
Expand Down Expand Up @@ -134,18 +135,18 @@ public async Task<IActionResult> CreatePullPayment(string storeId, CreatePullPay
{
ModelState.AddModelError(nameof(request.BOLT11Expiration), $"The BOLT11 expiration should be positive");
}
PaymentMethodId?[]? paymentMethods = null;
if (request.PaymentMethods is { } paymentMethodsStr)
PayoutMethodId?[]? payoutMethods = null;
if (request.PaymentMethods is { } payoutMethodsStr)
{
paymentMethods = paymentMethodsStr.Select(s =>
payoutMethods = payoutMethodsStr.Select(s =>
{
PaymentMethodId.TryParse(s, out var pmi);
PayoutMethodId.TryParse(s, out var pmi);
return pmi;
}).ToArray();
var supported = (await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData())).ToArray();
for (int i = 0; i < paymentMethods.Length; i++)
var supported = _payoutHandlers.GetSupportedPayoutMethods(HttpContext.GetStoreData());
for (int i = 0; i < payoutMethods.Length; i++)
{
if (!supported.Contains(paymentMethods[i]))
if (!supported.Contains(payoutMethods[i]))
{
request.AddModelError(paymentRequest => paymentRequest.PaymentMethods[i], "Invalid or unsupported payment method", this);
}
Expand All @@ -168,7 +169,7 @@ public async Task<IActionResult> CreatePullPayment(string storeId, CreatePullPay
Amount = request.Amount,
Currency = request.Currency,
StoreId = storeId,
PaymentMethodIds = paymentMethods,
PayoutMethodIds = payoutMethods,
AutoApproveClaims = request.AutoApproveClaims
});
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
Expand Down Expand Up @@ -390,7 +391,8 @@ private Client.Models.PayoutData ToModel(Data.PayoutData p)
};
model.Destination = blob.Destination;
model.PaymentMethod = p.PaymentMethodId;
model.CryptoCode = p.GetPaymentMethodId().CryptoCode;
var currency = this._payoutHandlers.TryGet(p.GetPayoutMethodId())?.Currency;
model.CryptoCode = currency;
model.PaymentProof = p.GetProofBlobJson();
return model;
}
Expand All @@ -399,13 +401,13 @@ private Client.Models.PayoutData ToModel(Data.PayoutData p)
[AllowAnonymous]
public async Task<IActionResult> CreatePayout(string pullPaymentId, CreatePayoutRequest request, CancellationToken cancellationToken)
{
if (!PaymentMethodId.TryParse(request?.PaymentMethod, out var paymentMethodId))
if (!PayoutMethodId.TryParse(request?.PaymentMethod, out var payoutMethodId))
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
return this.CreateValidationError(ModelState);
}

var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethodId);
var payoutHandler = _payoutHandlers.TryGet(payoutMethodId);
if (payoutHandler is null)
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
Expand All @@ -417,14 +419,14 @@ public async Task<IActionResult> CreatePayout(string pullPaymentId, CreatePayout
if (pp is null)
return PullPaymentNotFound();
var ppBlob = pp.GetBlob();
var destination = await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, request!.Destination, ppBlob, cancellationToken);
var destination = await payoutHandler.ParseAndValidateClaimDestination(request!.Destination, ppBlob, cancellationToken);
if (destination.destination is null)
{
ModelState.AddModelError(nameof(request.Destination), destination.error ?? "The destination is invalid for the payment specified");
return this.CreateValidationError(ModelState);
}

var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, request.Amount, paymentMethodId.CryptoCode, ppBlob.Currency);
var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, request.Amount, payoutHandler.Currency, ppBlob.Currency);
if (amtError.error is not null)
{
ModelState.AddModelError(nameof(request.Amount), amtError.error );
Expand All @@ -436,7 +438,7 @@ public async Task<IActionResult> CreatePayout(string pullPaymentId, CreatePayout
Destination = destination.destination,
PullPaymentId = pullPaymentId,
Value = request.Amount,
PaymentMethodId = paymentMethodId,
PayoutMethodId = payoutMethodId,
StoreId = pp.StoreId
});

Expand All @@ -456,13 +458,13 @@ public async Task<IActionResult> CreatePayoutThroughStore(string storeId, Create
}
}

if (request is null || !PaymentMethodId.TryParse(request?.PaymentMethod, out var paymentMethodId))
if (request?.PaymentMethod is null || !PayoutMethodId.TryParse(request?.PaymentMethod, out var paymentMethodId))
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
return this.CreateValidationError(ModelState);
}

var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethodId);
var payoutHandler = _payoutHandlers.TryGet(paymentMethodId);
if (payoutHandler is null)
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
Expand All @@ -482,7 +484,7 @@ public async Task<IActionResult> CreatePayoutThroughStore(string storeId, Create
return PullPaymentNotFound();
ppBlob = pp.GetBlob();
}
var destination = await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, request!.Destination, ppBlob, default);
var destination = await payoutHandler.ParseAndValidateClaimDestination(request!.Destination, ppBlob, default);
if (destination.destination is null)
{
ModelState.AddModelError(nameof(request.Destination), destination.error ?? "The destination is invalid for the payment specified");
Expand All @@ -508,7 +510,7 @@ public async Task<IActionResult> CreatePayoutThroughStore(string storeId, Create
PullPaymentId = request.PullPaymentId,
PreApprove = request.Approved,
Value = request.Amount,
PaymentMethodId = paymentMethodId,
PayoutMethodId = paymentMethodId,
StoreId = storeId,
Metadata = request.Metadata
});
Expand Down