Skip to content

Commit

Permalink
Decouple PaymentMethodId from PayoutMethodId
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolasDorier committed Apr 24, 2024
1 parent e4440ca commit f82fc3f
Show file tree
Hide file tree
Showing 63 changed files with 833 additions and 546 deletions.
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
5 changes: 3 additions & 2 deletions BTCPayServer.Tests/SeleniumTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2039,6 +2039,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 @@ -2219,7 +2220,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 @@ -2232,7 +2233,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
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 @@ -424,7 +439,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

0 comments on commit f82fc3f

Please sign in to comment.