Decouple PaymentMethodId from PayoutMethodId (#5944)

This commit is contained in:
Nicolas Dorier
2024-05-01 10:22:07 +09:00
committed by GitHub
parent 247afe6a7b
commit 9db9c5e936
63 changed files with 835 additions and 548 deletions

View File

@@ -363,7 +363,10 @@ namespace BTCPayServer.Tests
if (multiCurrency) if (multiCurrency)
user.RegisterDerivationScheme("LTC"); user.RegisterDerivationScheme("LTC");
foreach (var rateSelection in new[] { "FiatOption", "CurrentRateOption", "RateThenOption", "CustomOption" }) foreach (var rateSelection in new[] { "FiatOption", "CurrentRateOption", "RateThenOption", "CustomOption" })
{
TestLogs.LogInformation((multiCurrency, rateSelection).ToString());
await CanCreateRefundsCore(s, user, multiCurrency, rateSelection); await CanCreateRefundsCore(s, user, multiCurrency, rateSelection);
}
} }
} }
} }
@@ -399,11 +402,10 @@ namespace BTCPayServer.Tests
if (multiCurrency) if (multiCurrency)
{ {
s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1)); s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1));
s.Driver.WaitUntilAvailable(By.Id("SelectedPaymentMethod"), TimeSpan.FromSeconds(1)); s.Driver.WaitUntilAvailable(By.Id("SelectedPayoutMethod"), TimeSpan.FromSeconds(1));
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).SendKeys("BTC" + Keys.Enter); s.Driver.FindElement(By.Id("SelectedPayoutMethod")).SendKeys("BTC" + Keys.Enter);
s.Driver.FindElement(By.Id("ok")).Click(); s.Driver.FindElement(By.Id("ok")).Click();
} }
s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1)); 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("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 Assert.Contains("1.10000000 BTC", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before

View File

@@ -3450,7 +3450,7 @@ namespace BTCPayServer.Tests
Assert.Equal(connStr, methods.FirstOrDefault(m => m.PaymentMethodId == "BTC-LN")?.Config["connectionString"].Value<string>()); Assert.Equal(connStr, methods.FirstOrDefault(m => m.PaymentMethodId == "BTC-LN")?.Config["connectionString"].Value<string>());
methods = await adminClient.GetStorePaymentMethods(store.Id); methods = await adminClient.GetStorePaymentMethods(store.Id);
Assert.Null(methods.FirstOrDefault(m => m.PaymentMethodId == "BTC-LN").Config); 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"); await adminClient.RemoveStorePaymentMethod(store.Id, "BTC-LN");
// Alternative way of setting the connection string // Alternative way of setting the connection string
@@ -3948,7 +3948,7 @@ namespace BTCPayServer.Tests
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address, txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee); tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid); }, 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 () => await TestUtils.EventuallyAsync(async () =>
{ {
Assert.Equal(4, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count()); Assert.Equal(4, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());

View File

@@ -2094,6 +2094,7 @@ namespace BTCPayServer.Tests
public async Task CanUsePullPaymentsViaUI() public async Task CanUsePullPaymentsViaUI()
{ {
using var s = CreateSeleniumTester(); using var s = CreateSeleniumTester();
s.Server.DeleteStore = false;
s.Server.ActivateLightning(LightningConnectionType.LndREST); s.Server.ActivateLightning(LightningConnectionType.LndREST);
await s.StartAsync(); await s.StartAsync();
await s.Server.EnsureChannelsSetup(); await s.Server.EnsureChannelsSetup();
@@ -2274,7 +2275,7 @@ namespace BTCPayServer.Tests
s.GoToStore(newStore.storeId, StoreNavPages.PullPayments); s.GoToStore(newStore.storeId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click(); 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); Assert.Equal(2, paymentMethodOptions.Count);
s.Driver.FindElement(By.Id("Name")).SendKeys("Lightning Test"); s.Driver.FindElement(By.Id("Name")).SendKeys("Lightning Test");
@@ -2287,7 +2288,7 @@ namespace BTCPayServer.Tests
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
// Bitcoin-only, SelectedPaymentMethod should not be displayed // 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( var bolt = (await s.Server.CustomerLightningD.CreateInvoice(
payoutAmount, payoutAmount,
@@ -3072,7 +3073,7 @@ namespace BTCPayServer.Tests
// Check that pull payment has lightning option // Check that pull payment has lightning option
s.GoToStore(s.StoreId, StoreNavPages.PullPayments); s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click(); 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("Name")).SendKeys("PP1");
s.Driver.FindElement(By.Id("Amount")).Clear(); s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001"); s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");

View File

@@ -12,6 +12,7 @@ using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payouts;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Security.Greenfield; using BTCPayServer.Security.Greenfield;
using BTCPayServer.Services; using BTCPayServer.Services;
@@ -46,6 +47,7 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly ApplicationDbContextFactory _dbContextFactory; private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly Dictionary<PaymentMethodId, IPaymentLinkExtension> _paymentLinkExtensions; private readonly Dictionary<PaymentMethodId, IPaymentLinkExtension> _paymentLinkExtensions;
private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _handlers;
public LanguageService LanguageService { get; } public LanguageService LanguageService { get; }
@@ -58,6 +60,7 @@ namespace BTCPayServer.Controllers.Greenfield
ApplicationDbContextFactory dbContextFactory, ApplicationDbContextFactory dbContextFactory,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
Dictionary<PaymentMethodId, IPaymentLinkExtension> paymentLinkExtensions, Dictionary<PaymentMethodId, IPaymentLinkExtension> paymentLinkExtensions,
PayoutMethodHandlerDictionary payoutHandlers,
PaymentMethodHandlerDictionary handlers) PaymentMethodHandlerDictionary handlers)
{ {
_invoiceController = invoiceController; _invoiceController = invoiceController;
@@ -71,6 +74,7 @@ namespace BTCPayServer.Controllers.Greenfield
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_authorizationService = authorizationService; _authorizationService = authorizationService;
_paymentLinkExtensions = paymentLinkExtensions; _paymentLinkExtensions = paymentLinkExtensions;
_payoutHandlers = payoutHandlers;
_handlers = handlers; _handlers = handlers;
LanguageService = languageService; LanguageService = languageService;
} }
@@ -206,10 +210,13 @@ namespace BTCPayServer.Controllers.Greenfield
{ {
for (int i = 0; i < request.Checkout.PaymentMethods.Length; i++) 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], request.AddModelError(invoiceRequest => invoiceRequest.Checkout.PaymentMethods[i],
"Invalid payment method", this); "Invalid PaymentMethodId", this);
} }
} }
} }
@@ -394,10 +401,18 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateAPIError("non-refundable", "Cannot refund this invoice"); return this.CreateAPIError("non-refundable", "Cannot refund this invoice");
} }
PaymentPrompt? paymentPrompt = null; PaymentPrompt? paymentPrompt = null;
PaymentMethodId? paymentMethodId = null; PayoutMethodId? payoutMethodId = null;
if (request.PaymentMethod is not null && PaymentMethodId.TryParse(request.PaymentMethod, out paymentMethodId)) 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) if (paymentPrompt is null)
{ {
@@ -405,7 +420,7 @@ namespace BTCPayServer.Controllers.Greenfield
} }
if (request.RefundVariant is null) if (request.RefundVariant is null)
ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory"); 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); return this.CreateValidationError(ModelState);
var accounting = paymentPrompt.Calculate(); var accounting = paymentPrompt.Calculate();
@@ -425,7 +440,7 @@ namespace BTCPayServer.Controllers.Greenfield
Name = request.Name ?? $"Refund {invoice.Id}", Name = request.Name ?? $"Refund {invoice.Id}",
Description = request.Description, Description = request.Description,
StoreId = storeId, StoreId = storeId,
PaymentMethodIds = new[] { paymentMethodId }, PayoutMethodIds = new[] { payoutMethodId },
}; };
if (request.RefundVariant != RefundVariant.Custom) if (request.RefundVariant != RefundVariant.Custom)

View File

@@ -36,7 +36,7 @@ namespace BTCPayServer.Controllers.Greenfield
{ {
Name = factory.Processor, Name = factory.Processor,
FriendlyName = factory.FriendlyName, FriendlyName = factory.FriendlyName,
PaymentMethods = factory.GetSupportedPaymentMethods().Select(id => id.ToString()) PaymentMethods = factory.GetSupportedPayoutMethods().Select(id => id.ToString())
.ToArray() .ToArray()
})); }));
} }

View File

@@ -16,6 +16,7 @@ using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.NTag424; using BTCPayServer.NTag424;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Security; using BTCPayServer.Security;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
@@ -42,7 +43,7 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly ApplicationDbContextFactory _dbContextFactory; private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly CurrencyNameTable _currencyNameTable; private readonly CurrencyNameTable _currencyNameTable;
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings; private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers; private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly BTCPayNetworkProvider _networkProvider; private readonly BTCPayNetworkProvider _networkProvider;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly SettingsRepository _settingsRepository; private readonly SettingsRepository _settingsRepository;
@@ -53,7 +54,7 @@ namespace BTCPayServer.Controllers.Greenfield
ApplicationDbContextFactory dbContextFactory, ApplicationDbContextFactory dbContextFactory,
CurrencyNameTable currencyNameTable, CurrencyNameTable currencyNameTable,
Services.BTCPayNetworkJsonSerializerSettings serializerSettings, Services.BTCPayNetworkJsonSerializerSettings serializerSettings,
IEnumerable<IPayoutHandler> payoutHandlers, PayoutMethodHandlerDictionary payoutHandlers,
BTCPayNetworkProvider btcPayNetworkProvider, BTCPayNetworkProvider btcPayNetworkProvider,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
SettingsRepository settingsRepository, SettingsRepository settingsRepository,
@@ -134,18 +135,18 @@ namespace BTCPayServer.Controllers.Greenfield
{ {
ModelState.AddModelError(nameof(request.BOLT11Expiration), $"The BOLT11 expiration should be positive"); ModelState.AddModelError(nameof(request.BOLT11Expiration), $"The BOLT11 expiration should be positive");
} }
PaymentMethodId?[]? paymentMethods = null; PayoutMethodId?[]? payoutMethods = null;
if (request.PaymentMethods is { } paymentMethodsStr) 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; return pmi;
}).ToArray(); }).ToArray();
var supported = (await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData())).ToArray(); var supported = _payoutHandlers.GetSupportedPayoutMethods(HttpContext.GetStoreData());
for (int i = 0; i < paymentMethods.Length; i++) 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); request.AddModelError(paymentRequest => paymentRequest.PaymentMethods[i], "Invalid or unsupported payment method", this);
} }
@@ -168,7 +169,7 @@ namespace BTCPayServer.Controllers.Greenfield
Amount = request.Amount, Amount = request.Amount,
Currency = request.Currency, Currency = request.Currency,
StoreId = storeId, StoreId = storeId,
PaymentMethodIds = paymentMethods, PayoutMethodIds = payoutMethods,
AutoApproveClaims = request.AutoApproveClaims AutoApproveClaims = request.AutoApproveClaims
}); });
var pp = await _pullPaymentService.GetPullPayment(ppId, false); var pp = await _pullPaymentService.GetPullPayment(ppId, false);
@@ -390,7 +391,8 @@ namespace BTCPayServer.Controllers.Greenfield
}; };
model.Destination = blob.Destination; model.Destination = blob.Destination;
model.PaymentMethod = p.PaymentMethodId; model.PaymentMethod = p.PaymentMethodId;
model.CryptoCode = p.GetPaymentMethodId().CryptoCode; var currency = this._payoutHandlers.TryGet(p.GetPayoutMethodId())?.Currency;
model.CryptoCode = currency;
model.PaymentProof = p.GetProofBlobJson(); model.PaymentProof = p.GetProofBlobJson();
return model; return model;
} }
@@ -399,13 +401,13 @@ namespace BTCPayServer.Controllers.Greenfield
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> CreatePayout(string pullPaymentId, CreatePayoutRequest request, CancellationToken cancellationToken) 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"); ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
return this.CreateValidationError(ModelState); return this.CreateValidationError(ModelState);
} }
var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethodId); var payoutHandler = _payoutHandlers.TryGet(payoutMethodId);
if (payoutHandler is null) if (payoutHandler is null)
{ {
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method"); ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
@@ -417,14 +419,14 @@ namespace BTCPayServer.Controllers.Greenfield
if (pp is null) if (pp is null)
return PullPaymentNotFound(); return PullPaymentNotFound();
var ppBlob = pp.GetBlob(); 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) if (destination.destination is null)
{ {
ModelState.AddModelError(nameof(request.Destination), destination.error ?? "The destination is invalid for the payment specified"); ModelState.AddModelError(nameof(request.Destination), destination.error ?? "The destination is invalid for the payment specified");
return this.CreateValidationError(ModelState); 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) if (amtError.error is not null)
{ {
ModelState.AddModelError(nameof(request.Amount), amtError.error ); ModelState.AddModelError(nameof(request.Amount), amtError.error );
@@ -436,7 +438,7 @@ namespace BTCPayServer.Controllers.Greenfield
Destination = destination.destination, Destination = destination.destination,
PullPaymentId = pullPaymentId, PullPaymentId = pullPaymentId,
Value = request.Amount, Value = request.Amount,
PaymentMethodId = paymentMethodId, PayoutMethodId = payoutMethodId,
StoreId = pp.StoreId StoreId = pp.StoreId
}); });
@@ -456,13 +458,13 @@ namespace BTCPayServer.Controllers.Greenfield
} }
} }
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"); ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
return this.CreateValidationError(ModelState); return this.CreateValidationError(ModelState);
} }
var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethodId); var payoutHandler = _payoutHandlers.TryGet(paymentMethodId);
if (payoutHandler is null) if (payoutHandler is null)
{ {
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method"); ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
@@ -482,7 +484,7 @@ namespace BTCPayServer.Controllers.Greenfield
return PullPaymentNotFound(); return PullPaymentNotFound();
ppBlob = pp.GetBlob(); 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) if (destination.destination is null)
{ {
ModelState.AddModelError(nameof(request.Destination), destination.error ?? "The destination is invalid for the payment specified"); ModelState.AddModelError(nameof(request.Destination), destination.error ?? "The destination is invalid for the payment specified");
@@ -508,7 +510,7 @@ namespace BTCPayServer.Controllers.Greenfield
PullPaymentId = request.PullPaymentId, PullPaymentId = request.PullPaymentId,
PreApprove = request.Approved, PreApprove = request.Approved,
Value = request.Amount, Value = request.Amount,
PaymentMethodId = paymentMethodId, PayoutMethodId = paymentMethodId,
StoreId = storeId, StoreId = storeId,
Metadata = request.Metadata Metadata = request.Metadata
}); });

View File

@@ -9,6 +9,7 @@ using BTCPayServer.Data;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.PayoutProcessors; using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.Lightning; using BTCPayServer.PayoutProcessors.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
@@ -34,18 +35,18 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory")] [HttpGet("~/api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory")]
[HttpGet("~/api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{paymentMethod}")] [HttpGet("~/api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{payoutMethodId}")]
public async Task<IActionResult> GetStoreLightningAutomatedPayoutProcessors( public async Task<IActionResult> GetStoreLightningAutomatedPayoutProcessors(
string storeId, string? paymentMethod) string storeId, string? payoutMethodId)
{ {
var paymentMethodId = !string.IsNullOrEmpty(paymentMethod) ? PaymentMethodId.Parse(paymentMethod) : null; var paymentMethodId = !string.IsNullOrEmpty(payoutMethodId) ? PayoutMethodId.Parse(payoutMethodId) : null;
var configured = var configured =
await _payoutProcessorService.GetProcessors( await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery() new PayoutProcessorService.PayoutProcessorQuery()
{ {
Stores = new[] { storeId }, Stores = new[] { storeId },
Processors = new[] { LightningAutomatedPayoutSenderFactory.ProcessorName }, Processors = new[] { LightningAutomatedPayoutSenderFactory.ProcessorName },
PaymentMethods = paymentMethodId is null ? null : new[] { paymentMethodId } PayoutMethodIds = paymentMethodId is null ? null : new[] { paymentMethodId }
}); });
return Ok(configured.Select(ToModel).ToArray()); return Ok(configured.Select(ToModel).ToArray());
@@ -73,27 +74,27 @@ namespace BTCPayServer.Controllers.Greenfield
} }
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{paymentMethod}")] [HttpPut("~/api/v1/stores/{storeId}/payout-processors/LightningAutomatedPayoutSenderFactory/{payoutMethodId}")]
public async Task<IActionResult> UpdateStoreLightningAutomatedPayoutProcessor( public async Task<IActionResult> UpdateStoreLightningAutomatedPayoutProcessor(
string storeId, string paymentMethod, LightningAutomatedPayoutSettings request) string storeId, string payoutMethodId, LightningAutomatedPayoutSettings request)
{ {
AutomatedPayoutConstants.ValidateInterval(ModelState, request.IntervalSeconds, nameof(request.IntervalSeconds)); AutomatedPayoutConstants.ValidateInterval(ModelState, request.IntervalSeconds, nameof(request.IntervalSeconds));
if (!ModelState.IsValid) if (!ModelState.IsValid)
return this.CreateValidationError(ModelState); return this.CreateValidationError(ModelState);
var paymentMethodId = PaymentMethodId.Parse(paymentMethod); var pmi = PayoutMethodId.Parse(payoutMethodId);
var activeProcessor = var activeProcessor =
(await _payoutProcessorService.GetProcessors( (await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery() new PayoutProcessorService.PayoutProcessorQuery()
{ {
Stores = new[] { storeId }, Stores = new[] { storeId },
Processors = new[] { LightningAutomatedPayoutSenderFactory.ProcessorName }, Processors = new[] { LightningAutomatedPayoutSenderFactory.ProcessorName },
PaymentMethods = new[] { paymentMethodId } PayoutMethodIds = new[] { pmi }
})) }))
.FirstOrDefault(); .FirstOrDefault();
activeProcessor ??= new PayoutProcessorData(); activeProcessor ??= new PayoutProcessorData();
activeProcessor.HasTypedBlob<LightningAutomatedPayoutBlob>().SetBlob(FromModel(request)); activeProcessor.HasTypedBlob<LightningAutomatedPayoutBlob>().SetBlob(FromModel(request));
activeProcessor.StoreId = storeId; activeProcessor.StoreId = storeId;
activeProcessor.PaymentMethod = paymentMethodId.ToString(); activeProcessor.PaymentMethod = pmi.ToString();
activeProcessor.Processor = LightningAutomatedPayoutSenderFactory.ProcessorName; activeProcessor.Processor = LightningAutomatedPayoutSenderFactory.ProcessorName;
var tcs = new TaskCompletionSource(); var tcs = new TaskCompletionSource();
_eventAggregator.Publish(new PayoutProcessorUpdated() _eventAggregator.Publish(new PayoutProcessorUpdated()

View File

@@ -10,6 +10,7 @@ using BTCPayServer.Data;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.PayoutProcessors; using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.OnChain; using BTCPayServer.PayoutProcessors.OnChain;
using BTCPayServer.Payouts;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
@@ -39,14 +40,14 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> GetStoreOnChainAutomatedPayoutProcessors( public async Task<IActionResult> GetStoreOnChainAutomatedPayoutProcessors(
string storeId, string? paymentMethod) string storeId, string? paymentMethod)
{ {
var paymentMethodId = !string.IsNullOrEmpty(paymentMethod) ? PaymentMethodId.Parse(paymentMethod) : null; var paymentMethodId = !string.IsNullOrEmpty(paymentMethod) ? PayoutMethodId.Parse(paymentMethod) : null;
var configured = var configured =
await _payoutProcessorService.GetProcessors( await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery() new PayoutProcessorService.PayoutProcessorQuery()
{ {
Stores = new[] { storeId }, Stores = new[] { storeId },
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName }, Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
PaymentMethods = paymentMethodId is null ? null : new[] { paymentMethodId } PayoutMethodIds = paymentMethodId is null ? null : new[] { paymentMethodId }
}); });
return Ok(configured.Select(ToModel).ToArray()); return Ok(configured.Select(ToModel).ToArray());
@@ -86,20 +87,20 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(nameof(request.FeeBlockTarget), "The feeBlockTarget should be between 1 and 1000"); ModelState.AddModelError(nameof(request.FeeBlockTarget), "The feeBlockTarget should be between 1 and 1000");
if (!ModelState.IsValid) if (!ModelState.IsValid)
return this.CreateValidationError(ModelState); return this.CreateValidationError(ModelState);
var paymentMethodId = PaymentMethodId.Parse(paymentMethod); var payoutMethodId = PayoutMethodId.Parse(paymentMethod);
var activeProcessor = var activeProcessor =
(await _payoutProcessorService.GetProcessors( (await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery() new PayoutProcessorService.PayoutProcessorQuery()
{ {
Stores = new[] { storeId }, Stores = new[] { storeId },
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName }, Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
PaymentMethods = new[] { paymentMethodId } PayoutMethodIds = new[] { payoutMethodId }
})) }))
.FirstOrDefault(); .FirstOrDefault();
activeProcessor ??= new PayoutProcessorData(); activeProcessor ??= new PayoutProcessorData();
activeProcessor.HasTypedBlob<OnChainAutomatedPayoutBlob>().SetBlob(FromModel(request)); activeProcessor.HasTypedBlob<OnChainAutomatedPayoutBlob>().SetBlob(FromModel(request));
activeProcessor.StoreId = storeId; activeProcessor.StoreId = storeId;
activeProcessor.PaymentMethod = paymentMethodId.ToString(); activeProcessor.PaymentMethod = payoutMethodId.ToString();
activeProcessor.Processor = OnChainAutomatedPayoutSenderFactory.ProcessorName; activeProcessor.Processor = OnChainAutomatedPayoutSenderFactory.ProcessorName;
var tcs = new TaskCompletionSource(); var tcs = new TaskCompletionSource();
_eventAggregator.Publish(new PayoutProcessorUpdated() _eventAggregator.Publish(new PayoutProcessorUpdated()

View File

@@ -7,6 +7,7 @@ using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.PayoutProcessors; using BTCPayServer.PayoutProcessors;
using BTCPayServer.Payouts;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -54,7 +55,7 @@ namespace BTCPayServer.Controllers.Greenfield
{ {
Stores = new[] { storeId }, Stores = new[] { storeId },
Processors = new[] { processor }, Processors = new[] { processor },
PaymentMethods = new[] { PaymentMethodId.Parse(paymentMethod) } PayoutMethodIds = new[] { PayoutMethodId.Parse(paymentMethod) }
})).FirstOrDefault(); })).FirstOrDefault();
if (matched is null) if (matched is null)
{ {

View File

@@ -20,6 +20,7 @@ using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
@@ -265,7 +266,7 @@ namespace BTCPayServer.Controllers
[HttpGet("invoices/{invoiceId}/refund")] [HttpGet("invoices/{invoiceId}/refund")]
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Refund([FromServices] IEnumerable<IPayoutHandler> payoutHandlers, string invoiceId, CancellationToken cancellationToken) public async Task<IActionResult> Refund(string invoiceId, CancellationToken cancellationToken)
{ {
await using var ctx = _dbContextFactory.CreateContext(); await using var ctx = _dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
@@ -290,24 +291,8 @@ namespace BTCPayServer.Controllers
new { pullPaymentId = ppId }); new { pullPaymentId = ppId });
} }
var paymentMethods = invoice.GetBlob().GetPaymentPrompts(); var payoutMethodIds = _payoutHandlers.GetSupportedPayoutMethods(this.GetCurrentStore());
var pmis = paymentMethods.Select(method => method.PaymentMethodId).ToHashSet(); if (!payoutMethodIds.Any())
// If LNURL is contained, add the LN too as a possible option
foreach (var pmi in pmis.ToList())
{
if (!_handlers.TryGetValue(pmi, out var h))
{
pmis.Remove(pmi);
continue;
}
if (h is LNURLPayPaymentHandler lh)
{
pmis.Add(PaymentTypes.LN.GetPaymentMethodId(lh.Network.CryptoCode));
}
}
var relevant = payoutHandlers.Where(handler => pmis.Any(handler.CanHandle));
var options = (await relevant.GetSupportedPaymentMethods(invoice.StoreData)).Where(id => pmis.Contains(id)).ToHashSet();
if (!options.Any())
{ {
var vm = new RefundModel { Title = "No matching payment method" }; var vm = new RefundModel { Title = "No matching payment method" };
ModelState.AddModelError(nameof(vm.AvailablePaymentMethods), ModelState.AddModelError(nameof(vm.AvailablePaymentMethods),
@@ -315,18 +300,22 @@ namespace BTCPayServer.Controllers
return View("_RefundModal", vm); return View("_RefundModal", vm);
} }
var defaultRefund = invoice.Payments // Find the most similar payment method to the one used for the invoice
.Select(p => p.GetBlob()) var defaultRefund =
.Select(p => p.PaymentMethodId) PaymentMethodId.GetSimilarities(
.FirstOrDefault(p => p != null && options.Contains(p)); invoice.Payments.Select(o => o.GetPaymentMethodId()),
payoutMethodIds)
.OrderByDescending(o => o.similarity)
.Select(o => o.b)
.FirstOrDefault();
var refund = new RefundModel var refund = new RefundModel
{ {
Title = "Payment method", Title = "Payment method",
AvailablePaymentMethods = AvailablePaymentMethods =
new SelectList(options.Select(id => new SelectListItem(id.ToString(), id.ToString())), new SelectList(payoutMethodIds.Select(id => new SelectListItem(id.ToString(), id.ToString())),
"Value", "Text"), "Value", "Text"),
SelectedPaymentMethod = defaultRefund?.ToString() ?? options.First().ToString() SelectedPayoutMethod = defaultRefund?.ToString() ?? payoutMethodIds.First().ToString()
}; };
// Nothing to select, skip to next // Nothing to select, skip to next
@@ -351,31 +340,35 @@ namespace BTCPayServer.Controllers
return NotFound(); return NotFound();
var store = GetCurrentStore(); var store = GetCurrentStore();
var paymentMethodId = PaymentMethodId.Parse(model.SelectedPaymentMethod); var pmi = PayoutMethodId.Parse(model.SelectedPayoutMethod);
var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true); var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true);
RateRules rules; RateRules rules;
RateResult rateResult; RateResult rateResult;
CreatePullPayment createPullPayment; CreatePullPayment createPullPayment;
var pms = invoice.GetPaymentPrompts();
if (!pms.TryGetValue(paymentMethodId, out var paymentMethod)) var pmis = _payoutHandlers.GetSupportedPayoutMethods(store);
if (!pmis.Contains(pmi))
{ {
// We included Lightning if only LNURL was set, so this must be LNURL ModelState.AddModelError(nameof(model.SelectedPayoutMethod), $"Invalid payout method");
if (_handlers.TryGetValue(paymentMethodId, out var h) && h is LightningLikePaymentHandler lnh) return View("_RefundModal", model);
{
pms.TryGetValue(PaymentTypes.LNURL.GetPaymentMethodId(lnh.Network.CryptoCode), out paymentMethod);
}
} }
if (paymentMethod is null || paymentMethod.Currency is null)
var paymentMethodId = PaymentMethodId.GetSimilarities([pmi], invoice.GetPayments(false).Select(p => p.PaymentMethodId))
.OrderByDescending(o => o.similarity)
.Select(o => o.b)
.FirstOrDefault();
var paymentMethod = paymentMethodId is null ? null : invoice.GetPaymentPrompt(paymentMethodId);
if (paymentMethod?.Currency is null)
{ {
ModelState.AddModelError(nameof(model.SelectedPaymentMethod), $"Invalid payment method"); ModelState.AddModelError(nameof(model.SelectedPayoutMethod), $"Invalid payout method");
return View("_RefundModal", model); return View("_RefundModal", model);
} }
var accounting = paymentMethod.Calculate(); var accounting = paymentMethod.Calculate();
decimal cryptoPaid = accounting.Paid; decimal cryptoPaid = accounting.Paid;
decimal dueAmount = accounting.TotalDue; decimal dueAmount = accounting.TotalDue;
var paymentMethodCurrency = paymentMethodId.CryptoCode; var paymentMethodCurrency = paymentMethod.Currency;
var isPaidOver = invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver; var isPaidOver = invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver;
decimal? overpaidAmount = isPaidOver ? Math.Round(cryptoPaid - dueAmount, paymentMethod.Divisibility) : null; decimal? overpaidAmount = isPaidOver ? Math.Round(cryptoPaid - dueAmount, paymentMethod.Divisibility) : null;
@@ -421,7 +414,7 @@ namespace BTCPayServer.Controllers
createPullPayment = new CreatePullPayment createPullPayment = new CreatePullPayment
{ {
Name = $"Refund {invoice.Id}", Name = $"Refund {invoice.Id}",
PaymentMethodIds = new[] { paymentMethodId }, PayoutMethodIds = new[] { pmi },
StoreId = invoice.StoreId, StoreId = invoice.StoreId,
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration
}; };

View File

@@ -34,6 +34,7 @@ using Newtonsoft.Json.Linq;
using StoreData = BTCPayServer.Data.StoreData; using StoreData = BTCPayServer.Data.StoreData;
using Serilog.Filters; using Serilog.Filters;
using PeterO.Numbers; using PeterO.Numbers;
using BTCPayServer.Payouts;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@@ -49,6 +50,7 @@ namespace BTCPayServer.Controllers
private readonly DisplayFormatter _displayFormatter; private readonly DisplayFormatter _displayFormatter;
readonly EventAggregator _EventAggregator; readonly EventAggregator _EventAggregator;
readonly BTCPayNetworkProvider _NetworkProvider; readonly BTCPayNetworkProvider _NetworkProvider;
private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _handlers;
private readonly ApplicationDbContextFactory _dbContextFactory; private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly PullPaymentHostedService _paymentHostedService; private readonly PullPaymentHostedService _paymentHostedService;
@@ -77,6 +79,7 @@ namespace BTCPayServer.Controllers
EventAggregator eventAggregator, EventAggregator eventAggregator,
ContentSecurityPolicies csp, ContentSecurityPolicies csp,
BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider networkProvider,
PayoutMethodHandlerDictionary payoutHandlers,
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
ApplicationDbContextFactory dbContextFactory, ApplicationDbContextFactory dbContextFactory,
PullPaymentHostedService paymentHostedService, PullPaymentHostedService paymentHostedService,
@@ -102,6 +105,7 @@ namespace BTCPayServer.Controllers
_UserManager = userManager; _UserManager = userManager;
_EventAggregator = eventAggregator; _EventAggregator = eventAggregator;
_NetworkProvider = networkProvider; _NetworkProvider = networkProvider;
this._payoutHandlers = payoutHandlers;
_handlers = paymentMethodHandlerDictionary; _handlers = paymentMethodHandlerDictionary;
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_paymentHostedService = paymentHostedService; _paymentHostedService = paymentHostedService;

View File

@@ -19,6 +19,7 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Plugins; using BTCPayServer.Plugins;
using BTCPayServer.Plugins.Crowdfund; using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.PointOfSale; using BTCPayServer.Plugins.PointOfSale;
@@ -48,12 +49,12 @@ namespace BTCPayServer
{ {
private readonly InvoiceRepository _invoiceRepository; private readonly InvoiceRepository _invoiceRepository;
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly AppService _appService; private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController; private readonly UIInvoiceController _invoiceController;
private readonly LinkGenerator _linkGenerator; private readonly LinkGenerator _linkGenerator;
private readonly LightningAddressService _lightningAddressService; private readonly LightningAddressService _lightningAddressService;
private readonly LightningLikePayoutHandler _lightningLikePayoutHandler;
private readonly PullPaymentHostedService _pullPaymentHostedService; private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings; private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly IPluginHookService _pluginHookService; private readonly IPluginHookService _pluginHookService;
@@ -62,13 +63,13 @@ namespace BTCPayServer
public UILNURLController(InvoiceRepository invoiceRepository, public UILNURLController(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator, EventAggregator eventAggregator,
PayoutMethodHandlerDictionary payoutHandlers,
PaymentMethodHandlerDictionary handlers, PaymentMethodHandlerDictionary handlers,
StoreRepository storeRepository, StoreRepository storeRepository,
AppService appService, AppService appService,
UIInvoiceController invoiceController, UIInvoiceController invoiceController,
LinkGenerator linkGenerator, LinkGenerator linkGenerator,
LightningAddressService lightningAddressService, LightningAddressService lightningAddressService,
LightningLikePayoutHandler lightningLikePayoutHandler,
PullPaymentHostedService pullPaymentHostedService, PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
IPluginHookService pluginHookService, IPluginHookService pluginHookService,
@@ -76,13 +77,13 @@ namespace BTCPayServer
{ {
_invoiceRepository = invoiceRepository; _invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_payoutHandlers = payoutHandlers;
_handlers = handlers; _handlers = handlers;
_storeRepository = storeRepository; _storeRepository = storeRepository;
_appService = appService; _appService = appService;
_invoiceController = invoiceController; _invoiceController = invoiceController;
_linkGenerator = linkGenerator; _linkGenerator = linkGenerator;
_lightningAddressService = lightningAddressService; _lightningAddressService = lightningAddressService;
_lightningLikePayoutHandler = lightningLikePayoutHandler;
_pullPaymentHostedService = pullPaymentHostedService; _pullPaymentHostedService = pullPaymentHostedService;
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings; _btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
_pluginHookService = pluginHookService; _pluginHookService = pluginHookService;
@@ -101,10 +102,11 @@ namespace BTCPayServer
{ {
return NotFound(); return NotFound();
} }
var pmi = PaymentTypes.LN.GetPaymentMethodId(cryptoCode); var pmi = PayoutTypes.LN.GetPayoutMethodId(cryptoCode);
var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var pp = await _pullPaymentHostedService.GetPullPayment(pullPaymentId, true); var pp = await _pullPaymentHostedService.GetPullPayment(pullPaymentId, true);
if (!pp.IsRunning() || !pp.IsSupported(pmi)) if (!pp.IsRunning() || !pp.IsSupported(pmi) || !_payoutHandlers.TryGetValue(pmi, out var payoutHandler))
{ {
return NotFound(); return NotFound();
} }
@@ -126,7 +128,7 @@ namespace BTCPayServer
CurrentBalance = LightMoney.FromUnit(remaining, unit), CurrentBalance = LightMoney.FromUnit(remaining, unit),
MinWithdrawable = MinWithdrawable =
LightMoney.FromUnit( LightMoney.FromUnit(
Math.Min(await _lightningLikePayoutHandler.GetMinimumPayoutAmount(pmi, null), remaining), Math.Min(await payoutHandler.GetMinimumPayoutAmount(null), remaining),
unit), unit),
Tag = "withdrawRequest", Tag = "withdrawRequest",
Callback = new Uri(Request.GetCurrentUrl()), Callback = new Uri(Request.GetCurrentUrl()),
@@ -147,7 +149,7 @@ namespace BTCPayServer
if (result.MinimumAmount < request.MinWithdrawable || result.MinimumAmount > request.MaxWithdrawable) if (result.MinimumAmount < request.MinWithdrawable || result.MinimumAmount > request.MaxWithdrawable)
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = $"Payment request was not within bounds ({request.MinWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} - {request.MaxWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} sats)" }); return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = $"Payment request was not within bounds ({request.MinWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} - {request.MaxWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} sats)" });
var store = await _storeRepository.FindStore(pp.StoreId); var store = await _storeRepository.FindStore(pp.StoreId);
var pm = store!.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, _handlers); var pm = store!.GetPaymentMethodConfig<LightningPaymentMethodConfig>(paymentMethodId, _handlers);
if (pm is null) if (pm is null)
{ {
return NotFound(); return NotFound();
@@ -156,7 +158,7 @@ namespace BTCPayServer
var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest
{ {
Destination = new BoltInvoiceClaimDestination(pr, result), Destination = new BoltInvoiceClaimDestination(pr, result),
PaymentMethodId = pmi, PayoutMethodId = pmi,
PullPaymentId = pullPaymentId, PullPaymentId = pullPaymentId,
StoreId = pp.StoreId, StoreId = pp.StoreId,
Value = result.MinimumAmount.ToDecimal(unit) Value = result.MinimumAmount.ToDecimal(unit)
@@ -174,7 +176,7 @@ namespace BTCPayServer
lightningHandler.CreateLightningClient(pm); lightningHandler.CreateLightningClient(pm);
var payResult = await UILightningLikePayoutController.TrypayBolt(client, var payResult = await UILightningLikePayoutController.TrypayBolt(client,
claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings), claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings),
claimResponse.PayoutData, result, pmi, cancellationToken); claimResponse.PayoutData, result, payoutHandler.Currency, cancellationToken);
switch (payResult.Result) switch (payResult.Result)
{ {

View File

@@ -20,6 +20,7 @@ using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels; using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.NTag424; using BTCPayServer.NTag424;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
@@ -42,7 +43,7 @@ namespace BTCPayServer.Controllers
private readonly PullPaymentHostedService _pullPaymentHostedService; private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkProvider _networkProvider; private readonly BTCPayNetworkProvider _networkProvider;
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings; private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers; private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly BTCPayServerEnvironment _env; private readonly BTCPayServerEnvironment _env;
private readonly SettingsRepository _settingsRepository; private readonly SettingsRepository _settingsRepository;
@@ -53,7 +54,7 @@ namespace BTCPayServer.Controllers
PullPaymentHostedService pullPaymentHostedService, PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider networkProvider,
BTCPayNetworkJsonSerializerSettings serializerSettings, BTCPayNetworkJsonSerializerSettings serializerSettings,
IEnumerable<IPayoutHandler> payoutHandlers, PayoutMethodHandlerDictionary payoutHandlers,
StoreRepository storeRepository, StoreRepository storeRepository,
BTCPayServerEnvironment env, BTCPayServerEnvironment env,
SettingsRepository settingsRepository) SettingsRepository settingsRepository)
@@ -92,7 +93,7 @@ namespace BTCPayServer.Controllers
{ {
Entity = o, Entity = o,
Blob = o.GetBlob(_serializerSettings), Blob = o.GetBlob(_serializerSettings),
ProofBlob = _payoutHandlers.FindPayoutHandler(o.GetPaymentMethodId())?.ParseProof(o) ProofBlob = _payoutHandlers.TryGet(o.GetPayoutMethodId())?.ParseProof(o)
}); });
var cd = _currencyNameTable.GetCurrencyData(blob.Currency, false); var cd = _currencyNameTable.GetCurrencyData(blob.Currency, false);
var totalPaid = payouts.Where(p => p.Entity.State != PayoutState.Cancelled).Select(p => p.Blob.Amount).Sum(); var totalPaid = payouts.Where(p => p.Entity.State != PayoutState.Cancelled).Select(p => p.Blob.Amount).Sum();
@@ -225,29 +226,31 @@ namespace BTCPayServer.Controllers
var ppBlob = pp.GetBlob(); var ppBlob = pp.GetBlob();
var supported = ppBlob.SupportedPaymentMethods; var supported = ppBlob.SupportedPaymentMethods;
PaymentMethodId paymentMethodId = null; PayoutMethodId payoutMethodId = null;
IClaimDestination destination = null; IClaimDestination destination = null;
if (string.IsNullOrEmpty(vm.SelectedPaymentMethod)) IPayoutHandler payoutHandler = null;
if (string.IsNullOrEmpty(vm.SelectedPayoutMethod))
{ {
foreach (var pmId in supported) foreach (var pmId in supported)
{ {
var handler = _payoutHandlers.FindPayoutHandler(pmId); var handler = _payoutHandlers.TryGet(pmId);
(IClaimDestination dst, string err) = handler == null (IClaimDestination dst, string err) = handler == null
? (null, "No payment handler found for this payment method") ? (null, "No payment handler found for this payment method")
: await handler.ParseAndValidateClaimDestination(pmId, vm.Destination, ppBlob, cancellationToken); : await handler.ParseAndValidateClaimDestination(vm.Destination, ppBlob, cancellationToken);
if (dst is not null && err is null) if (dst is not null && err is null)
{ {
paymentMethodId = pmId; payoutMethodId = pmId;
destination = dst; destination = dst;
payoutHandler = handler;
break; break;
} }
} }
} }
else else
{ {
paymentMethodId = supported.FirstOrDefault(id => vm.SelectedPaymentMethod == id.ToString()); payoutMethodId = supported.FirstOrDefault(id => vm.SelectedPayoutMethod == id.ToString());
var payoutHandler = paymentMethodId is null ? null : _payoutHandlers.FindPayoutHandler(paymentMethodId); payoutHandler = payoutMethodId is null ? null : _payoutHandlers.TryGet(payoutMethodId);
destination = payoutHandler is null ? null : (await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken)).destination; destination = payoutHandler is null ? null : (await payoutHandler.ParseAndValidateClaimDestination(vm.Destination, ppBlob, cancellationToken)).destination;
} }
if (destination is null) if (destination is null)
@@ -255,8 +258,7 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.Destination), "Invalid destination or payment method"); ModelState.AddModelError(nameof(vm.Destination), "Invalid destination or payment method");
return await ViewPullPayment(pullPaymentId); return await ViewPullPayment(pullPaymentId);
} }
var amtError = ClaimRequest.IsPayoutAmountOk(destination, vm.ClaimedAmount == 0 ? null : vm.ClaimedAmount, payoutHandler.Currency, ppBlob.Currency);
var amtError = ClaimRequest.IsPayoutAmountOk(destination, vm.ClaimedAmount == 0 ? null : vm.ClaimedAmount, paymentMethodId.CryptoCode, ppBlob.Currency);
if (amtError.error is not null) if (amtError.error is not null)
{ {
ModelState.AddModelError(nameof(vm.ClaimedAmount), amtError.error); ModelState.AddModelError(nameof(vm.ClaimedAmount), amtError.error);
@@ -276,7 +278,7 @@ namespace BTCPayServer.Controllers
Destination = destination, Destination = destination,
PullPaymentId = pullPaymentId, PullPaymentId = pullPaymentId,
Value = vm.ClaimedAmount, Value = vm.ClaimedAmount,
PaymentMethodId = paymentMethodId, PayoutMethodId = payoutMethodId,
StoreId = pp.StoreId StoreId = pp.StoreId
}); });

View File

@@ -13,6 +13,7 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Models.WalletViewModels; using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.PayoutProcessors; using BTCPayServer.PayoutProcessors;
using BTCPayServer.Payouts;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -31,7 +32,7 @@ namespace BTCPayServer.Controllers
public class UIStorePullPaymentsController : Controller public class UIStorePullPaymentsController : Controller
{ {
private readonly BTCPayNetworkProvider _btcPayNetworkProvider; private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers; private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly CurrencyNameTable _currencyNameTable; private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter; private readonly DisplayFormatter _displayFormatter;
private readonly PullPaymentHostedService _pullPaymentService; private readonly PullPaymentHostedService _pullPaymentService;
@@ -50,7 +51,7 @@ namespace BTCPayServer.Controllers
} }
public UIStorePullPaymentsController(BTCPayNetworkProvider btcPayNetworkProvider, public UIStorePullPaymentsController(BTCPayNetworkProvider btcPayNetworkProvider,
IEnumerable<IPayoutHandler> payoutHandlers, PayoutMethodHandlerDictionary payoutHandlers,
CurrencyNameTable currencyNameTable, CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter, DisplayFormatter displayFormatter,
PullPaymentHostedService pullPaymentHostedService, PullPaymentHostedService pullPaymentHostedService,
@@ -74,12 +75,12 @@ namespace BTCPayServer.Controllers
[HttpGet("stores/{storeId}/pull-payments/new")] [HttpGet("stores/{storeId}/pull-payments/new")]
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> NewPullPayment(string storeId) public IActionResult NewPullPayment(string storeId)
{ {
if (CurrentStore is null) if (CurrentStore is null)
return NotFound(); return NotFound();
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore); var paymentMethods = _payoutHandlers.GetSupportedPayoutMethods(CurrentStore);
if (!paymentMethods.Any()) if (!paymentMethods.Any())
{ {
TempData.SetStatusMessageModel(new StatusMessageModel TempData.SetStatusMessageModel(new StatusMessageModel
@@ -96,7 +97,7 @@ namespace BTCPayServer.Controllers
Currency = CurrentStore.GetStoreBlob().DefaultCurrency, Currency = CurrentStore.GetStoreBlob().DefaultCurrency,
CustomCSSLink = "", CustomCSSLink = "",
EmbeddedCSS = "", EmbeddedCSS = "",
PaymentMethodItems = PayoutMethodsItem =
paymentMethods.Select(id => new SelectListItem(id.ToString(), id.ToString(), true)) paymentMethods.Select(id => new SelectListItem(id.ToString(), id.ToString(), true))
}); });
} }
@@ -108,19 +109,19 @@ namespace BTCPayServer.Controllers
if (CurrentStore is null) if (CurrentStore is null)
return NotFound(); return NotFound();
var paymentMethodOptions = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore); var paymentMethodOptions = _payoutHandlers.GetSupportedPayoutMethods(CurrentStore);
model.PaymentMethodItems = model.PayoutMethodsItem =
paymentMethodOptions.Select(id => new SelectListItem(id.ToString(), id.ToString(), true)); paymentMethodOptions.Select(id => new SelectListItem(id.ToString(), id.ToString(), true));
model.Name ??= string.Empty; model.Name ??= string.Empty;
model.Currency = model.Currency?.ToUpperInvariant()?.Trim() ?? String.Empty; model.Currency = model.Currency?.ToUpperInvariant()?.Trim() ?? String.Empty;
model.PaymentMethods ??= new List<string>(); model.PayoutMethods ??= new List<string>();
if (!model.PaymentMethods.Any()) if (!model.PayoutMethods.Any())
{ {
// Since we assign all payment methods to be selected by default above we need to update // Since we assign all payment methods to be selected by default above we need to update
// them here to reflect user's selection so that they can correct their mistake // them here to reflect user's selection so that they can correct their mistake
model.PaymentMethodItems = model.PayoutMethodsItem =
paymentMethodOptions.Select(id => new SelectListItem(id.ToString(), id.ToString(), false)); paymentMethodOptions.Select(id => new SelectListItem(id.ToString(), id.ToString(), false));
ModelState.AddModelError(nameof(model.PaymentMethods), "You need at least one payment method"); ModelState.AddModelError(nameof(model.PayoutMethods), "You need at least one payout method");
} }
if (_currencyNameTable.GetCurrencyData(model.Currency, false) is null) if (_currencyNameTable.GetCurrencyData(model.Currency, false) is null)
{ {
@@ -135,10 +136,10 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(model.Name), "The name should be maximum 50 characters."); ModelState.AddModelError(nameof(model.Name), "The name should be maximum 50 characters.");
} }
var selectedPaymentMethodIds = model.PaymentMethods.Select(PaymentMethodId.Parse).ToArray(); var selectedPaymentMethodIds = model.PayoutMethods.Select(PayoutMethodId.Parse).ToArray();
if (!selectedPaymentMethodIds.All(id => selectedPaymentMethodIds.Contains(id))) if (!selectedPaymentMethodIds.All(id => paymentMethodOptions.Contains(id)))
{ {
ModelState.AddModelError(nameof(model.Name), "Not all payment methods are supported"); ModelState.AddModelError(nameof(model.Name), "Not all payout methods are supported");
} }
if (!ModelState.IsValid) if (!ModelState.IsValid)
return View(model); return View(model);
@@ -151,7 +152,7 @@ namespace BTCPayServer.Controllers
Amount = model.Amount, Amount = model.Amount,
Currency = model.Currency, Currency = model.Currency,
StoreId = storeId, StoreId = storeId,
PaymentMethodIds = selectedPaymentMethodIds, PayoutMethodIds = selectedPaymentMethodIds,
EmbeddedCSS = model.EmbeddedCSS, EmbeddedCSS = model.EmbeddedCSS,
CustomCSSLink = model.CustomCSSLink, CustomCSSLink = model.CustomCSSLink,
BOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration), BOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration),
@@ -196,7 +197,7 @@ namespace BTCPayServer.Controllers
} }
} }
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData()); var paymentMethods = _payoutHandlers.GetSupportedPayoutMethods(HttpContext.GetStoreData());
if (!paymentMethods.Any()) if (!paymentMethods.Any())
{ {
TempData.SetStatusMessageModel(new StatusMessageModel TempData.SetStatusMessageModel(new StatusMessageModel
@@ -290,11 +291,11 @@ namespace BTCPayServer.Controllers
if (vm is null) if (vm is null)
return NotFound(); return NotFound();
vm.PaymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData()); vm.PayoutMethods = _payoutHandlers.GetSupportedPayoutMethods(HttpContext.GetStoreData());
vm.HasPayoutProcessor = await HasPayoutProcessor(storeId, vm.PaymentMethodId); vm.HasPayoutProcessor = await HasPayoutProcessor(storeId, vm.PayoutMethodId);
var paymentMethodId = PaymentMethodId.Parse(vm.PaymentMethodId); var payoutMethodId = PayoutMethodId.Parse(vm.PayoutMethodId);
var handler = _payoutHandlers var handler = _payoutHandlers
.FindPayoutHandler(paymentMethodId); .TryGet(payoutMethodId);
var commandState = Enum.Parse<PayoutState>(vm.Command.Split("-").First()); var commandState = Enum.Parse<PayoutState>(vm.Command.Split("-").First());
var payoutIds = vm.GetSelectedPayouts(commandState); var payoutIds = vm.GetSelectedPayouts(commandState);
if (payoutIds.Length == 0) if (payoutIds.Length == 0)
@@ -309,7 +310,7 @@ namespace BTCPayServer.Controllers
{ {
storeId = storeId, storeId = storeId,
pullPaymentId = vm.PullPaymentId, pullPaymentId = vm.PullPaymentId,
paymentMethodId = paymentMethodId.ToString() payoutMethodId = payoutMethodId.ToString()
}); });
} }
@@ -331,7 +332,7 @@ namespace BTCPayServer.Controllers
await using var ctx = this._dbContextFactory.CreateContext(); await using var ctx = this._dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var payouts = var payouts =
await GetPayoutsForPaymentMethod(paymentMethodId, ctx, payoutIds, storeId, cancellationToken); await GetPayoutsForPaymentMethod(payoutMethodId, ctx, payoutIds, storeId, cancellationToken);
var failed = false; var failed = false;
for (int i = 0; i < payouts.Count; i++) for (int i = 0; i < payouts.Count; i++)
@@ -391,7 +392,7 @@ namespace BTCPayServer.Controllers
case "pay": case "pay":
{ {
if (handler is { }) if (handler is { })
return await handler.InitiatePayment(paymentMethodId, payoutIds); return await handler.InitiatePayment(payoutIds);
TempData.SetStatusMessageModel(new StatusMessageModel() TempData.SetStatusMessageModel(new StatusMessageModel()
{ {
Message = "Paying via this payment method is not supported", Message = "Paying via this payment method is not supported",
@@ -405,7 +406,7 @@ namespace BTCPayServer.Controllers
await using var ctx = this._dbContextFactory.CreateContext(); await using var ctx = this._dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var payouts = var payouts =
await GetPayoutsForPaymentMethod(paymentMethodId, ctx, payoutIds, storeId, cancellationToken); await GetPayoutsForPaymentMethod(payoutMethodId, ctx, payoutIds, storeId, cancellationToken);
for (int i = 0; i < payouts.Count; i++) for (int i = 0; i < payouts.Count; i++)
{ {
var payout = payouts[i]; var payout = payouts[i];
@@ -426,7 +427,7 @@ namespace BTCPayServer.Controllers
{ {
storeId = storeId, storeId = storeId,
pullPaymentId = vm.PullPaymentId, pullPaymentId = vm.PullPaymentId,
paymentMethodId = paymentMethodId.ToString() payoutMethodId = payoutMethodId.ToString()
}); });
} }
} }
@@ -455,11 +456,11 @@ namespace BTCPayServer.Controllers
{ {
storeId = storeId, storeId = storeId,
pullPaymentId = vm.PullPaymentId, pullPaymentId = vm.PullPaymentId,
paymentMethodId = paymentMethodId.ToString() payoutMethodId = payoutMethodId.ToString()
}); });
} }
private static async Task<List<PayoutData>> GetPayoutsForPaymentMethod(PaymentMethodId paymentMethodId, private static async Task<List<PayoutData>> GetPayoutsForPaymentMethod(PayoutMethodId payoutMethodId,
ApplicationDbContext ctx, string[] payoutIds, ApplicationDbContext ctx, string[] payoutIds,
string storeId, CancellationToken cancellationToken) string storeId, CancellationToken cancellationToken)
{ {
@@ -469,7 +470,7 @@ namespace BTCPayServer.Controllers
IncludeStoreData = true, IncludeStoreData = true,
Stores = new[] { storeId }, Stores = new[] { storeId },
PayoutIds = payoutIds, PayoutIds = payoutIds,
PaymentMethods = new[] { paymentMethodId.ToString() } PayoutMethods = new[] { payoutMethodId.ToString() }
}, ctx, cancellationToken); }, ctx, cancellationToken);
} }
@@ -477,10 +478,10 @@ namespace BTCPayServer.Controllers
[HttpGet("stores/{storeId}/payouts")] [HttpGet("stores/{storeId}/payouts")]
[Authorize(Policy = Policies.CanViewPayouts, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanViewPayouts, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Payouts( public async Task<IActionResult> Payouts(
string storeId, string pullPaymentId, string paymentMethodId, PayoutState payoutState, string storeId, string pullPaymentId, string payoutMethodId, PayoutState payoutState,
int skip = 0, int count = 50) int skip = 0, int count = 50)
{ {
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData()); var paymentMethods = _payoutHandlers.GetSupportedPayoutMethods(HttpContext.GetStoreData());
if (!paymentMethods.Any()) if (!paymentMethods.Any())
{ {
TempData.SetStatusMessageModel(new StatusMessageModel TempData.SetStatusMessageModel(new StatusMessageModel
@@ -491,17 +492,17 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId }); return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
} }
paymentMethodId ??= paymentMethods.First().ToString(); payoutMethodId ??= paymentMethods.First().ToString();
var vm = this.ParseListQuery(new PayoutsModel var vm = this.ParseListQuery(new PayoutsModel
{ {
PaymentMethods = paymentMethods, PayoutMethods = paymentMethods,
PaymentMethodId = paymentMethodId, PayoutMethodId = payoutMethodId,
PullPaymentId = pullPaymentId, PullPaymentId = pullPaymentId,
PayoutState = payoutState, PayoutState = payoutState,
Skip = skip, Skip = skip,
Count = count, Count = count,
Payouts = new List<PayoutsModel.PayoutModel>(), Payouts = new List<PayoutsModel.PayoutModel>(),
HasPayoutProcessor = await HasPayoutProcessor(storeId, paymentMethodId) HasPayoutProcessor = await HasPayoutProcessor(storeId, payoutMethodId)
}); });
await using var ctx = _dbContextFactory.CreateContext(); await using var ctx = _dbContextFactory.CreateContext();
var payoutRequest = var payoutRequest =
@@ -512,15 +513,15 @@ namespace BTCPayServer.Controllers
vm.PullPaymentName = (await ctx.PullPayments.FindAsync(pullPaymentId)).GetBlob().Name; vm.PullPaymentName = (await ctx.PullPayments.FindAsync(pullPaymentId)).GetBlob().Name;
} }
if (vm.PaymentMethodId != null) vm.PayoutMethodCount = (await payoutRequest.GroupBy(data => data.PaymentMethodId)
{
var pmiStr = vm.PaymentMethodId;
payoutRequest = payoutRequest.Where(p => p.PaymentMethodId == pmiStr);
}
vm.PaymentMethodCount = (await payoutRequest.GroupBy(data => data.PaymentMethodId)
.Select(datas => new { datas.Key, Count = datas.Count() }).ToListAsync()) .Select(datas => new { datas.Key, Count = datas.Count() }).ToListAsync())
.ToDictionary(datas => datas.Key, arg => arg.Count); .ToDictionary(datas => datas.Key, arg => arg.Count);
if (vm.PayoutMethodId != null)
{
var pmiStr = vm.PayoutMethodId;
payoutRequest = payoutRequest.Where(p => p.PaymentMethodId == pmiStr);
}
vm.PayoutStateCount = payoutRequest.GroupBy(data => data.State) vm.PayoutStateCount = payoutRequest.GroupBy(data => data.State)
.Select(e => new { e.Key, Count = e.Count() }) .Select(e => new { e.Key, Count = e.Count() })
.ToDictionary(arg => arg.Key, arg => arg.Count); .ToDictionary(arg => arg.Key, arg => arg.Count);
@@ -566,6 +567,8 @@ namespace BTCPayServer.Controllers
{ {
payoutSourceLink = Url.Action("ViewPullPayment", "UIPullPayment", new { pullPaymentId = item.PullPayment?.Id }); payoutSourceLink = Url.Action("ViewPullPayment", "UIPullPayment", new { pullPaymentId = item.PullPayment?.Id });
} }
var pCurrency = _payoutHandlers.TryGet(PayoutMethodId.Parse(item.Payout.PaymentMethodId))?.Currency;
var m = new PayoutsModel.PayoutModel var m = new PayoutsModel.PayoutModel
{ {
PullPaymentId = item.PullPayment?.Id, PullPaymentId = item.PullPayment?.Id,
@@ -573,11 +576,11 @@ namespace BTCPayServer.Controllers
SourceLink = payoutSourceLink, SourceLink = payoutSourceLink,
Date = item.Payout.Date, Date = item.Payout.Date,
PayoutId = item.Payout.Id, PayoutId = item.Payout.Id,
Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? PaymentMethodId.Parse(item.Payout.PaymentMethodId).CryptoCode), Amount = _displayFormatter.Currency(payoutBlob.Amount, ppBlob?.Currency ?? pCurrency),
Destination = payoutBlob.Destination Destination = payoutBlob.Destination
}; };
var handler = _payoutHandlers var handler = _payoutHandlers
.FindPayoutHandler(item.Payout.GetPaymentMethodId()); .TryGet(item.Payout.GetPayoutMethodId());
var proofBlob = handler?.ParseProof(item.Payout); var proofBlob = handler?.ParseProof(item.Payout);
m.ProofLink = proofBlob?.Link; m.ProofLink = proofBlob?.Link;
vm.Payouts.Add(m); vm.Payouts.Add(m);
@@ -585,12 +588,15 @@ namespace BTCPayServer.Controllers
return View(vm); return View(vm);
} }
private async Task<bool> HasPayoutProcessor(string storeId, string paymentMethodId) private async Task<bool> HasPayoutProcessor(string storeId, PayoutMethodId payoutMethodId)
{ {
var pmId = PaymentMethodId.Parse(paymentMethodId);
var processors = await _payoutProcessorService.GetProcessors( var processors = await _payoutProcessorService.GetProcessors(
new PayoutProcessorService.PayoutProcessorQuery { Stores = [storeId], PaymentMethods = [pmId] }); new PayoutProcessorService.PayoutProcessorQuery { Stores = [storeId], PayoutMethodIds = [payoutMethodId] });
return _payoutProcessorFactories.Any(factory => factory.GetSupportedPaymentMethods().Contains(pmId)) && processors.Any(); return _payoutProcessorFactories.Any(factory => factory.GetSupportedPayoutMethods().Contains(payoutMethodId)) && processors.Any();
}
private async Task<bool> HasPayoutProcessor(string storeId, string payoutMethodId)
{
return PayoutMethodId.TryParse(payoutMethodId, out var pmId) && await HasPayoutProcessor(storeId, pmId);
} }
} }
} }

View File

@@ -21,6 +21,7 @@ using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.PayJoin; using BTCPayServer.Payments.PayJoin;
using BTCPayServer.Payouts;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels; using BTCPayServer.Services.Labels;
@@ -761,14 +762,14 @@ namespace BTCPayServer.Controllers
CreatePSBTResponse psbtResponse; CreatePSBTResponse psbtResponse;
if (command == "schedule") if (command == "schedule")
{ {
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(walletId.CryptoCode); var pmi = PayoutTypes.CHAIN.GetPayoutMethodId(walletId.CryptoCode);
var claims = var claims =
vm.Outputs.Where(output => string.IsNullOrEmpty(output.PayoutId)).Select(output => new ClaimRequest() vm.Outputs.Where(output => string.IsNullOrEmpty(output.PayoutId)).Select(output => new ClaimRequest()
{ {
Destination = new AddressClaimDestination( Destination = new AddressClaimDestination(
BitcoinAddress.Create(output.DestinationAddress, network.NBitcoinNetwork)), BitcoinAddress.Create(output.DestinationAddress, network.NBitcoinNetwork)),
Value = output.Amount, Value = output.Amount,
PaymentMethodId = pmi, PayoutMethodId = pmi,
StoreId = walletId.StoreId, StoreId = walletId.StoreId,
PreApprove = true, PreApprove = true,
}).ToArray(); }).ToArray();

View File

@@ -38,13 +38,16 @@ namespace BTCPayServer.Data
paymentData.Blob2 = JToken.FromObject(blob, InvoiceDataExtensions.DefaultSerializer).ToString(Newtonsoft.Json.Formatting.None); paymentData.Blob2 = JToken.FromObject(blob, InvoiceDataExtensions.DefaultSerializer).ToString(Newtonsoft.Json.Formatting.None);
return paymentData; return paymentData;
} }
public static PaymentMethodId GetPaymentMethodId(this PaymentData paymentData)
{
return PaymentMethodId.Parse(paymentData.Type);
}
public static PaymentEntity GetBlob(this PaymentData paymentData) public static PaymentEntity GetBlob(this PaymentData paymentData)
{ {
var entity = JToken.Parse(paymentData.Blob2).ToObject<PaymentEntity>(InvoiceDataExtensions.DefaultSerializer) ?? throw new FormatException($"Invalid {nameof(PaymentEntity)}"); var entity = JToken.Parse(paymentData.Blob2).ToObject<PaymentEntity>(InvoiceDataExtensions.DefaultSerializer) ?? throw new FormatException($"Invalid {nameof(PaymentEntity)}");
entity.Status = paymentData.Status!.Value; entity.Status = paymentData.Status!.Value;
entity.Currency = paymentData.Currency; entity.Currency = paymentData.Currency;
entity.PaymentMethodId = PaymentMethodId.Parse(paymentData.Type); entity.PaymentMethodId = GetPaymentMethodId(paymentData);
entity.Value = paymentData.Amount!.Value; entity.Value = paymentData.Amount!.Value;
entity.Id = paymentData.Id; entity.Id = paymentData.Id;
entity.ReceivedTime = paymentData.Created!.Value; entity.ReceivedTime = paymentData.Created!.Value;

View File

@@ -14,6 +14,7 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payouts;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications;
@@ -31,10 +32,11 @@ using NewBlockEvent = BTCPayServer.Events.NewBlockEvent;
using PayoutData = BTCPayServer.Data.PayoutData; using PayoutData = BTCPayServer.Data.PayoutData;
using StoreData = BTCPayServer.Data.StoreData; using StoreData = BTCPayServer.Data.StoreData;
public class BitcoinLikePayoutHandler : IPayoutHandler public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
{ {
public string Currency { get; }
private readonly BTCPayNetworkProvider _btcPayNetworkProvider; private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _paymentHandlers;
private readonly ExplorerClientProvider _explorerClientProvider; private readonly ExplorerClientProvider _explorerClientProvider;
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings; private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
private readonly ApplicationDbContextFactory _dbContextFactory; private readonly ApplicationDbContextFactory _dbContextFactory;
@@ -43,9 +45,15 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly TransactionLinkProviders _transactionLinkProviders; private readonly TransactionLinkProviders _transactionLinkProviders;
PayoutMethodId IHandler<PayoutMethodId>.Id => PayoutMethodId;
public PayoutMethodId PayoutMethodId { get; }
public PaymentMethodId PaymentMethodId { get; }
public BTCPayNetwork Network { get; }
public WalletRepository WalletRepository { get; } public WalletRepository WalletRepository { get; }
public BitcoinLikePayoutHandler(BTCPayNetworkProvider btcPayNetworkProvider, public BitcoinLikePayoutHandler(BTCPayNetworkProvider btcPayNetworkProvider,
PayoutMethodId payoutMethodId,
BTCPayNetwork network,
PaymentMethodHandlerDictionary handlers, PaymentMethodHandlerDictionary handlers,
WalletRepository walletRepository, WalletRepository walletRepository,
ExplorerClientProvider explorerClientProvider, ExplorerClientProvider explorerClientProvider,
@@ -57,51 +65,46 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
TransactionLinkProviders transactionLinkProviders) TransactionLinkProviders transactionLinkProviders)
{ {
_btcPayNetworkProvider = btcPayNetworkProvider; _btcPayNetworkProvider = btcPayNetworkProvider;
_handlers = handlers; PayoutMethodId = payoutMethodId;
PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
Network = network;
_paymentHandlers = handlers;
WalletRepository = walletRepository; WalletRepository = walletRepository;
_explorerClientProvider = explorerClientProvider; _explorerClientProvider = explorerClientProvider;
_jsonSerializerSettings = jsonSerializerSettings; _jsonSerializerSettings = jsonSerializerSettings;
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_notificationSender = notificationSender; _notificationSender = notificationSender;
Currency = network.CryptoCode;
this.Logs = logs; this.Logs = logs;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_transactionLinkProviders = transactionLinkProviders; _transactionLinkProviders = transactionLinkProviders;
} }
public bool CanHandle(PaymentMethodId paymentMethod)
{
return _handlers.TryGetValue(paymentMethod, out var h) &&
h is BitcoinLikePaymentHandler { Network: { ReadonlyWallet: false } };
}
public async Task TrackClaim(ClaimRequest claimRequest, PayoutData payoutData) public async Task TrackClaim(ClaimRequest claimRequest, PayoutData payoutData)
{ {
var network = _handlers.GetNetwork(claimRequest.PaymentMethodId); var explorerClient = _explorerClientProvider.GetExplorerClient(Network);
var explorerClient = _explorerClientProvider.GetExplorerClient(network);
if (claimRequest.Destination is IBitcoinLikeClaimDestination bitcoinLikeClaimDestination) if (claimRequest.Destination is IBitcoinLikeClaimDestination bitcoinLikeClaimDestination)
{ {
await explorerClient.TrackAsync(TrackedSource.Create(bitcoinLikeClaimDestination.Address)); await explorerClient.TrackAsync(TrackedSource.Create(bitcoinLikeClaimDestination.Address));
await WalletRepository.AddWalletTransactionAttachment( await WalletRepository.AddWalletTransactionAttachment(
new WalletId(claimRequest.StoreId, network.CryptoCode), new WalletId(claimRequest.StoreId, Network.CryptoCode),
bitcoinLikeClaimDestination.Address.ToString(), bitcoinLikeClaimDestination.Address.ToString(),
Attachment.Payout(payoutData.PullPaymentDataId, payoutData.Id), WalletObjectData.Types.Address); Attachment.Payout(payoutData.PullPaymentDataId, payoutData.Id), WalletObjectData.Types.Address);
} }
} }
public Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination, CancellationToken cancellationToken) public Task<(IClaimDestination destination, string error)> ParseClaimDestination(string destination, CancellationToken cancellationToken)
{ {
var network = _handlers.GetNetwork(paymentMethodId);
destination = destination.Trim(); destination = destination.Trim();
try try
{ {
if (destination.StartsWith($"{network.NBitcoinNetwork.UriScheme}:", StringComparison.OrdinalIgnoreCase)) if (destination.StartsWith($"{Network.NBitcoinNetwork.UriScheme}:", StringComparison.OrdinalIgnoreCase))
{ {
return Task.FromResult<(IClaimDestination, string)>((new UriClaimDestination(new BitcoinUrlBuilder(destination, network.NBitcoinNetwork)), null)); return Task.FromResult<(IClaimDestination, string)>((new UriClaimDestination(new BitcoinUrlBuilder(destination, Network.NBitcoinNetwork)), null));
} }
return Task.FromResult<(IClaimDestination, string)>((new AddressClaimDestination(BitcoinAddress.Create(destination, network.NBitcoinNetwork)), null)); return Task.FromResult<(IClaimDestination, string)>((new AddressClaimDestination(BitcoinAddress.Create(destination, Network.NBitcoinNetwork)), null));
} }
catch catch
{ {
@@ -119,18 +122,16 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
{ {
if (payout?.Proof is null) if (payout?.Proof is null)
return null; return null;
var paymentMethodId = payout.GetPaymentMethodId(); var payoutMethodId = payout.GetPayoutMethodId();
if (paymentMethodId is null) if (payoutMethodId is null)
return null;
var cryptoCode = _handlers.TryGetNetwork(paymentMethodId)?.CryptoCode;
if (cryptoCode is null)
return null; return null;
var cryptoCode = Network.CryptoCode;
ParseProofType(payout.Proof, out var raw, out var proofType); ParseProofType(payout.Proof, out var raw, out var proofType);
if (proofType == PayoutTransactionOnChainBlob.Type) if (proofType == PayoutTransactionOnChainBlob.Type)
{ {
var res = raw.ToObject<PayoutTransactionOnChainBlob>( var res = raw.ToObject<PayoutTransactionOnChainBlob>(
JsonSerializer.Create(_jsonSerializerSettings.GetSerializer(cryptoCode))); JsonSerializer.Create(_jsonSerializerSettings.GetSerializer(payoutMethodId)));
if (res == null) if (res == null)
return null; return null;
res.LinkTemplate = _transactionLinkProviders.GetBlockExplorerLink(cryptoCode); res.LinkTemplate = _transactionLinkProviders.GetBlockExplorerLink(cryptoCode);
@@ -184,10 +185,9 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
} }
} }
public Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethodId, IClaimDestination claimDestination) public Task<decimal> GetMinimumPayoutAmount(IClaimDestination claimDestination)
{ {
var network = _handlers.TryGetNetwork(paymentMethodId); if (Network
if (network?
.NBitcoinNetwork? .NBitcoinNetwork?
.Consensus? .Consensus?
.ConsensusFactory? .ConsensusFactory?
@@ -226,8 +226,8 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
Stores = new[] { storeId }, Stores = new[] { storeId },
PayoutIds = payoutIds PayoutIds = payoutIds
}, context)).Where(data => }, context)).Where(data =>
PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) && PayoutMethodId.TryParse(data.PaymentMethodId, out var payoutMethodId) &&
CanHandle(paymentMethodId)) payoutMethodId == PayoutMethodId)
.Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple => tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == false); .Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple => tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == false);
foreach (var valueTuple in payouts) foreach (var valueTuple in payouts)
{ {
@@ -251,8 +251,8 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
Stores = new[] { storeId }, Stores = new[] { storeId },
PayoutIds = payoutIds PayoutIds = payoutIds
}, context)).Where(data => }, context)).Where(data =>
PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) && PayoutMethodId.TryParse(data.PaymentMethodId, out var payoutMethodId) &&
CanHandle(paymentMethodId)) payoutMethodId == PayoutMethodId)
.Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple => tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == true); .Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple => tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == true);
foreach (var valueTuple in payouts) foreach (var valueTuple in payouts)
{ {
@@ -273,25 +273,23 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
return null; return null;
} }
public Task<IEnumerable<PaymentMethodId>> GetSupportedPaymentMethods(StoreData storeData) public bool IsSupported(StoreData storeData)
{ {
return Task.FromResult(storeData.GetPaymentMethodConfigs<DerivationSchemeSettings>(_handlers, true).Select(c => c.Key)); return storeData.GetDerivationSchemeSettings(_paymentHandlers, Network.CryptoCode, true)?.AccountDerivation is not null;
} }
public async Task<IActionResult> InitiatePayment(PaymentMethodId paymentMethodId, string[] payoutIds) public async Task<IActionResult> InitiatePayment(string[] payoutIds)
{ {
await using var ctx = this._dbContextFactory.CreateContext(); await using var ctx = this._dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var pmi = paymentMethodId;
var payouts = await ctx.Payouts.Include(data => data.PullPaymentData) var payouts = await ctx.Payouts.Include(data => data.PullPaymentData)
.Where(data => payoutIds.Contains(data.Id) .Where(data => payoutIds.Contains(data.Id)
&& pmi.ToString() == data.PaymentMethodId && PayoutMethodId.ToString() == data.PaymentMethodId
&& data.State == PayoutState.AwaitingPayment) && data.State == PayoutState.AwaitingPayment)
.ToListAsync(); .ToListAsync();
var pullPaymentIds = payouts.Select(data => data.PullPaymentDataId).Distinct().Where(s => s != null).ToArray(); var pullPaymentIds = payouts.Select(data => data.PullPaymentDataId).Distinct().Where(s => s != null).ToArray();
var storeId = payouts.First().StoreDataId; var storeId = payouts.First().StoreDataId;
var network = _handlers.GetNetwork(paymentMethodId);
List<string> bip21 = new List<string>(); List<string> bip21 = new List<string>();
foreach (var payout in payouts) foreach (var payout in payouts)
{ {
@@ -300,9 +298,9 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
continue; continue;
} }
var blob = payout.GetBlob(_jsonSerializerSettings); var blob = payout.GetBlob(_jsonSerializerSettings);
if (payout.GetPaymentMethodId() != paymentMethodId) if (payout.GetPayoutMethodId() != PayoutMethodId)
continue; continue;
var claim = await ParseClaimDestination(paymentMethodId, blob.Destination, default); var claim = await ParseClaimDestination(blob.Destination, default);
switch (claim.destination) switch (claim.destination)
{ {
case UriClaimDestination uriClaimDestination: case UriClaimDestination uriClaimDestination:
@@ -312,17 +310,17 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
bip21.Add(newUri.Uri.ToString()); bip21.Add(newUri.Uri.ToString());
break; break;
case AddressClaimDestination addressClaimDestination: case AddressClaimDestination addressClaimDestination:
var bip21New = network.GenerateBIP21(addressClaimDestination.Address.ToString(), blob.CryptoAmount.Value); var bip21New = Network.GenerateBIP21(addressClaimDestination.Address.ToString(), blob.CryptoAmount.Value);
bip21New.QueryParams.Add("payout", payout.Id); bip21New.QueryParams.Add("payout", payout.Id);
bip21.Add(bip21New.ToString()); bip21.Add(bip21New.ToString());
break; break;
} }
} }
if (bip21.Any()) if (bip21.Any())
return new RedirectToActionResult("WalletSend", "UIWallets", new { walletId = new WalletId(storeId, network.CryptoCode).ToString(), bip21 }); return new RedirectToActionResult("WalletSend", "UIWallets", new { walletId = new WalletId(storeId, Network.CryptoCode).ToString(), bip21 });
return new RedirectToActionResult("Payouts", "UIWallets", new return new RedirectToActionResult("Payouts", "UIWallets", new
{ {
walletId = new WalletId(storeId, network.CryptoCode).ToString(), walletId = new WalletId(storeId, Network.CryptoCode).ToString(),
pullPaymentId = pullPaymentIds.Length == 1 ? pullPaymentIds.First() : null pullPaymentId = pullPaymentIds.Length == 1 ? pullPaymentIds.First() : null
}); });
} }
@@ -347,7 +345,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
} }
foreach (var txid in proof.Candidates.ToList()) foreach (var txid in proof.Candidates.ToList())
{ {
var explorer = _explorerClientProvider.GetExplorerClient(payout.GetPaymentMethodId().CryptoCode); var explorer = _explorerClientProvider.GetExplorerClient(Network.CryptoCode);
var tx = await explorer.GetTransactionAsync(txid); var tx = await explorer.GetTransactionAsync(txid);
if (tx is null) if (tx is null)
{ {
@@ -447,7 +445,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
return; return;
var derivationSchemeSettings = payout.StoreData var derivationSchemeSettings = payout.StoreData
.GetDerivationSchemeSettings(_handlers, newTransaction.CryptoCode)?.AccountDerivation; .GetDerivationSchemeSettings(_paymentHandlers, newTransaction.CryptoCode)?.AccountDerivation;
if (derivationSchemeSettings is null) if (derivationSchemeSettings is null)
return; return;
@@ -494,7 +492,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
public void SetProofBlob(PayoutData data, PayoutTransactionOnChainBlob blob) public void SetProofBlob(PayoutData data, PayoutTransactionOnChainBlob blob)
{ {
data.SetProofBlob(blob, _jsonSerializerSettings.GetSerializer(data.GetPaymentMethodId().CryptoCode)); data.SetProofBlob(blob, _jsonSerializerSettings.GetSerializer(data.GetPayoutMethodId()));
} }
} }

View File

@@ -3,25 +3,30 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using PayoutData = BTCPayServer.Data.PayoutData; using PayoutData = BTCPayServer.Data.PayoutData;
using StoreData = BTCPayServer.Data.StoreData; using StoreData = BTCPayServer.Data.StoreData;
public interface IPayoutHandler public interface IPayoutHandler : IHandler<PayoutMethodId>
{ {
public bool CanHandle(PaymentMethodId paymentMethod); PayoutMethodId IHandler<PayoutMethodId>.Id => PayoutMethodId;
string Currency { get; }
public PayoutMethodId PayoutMethodId { get; }
bool IsSupported(StoreData storeData);
public Task TrackClaim(ClaimRequest claimRequest, PayoutData payoutData); public Task TrackClaim(ClaimRequest claimRequest, PayoutData payoutData);
//Allows payout handler to parse payout destinations on its own //Allows payout handler to parse payout destinations on its own
public Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination, CancellationToken cancellationToken); public Task<(IClaimDestination destination, string error)> ParseClaimDestination(string destination, CancellationToken cancellationToken);
public (bool valid, string? error) ValidateClaimDestination(IClaimDestination claimDestination, PullPaymentBlob? pullPaymentBlob); public (bool valid, string? error) ValidateClaimDestination(IClaimDestination claimDestination, PullPaymentBlob? pullPaymentBlob);
public async Task<(IClaimDestination? destination, string? error)> ParseAndValidateClaimDestination(PaymentMethodId paymentMethodId, string destination, PullPaymentBlob? pullPaymentBlob, CancellationToken cancellationToken) public async Task<(IClaimDestination? destination, string? error)> ParseAndValidateClaimDestination(string destination, PullPaymentBlob? pullPaymentBlob, CancellationToken cancellationToken)
{ {
var res = await ParseClaimDestination(paymentMethodId, destination, cancellationToken); var res = await ParseClaimDestination(destination, cancellationToken);
if (res.destination is null) if (res.destination is null)
return res; return res;
var res2 = ValidateClaimDestination(res.destination, pullPaymentBlob); var res2 = ValidateClaimDestination(res.destination, pullPaymentBlob);
@@ -34,9 +39,8 @@ public interface IPayoutHandler
void StartBackgroundCheck(Action<Type[]> subscribe); void StartBackgroundCheck(Action<Type[]> subscribe);
//allows you to process events that the main pull payment hosted service is subscribed to //allows you to process events that the main pull payment hosted service is subscribed to
Task BackgroundCheck(object o); Task BackgroundCheck(object o);
Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethod, IClaimDestination claimDestination); Task<decimal> GetMinimumPayoutAmount(IClaimDestination claimDestination);
Dictionary<PayoutState, List<(string Action, string Text)>> GetPayoutSpecificActions(); Dictionary<PayoutState, List<(string Action, string Text)>> GetPayoutSpecificActions();
Task<StatusMessageModel> DoSpecificAction(string action, string[] payoutIds, string storeId); Task<StatusMessageModel> DoSpecificAction(string action, string[] payoutIds, string storeId);
Task<IEnumerable<PaymentMethodId>> GetSupportedPaymentMethods(StoreData storeData); Task<IActionResult> InitiatePayment(string[] payoutIds);
Task<IActionResult> InitiatePayment(PaymentMethodId paymentMethodId, string[] payoutIds);
} }

View File

@@ -6,45 +6,60 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using LNURL; using LNURL;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MimeKit;
using NBitcoin; using NBitcoin;
namespace BTCPayServer.Data.Payouts.LightningLike namespace BTCPayServer.Data.Payouts.LightningLike
{ {
public class LightningLikePayoutHandler : IPayoutHandler public class LightningLikePayoutHandler : IPayoutHandler, IHasNetwork
{ {
public string Currency { get; }
public PayoutMethodId PayoutMethodId { get; }
public PaymentMethodId PaymentMethodId { get; }
private readonly IOptions<LightningNetworkOptions> _options;
private PaymentMethodHandlerDictionary _paymentHandlers;
public BTCPayNetwork Network { get; }
public const string LightningLikePayoutHandlerOnionNamedClient = public const string LightningLikePayoutHandlerOnionNamedClient =
nameof(LightningLikePayoutHandlerOnionNamedClient); nameof(LightningLikePayoutHandlerOnionNamedClient);
public const string LightningLikePayoutHandlerClearnetNamedClient = public const string LightningLikePayoutHandlerClearnetNamedClient =
nameof(LightningLikePayoutHandlerClearnetNamedClient); nameof(LightningLikePayoutHandlerClearnetNamedClient);
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly UserService _userService; private readonly UserService _userService;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
public LightningLikePayoutHandler(PaymentMethodHandlerDictionary handlers, public LightningLikePayoutHandler(
PayoutMethodId payoutMethodId,
IOptions<LightningNetworkOptions> options,
BTCPayNetwork network,
PaymentMethodHandlerDictionary paymentHandlers,
IHttpClientFactory httpClientFactory, UserService userService, IAuthorizationService authorizationService) IHttpClientFactory httpClientFactory, UserService userService, IAuthorizationService authorizationService)
{ {
_handlers = handlers; _paymentHandlers = paymentHandlers;
Network = network;
PayoutMethodId = payoutMethodId;
_options = options;
PaymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_userService = userService; _userService = userService;
_authorizationService = authorizationService; _authorizationService = authorizationService;
} Currency = network.CryptoCode;
public bool CanHandle(PaymentMethodId paymentMethod)
{
return _handlers.TryGetValue(paymentMethod, out var h) && h is ILightningPaymentHandler;
} }
public Task TrackClaim(ClaimRequest claimRequest, PayoutData payoutData) public Task TrackClaim(ClaimRequest claimRequest, PayoutData payoutData)
@@ -59,10 +74,9 @@ namespace BTCPayServer.Data.Payouts.LightningLike
: LightningLikePayoutHandlerClearnetNamedClient); : LightningLikePayoutHandlerClearnetNamedClient);
} }
public async Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination, CancellationToken cancellationToken) public async Task<(IClaimDestination destination, string error)> ParseClaimDestination(string destination, CancellationToken cancellationToken)
{ {
destination = destination.Trim(); destination = destination.Trim();
var network = ((IHasNetwork)_handlers[paymentMethodId]).Network;
try try
{ {
string lnurlTag = null; string lnurlTag = null;
@@ -92,7 +106,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
} }
var result = var result =
BOLT11PaymentRequest.TryParse(destination, out var invoice, network.NBitcoinNetwork) BOLT11PaymentRequest.TryParse(destination, out var invoice, Network.NBitcoinNetwork)
? new BoltInvoiceClaimDestination(destination, invoice) ? new BoltInvoiceClaimDestination(destination, invoice)
: null; : null;
@@ -144,7 +158,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethodId, IClaimDestination claimDestination) public Task<decimal> GetMinimumPayoutAmount(IClaimDestination claimDestination)
{ {
return Task.FromResult(Money.Satoshis(1).ToDecimal(MoneyUnit.BTC)); return Task.FromResult(Money.Satoshis(1).ToDecimal(MoneyUnit.BTC));
} }
@@ -159,34 +173,14 @@ namespace BTCPayServer.Data.Payouts.LightningLike
return Task.FromResult<StatusMessageModel>(null); return Task.FromResult<StatusMessageModel>(null);
} }
public async Task<IEnumerable<PaymentMethodId>> GetSupportedPaymentMethods(StoreData storeData) public bool IsSupported(StoreData storeData)
{ {
var result = new List<PaymentMethodId>(); return storeData.GetPaymentMethodConfig<LightningPaymentMethodConfig>(PaymentMethodId, _paymentHandlers, true)?.IsConfigured(Network, _options.Value) is true;
var methods = storeData.GetPaymentMethodConfigs<LightningPaymentMethodConfig>(_handlers, true);
foreach (var m in methods)
{
if (!m.Value.IsInternalNode)
{
result.Add(m.Key);
continue;
}
foreach (UserStore storeDataUserStore in storeData.UserStores)
{
if (!await _userService.IsAdminUser(storeDataUserStore.ApplicationUserId))
continue;
result.Add(m.Key);
break;
}
}
return result;
} }
public Task<IActionResult> InitiatePayment(PaymentMethodId paymentMethodId, string[] payoutIds) public Task<IActionResult> InitiatePayment(string[] payoutIds)
{ {
var cryptoCode = _handlers.GetNetwork(paymentMethodId).CryptoCode; var cryptoCode = Network.CryptoCode;
return Task.FromResult<IActionResult>(new RedirectToActionResult("ConfirmLightningPayout", return Task.FromResult<IActionResult>(new RedirectToActionResult("ConfirmLightningPayout",
"UILightningLikePayout", new { cryptoCode, payoutIds })); "UILightningLikePayout", new { cryptoCode, payoutIds }));
} }

View File

@@ -12,6 +12,7 @@ using BTCPayServer.Lightning;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Security; using BTCPayServer.Security;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
@@ -33,7 +34,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
private readonly ApplicationDbContextFactory _applicationDbContextFactory; private readonly ApplicationDbContextFactory _applicationDbContextFactory;
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings; private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers; private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _handlers;
private readonly LightningClientFactoryService _lightningClientFactoryService; private readonly LightningClientFactoryService _lightningClientFactoryService;
private readonly IOptions<LightningNetworkOptions> _options; private readonly IOptions<LightningNetworkOptions> _options;
@@ -44,7 +45,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public UILightningLikePayoutController(ApplicationDbContextFactory applicationDbContextFactory, public UILightningLikePayoutController(ApplicationDbContextFactory applicationDbContextFactory,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
IEnumerable<IPayoutHandler> payoutHandlers, PayoutMethodHandlerDictionary payoutHandlers,
PaymentMethodHandlerDictionary handlers, PaymentMethodHandlerDictionary handlers,
StoreRepository storeRepository, StoreRepository storeRepository,
LightningClientFactoryService lightningClientFactoryService, LightningClientFactoryService lightningClientFactoryService,
@@ -64,7 +65,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
} }
private async Task<List<PayoutData>> GetPayouts(ApplicationDbContext dbContext, PaymentMethodId pmi, private async Task<List<PayoutData>> GetPayouts(ApplicationDbContext dbContext, PayoutMethodId pmi,
string[] payoutIds) string[] payoutIds)
{ {
var userId = _userManager.GetUserId(User); var userId = _userManager.GetUserId(User);
@@ -103,7 +104,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{ {
await SetStoreContext(); await SetStoreContext();
var pmi = PaymentTypes.LN.GetPaymentMethodId(cryptoCode); var pmi = PayoutTypes.LN.GetPayoutMethodId(cryptoCode);
await using var ctx = _applicationDbContextFactory.CreateContext(); await using var ctx = _applicationDbContextFactory.CreateContext();
var payouts = await GetPayouts(ctx, pmi, payoutIds); var payouts = await GetPayouts(ctx, pmi, payoutIds);
@@ -127,14 +128,14 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{ {
await SetStoreContext(); await SetStoreContext();
var pmi = PaymentTypes.LN.GetPaymentMethodId(cryptoCode); var pmi = PayoutTypes.LN.GetPayoutMethodId(cryptoCode);
var payoutHandler = (LightningLikePayoutHandler)_payoutHandlers.FindPayoutHandler(pmi); var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var payoutHandler = (LightningLikePayoutHandler)_payoutHandlers.TryGet(pmi);
await using var ctx = _applicationDbContextFactory.CreateContext(); await using var ctx = _applicationDbContextFactory.CreateContext();
var payouts = (await GetPayouts(ctx, pmi, payoutIds)).GroupBy(data => data.StoreDataId); var payouts = (await GetPayouts(ctx, pmi, payoutIds)).GroupBy(data => data.StoreDataId);
var results = new List<ResultVM>(); var results = new List<ResultVM>();
var network = ((IHasNetwork)_handlers[pmi]).Network;
//we group per store and init the transfers by each //we group per store and init the transfers by each
@@ -143,7 +144,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{ {
var store = payoutDatas.First().StoreData; var store = payoutDatas.First().StoreData;
var lightningSupportedPaymentMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, _handlers); var lightningSupportedPaymentMethod = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(paymentMethodId, _handlers);
if (lightningSupportedPaymentMethod.IsInternalNode && !authorizedForInternalNode) if (lightningSupportedPaymentMethod.IsInternalNode && !authorizedForInternalNode)
{ {
@@ -164,33 +165,33 @@ namespace BTCPayServer.Data.Payouts.LightningLike
} }
var client = var client =
lightningSupportedPaymentMethod.CreateLightningClient(network, _options.Value, lightningSupportedPaymentMethod.CreateLightningClient(payoutHandler.Network, _options.Value,
_lightningClientFactoryService); _lightningClientFactoryService);
foreach (var payoutData in payoutDatas) foreach (var payoutData in payoutDatas)
{ {
ResultVM result; ResultVM result;
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings); var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
var claim = await payoutHandler.ParseClaimDestination(pmi, blob.Destination, cancellationToken); var claim = await payoutHandler.ParseClaimDestination(blob.Destination, cancellationToken);
try try
{ {
switch (claim.destination) switch (claim.destination)
{ {
case LNURLPayClaimDestinaton lnurlPayClaimDestinaton: case LNURLPayClaimDestinaton lnurlPayClaimDestinaton:
var lnurlResult = await GetInvoiceFromLNURL(payoutData, payoutHandler, blob, var lnurlResult = await GetInvoiceFromLNURL(payoutData, payoutHandler, blob,
lnurlPayClaimDestinaton, network.NBitcoinNetwork, cancellationToken); lnurlPayClaimDestinaton, payoutHandler.Network.NBitcoinNetwork, cancellationToken);
if (lnurlResult.Item2 is not null) if (lnurlResult.Item2 is not null)
{ {
result = lnurlResult.Item2; result = lnurlResult.Item2;
} }
else else
{ {
result = await TrypayBolt(client, blob, payoutData, lnurlResult.Item1, pmi, cancellationToken); result = await TrypayBolt(client, blob, payoutData, lnurlResult.Item1, payoutHandler.Currency, cancellationToken);
} }
break; break;
case BoltInvoiceClaimDestination item1: case BoltInvoiceClaimDestination item1:
result = await TrypayBolt(client, blob, payoutData, item1.PaymentRequest, pmi, cancellationToken); result = await TrypayBolt(client, blob, payoutData, item1.PaymentRequest, payoutHandler.Currency, cancellationToken);
break; break;
default: default:
@@ -276,18 +277,17 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public static async Task<ResultVM> TrypayBolt( public static async Task<ResultVM> TrypayBolt(
ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest, ILightningClient lightningClient, PayoutBlob payoutBlob, PayoutData payoutData, BOLT11PaymentRequest bolt11PaymentRequest,
PaymentMethodId pmi, CancellationToken cancellationToken) string payoutCurrency, CancellationToken cancellationToken)
{ {
var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC); var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
if (boltAmount > payoutBlob.CryptoAmount) if (boltAmount > payoutBlob.CryptoAmount)
{ {
payoutData.State = PayoutState.Cancelled; payoutData.State = PayoutState.Cancelled;
return new ResultVM return new ResultVM
{ {
PayoutId = payoutData.Id, PayoutId = payoutData.Id,
Result = PayResult.Error, Result = PayResult.Error,
Message = $"The BOLT11 invoice amount ({boltAmount} {pmi.CryptoCode}) did not match the payout's amount ({payoutBlob.CryptoAmount.GetValueOrDefault()} {pmi.CryptoCode})", Message = $"The BOLT11 invoice amount ({boltAmount} {payoutCurrency}) did not match the payout's amount ({payoutBlob.CryptoAmount.GetValueOrDefault()} {payoutCurrency})",
Destination = payoutBlob.Destination Destination = payoutBlob.Destination
}; };
} }

View File

@@ -5,6 +5,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Services; using BTCPayServer.Services;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -28,9 +29,9 @@ namespace BTCPayServer.Data
return payout; return payout;
} }
public static PaymentMethodId GetPaymentMethodId(this PayoutData data) public static PayoutMethodId GetPayoutMethodId(this PayoutData data)
{ {
return PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) ? paymentMethodId : null; return PayoutMethodId.TryParse(data.PaymentMethodId, out var pmi) ? pmi : null;
} }
public static string GetPayoutSource(this PayoutData data, BTCPayNetworkJsonSerializerSettings jsonSerializerSettings) public static string GetPayoutSource(this PayoutData data, BTCPayNetworkJsonSerializerSettings jsonSerializerSettings)
@@ -44,13 +45,13 @@ namespace BTCPayServer.Data
public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers) public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers)
{ {
var result = JsonConvert.DeserializeObject<PayoutBlob>(data.Blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)); var result = JsonConvert.DeserializeObject<PayoutBlob>(data.Blob, serializers.GetSerializer(data.GetPayoutMethodId()));
result.Metadata ??= new JObject(); result.Metadata ??= new JObject();
return result; return result;
} }
public static void SetBlob(this PayoutData data, PayoutBlob blob, BTCPayNetworkJsonSerializerSettings serializers) public static void SetBlob(this PayoutData data, PayoutBlob blob, BTCPayNetworkJsonSerializerSettings serializers)
{ {
data.Blob = JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode)).ToString(); data.Blob = JsonConvert.SerializeObject(blob, serializers.GetSerializer(data.GetPayoutMethodId())).ToString();
} }
public static JObject GetProofBlobJson(this PayoutData data) public static JObject GetProofBlobJson(this PayoutData data)
@@ -83,10 +84,9 @@ namespace BTCPayServer.Data
} }
} }
public static async Task<List<PaymentMethodId>> GetSupportedPaymentMethods( public static HashSet<PayoutMethodId> GetSupportedPayoutMethods(this PayoutMethodHandlerDictionary payoutHandlers, StoreData storeData)
this IEnumerable<IPayoutHandler> payoutHandlers, StoreData storeData)
{ {
return (await Task.WhenAll(payoutHandlers.Select(handler => handler.GetSupportedPaymentMethods(storeData)))).SelectMany(ids => ids).ToList(); return payoutHandlers.Where(handler => handler.IsSupported(storeData)).Select(p => p.PayoutMethodId).ToHashSet();
} }
} }
} }

View File

@@ -3,6 +3,7 @@ using System.ComponentModel;
using BTCPayServer.Client.JsonConverters; using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters; using BTCPayServer.JsonConverters;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace BTCPayServer.Data namespace BTCPayServer.Data
@@ -27,8 +28,8 @@ namespace BTCPayServer.Data
public TimeSpan BOLT11Expiration { get; set; } public TimeSpan BOLT11Expiration { get; set; }
[JsonProperty(ItemConverterType = typeof(PaymentMethodIdJsonConverter))] [JsonProperty(ItemConverterType = typeof(PayoutMethodIdJsonConverter))]
public PaymentMethodId[] SupportedPaymentMethods { get; set; } public PayoutMethodId[] SupportedPaymentMethods { get; set; }
public bool AutoApproveClaims { get; set; } public bool AutoApproveClaims { get; set; }

View File

@@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using BTCPayServer.Payouts;
using NBitcoin.JsonConverters; using NBitcoin.JsonConverters;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -19,7 +20,7 @@ namespace BTCPayServer.Data
data.Blob = JsonConvert.SerializeObject(blob).ToString(); data.Blob = JsonConvert.SerializeObject(blob).ToString();
} }
public static bool IsSupported(this PullPaymentData data, Payments.PaymentMethodId paymentId) public static bool IsSupported(this PullPaymentData data, PayoutMethodId paymentId)
{ {
return data.GetBlob().SupportedPaymentMethods.Contains(paymentId); return data.GetBlob().SupportedPaymentMethods.Contains(paymentId);
} }

View File

@@ -25,6 +25,7 @@ using BTCPayServer.NTag424;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Security; using BTCPayServer.Security;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Reporting; using BTCPayServer.Services.Reporting;
@@ -336,13 +337,6 @@ namespace BTCPayServer
return transactions.Select(t => t.Result).Where(t => t != null).ToDictionary(o => o.Transaction.GetHash()); return transactions.Select(t => t.Result).Where(t => t != null).ToDictionary(o => o.Transaction.GetHash());
} }
#nullable enable
public static IPayoutHandler? FindPayoutHandler(this IEnumerable<IPayoutHandler> handlers, PaymentMethodId paymentMethodId)
{
return handlers.FirstOrDefault(h => h.CanHandle(paymentMethodId));
}
#nullable restore
public static async Task<PSBT> UpdatePSBT(this ExplorerClientProvider explorerClientProvider, DerivationSchemeSettings derivationSchemeSettings, PSBT psbt) public static async Task<PSBT> UpdatePSBT(this ExplorerClientProvider explorerClientProvider, DerivationSchemeSettings derivationSchemeSettings, PSBT psbt)
{ {
var result = await explorerClientProvider.GetExplorerClient(psbt.Network.NetworkSet.CryptoCode).UpdatePSBTAsync(new UpdatePSBTRequest() var result = await explorerClientProvider.GetExplorerClient(psbt.Network.NetworkSet.CryptoCode).UpdatePSBTAsync(new UpdatePSBTRequest()
@@ -436,19 +430,23 @@ namespace BTCPayServer
var h = (BitcoinLikePaymentHandler)handlers[pmi]; var h = (BitcoinLikePaymentHandler)handlers[pmi];
return h; return h;
} }
public static BTCPayNetwork? TryGetNetwork(this PaymentMethodHandlerDictionary handlers, PaymentMethodId paymentMethodId) public static BTCPayNetwork? TryGetNetwork<TId, THandler>(this HandlersDictionary<TId, THandler> handlers, TId id)
where THandler : IHandler<TId>
where TId : notnull
{ {
if (paymentMethodId is not null && if (id is not null &&
handlers.TryGetValue(paymentMethodId, out var value) && handlers.TryGetValue(id, out var value) &&
value is IHasNetwork { Network: var n }) value is IHasNetwork { Network: var n })
{ {
return n; return n;
} }
return null; return null;
} }
public static BTCPayNetwork GetNetwork(this PaymentMethodHandlerDictionary handlers, PaymentMethodId paymentMethodId) public static BTCPayNetwork GetNetwork<TId, THandler>(this HandlersDictionary<TId, THandler> handlers, TId id)
where THandler : IHandler<TId>
where TId : notnull
{ {
return TryGetNetwork(handlers, paymentMethodId) ?? throw new KeyNotFoundException($"Network for {paymentMethodId} is not found"); return TryGetNetwork(handlers, id) ?? throw new KeyNotFoundException($"Network for {id} is not found");
} }
public static LightningPaymentMethodConfig? GetLightningConfig(this PaymentMethodHandlerDictionary handlers, Data.StoreData store, BTCPayNetwork network) public static LightningPaymentMethodConfig? GetLightningConfig(this PaymentMethodHandlerDictionary handlers, Data.StoreData store, BTCPayNetwork network)
{ {

View File

@@ -38,10 +38,10 @@ namespace BTCPayServer
return permissionSet.Contains(permission, storeId); return permissionSet.Contains(permission, storeId);
} }
public static DerivationSchemeSettings? GetDerivationSchemeSettings(this StoreData store, PaymentMethodHandlerDictionary handlers, string cryptoCode) public static DerivationSchemeSettings? GetDerivationSchemeSettings(this StoreData store, PaymentMethodHandlerDictionary handlers, string cryptoCode, bool onlyEnabled = false)
{ {
var pmi = Payments.PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode); var pmi = Payments.PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
return store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers); return store.GetPaymentMethodConfig<DerivationSchemeSettings>(pmi, handlers, onlyEnabled);
} }
} }
} }

View File

@@ -0,0 +1,48 @@
#nullable enable
using BTCPayServer.Payments;
using System.Diagnostics.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Collections;
using Amazon.Runtime.Internal.Transform;
namespace BTCPayServer
{
public interface IHandler<TId> { TId Id { get; } }
public class HandlersDictionary<TId, THandler> : IEnumerable<THandler> where THandler: IHandler<TId>
where TId: notnull
{
public HandlersDictionary(IEnumerable<THandler> handlers)
{
foreach (var handler in handlers)
{
_mappedHandlers.Add(handler.Id, handler);
}
}
private readonly Dictionary<TId, THandler> _mappedHandlers =
new Dictionary<TId, THandler>();
public bool TryGetValue(TId id, [MaybeNullWhen(false)] out THandler value)
{
ArgumentNullException.ThrowIfNull(id);
return _mappedHandlers.TryGetValue(id, out value);
}
public THandler? TryGet(TId id)
{
ArgumentNullException.ThrowIfNull(id);
_mappedHandlers.TryGetValue(id, out var value);
return value;
}
public bool Support(TId id) => _mappedHandlers.ContainsKey(id);
public THandler this[TId index] => _mappedHandlers[index];
public IEnumerator<THandler> GetEnumerator()
{
return _mappedHandlers.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@@ -10,6 +10,7 @@ using BTCPayServer.Data;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using BTCPayServer.Models.WalletViewModels; using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
@@ -39,7 +40,7 @@ namespace BTCPayServer.HostedServices
public string Currency { get; set; } public string Currency { get; set; }
public string CustomCSSLink { get; set; } public string CustomCSSLink { get; set; }
public string EmbeddedCSS { get; set; } public string EmbeddedCSS { get; set; }
public PaymentMethodId[] PaymentMethodIds { get; set; } public PayoutMethodId[] PayoutMethodIds { get; set; }
public TimeSpan? Period { get; set; } public TimeSpan? Period { get; set; }
public bool AutoApproveClaims { get; set; } public bool AutoApproveClaims { get; set; }
public TimeSpan? BOLT11Expiration { get; set; } public TimeSpan? BOLT11Expiration { get; set; }
@@ -131,7 +132,7 @@ namespace BTCPayServer.HostedServices
Currency = create.Currency, Currency = create.Currency,
Limit = create.Amount, Limit = create.Amount,
Period = o.Period is long periodSeconds ? (TimeSpan?)TimeSpan.FromSeconds(periodSeconds) : null, Period = o.Period is long periodSeconds ? (TimeSpan?)TimeSpan.FromSeconds(periodSeconds) : null,
SupportedPaymentMethods = create.PaymentMethodIds, SupportedPaymentMethods = create.PayoutMethodIds,
AutoApproveClaims = create.AutoApproveClaims, AutoApproveClaims = create.AutoApproveClaims,
View = new PullPaymentBlob.PullPaymentView() View = new PullPaymentBlob.PullPaymentView()
{ {
@@ -153,7 +154,7 @@ namespace BTCPayServer.HostedServices
public PayoutState[] States { get; set; } public PayoutState[] States { get; set; }
public string[] PullPayments { get; set; } public string[] PullPayments { get; set; }
public string[] PayoutIds { get; set; } public string[] PayoutIds { get; set; }
public string[] PaymentMethods { get; set; } public string[] PayoutMethods { get; set; }
public string[] Stores { get; set; } public string[] Stores { get; set; }
public bool IncludeArchived { get; set; } public bool IncludeArchived { get; set; }
public bool IncludeStoreData { get; set; } public bool IncludeStoreData { get; set; }
@@ -203,16 +204,16 @@ namespace BTCPayServer.HostedServices
} }
} }
if (payoutQuery.PaymentMethods is not null) if (payoutQuery.PayoutMethods is not null)
{ {
if (payoutQuery.PaymentMethods.Length == 1) if (payoutQuery.PayoutMethods.Length == 1)
{ {
var pm = payoutQuery.PaymentMethods[0]; var pm = payoutQuery.PayoutMethods[0];
query = query.Where(data => pm == data.PaymentMethodId); query = query.Where(data => pm == data.PaymentMethodId);
} }
else else
{ {
query = query.Where(data => payoutQuery.PaymentMethods.Contains(data.PaymentMethodId)); query = query.Where(data => payoutQuery.PayoutMethods.Contains(data.PaymentMethodId));
} }
} }
@@ -284,10 +285,9 @@ namespace BTCPayServer.HostedServices
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings, BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
EventAggregator eventAggregator, EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider networkProvider,
PaymentMethodHandlerDictionary handlers, PayoutMethodHandlerDictionary handlers,
NotificationSender notificationSender, NotificationSender notificationSender,
RateFetcher rateFetcher, RateFetcher rateFetcher,
IEnumerable<IPayoutHandler> payoutHandlers,
ILogger<PullPaymentHostedService> logger, ILogger<PullPaymentHostedService> logger,
Logs logs, Logs logs,
DisplayFormatter displayFormatter, DisplayFormatter displayFormatter,
@@ -300,7 +300,6 @@ namespace BTCPayServer.HostedServices
_handlers = handlers; _handlers = handlers;
_notificationSender = notificationSender; _notificationSender = notificationSender;
_rateFetcher = rateFetcher; _rateFetcher = rateFetcher;
_payoutHandlers = payoutHandlers;
_logger = logger; _logger = logger;
_currencyNameTable = currencyNameTable; _currencyNameTable = currencyNameTable;
_displayFormatter = displayFormatter; _displayFormatter = displayFormatter;
@@ -311,10 +310,9 @@ namespace BTCPayServer.HostedServices
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings; private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly BTCPayNetworkProvider _networkProvider; private readonly BTCPayNetworkProvider _networkProvider;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PayoutMethodHandlerDictionary _handlers;
private readonly NotificationSender _notificationSender; private readonly NotificationSender _notificationSender;
private readonly RateFetcher _rateFetcher; private readonly RateFetcher _rateFetcher;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly ILogger<PullPaymentHostedService> _logger; private readonly ILogger<PullPaymentHostedService> _logger;
private readonly CurrencyNameTable _currencyNameTable; private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter; private readonly DisplayFormatter _displayFormatter;
@@ -323,7 +321,7 @@ namespace BTCPayServer.HostedServices
internal override Task[] InitializeTasks() internal override Task[] InitializeTasks()
{ {
_Channel = Channel.CreateUnbounded<object>(); _Channel = Channel.CreateUnbounded<object>();
foreach (IPayoutHandler payoutHandler in _payoutHandlers) foreach (IPayoutHandler payoutHandler in _handlers)
{ {
payoutHandler.StartBackgroundCheck(Subscribe); payoutHandler.StartBackgroundCheck(Subscribe);
} }
@@ -363,7 +361,7 @@ namespace BTCPayServer.HostedServices
await HandleMarkPaid(paid); await HandleMarkPaid(paid);
} }
foreach (IPayoutHandler payoutHandler in _payoutHandlers) foreach (IPayoutHandler payoutHandler in _handlers)
{ {
try try
{ {
@@ -380,7 +378,7 @@ namespace BTCPayServer.HostedServices
public bool SupportsLNURL(PullPaymentBlob blob) public bool SupportsLNURL(PullPaymentBlob blob)
{ {
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id => var pms = blob.SupportedPaymentMethods.FirstOrDefault(id =>
PaymentTypes.LN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode) PayoutTypes.LN.GetPayoutMethodId(_networkProvider.DefaultNetwork.CryptoCode)
== id); == id);
return pms is not null && _lnurlSupportedCurrencies.Contains(blob.Currency); return pms is not null && _lnurlSupportedCurrencies.Contains(blob.Currency);
} }
@@ -388,7 +386,7 @@ namespace BTCPayServer.HostedServices
public Task<RateResult> GetRate(PayoutData payout, string explicitRateRule, CancellationToken cancellationToken) public Task<RateResult> GetRate(PayoutData payout, string explicitRateRule, CancellationToken cancellationToken)
{ {
var ppBlob = payout.PullPaymentData?.GetBlob(); var ppBlob = payout.PullPaymentData?.GetBlob();
var payoutPaymentMethod = payout.GetPaymentMethodId(); var payoutPaymentMethod = payout.GetPayoutMethodId();
var cryptoCode = _handlers.TryGetNetwork(payoutPaymentMethod)?.NBXplorerNetwork.CryptoCode; var cryptoCode = _handlers.TryGetNetwork(payoutPaymentMethod)?.NBXplorerNetwork.CryptoCode;
var currencyPair = new Rating.CurrencyPair(cryptoCode, var currencyPair = new Rating.CurrencyPair(cryptoCode,
ppBlob?.Currency ?? cryptoCode); ppBlob?.Currency ?? cryptoCode);
@@ -450,7 +448,7 @@ namespace BTCPayServer.HostedServices
return; return;
} }
if (!PaymentMethodId.TryParse(payout.PaymentMethodId, out var paymentMethod)) if (!PayoutMethodId.TryParse(payout.PaymentMethodId, out var paymentMethod))
{ {
req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null)); req.Completion.SetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.NotFound, null));
return; return;
@@ -468,12 +466,12 @@ namespace BTCPayServer.HostedServices
cryptoCode == payout.PullPaymentData.GetBlob().Currency) cryptoCode == payout.PullPaymentData.GetBlob().Currency)
req.Rate = 1.0m; req.Rate = 1.0m;
var cryptoAmount = payoutBlob.Amount / req.Rate; var cryptoAmount = payoutBlob.Amount / req.Rate;
var payoutHandler = _payoutHandlers.FindPayoutHandler(paymentMethod); var payoutHandler = _handlers.TryGet(paymentMethod);
if (payoutHandler is null) if (payoutHandler is null)
throw new InvalidOperationException($"No payout handler for {paymentMethod}"); throw new InvalidOperationException($"No payout handler for {paymentMethod}");
var dest = await payoutHandler.ParseClaimDestination(paymentMethod, payoutBlob.Destination, default); var dest = await payoutHandler.ParseClaimDestination(payoutBlob.Destination, default);
decimal minimumCryptoAmount = decimal minimumCryptoAmount =
await payoutHandler.GetMinimumPayoutAmount(paymentMethod, dest.destination); await payoutHandler.GetMinimumPayoutAmount(dest.destination);
if (cryptoAmount < minimumCryptoAmount) if (cryptoAmount < minimumCryptoAmount)
{ {
req.Completion.TrySetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.TooLowAmount, null)); req.Completion.TrySetResult(new PayoutApproval.ApprovalResult(PayoutApproval.Result.TooLowAmount, null));
@@ -572,7 +570,7 @@ namespace BTCPayServer.HostedServices
ppBlob = pp.GetBlob(); ppBlob = pp.GetBlob();
if (!ppBlob.SupportedPaymentMethods.Contains(req.ClaimRequest.PaymentMethodId)) if (!ppBlob.SupportedPaymentMethods.Contains(req.ClaimRequest.PayoutMethodId))
{ {
req.Completion.TrySetResult( req.Completion.TrySetResult(
new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.PaymentMethodNotSupported)); new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.PaymentMethodNotSupported));
@@ -581,7 +579,7 @@ namespace BTCPayServer.HostedServices
} }
var payoutHandler = var payoutHandler =
_payoutHandlers.FindPayoutHandler(req.ClaimRequest.PaymentMethodId); _handlers.TryGet(req.ClaimRequest.PayoutMethodId);
if (payoutHandler is null) if (payoutHandler is null)
{ {
req.Completion.TrySetResult( req.Completion.TrySetResult(
@@ -602,8 +600,7 @@ namespace BTCPayServer.HostedServices
} }
if (req.ClaimRequest.Value < if (req.ClaimRequest.Value <
await payoutHandler.GetMinimumPayoutAmount(req.ClaimRequest.PaymentMethodId, await payoutHandler.GetMinimumPayoutAmount(req.ClaimRequest.Destination))
req.ClaimRequest.Destination))
{ {
req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.AmountTooLow)); req.Completion.TrySetResult(new ClaimRequest.ClaimResponse(ClaimRequest.ClaimResult.AmountTooLow));
return; return;
@@ -636,7 +633,7 @@ namespace BTCPayServer.HostedServices
Date = now, Date = now,
State = PayoutState.AwaitingApproval, State = PayoutState.AwaitingApproval,
PullPaymentDataId = req.ClaimRequest.PullPaymentId, PullPaymentDataId = req.ClaimRequest.PullPaymentId,
PaymentMethodId = req.ClaimRequest.PaymentMethodId.ToString(), PaymentMethodId = req.ClaimRequest.PayoutMethodId.ToString(),
Destination = req.ClaimRequest.Destination.Id, Destination = req.ClaimRequest.Destination.Id,
StoreDataId = req.ClaimRequest.StoreId ?? pp?.StoreId StoreDataId = req.ClaimRequest.StoreId ?? pp?.StoreId
}; };
@@ -683,7 +680,7 @@ namespace BTCPayServer.HostedServices
new PayoutNotification() new PayoutNotification()
{ {
StoreId = payout.StoreDataId, StoreId = payout.StoreDataId,
Currency = ppBlob?.Currency ?? _handlers.TryGetNetwork(req.ClaimRequest.PaymentMethodId)?.NBXplorerNetwork.CryptoCode, Currency = ppBlob?.Currency ?? _handlers.TryGetNetwork(req.ClaimRequest.PayoutMethodId)?.NBXplorerNetwork.CryptoCode,
Status = payout.State, Status = payout.State,
PaymentMethod = payout.PaymentMethodId, PaymentMethod = payout.PaymentMethodId,
PayoutId = payout.Id PayoutId = payout.Id
@@ -961,7 +958,7 @@ namespace BTCPayServer.HostedServices
PaymentMethodNotSupported, PaymentMethodNotSupported,
} }
public PaymentMethodId PaymentMethodId { get; set; } public PayoutMethodId PayoutMethodId { get; set; }
public string PullPaymentId { get; set; } public string PullPaymentId { get; set; }
public decimal? Value { get; set; } public decimal? Value { get; set; }
public IClaimDestination Destination { get; set; } public IClaimDestination Destination { get; set; }

View File

@@ -70,6 +70,8 @@ using BTCPayServer.Services.Reporting;
using BTCPayServer.Services.WalletFileParsing; using BTCPayServer.Services.WalletFileParsing;
using BTCPayServer.Payments.LNURLPay; using BTCPayServer.Payments.LNURLPay;
using System.Collections.Generic; using System.Collections.Generic;
using BTCPayServer.Payouts;
@@ -369,10 +371,6 @@ namespace BTCPayServer.Hosting
services.AddReportProvider<PayoutsReportProvider>(); services.AddReportProvider<PayoutsReportProvider>();
services.AddReportProvider<LegacyInvoiceExportReportProvider>(); services.AddReportProvider<LegacyInvoiceExportReportProvider>();
services.AddWebhooks(); services.AddWebhooks();
services.AddSingleton<BitcoinLikePayoutHandler>();
services.AddSingleton<IPayoutHandler>(provider => provider.GetRequiredService<BitcoinLikePayoutHandler>());
services.AddSingleton<IPayoutHandler>(provider => provider.GetRequiredService<LightningLikePayoutHandler>());
services.AddSingleton<LightningLikePayoutHandler>();
services.AddSingleton<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>(o => services.AddSingleton<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>(o =>
o.GetRequiredService<IEnumerable<IPaymentMethodBitpayAPIExtension>>().ToDictionary(o => o.PaymentMethodId, o => o)); o.GetRequiredService<IEnumerable<IPaymentMethodBitpayAPIExtension>>().ToDictionary(o => o.PaymentMethodId, o => o));
@@ -397,7 +395,9 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
services.AddSingleton<PaymentMethodHandlerDictionary>(); services.AddSingleton<PaymentMethodHandlerDictionary>();
services.AddSingleton<PaymentMethodViewProvider>(); services.AddSingleton<PaymentMethodViewProvider>();
services.AddSingleton<PayoutMethodHandlerDictionary>();
services.AddSingleton<NotificationManager>(); services.AddSingleton<NotificationManager>();
services.AddScoped<NotificationSender>(); services.AddScoped<NotificationSender>();
@@ -580,6 +580,13 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
(IPaymentMethodBitpayAPIExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinPaymentMethodBitpayAPIExtension), new object[] { pmi })); (IPaymentMethodBitpayAPIExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinPaymentMethodBitpayAPIExtension), new object[] { pmi }));
services.AddSingleton<IPaymentMethodViewExtension>(provider => services.AddSingleton<IPaymentMethodViewExtension>(provider =>
(IPaymentMethodViewExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinPaymentMethodViewExtension), new object[] { pmi })); (IPaymentMethodViewExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinPaymentMethodViewExtension), new object[] { pmi }));
if (!network.ReadonlyWallet && network.WalletSupported)
{
var payoutMethodId = PayoutTypes.CHAIN.GetPayoutMethodId(network.CryptoCode);
services.AddSingleton<IPayoutHandler>(provider =>
(IPayoutHandler)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinLikePayoutHandler), new object[] { payoutMethodId, network }));
}
} }
if (network.NBitcoinNetwork.Consensus.SupportSegwit && network.SupportLightning) if (network.NBitcoinNetwork.Consensus.SupportSegwit && network.SupportLightning)
{ {
@@ -596,6 +603,9 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
(IPaymentMethodViewExtension)ActivatorUtilities.CreateInstance(provider, typeof(LightningPaymentMethodViewExtension), new object[] { pmi })); (IPaymentMethodViewExtension)ActivatorUtilities.CreateInstance(provider, typeof(LightningPaymentMethodViewExtension), new object[] { pmi }));
services.AddSingleton<IPaymentMethodBitpayAPIExtension>(provider => services.AddSingleton<IPaymentMethodBitpayAPIExtension>(provider =>
(IPaymentMethodBitpayAPIExtension)ActivatorUtilities.CreateInstance(provider, typeof(LightningPaymentMethodBitpayAPIExtension), new object[] { pmi })); (IPaymentMethodBitpayAPIExtension)ActivatorUtilities.CreateInstance(provider, typeof(LightningPaymentMethodBitpayAPIExtension), new object[] { pmi }));
var payoutMethodId = PayoutTypes.LN.GetPayoutMethodId(network.CryptoCode);
services.AddSingleton<IPayoutHandler>(provider =>
(IPayoutHandler)ActivatorUtilities.CreateInstance(provider, typeof(LightningLikePayoutHandler), new object[] { payoutMethodId, network }));
} }
// LNURL // LNURL
{ {

View File

@@ -15,6 +15,7 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.PayoutProcessors; using BTCPayServer.PayoutProcessors;
using BTCPayServer.Payouts;
using BTCPayServer.Plugins.Crowdfund; using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.PointOfSale; using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Plugins.PointOfSale.Models;
@@ -47,7 +48,7 @@ namespace BTCPayServer.Hosting
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _handlers;
private readonly SettingsRepository _Settings; private readonly SettingsRepository _Settings;
private readonly AppService _appService; private readonly AppService _appService;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers; private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings; private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly LightningAddressService _lightningAddressService; private readonly LightningAddressService _lightningAddressService;
private readonly ILogger<MigrationStartupTask> _logger; private readonly ILogger<MigrationStartupTask> _logger;
@@ -62,7 +63,7 @@ namespace BTCPayServer.Hosting
IOptions<LightningNetworkOptions> lightningOptions, IOptions<LightningNetworkOptions> lightningOptions,
SettingsRepository settingsRepository, SettingsRepository settingsRepository,
AppService appService, AppService appService,
IEnumerable<IPayoutHandler> payoutHandlers, PayoutMethodHandlerDictionary payoutHandlers,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
LightningAddressService lightningAddressService, LightningAddressService lightningAddressService,
ILogger<MigrationStartupTask> logger, ILogger<MigrationStartupTask> logger,
@@ -238,7 +239,7 @@ namespace BTCPayServer.Hosting
var processors = await ctx.PayoutProcessors.ToArrayAsync(); var processors = await ctx.PayoutProcessors.ToArrayAsync();
foreach (var processor in processors) foreach (var processor in processors)
{ {
processor.PaymentMethod = processor.GetPaymentMethodId().ToString(); processor.PaymentMethod = processor.GetPayoutMethodId().ToString();
} }
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
@@ -641,18 +642,18 @@ WHERE cte.""Id""=p.""Id""
await using var ctx = _DBContextFactory.CreateContext(); await using var ctx = _DBContextFactory.CreateContext();
foreach (var payoutData in await ctx.Payouts.AsQueryable().ToArrayAsync()) foreach (var payoutData in await ctx.Payouts.AsQueryable().ToArrayAsync())
{ {
var pmi = payoutData.GetPaymentMethodId(); var pmi = payoutData.GetPayoutMethodId();
if (pmi is null) if (pmi is null)
{ {
continue; continue;
} }
var handler = _payoutHandlers var handler = _payoutHandlers
.FindPayoutHandler(pmi); .TryGet(pmi);
if (handler is null) if (handler is null)
{ {
continue; continue;
} }
var claim = await handler?.ParseClaimDestination(pmi, payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings).Destination, default); var claim = await handler?.ParseClaimDestination(payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings).Destination, default);
payoutData.Destination = claim.destination?.Id; payoutData.Destination = claim.destination?.Id;
} }
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();

View File

@@ -0,0 +1,29 @@
using System;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.JsonConverters
{
public class PayoutMethodIdJsonConverter : JsonConverter<PayoutMethodId>
{
public override PayoutMethodId ReadJson(JsonReader reader, Type objectType, PayoutMethodId existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.String)
throw new JsonObjectException("A payment method id should be a string", reader);
if (PayoutMethodId.TryParse((string)reader.Value, out var result))
return result;
return null;
// We need to do this gracefully as we have removed support for a payment type in the past which will throw here on your store each time it is loaded.
// throw new JsonObjectException($"Invalid payment method id ({(string)reader.Value})", reader);
}
public override void WriteJson(JsonWriter writer, PayoutMethodId value, JsonSerializer serializer)
{
if (value != null)
writer.WriteValue(value.ToString());
}
}
}

View File

@@ -14,8 +14,8 @@ namespace BTCPayServer.Models.InvoicingModels
public string Title { get; set; } public string Title { get; set; }
public SelectList AvailablePaymentMethods { get; set; } public SelectList AvailablePaymentMethods { get; set; }
[Display(Name = "Select the payment method used for refund")] [Display(Name = "Select the payout method used for refund")]
public string SelectedPaymentMethod { get; set; } public string SelectedPayoutMethod { get; set; }
public RefundSteps RefundStep { get; set; } public RefundSteps RefundStep { get; set; }
public string SelectedRefundOption { get; set; } public string SelectedRefundOption { get; set; }
public decimal CryptoAmountNow { get; set; } public decimal CryptoAmountNow { get; set; }

View File

@@ -5,6 +5,7 @@ using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using PullPaymentData = BTCPayServer.Data.PullPaymentData; using PullPaymentData = BTCPayServer.Data.PullPaymentData;
@@ -21,9 +22,9 @@ namespace BTCPayServer.Models
Id = data.Id; Id = data.Id;
StoreId = data.StoreId; StoreId = data.StoreId;
var blob = data.GetBlob(); var blob = data.GetBlob();
PaymentMethods = blob.SupportedPaymentMethods; PayoutMethodIds = blob.SupportedPaymentMethods;
BitcoinOnly = blob.SupportedPaymentMethods.All(p => p.CryptoCode == "BTC"); BitcoinOnly = blob.SupportedPaymentMethods.All(p => p == PayoutTypes.CHAIN.GetPayoutMethodId("BTC") || p == PayoutTypes.LN.GetPayoutMethodId("BTC"));
SelectedPaymentMethod = PaymentMethods.First().ToString(); SelectedPayoutMethod = PayoutMethodIds.First().ToString();
Archived = data.Archived; Archived = data.Archived;
AutoApprove = blob.AutoApproveClaims; AutoApprove = blob.AutoApproveClaims;
Title = blob.View.Title; Title = blob.View.Title;
@@ -67,9 +68,9 @@ namespace BTCPayServer.Models
public string StoreId { get; set; } public string StoreId { get; set; }
public string SelectedPaymentMethod { get; set; } public string SelectedPayoutMethod { get; set; }
public PaymentMethodId[] PaymentMethods { get; set; } public PayoutMethodId[] PayoutMethodIds { get; set; }
public string SetupDeepLink { get; set; } public string SetupDeepLink { get; set; }
public string ResetDeepLink { get; set; } public string ResetDeepLink { get; set; }

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
namespace BTCPayServer.Models.WalletViewModels namespace BTCPayServer.Models.WalletViewModels
{ {
@@ -11,12 +12,12 @@ namespace BTCPayServer.Models.WalletViewModels
public string PullPaymentId { get; set; } public string PullPaymentId { get; set; }
public string Command { get; set; } public string Command { get; set; }
public Dictionary<PayoutState, int> PayoutStateCount { get; set; } public Dictionary<PayoutState, int> PayoutStateCount { get; set; }
public Dictionary<string, int> PaymentMethodCount { get; set; } public Dictionary<string, int> PayoutMethodCount { get; set; }
public string PaymentMethodId { get; set; } public string PayoutMethodId { get; set; }
public List<PayoutModel> Payouts { get; set; } public List<PayoutModel> Payouts { get; set; }
public override int CurrentPageCount => Payouts.Count; public override int CurrentPageCount => Payouts.Count;
public IEnumerable<PaymentMethodId> PaymentMethods { get; set; } public IEnumerable<PayoutMethodId> PayoutMethods { get; set; }
public PayoutState PayoutState { get; set; } public PayoutState PayoutState { get; set; }
public string PullPaymentName { get; set; } public string PullPaymentName { get; set; }
public bool HasPayoutProcessor { get; set; } public bool HasPayoutProcessor { get; set; }

View File

@@ -62,9 +62,9 @@ namespace BTCPayServer.Models.WalletViewModels
[Display(Name = "Custom CSS Code")] [Display(Name = "Custom CSS Code")]
public string EmbeddedCSS { get; set; } public string EmbeddedCSS { get; set; }
[Display(Name = "Payment Methods")] [Display(Name = "Payout Methods")]
public IEnumerable<string> PaymentMethods { get; set; } public IEnumerable<string> PayoutMethods { get; set; }
public IEnumerable<SelectListItem> PaymentMethodItems { get; set; } public IEnumerable<SelectListItem> PayoutMethodsItem { get; set; }
[Display(Name = "Minimum acceptable expiration time for BOLT11 for refunds")] [Display(Name = "Minimum acceptable expiration time for BOLT11 for refunds")]
[Range(0, 365 * 10)] [Range(0, 365 * 10)]
public long BOLT11Expiration { get; set; } = 30; public long BOLT11Expiration { get; set; } = 30;

View File

@@ -31,8 +31,9 @@ namespace BTCPayServer.Payments
/// <summary> /// <summary>
/// This class customize invoice creation by the creation of payment details for the PaymentMethod during invoice creation /// This class customize invoice creation by the creation of payment details for the PaymentMethod during invoice creation
/// </summary> /// </summary>
public interface IPaymentMethodHandler public interface IPaymentMethodHandler : IHandler<PaymentMethodId>
{ {
PaymentMethodId IHandler<PaymentMethodId>.Id => PaymentMethodId;
PaymentMethodId PaymentMethodId { get; } PaymentMethodId PaymentMethodId { get; }
/// <summary> /// <summary>
/// The creation of the prompt details and prompt data /// The creation of the prompt details and prompt data

View File

@@ -7,7 +7,10 @@ namespace BTCPayServer.Payments.Lightning
public static class LightningExtensions public static class LightningExtensions
{ {
public static bool IsConfigured(this LightningPaymentMethodConfig supportedPaymentMethod, BTCPayNetwork network, LightningNetworkOptions options)
{
return supportedPaymentMethod.GetExternalLightningUrl() is not null || (supportedPaymentMethod.IsInternalNode && options.InternalLightningByCryptoCode.ContainsKey(network.CryptoCode));
}
public static ILightningClient CreateLightningClient(this LightningPaymentMethodConfig supportedPaymentMethod, BTCPayNetwork network, LightningNetworkOptions options, LightningClientFactoryService lightningClientFactory) public static ILightningClient CreateLightningClient(this LightningPaymentMethodConfig supportedPaymentMethod, BTCPayNetwork network, LightningNetworkOptions options, LightningClientFactoryService lightningClientFactory)
{ {
var external = supportedPaymentMethod.GetExternalLightningUrl(); var external = supportedPaymentMethod.GetExternalLightningUrl();
@@ -17,7 +20,7 @@ namespace BTCPayServer.Payments.Lightning
} }
else else
{ {
if (!options.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode, out var connectionString)) if (!supportedPaymentMethod.IsInternalNode || !options.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode, out var connectionString))
throw new PaymentMethodUnavailableException("No internal node configured"); throw new PaymentMethodUnavailableException("No internal node configured");
return connectionString; return connectionString;
} }

View File

@@ -7,6 +7,7 @@ using BTCPayServer.Data;
using BTCPayServer.Data.Payouts.LightningLike; using BTCPayServer.Data.Payouts.LightningLike;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
@@ -22,10 +23,10 @@ public class LightningPendingPayoutListener : BaseAsyncService
private readonly LightningClientFactoryService _lightningClientFactoryService; private readonly LightningClientFactoryService _lightningClientFactoryService;
private readonly ApplicationDbContextFactory _applicationDbContextFactory; private readonly ApplicationDbContextFactory _applicationDbContextFactory;
private readonly PullPaymentHostedService _pullPaymentHostedService; private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly LightningLikePayoutHandler _lightningLikePayoutHandler;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly IOptions<LightningNetworkOptions> _options; private readonly IOptions<LightningNetworkOptions> _options;
private readonly BTCPayNetworkProvider _networkProvider; private readonly BTCPayNetworkProvider _networkProvider;
private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _handlers;
public static int SecondsDelay = 60 * 10; public static int SecondsDelay = 60 * 10;
@@ -33,21 +34,21 @@ public class LightningPendingPayoutListener : BaseAsyncService
LightningClientFactoryService lightningClientFactoryService, LightningClientFactoryService lightningClientFactoryService,
ApplicationDbContextFactory applicationDbContextFactory, ApplicationDbContextFactory applicationDbContextFactory,
PullPaymentHostedService pullPaymentHostedService, PullPaymentHostedService pullPaymentHostedService,
LightningLikePayoutHandler lightningLikePayoutHandler,
StoreRepository storeRepository, StoreRepository storeRepository,
IOptions<LightningNetworkOptions> options, IOptions<LightningNetworkOptions> options,
BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider networkProvider,
PayoutMethodHandlerDictionary payoutHandlers,
PaymentMethodHandlerDictionary handlers, PaymentMethodHandlerDictionary handlers,
ILogger<LightningPendingPayoutListener> logger) : base(logger) ILogger<LightningPendingPayoutListener> logger) : base(logger)
{ {
_lightningClientFactoryService = lightningClientFactoryService; _lightningClientFactoryService = lightningClientFactoryService;
_applicationDbContextFactory = applicationDbContextFactory; _applicationDbContextFactory = applicationDbContextFactory;
_pullPaymentHostedService = pullPaymentHostedService; _pullPaymentHostedService = pullPaymentHostedService;
_lightningLikePayoutHandler = lightningLikePayoutHandler;
_storeRepository = storeRepository; _storeRepository = storeRepository;
_options = options; _options = options;
_networkProvider = networkProvider; _networkProvider = networkProvider;
_payoutHandlers = payoutHandlers;
_handlers = handlers; _handlers = handlers;
} }
@@ -64,7 +65,7 @@ public class LightningPendingPayoutListener : BaseAsyncService
new PullPaymentHostedService.PayoutQuery() new PullPaymentHostedService.PayoutQuery()
{ {
States = new PayoutState[] { PayoutState.InProgress }, States = new PayoutState[] { PayoutState.InProgress },
PaymentMethods = networks.Keys.Select(id => id.ToString()).ToArray() PayoutMethods = networks.Keys.Select(id => id.ToString()).ToArray()
}, context); }, context);
var storeIds = payouts.Select(data => data.StoreDataId).Distinct(); var storeIds = payouts.Select(data => data.StoreDataId).Distinct();
var stores = (await Task.WhenAll(storeIds.Select(_storeRepository.FindStore))) var stores = (await Task.WhenAll(storeIds.Select(_storeRepository.FindStore)))
@@ -100,7 +101,8 @@ public class LightningPendingPayoutListener : BaseAsyncService
pm.CreateLightningClient(networks[pmi], _options.Value, _lightningClientFactoryService); pm.CreateLightningClient(networks[pmi], _options.Value, _lightningClientFactoryService);
foreach (PayoutData payoutData in payoutByStoreByPaymentMethod) foreach (PayoutData payoutData in payoutByStoreByPaymentMethod)
{ {
var proof = _lightningLikePayoutHandler.ParseProof(payoutData); var handler = _payoutHandlers.TryGet(payoutData.GetPayoutMethodId());
var proof = handler is null ? null : handler.ParseProof(payoutData);
switch (proof) switch (proof)
{ {
case null: case null:

View File

@@ -4,8 +4,6 @@ using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using BTCPayServer.Services.Altcoins.Monero.Payments;
using BTCPayServer.Services.Altcoins.Zcash.Payments;
namespace BTCPayServer.Payments namespace BTCPayServer.Payments
{ {
@@ -15,24 +13,53 @@ namespace BTCPayServer.Payments
/// </summary> /// </summary>
public class PaymentMethodId public class PaymentMethodId
{ {
public PaymentMethodId? FindNearest(IEnumerable<PaymentMethodId> others) public T? FindNearest<T>(IEnumerable<T> others)
{ {
ArgumentNullException.ThrowIfNull(others); ArgumentNullException.ThrowIfNull(others);
return others.FirstOrDefault(f => f == this) ?? return
others.FirstOrDefault(f => f._CryptoCode == _CryptoCode); GetSimilarities([this], others)
.OrderByDescending(o => o.similarity)
.Select(o => o.b)
.FirstOrDefault();
} }
public PaymentMethodId(string cryptoCode, string paymentType)
/// <summary>
/// Returns the carthesian product of the two enumerables with the similarity between each pair's strings.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <param name="aItems"></param>
/// <param name="bItems"></param>
/// <returns></returns>
public static IEnumerable<(T a, U b, int similarity)> GetSimilarities<T, U>(IEnumerable<T> aItems, IEnumerable<U> bItems)
{ {
ArgumentNullException.ThrowIfNull(cryptoCode); return from a in aItems
ArgumentNullException.ThrowIfNull(paymentType); from b in bItems
_CryptoCode = cryptoCode.ToUpperInvariant(); select (a, b, CalculateDistance(a.ToString()!, b.ToString()!));
_Id = $"{_CryptoCode}-{paymentType}"; }
private static int CalculateDistance(string a, string b)
{
int similarity = 0;
for (int i = 0; i < Math.Min(a.Length, b.Length); i++)
{
if (a[i] == b[i])
similarity++;
else
break;
}
if (a.Length == b.Length)
similarity++;
return similarity;
}
public PaymentMethodId(string id)
{
ArgumentNullException.ThrowIfNull(id);
_Id = id;
} }
string _Id; string _Id;
string _CryptoCode;
public string CryptoCode => _CryptoCode;
public override bool Equals(object? obj) public override bool Equals(object? obj)
{ {
@@ -93,6 +120,11 @@ namespace BTCPayServer.Payments
{ {
str ??= ""; str ??= "";
str = str.Trim(); str = str.Trim();
if (str.Length == 0)
{
paymentMethodId = null;
return false;
}
paymentMethodId = null; paymentMethodId = null;
var parts = str.Split(Separators, StringSplitOptions.RemoveEmptyEntries); var parts = str.Split(Separators, StringSplitOptions.RemoveEmptyEntries);
@@ -101,7 +133,7 @@ namespace BTCPayServer.Payments
{ {
if (LegacySupportedCryptos.Contains(cryptoCode)) if (LegacySupportedCryptos.Contains(cryptoCode))
{ {
paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode.ToUpperInvariant()); paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
return true; return true;
} }
} }
@@ -118,13 +150,14 @@ namespace BTCPayServer.Payments
paymentMethodId = type.GetPaymentMethodId(cryptoCode); paymentMethodId = type.GetPaymentMethodId(cryptoCode);
return true; return true;
} }
paymentMethodId = new PaymentMethodId(cryptoCode, paymentType); paymentMethodId = new PaymentMethodId($"{cryptoCode.ToUpperInvariant()}-{paymentType}");
return true; return true;
} }
} }
} }
return false; paymentMethodId = new PaymentMethodId(str);
return true;
} }
private static PaymentType? GetPaymentType(string paymentType) private static PaymentType? GetPaymentType(string paymentType)

View File

@@ -19,6 +19,10 @@ namespace BTCPayServer.Payments
{ {
_paymentType = paymentType; _paymentType = paymentType;
} }
public PaymentMethodId GetPaymentMethodId(string cryptoCode) => new (cryptoCode, _paymentType); public PaymentMethodId GetPaymentMethodId(string cryptoCode) => new ($"{cryptoCode.ToUpperInvariant()}-{_paymentType}");
public override string ToString()
{
return _paymentType;
}
} }
} }

View File

@@ -9,6 +9,7 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
@@ -39,25 +40,28 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
protected readonly StoreRepository _storeRepository; protected readonly StoreRepository _storeRepository;
protected readonly PayoutProcessorData PayoutProcessorSettings; protected readonly PayoutProcessorData PayoutProcessorSettings;
protected readonly ApplicationDbContextFactory _applicationDbContextFactory; protected readonly ApplicationDbContextFactory _applicationDbContextFactory;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _paymentHandlers;
protected readonly PayoutMethodId PayoutMethodId;
protected readonly PaymentMethodId PaymentMethodId; protected readonly PaymentMethodId PaymentMethodId;
private readonly IPluginHookService _pluginHookService; private readonly IPluginHookService _pluginHookService;
protected readonly EventAggregator _eventAggregator; protected readonly EventAggregator _eventAggregator;
protected BaseAutomatedPayoutProcessor( protected BaseAutomatedPayoutProcessor(
PaymentMethodId paymentMethodId,
ILoggerFactory logger, ILoggerFactory logger,
StoreRepository storeRepository, StoreRepository storeRepository,
PayoutProcessorData payoutProcessorSettings, PayoutProcessorData payoutProcessorSettings,
ApplicationDbContextFactory applicationDbContextFactory, ApplicationDbContextFactory applicationDbContextFactory,
PaymentMethodHandlerDictionary handlers, PaymentMethodHandlerDictionary paymentHandlers,
IPluginHookService pluginHookService, IPluginHookService pluginHookService,
EventAggregator eventAggregator) : base(logger.CreateLogger($"{payoutProcessorSettings.Processor}:{payoutProcessorSettings.StoreId}:{payoutProcessorSettings.PaymentMethod}")) EventAggregator eventAggregator) : base(logger.CreateLogger($"{payoutProcessorSettings.Processor}:{payoutProcessorSettings.StoreId}:{payoutProcessorSettings.PaymentMethod}"))
{ {
PaymentMethodId = paymentMethodId;
_storeRepository = storeRepository; _storeRepository = storeRepository;
PayoutProcessorSettings = payoutProcessorSettings; PayoutProcessorSettings = payoutProcessorSettings;
PaymentMethodId = PayoutProcessorSettings.GetPaymentMethodId(); PayoutMethodId = PayoutProcessorSettings.GetPayoutMethodId();
_applicationDbContextFactory = applicationDbContextFactory; _applicationDbContextFactory = applicationDbContextFactory;
_handlers = handlers; _paymentHandlers = paymentHandlers;
_pluginHookService = pluginHookService; _pluginHookService = pluginHookService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
this.NoLogsOnExit = true; this.NoLogsOnExit = true;
@@ -80,7 +84,7 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
{ {
if (arg.Type == PayoutEvent.PayoutEventType.Approved && if (arg.Type == PayoutEvent.PayoutEventType.Approved &&
PayoutProcessorSettings.StoreId == arg.Payout.StoreDataId && PayoutProcessorSettings.StoreId == arg.Payout.StoreDataId &&
arg.Payout.GetPaymentMethodId() == PaymentMethodId && arg.Payout.GetPayoutMethodId() == PayoutMethodId &&
GetBlob(PayoutProcessorSettings).ProcessNewPayoutsInstantly) GetBlob(PayoutProcessorSettings).ProcessNewPayoutsInstantly)
{ {
SkipInterval(); SkipInterval();
@@ -100,7 +104,7 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
private async Task Act() private async Task Act()
{ {
var store = await _storeRepository.FindStore(PayoutProcessorSettings.StoreId); var store = await _storeRepository.FindStore(PayoutProcessorSettings.StoreId);
var paymentMethod = store?.GetPaymentMethodConfig(PaymentMethodId, _handlers, true); var paymentMethod = store?.GetPaymentMethodConfig(PaymentMethodId, _paymentHandlers, true);
var blob = GetBlob(PayoutProcessorSettings); var blob = GetBlob(PayoutProcessorSettings);
if (paymentMethod is not null) if (paymentMethod is not null)
@@ -110,7 +114,7 @@ public abstract class BaseAutomatedPayoutProcessor<T> : BaseAsyncService where T
new PullPaymentHostedService.PayoutQuery() new PullPaymentHostedService.PayoutQuery()
{ {
States = new[] { PayoutState.AwaitingPayment }, States = new[] { PayoutState.AwaitingPayment },
PaymentMethods = new[] { PayoutProcessorSettings.PaymentMethod }, PayoutMethods = new[] { PayoutProcessorSettings.PaymentMethod },
Stores = new[] {PayoutProcessorSettings.StoreId} Stores = new[] {PayoutProcessorSettings.StoreId}
}, context, CancellationToken); }, context, CancellationToken);

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
@@ -11,8 +12,8 @@ public interface IPayoutProcessorFactory
{ {
public string Processor { get; } public string Processor { get; }
public string FriendlyName { get; } public string FriendlyName { get; }
public string ConfigureLink(string storeId, PaymentMethodId paymentMethodId, HttpRequest request); public string ConfigureLink(string storeId, PayoutMethodId payoutMethodId, HttpRequest request);
public IEnumerable<PaymentMethodId> GetSupportedPaymentMethods(); public IEnumerable<PayoutMethodId> GetSupportedPayoutMethods();
public Task<IHostedService> ConstructProcessor(PayoutProcessorData settings); public Task<IHostedService> ConstructProcessor(PayoutProcessorData settings);
public Task<bool> CanRemove() => Task.FromResult(true); public Task<bool> CanRemove() => Task.FromResult(true);
} }

View File

@@ -14,6 +14,7 @@ using BTCPayServer.Lightning;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
@@ -35,12 +36,14 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
private readonly IOptions<LightningNetworkOptions> _options; private readonly IOptions<LightningNetworkOptions> _options;
private readonly PullPaymentHostedService _pullPaymentHostedService; private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly LightningLikePayoutHandler _payoutHandler; private readonly LightningLikePayoutHandler _payoutHandler;
public BTCPayNetwork Network => _payoutHandler.Network;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PaymentMethodHandlerDictionary _handlers;
public LightningAutomatedPayoutProcessor( public LightningAutomatedPayoutProcessor(
PayoutMethodId payoutMethodId,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
LightningClientFactoryService lightningClientFactoryService, LightningClientFactoryService lightningClientFactoryService,
IEnumerable<IPayoutHandler> payoutHandlers, PayoutMethodHandlerDictionary payoutHandlers,
UserService userService, UserService userService,
ILoggerFactory logger, IOptions<LightningNetworkOptions> options, ILoggerFactory logger, IOptions<LightningNetworkOptions> options,
StoreRepository storeRepository, PayoutProcessorData payoutProcessorSettings, StoreRepository storeRepository, PayoutProcessorData payoutProcessorSettings,
@@ -49,7 +52,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
IPluginHookService pluginHookService, IPluginHookService pluginHookService,
EventAggregator eventAggregator, EventAggregator eventAggregator,
PullPaymentHostedService pullPaymentHostedService) : PullPaymentHostedService pullPaymentHostedService) :
base(logger, storeRepository, payoutProcessorSettings, applicationDbContextFactory, base(PaymentTypes.LN.GetPaymentMethodId(GetPayoutHandler(payoutHandlers, payoutMethodId).Network.CryptoCode), logger, storeRepository, payoutProcessorSettings, applicationDbContextFactory,
handlers, pluginHookService, eventAggregator) handlers, pluginHookService, eventAggregator)
{ {
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings; _btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
@@ -57,16 +60,18 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
_userService = userService; _userService = userService;
_options = options; _options = options;
_pullPaymentHostedService = pullPaymentHostedService; _pullPaymentHostedService = pullPaymentHostedService;
_payoutHandler = (LightningLikePayoutHandler)payoutHandlers.FindPayoutHandler(PaymentMethodId); _payoutHandler = GetPayoutHandler(payoutHandlers, payoutMethodId);
_handlers = handlers; _handlers = handlers;
} }
private static LightningLikePayoutHandler GetPayoutHandler(PayoutMethodHandlerDictionary payoutHandlers, PayoutMethodId payoutMethodId)
{
return (LightningLikePayoutHandler)payoutHandlers[payoutMethodId];
}
private async Task HandlePayout(PayoutData payoutData, ILightningClient lightningClient) private async Task HandlePayout(PayoutData payoutData, ILightningClient lightningClient)
{ {
if (payoutData.State != PayoutState.AwaitingPayment) if (payoutData.State != PayoutState.AwaitingPayment)
return; return;
if (!_handlers.TryGetValue(PaymentMethodId, out var handler) || handler is not IHasNetwork { Network: var network })
return;
var res = await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest() var res = await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest()
{ {
State = PayoutState.InProgress, PayoutId = payoutData.Id, Proof = null State = PayoutState.InProgress, PayoutId = payoutData.Id, Proof = null
@@ -77,7 +82,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
} }
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings); var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
var claim = await _payoutHandler.ParseClaimDestination(PaymentMethodId, blob.Destination, CancellationToken); var claim = await _payoutHandler.ParseClaimDestination(blob.Destination, CancellationToken);
try try
{ {
switch (claim.destination) switch (claim.destination)
@@ -85,7 +90,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
case LNURLPayClaimDestinaton lnurlPayClaimDestinaton: case LNURLPayClaimDestinaton lnurlPayClaimDestinaton:
var lnurlResult = await UILightningLikePayoutController.GetInvoiceFromLNURL(payoutData, var lnurlResult = await UILightningLikePayoutController.GetInvoiceFromLNURL(payoutData,
_payoutHandler, blob, _payoutHandler, blob,
lnurlPayClaimDestinaton, network.NBitcoinNetwork, CancellationToken); lnurlPayClaimDestinaton, Network.NBitcoinNetwork, CancellationToken);
if (lnurlResult.Item2 is null) if (lnurlResult.Item2 is null)
{ {
await TrypayBolt(lightningClient, blob, payoutData, await TrypayBolt(lightningClient, blob, payoutData,
@@ -113,8 +118,6 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
protected override async Task<bool> ProcessShouldSave(object paymentMethodConfig, List<PayoutData> payouts) protected override async Task<bool> ProcessShouldSave(object paymentMethodConfig, List<PayoutData> payouts)
{ {
if (!_handlers.TryGetValue(PaymentMethodId, out var handler) || handler is not IHasNetwork { Network: var network })
return false;
var processorBlob = GetBlob(PayoutProcessorSettings); var processorBlob = GetBlob(PayoutProcessorSettings);
var lightningSupportedPaymentMethod = (LightningPaymentMethodConfig)paymentMethodConfig; var lightningSupportedPaymentMethod = (LightningPaymentMethodConfig)paymentMethodConfig;
if (lightningSupportedPaymentMethod.IsInternalNode && if (lightningSupportedPaymentMethod.IsInternalNode &&
@@ -129,7 +132,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
} }
var client = var client =
lightningSupportedPaymentMethod.CreateLightningClient(network, _options.Value, lightningSupportedPaymentMethod.CreateLightningClient(Network, _options.Value,
_lightningClientFactoryService); _lightningClientFactoryService);
await Task.WhenAll(payouts.Select(data => HandlePayout(data, client))); await Task.WhenAll(payouts.Select(data => HandlePayout(data, client)));
@@ -143,6 +146,6 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
{ {
return (await UILightningLikePayoutController.TrypayBolt(lightningClient, payoutBlob, payoutData, return (await UILightningLikePayoutController.TrypayBolt(lightningClient, payoutBlob, payoutData,
bolt11PaymentRequest, bolt11PaymentRequest,
payoutData.GetPaymentMethodId(), CancellationToken)).Result is PayResult.Ok ; _payoutHandler.Currency, CancellationToken)).Result is PayResult.Ok ;
} }
} }

View File

@@ -3,7 +3,9 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Data.Payouts.LightningLike;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -13,38 +15,37 @@ namespace BTCPayServer.PayoutProcessors.Lightning;
public class LightningAutomatedPayoutSenderFactory : IPayoutProcessorFactory public class LightningAutomatedPayoutSenderFactory : IPayoutProcessorFactory
{ {
private readonly BTCPayNetworkProvider _btcPayNetworkProvider; private readonly PayoutMethodHandlerDictionary _handlers;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly LinkGenerator _linkGenerator; private readonly LinkGenerator _linkGenerator;
private readonly PayoutMethodId[] _supportedPayoutMethods;
public LightningAutomatedPayoutSenderFactory(
public LightningAutomatedPayoutSenderFactory(BTCPayNetworkProvider btcPayNetworkProvider, IServiceProvider serviceProvider, LinkGenerator linkGenerator) PayoutMethodHandlerDictionary handlers,
IServiceProvider serviceProvider,
LinkGenerator linkGenerator)
{ {
_btcPayNetworkProvider = btcPayNetworkProvider; _handlers = handlers;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_linkGenerator = linkGenerator; _linkGenerator = linkGenerator;
_supportedPayoutMethods = _handlers.OfType<LightningLikePayoutHandler>().Select(n => n.PayoutMethodId).ToArray();
} }
public string FriendlyName { get; } = "Automated Lightning Sender"; public string FriendlyName { get; } = "Automated Lightning Sender";
public string ConfigureLink(string storeId, PaymentMethodId paymentMethodId, HttpRequest request) public string ConfigureLink(string storeId, PayoutMethodId payoutMethodId, HttpRequest request)
{ {
var network = _handlers.TryGetNetwork(payoutMethodId);
return _linkGenerator.GetUriByAction("Configure", return _linkGenerator.GetUriByAction("Configure",
"UILightningAutomatedPayoutProcessors", new "UILightningAutomatedPayoutProcessors", new
{ {
storeId, storeId,
cryptoCode = paymentMethodId.CryptoCode cryptoCode = network.CryptoCode
}, request.Scheme, request.Host, request.PathBase); }, request.Scheme, request.Host, request.PathBase);
} }
public string Processor => ProcessorName; public string Processor => ProcessorName;
public static string ProcessorName => nameof(LightningAutomatedPayoutSenderFactory); public static string ProcessorName => nameof(LightningAutomatedPayoutSenderFactory);
public IEnumerable<PaymentMethodId> GetSupportedPaymentMethods() public IEnumerable<PayoutMethodId> GetSupportedPayoutMethods() => _supportedPayoutMethods;
{
return _btcPayNetworkProvider.GetAll().OfType<BTCPayNetwork>()
.Where(network => network.SupportLightning)
.Select(network =>
PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode));
}
public Task<IHostedService> ConstructProcessor(PayoutProcessorData settings) public Task<IHostedService> ConstructProcessor(PayoutProcessorData settings)
{ {
@@ -52,8 +53,8 @@ public class LightningAutomatedPayoutSenderFactory : IPayoutProcessorFactory
{ {
throw new NotSupportedException("This processor cannot handle the provided requirements"); throw new NotSupportedException("This processor cannot handle the provided requirements");
} }
var payoutMethodId = settings.GetPayoutMethodId();
return Task.FromResult<IHostedService>(ActivatorUtilities.CreateInstance<LightningAutomatedPayoutProcessor>(_serviceProvider, settings)); return Task.FromResult<IHostedService>(ActivatorUtilities.CreateInstance<LightningAutomatedPayoutProcessor>(_serviceProvider, settings, payoutMethodId));
} }
} }

View File

@@ -10,6 +10,7 @@ using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.PayoutProcessors.OnChain; using BTCPayServer.PayoutProcessors.OnChain;
using BTCPayServer.Payouts;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -37,8 +38,8 @@ public class UILightningAutomatedPayoutProcessorsController : Controller
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Configure(string storeId, string cryptoCode) public async Task<IActionResult> Configure(string storeId, string cryptoCode)
{ {
var id = GetPaymentMethodId(cryptoCode); var id = GetPayoutMethodId(cryptoCode);
if (!_lightningAutomatedPayoutSenderFactory.GetSupportedPaymentMethods().Any(i => id == i)) if (!_lightningAutomatedPayoutSenderFactory.GetSupportedPayoutMethods().Any(i => id == i))
{ {
TempData.SetStatusMessageModel(new StatusMessageModel() TempData.SetStatusMessageModel(new StatusMessageModel()
{ {
@@ -53,9 +54,9 @@ public class UILightningAutomatedPayoutProcessorsController : Controller
{ {
Stores = new[] { storeId }, Stores = new[] { storeId },
Processors = new[] { _lightningAutomatedPayoutSenderFactory.Processor }, Processors = new[] { _lightningAutomatedPayoutSenderFactory.Processor },
PaymentMethods = new[] PayoutMethodIds = new[]
{ {
PaymentTypes.LN.GetPaymentMethodId(cryptoCode) PayoutTypes.LN.GetPayoutMethodId(cryptoCode)
} }
})) }))
.FirstOrDefault(); .FirstOrDefault();
@@ -63,7 +64,7 @@ public class UILightningAutomatedPayoutProcessorsController : Controller
return View(new LightningTransferViewModel(activeProcessor is null ? new LightningAutomatedPayoutBlob() : LightningAutomatedPayoutProcessor.GetBlob(activeProcessor))); return View(new LightningTransferViewModel(activeProcessor is null ? new LightningAutomatedPayoutBlob() : LightningAutomatedPayoutProcessor.GetBlob(activeProcessor)));
} }
PaymentMethodId GetPaymentMethodId(string cryptoCode) => PaymentTypes.LN.GetPaymentMethodId(cryptoCode); PayoutMethodId GetPayoutMethodId(string cryptoCode) => PayoutTypes.LN.GetPayoutMethodId(cryptoCode);
[HttpPost("~/stores/{storeId}/payout-processors/lightning-automated/{cryptocode}")] [HttpPost("~/stores/{storeId}/payout-processors/lightning-automated/{cryptocode}")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
@@ -71,8 +72,8 @@ public class UILightningAutomatedPayoutProcessorsController : Controller
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
return View(automatedTransferBlob); return View(automatedTransferBlob);
var id = GetPaymentMethodId(cryptoCode); var id = GetPayoutMethodId(cryptoCode);
if (!_lightningAutomatedPayoutSenderFactory.GetSupportedPaymentMethods().Any(i => id == i)) if (!_lightningAutomatedPayoutSenderFactory.GetSupportedPayoutMethods().Any(i => id == i))
{ {
TempData.SetStatusMessageModel(new StatusMessageModel() TempData.SetStatusMessageModel(new StatusMessageModel()
{ {
@@ -87,16 +88,16 @@ public class UILightningAutomatedPayoutProcessorsController : Controller
{ {
Stores = new[] { storeId }, Stores = new[] { storeId },
Processors = new[] { _lightningAutomatedPayoutSenderFactory.Processor }, Processors = new[] { _lightningAutomatedPayoutSenderFactory.Processor },
PaymentMethods = new[] PayoutMethodIds = new[]
{ {
PaymentTypes.LN.GetPaymentMethodId(cryptoCode) PayoutTypes.LN.GetPayoutMethodId(cryptoCode)
} }
})) }))
.FirstOrDefault(); .FirstOrDefault();
activeProcessor ??= new PayoutProcessorData(); activeProcessor ??= new PayoutProcessorData();
activeProcessor.HasTypedBlob<LightningAutomatedPayoutBlob>().SetBlob(automatedTransferBlob.ToBlob()); activeProcessor.HasTypedBlob<LightningAutomatedPayoutBlob>().SetBlob(automatedTransferBlob.ToBlob());
activeProcessor.StoreId = storeId; activeProcessor.StoreId = storeId;
activeProcessor.PaymentMethod = PaymentTypes.LN.GetPaymentMethodId(cryptoCode).ToString(); activeProcessor.PaymentMethod = PayoutTypes.LN.GetPayoutMethodId(cryptoCode).ToString();
activeProcessor.Processor = _lightningAutomatedPayoutSenderFactory.Processor; activeProcessor.Processor = _lightningAutomatedPayoutSenderFactory.Processor;
var tcs = new TaskCompletionSource(); var tcs = new TaskCompletionSource();
_eventAggregator.Publish(new PayoutProcessorUpdated() _eventAggregator.Publish(new PayoutProcessorUpdated()

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
@@ -10,6 +11,7 @@ using BTCPayServer.Events;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payouts;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
@@ -29,37 +31,47 @@ namespace BTCPayServer.PayoutProcessors.OnChain
private readonly BTCPayWalletProvider _btcPayWalletProvider; private readonly BTCPayWalletProvider _btcPayWalletProvider;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings; private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly BitcoinLikePayoutHandler _bitcoinLikePayoutHandler; private readonly BitcoinLikePayoutHandler _bitcoinLikePayoutHandler;
private readonly PaymentMethodHandlerDictionary _handlers;
public BTCPayNetwork Network { get; }
private readonly IFeeProviderFactory _feeProviderFactory; private readonly IFeeProviderFactory _feeProviderFactory;
public OnChainAutomatedPayoutProcessor( public OnChainAutomatedPayoutProcessor(
ApplicationDbContextFactory applicationDbContextFactory, PayoutMethodId payoutMethodId,
ApplicationDbContextFactory applicationDbContextFactory,
ExplorerClientProvider explorerClientProvider, ExplorerClientProvider explorerClientProvider,
BTCPayWalletProvider btcPayWalletProvider, BTCPayWalletProvider btcPayWalletProvider,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
ILoggerFactory logger, ILoggerFactory logger,
BitcoinLikePayoutHandler bitcoinLikePayoutHandler,
EventAggregator eventAggregator, EventAggregator eventAggregator,
WalletRepository walletRepository, WalletRepository walletRepository,
StoreRepository storeRepository, StoreRepository storeRepository,
PayoutProcessorData payoutProcesserSettings, PayoutProcessorData payoutProcesserSettings,
PullPaymentHostedService pullPaymentHostedService, PullPaymentHostedService pullPaymentHostedService,
PaymentMethodHandlerDictionary handlers, PayoutMethodHandlerDictionary payoutHandlers,
PaymentMethodHandlerDictionary handlers,
IPluginHookService pluginHookService, IPluginHookService pluginHookService,
IFeeProviderFactory feeProviderFactory) : IFeeProviderFactory feeProviderFactory) :
base(logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory, base(
PaymentTypes.CHAIN.GetPaymentMethodId(GetPayoutHandler(payoutHandlers, payoutMethodId).Network.CryptoCode),
logger, storeRepository, payoutProcesserSettings, applicationDbContextFactory,
handlers, pluginHookService, eventAggregator) handlers, pluginHookService, eventAggregator)
{ {
_explorerClientProvider = explorerClientProvider; _explorerClientProvider = explorerClientProvider;
_btcPayWalletProvider = btcPayWalletProvider; _btcPayWalletProvider = btcPayWalletProvider;
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings; _btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
_bitcoinLikePayoutHandler = bitcoinLikePayoutHandler; _bitcoinLikePayoutHandler = GetPayoutHandler(payoutHandlers, payoutMethodId);
WalletRepository = walletRepository; Network = _bitcoinLikePayoutHandler.Network;
_handlers = handlers; WalletRepository = walletRepository;
_feeProviderFactory = feeProviderFactory; _feeProviderFactory = feeProviderFactory;
} }
public WalletRepository WalletRepository { get; } private static BitcoinLikePayoutHandler GetPayoutHandler(PayoutMethodHandlerDictionary payoutHandlers, PayoutMethodId payoutMethodId)
{
return (BitcoinLikePayoutHandler)payoutHandlers[payoutMethodId];
}
public WalletRepository WalletRepository { get; }
protected override async Task Process(object paymentMethodConfig, List<PayoutData> payouts) protected override async Task Process(object paymentMethodConfig, List<PayoutData> payouts)
{ {
@@ -67,13 +79,12 @@ namespace BTCPayServer.PayoutProcessors.OnChain
{ {
return; return;
} }
var network = _handlers.TryGetNetwork(this.PaymentMethodId); if (!_explorerClientProvider.IsAvailable(Network.CryptoCode))
if (network is null || !_explorerClientProvider.IsAvailable(network.CryptoCode))
{ {
return; return;
} }
var explorerClient = _explorerClientProvider.GetExplorerClient(network.CryptoCode); var explorerClient = _explorerClientProvider.GetExplorerClient(Network.CryptoCode);
var extKeyStr = await explorerClient.GetMetadataAsync<string>( var extKeyStr = await explorerClient.GetMetadataAsync<string>(
config.AccountDerivation, config.AccountDerivation,
@@ -83,12 +94,12 @@ namespace BTCPayServer.PayoutProcessors.OnChain
return; return;
} }
var wallet = _btcPayWalletProvider.GetWallet(PaymentMethodId.CryptoCode); var wallet = _btcPayWalletProvider.GetWallet(Network.CryptoCode);
var reccoins = (await wallet.GetUnspentCoins(config.AccountDerivation)).ToArray(); var reccoins = (await wallet.GetUnspentCoins(config.AccountDerivation)).ToArray();
var coins = reccoins.Select(coin => coin.Coin).ToArray(); var coins = reccoins.Select(coin => coin.Coin).ToArray();
var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork); var accountKey = ExtKey.Parse(extKeyStr, Network.NBitcoinNetwork);
var keys = reccoins.Select(coin => accountKey.Derive(coin.KeyPath).PrivateKey).ToArray(); var keys = reccoins.Select(coin => accountKey.Derive(coin.KeyPath).PrivateKey).ToArray();
Transaction workingTx = null; Transaction workingTx = null;
decimal? failedAmount = null; decimal? failedAmount = null;
@@ -102,7 +113,7 @@ namespace BTCPayServer.PayoutProcessors.OnChain
return; return;
} }
var feeRate = await this._feeProviderFactory.CreateFeeProvider(network).GetFeeRateAsync(Math.Max(processorBlob.FeeTargetBlock, 1)); var feeRate = await this._feeProviderFactory.CreateFeeProvider(Network).GetFeeRateAsync(Math.Max(processorBlob.FeeTargetBlock, 1));
var transfersProcessing = new List<KeyValuePair<PayoutData, PayoutBlob>>(); var transfersProcessing = new List<KeyValuePair<PayoutData, PayoutBlob>>();
foreach (var transferRequest in payoutToBlobs) foreach (var transferRequest in payoutToBlobs)
@@ -114,14 +125,14 @@ namespace BTCPayServer.PayoutProcessors.OnChain
} }
var claimDestination = var claimDestination =
await _bitcoinLikePayoutHandler.ParseClaimDestination(PaymentMethodId, blob.Destination, CancellationToken); await _bitcoinLikePayoutHandler.ParseClaimDestination(blob.Destination, CancellationToken);
if (!string.IsNullOrEmpty(claimDestination.error)) if (!string.IsNullOrEmpty(claimDestination.error))
{ {
continue; continue;
} }
var bitcoinClaimDestination = (IBitcoinLikeClaimDestination)claimDestination.destination; var bitcoinClaimDestination = (IBitcoinLikeClaimDestination)claimDestination.destination;
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder() var txBuilder = Network.NBitcoinNetwork.CreateTransactionBuilder()
.AddCoins(coins) .AddCoins(coins)
.AddKeys(keys); .AddKeys(keys);
@@ -179,7 +190,7 @@ namespace BTCPayServer.PayoutProcessors.OnChain
{ {
tcs.SetResult(false); tcs.SetResult(false);
} }
var walletId = new WalletId(PayoutProcessorSettings.StoreId, network.CryptoCode); var walletId = new WalletId(PayoutProcessorSettings.StoreId, Network.CryptoCode);
foreach (var payoutData in transfersProcessing) foreach (var payoutData in transfersProcessing)
{ {
await WalletRepository.AddWalletTransactionAttachment(walletId, await WalletRepository.AddWalletTransactionAttachment(walletId,

View File

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -15,41 +16,39 @@ namespace BTCPayServer.PayoutProcessors.OnChain;
public class OnChainAutomatedPayoutSenderFactory : EventHostedServiceBase, IPayoutProcessorFactory public class OnChainAutomatedPayoutSenderFactory : EventHostedServiceBase, IPayoutProcessorFactory
{ {
private readonly BTCPayNetworkProvider _btcPayNetworkProvider; private readonly PayoutMethodHandlerDictionary _handlers;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly LinkGenerator _linkGenerator; private readonly LinkGenerator _linkGenerator;
private readonly PayoutMethodId[] _supportedPayoutMethods;
public string FriendlyName { get; } = "Automated Bitcoin Sender"; public string FriendlyName { get; } = "Automated Bitcoin Sender";
public OnChainAutomatedPayoutSenderFactory(EventAggregator eventAggregator, public OnChainAutomatedPayoutSenderFactory(
PayoutMethodHandlerDictionary handlers,
EventAggregator eventAggregator,
ILogger<OnChainAutomatedPayoutSenderFactory> logger, ILogger<OnChainAutomatedPayoutSenderFactory> logger,
BTCPayNetworkProvider btcPayNetworkProvider,
IServiceProvider serviceProvider, LinkGenerator linkGenerator) : base(eventAggregator, logger) IServiceProvider serviceProvider, LinkGenerator linkGenerator) : base(eventAggregator, logger)
{ {
_btcPayNetworkProvider = btcPayNetworkProvider; _handlers = handlers;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_linkGenerator = linkGenerator; _linkGenerator = linkGenerator;
_supportedPayoutMethods = _handlers.OfType<BitcoinLikePayoutHandler>().Select(c => c.PayoutMethodId).ToArray();
} }
public string Processor => ProcessorName; public string Processor => ProcessorName;
public static string ProcessorName => nameof(OnChainAutomatedPayoutSenderFactory); public static string ProcessorName => nameof(OnChainAutomatedPayoutSenderFactory);
public string ConfigureLink(string storeId, PaymentMethodId paymentMethodId, HttpRequest request) public string ConfigureLink(string storeId, PayoutMethodId payoutMethodId, HttpRequest request)
{ {
var network = _handlers.GetNetwork(payoutMethodId);
return _linkGenerator.GetUriByAction("Configure", return _linkGenerator.GetUriByAction("Configure",
"UIOnChainAutomatedPayoutProcessors", new "UIOnChainAutomatedPayoutProcessors", new
{ {
storeId, storeId,
cryptoCode = paymentMethodId.CryptoCode cryptoCode = network.CryptoCode
}, request.Scheme, request.Host, request.PathBase); }, request.Scheme, request.Host, request.PathBase);
} }
public IEnumerable<PaymentMethodId> GetSupportedPaymentMethods() public IEnumerable<PayoutMethodId> GetSupportedPayoutMethods() => _supportedPayoutMethods;
{
return _btcPayNetworkProvider.GetAll().OfType<BTCPayNetwork>()
.Where(network => !network.ReadonlyWallet && network.WalletSupported)
.Select(network =>
PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode));
}
public Task<IHostedService> ConstructProcessor(PayoutProcessorData settings) public Task<IHostedService> ConstructProcessor(PayoutProcessorData settings)
{ {
@@ -57,7 +56,7 @@ public class OnChainAutomatedPayoutSenderFactory : EventHostedServiceBase, IPayo
{ {
throw new NotSupportedException("This processor cannot handle the provided requirements"); throw new NotSupportedException("This processor cannot handle the provided requirements");
} }
var payoutMethodId = settings.GetPayoutMethodId();
return Task.FromResult<IHostedService>(ActivatorUtilities.CreateInstance<OnChainAutomatedPayoutProcessor>(_serviceProvider, settings)); return Task.FromResult<IHostedService>(ActivatorUtilities.CreateInstance<OnChainAutomatedPayoutProcessor>(_serviceProvider, settings, payoutMethodId));
} }
} }

View File

@@ -9,6 +9,7 @@ using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -33,14 +34,14 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller
_onChainAutomatedPayoutSenderFactory = onChainAutomatedPayoutSenderFactory; _onChainAutomatedPayoutSenderFactory = onChainAutomatedPayoutSenderFactory;
_payoutProcessorService = payoutProcessorService; _payoutProcessorService = payoutProcessorService;
} }
PaymentMethodId GetPaymentMethodId(string cryptoCode) => PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode); PayoutMethodId GetPayoutMethod(string cryptoCode) => PayoutTypes.CHAIN.GetPayoutMethodId(cryptoCode);
[HttpGet("~/stores/{storeId}/payout-processors/onchain-automated/{cryptocode}")] [HttpGet("~/stores/{storeId}/payout-processors/onchain-automated/{cryptocode}")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Configure(string storeId, string cryptoCode) public async Task<IActionResult> Configure(string storeId, string cryptoCode)
{ {
var id = GetPaymentMethodId(cryptoCode); var id = GetPayoutMethod(cryptoCode);
if (!_onChainAutomatedPayoutSenderFactory.GetSupportedPaymentMethods().Any(i => id == i)) if (!_onChainAutomatedPayoutSenderFactory.GetSupportedPayoutMethods().Any(i => id == i))
{ {
TempData.SetStatusMessageModel(new StatusMessageModel() TempData.SetStatusMessageModel(new StatusMessageModel()
{ {
@@ -64,9 +65,9 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller
{ {
Stores = new[] { storeId }, Stores = new[] { storeId },
Processors = new[] { _onChainAutomatedPayoutSenderFactory.Processor }, Processors = new[] { _onChainAutomatedPayoutSenderFactory.Processor },
PaymentMethods = new[] PayoutMethodIds = new[]
{ {
PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode) PayoutTypes.CHAIN.GetPayoutMethodId(cryptoCode)
} }
})) }))
.FirstOrDefault(); .FirstOrDefault();
@@ -81,8 +82,8 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
return View(automatedTransferBlob); return View(automatedTransferBlob);
var id = GetPaymentMethodId(cryptoCode); var id = GetPayoutMethod(cryptoCode);
if (!_onChainAutomatedPayoutSenderFactory.GetSupportedPaymentMethods().Any(i => id == i)) if (!_onChainAutomatedPayoutSenderFactory.GetSupportedPayoutMethods().Any(i => id == i))
{ {
TempData.SetStatusMessageModel(new StatusMessageModel() TempData.SetStatusMessageModel(new StatusMessageModel()
{ {
@@ -97,16 +98,16 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller
{ {
Stores = new[] { storeId }, Stores = new[] { storeId },
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName }, Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
PaymentMethods = new[] PayoutMethodIds = new[]
{ {
PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode) PayoutTypes.CHAIN.GetPayoutMethodId(cryptoCode)
} }
})) }))
.FirstOrDefault(); .FirstOrDefault();
activeProcessor ??= new PayoutProcessorData(); activeProcessor ??= new PayoutProcessorData();
activeProcessor.HasTypedBlob<OnChainAutomatedPayoutBlob>().SetBlob(automatedTransferBlob.ToBlob()); activeProcessor.HasTypedBlob<OnChainAutomatedPayoutBlob>().SetBlob(automatedTransferBlob.ToBlob());
activeProcessor.StoreId = storeId; activeProcessor.StoreId = storeId;
activeProcessor.PaymentMethod = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode).ToString(); activeProcessor.PaymentMethod = PayoutTypes.CHAIN.GetPayoutMethodId(cryptoCode).ToString();
activeProcessor.Processor = _onChainAutomatedPayoutSenderFactory.Processor; activeProcessor.Processor = _onChainAutomatedPayoutSenderFactory.Processor;
var tcs = new TaskCompletionSource(); var tcs = new TaskCompletionSource();
_eventAggregator.Publish(new PayoutProcessorUpdated() _eventAggregator.Publish(new PayoutProcessorUpdated()

View File

@@ -8,6 +8,7 @@ using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -49,14 +50,14 @@ public class PayoutProcessorService : EventHostedServiceBase
{ {
} }
public PayoutProcessorQuery(string storeId, PaymentMethodId paymentMethodId) public PayoutProcessorQuery(string storeId, PayoutMethodId payoutMethodId)
{ {
Stores = new[] { storeId }; Stores = new[] { storeId };
PaymentMethods = new[] { paymentMethodId }; PayoutMethodIds = new[] { payoutMethodId };
} }
public string[] Stores { get; set; } public string[] Stores { get; set; }
public string[] Processors { get; set; } public string[] Processors { get; set; }
public PaymentMethodId[] PaymentMethods { get; set; } public PayoutMethodId[] PayoutMethodIds { get; set; }
} }
public async Task<List<PayoutProcessorData>> GetProcessors(PayoutProcessorQuery query) public async Task<List<PayoutProcessorData>> GetProcessors(PayoutProcessorQuery query)
@@ -72,9 +73,9 @@ public class PayoutProcessorService : EventHostedServiceBase
{ {
queryable = queryable.Where(data => query.Stores.Contains(data.StoreId)); queryable = queryable.Where(data => query.Stores.Contains(data.StoreId));
} }
if (query.PaymentMethods is not null) if (query.PayoutMethodIds is not null)
{ {
var paymentMethods = query.PaymentMethods.Select(d => d.ToString()).Distinct().ToArray(); var paymentMethods = query.PayoutMethodIds.Select(d => d.ToString()).Distinct().ToArray();
queryable = queryable.Where(data => paymentMethods.Contains(data.PaymentMethod)); queryable = queryable.Where(data => paymentMethods.Contains(data.PaymentMethod));
} }

View File

@@ -2,6 +2,7 @@ using BTCPayServer.Data;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.PayoutProcessors.Lightning; using BTCPayServer.PayoutProcessors.Lightning;
using BTCPayServer.PayoutProcessors.OnChain; using BTCPayServer.PayoutProcessors.OnChain;
using BTCPayServer.Payouts;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.PayoutProcessors; namespace BTCPayServer.PayoutProcessors;
@@ -18,8 +19,8 @@ public static class PayoutProcessorsExtensions
serviceCollection.AddHostedService(s => s.GetRequiredService<PayoutProcessorService>()); serviceCollection.AddHostedService(s => s.GetRequiredService<PayoutProcessorService>());
} }
public static PaymentMethodId GetPaymentMethodId(this PayoutProcessorData data) public static PayoutMethodId GetPayoutMethodId(this PayoutProcessorData data)
{ {
return PaymentMethodId.Parse(data.PaymentMethod); return PayoutMethodId.Parse(data.PaymentMethod);
} }
} }

View File

@@ -7,6 +7,7 @@ using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -45,9 +46,9 @@ public class UIPayoutProcessorsController : Controller
return View(_payoutProcessorFactories.Select(factory => return View(_payoutProcessorFactories.Select(factory =>
{ {
var conf = activeProcessors.FirstOrDefault(datas => datas.Key == factory.Processor) var conf = activeProcessors.FirstOrDefault(datas => datas.Key == factory.Processor)
?.ToDictionary(data => data.GetPaymentMethodId(), data => data) ?? ?.ToDictionary(data => data.GetPayoutMethodId(), data => data) ??
new Dictionary<PaymentMethodId, PayoutProcessorData>(); new Dictionary<PayoutMethodId, PayoutProcessorData>();
foreach (PaymentMethodId supportedPaymentMethod in factory.GetSupportedPaymentMethods()) foreach (var supportedPaymentMethod in factory.GetSupportedPayoutMethods())
{ {
conf.TryAdd(supportedPaymentMethod, null); conf.TryAdd(supportedPaymentMethod, null);
} }
@@ -80,7 +81,7 @@ public class UIPayoutProcessorsController : Controller
public class StorePayoutProcessorsView public class StorePayoutProcessorsView
{ {
public Dictionary<PaymentMethodId, PayoutProcessorData> Configured { get; set; } public Dictionary<PayoutMethodId, PayoutProcessorData> Configured { get; set; }
public IPayoutProcessorFactory Factory { get; set; } public IPayoutProcessorFactory Factory { get; set; }
} }
} }

View File

@@ -0,0 +1,16 @@
using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace BTCPayServer.Payouts
{
public class PayoutMethodHandlerDictionary : HandlersDictionary<PayoutMethodId, IPayoutHandler>
{
public PayoutMethodHandlerDictionary(IEnumerable<IPayoutHandler> payoutHandlers) : base(payoutHandlers)
{
}
}
}

View File

@@ -0,0 +1,85 @@
#nullable enable
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using BTCPayServer.Payments;
namespace BTCPayServer.Payouts
{
/// <summary>
/// A value object which represent a crypto currency with his payment type (ie, onchain or offchain)
/// </summary>
public class PayoutMethodId
{
PayoutMethodId(string id)
{
ArgumentNullException.ThrowIfNull(id);
_Id = id;
}
string _Id;
public override bool Equals(object? obj)
{
if (obj is PayoutMethodId id)
return ToString().Equals(id.ToString(), StringComparison.OrdinalIgnoreCase);
return false;
}
public static bool operator ==(PayoutMethodId? a, PayoutMethodId? b)
{
if (a is null && b is null)
return true;
if (a is PayoutMethodId ai && b is PayoutMethodId bi)
return ai.Equals(bi);
return false;
}
public static bool operator !=(PayoutMethodId? a, PayoutMethodId? b)
{
return !(a == b);
}
public override int GetHashCode()
{
#pragma warning disable CA1307 // Specify StringComparison
return ToString().GetHashCode();
#pragma warning restore CA1307 // Specify StringComparison
}
public override string ToString()
{
return _Id;
}
static char[] Separators = new[] { '_', '-' };
public static PayoutMethodId? TryParse(string? str)
{
TryParse(str, out var r);
return r;
}
public static bool TryParse(string? str, [MaybeNullWhen(false)] out PayoutMethodId payoutMethodId)
{
payoutMethodId = null;
if (!Payments.PaymentMethodId.TryParse(str, out var result))
return false;
var payoutId = result.ToString();
// -LNURL should just be -LN
var lnUrlSuffix = $"-{Payments.PaymentTypes.LNURL.ToString()}";
if (payoutId.EndsWith(lnUrlSuffix, StringComparison.Ordinal))
payoutId = payoutId.Substring(payoutId.Length - lnUrlSuffix.Length) + $"-{Payments.PaymentTypes.LN}";
payoutMethodId = new PayoutMethodId(payoutId);
return true;
}
public static PayoutMethodId Parse(string str)
{
if (!TryParse(str, out var result))
throw new FormatException("Invalid PayoutMethodId");
return result;
}
}
}

View File

@@ -0,0 +1,18 @@
using BTCPayServer.Payments;
namespace BTCPayServer.Payouts
{
public class PayoutTypes
{
public static readonly PayoutType LN = new("LN");
public static readonly PayoutType CHAIN = new("CHAIN");
}
public record PayoutType(string Id)
{
public PayoutMethodId GetPayoutMethodId(string cryptoCode) => PayoutMethodId.Parse($"{cryptoCode.ToUpperInvariant()}-{Id}");
public override string ToString()
{
return Id;
}
}
}

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using BTCPayServer.Payouts;
using NBitcoin; using NBitcoin;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -37,15 +38,17 @@ namespace BTCPayServer.Services
{ {
serializer.Converters.Add(converter); serializer.Converters.Add(converter);
} }
_Serializers.Add(network.CryptoCode, serializer); // TODO: Get rid of this serializer
_Serializers.Add(PayoutTypes.CHAIN.GetPayoutMethodId(network.CryptoCode), serializer);
_Serializers.Add(PayoutTypes.LN.GetPayoutMethodId(network.CryptoCode), serializer);
} }
} }
readonly Dictionary<string, JsonSerializerSettings> _Serializers = new Dictionary<string, JsonSerializerSettings>(); readonly Dictionary<PayoutMethodId, JsonSerializerSettings> _Serializers = new Dictionary<PayoutMethodId, JsonSerializerSettings>();
public JsonSerializerSettings GetSerializer(string cryptoCode) public JsonSerializerSettings GetSerializer(PayoutMethodId payoutMethodId)
{ {
ArgumentNullException.ThrowIfNull(cryptoCode); ArgumentNullException.ThrowIfNull(payoutMethodId);
_Serializers.TryGetValue(cryptoCode, out var serializer); _Serializers.TryGetValue(payoutMethodId, out var serializer);
return serializer; return serializer;
} }
} }

View File

@@ -10,41 +10,10 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Invoices namespace BTCPayServer.Services.Invoices
{ {
public class PaymentMethodHandlerDictionary : IEnumerable<IPaymentMethodHandler> public class PaymentMethodHandlerDictionary : HandlersDictionary<PaymentMethodId, IPaymentMethodHandler>
{ {
private readonly Dictionary<PaymentMethodId, IPaymentMethodHandler> _mappedHandlers = public PaymentMethodHandlerDictionary(IEnumerable<IPaymentMethodHandler> paymentMethodHandlers) : base(paymentMethodHandlers)
new Dictionary<PaymentMethodId, IPaymentMethodHandler>();
public PaymentMethodHandlerDictionary(IEnumerable<IPaymentMethodHandler> paymentMethodHandlers)
{ {
foreach (var paymentMethodHandler in paymentMethodHandlers)
{
_mappedHandlers.Add(paymentMethodHandler.PaymentMethodId, paymentMethodHandler);
}
}
public bool TryGetValue(PaymentMethodId paymentMethodId, [MaybeNullWhen(false)] out IPaymentMethodHandler value)
{
ArgumentNullException.ThrowIfNull(paymentMethodId);
return _mappedHandlers.TryGetValue(paymentMethodId, out value);
}
public IPaymentMethodHandler? TryGet(PaymentMethodId paymentMethodId)
{
ArgumentNullException.ThrowIfNull(paymentMethodId);
_mappedHandlers.TryGetValue(paymentMethodId, out var value);
return value;
}
public IPaymentMethodHandler this[PaymentMethodId index] => _mappedHandlers[index];
public bool Support(PaymentMethodId paymentMethod) => _mappedHandlers.ContainsKey(paymentMethod);
public IEnumerator<IPaymentMethodHandler> GetEnumerator()
{
return _mappedHandlers.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
} }
public object? ParsePaymentPromptDetails(PaymentPrompt prompt) public object? ParsePaymentPromptDetails(PaymentPrompt prompt)

View File

@@ -43,7 +43,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
new new
{ {
storeId = notification.StoreId, storeId = notification.StoreId,
paymentMethodId = notification.PaymentMethod, payoutMethodId = notification.PaymentMethod,
payoutState = PayoutState.AwaitingPayment payoutState = PayoutState.AwaitingPayment
}, _options.RootPath); }, _options.RootPath);
} }

View File

@@ -44,7 +44,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
}; };
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIStorePullPaymentsController.Payouts), vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIStorePullPaymentsController.Payouts),
"UIStorePullPayments", "UIStorePullPayments",
new { storeId = notification.StoreId, paymentMethodId = notification.PaymentMethod }, _options.RootPath); new { storeId = notification.StoreId, payoutMethodId = notification.PaymentMethod }, _options.RootPath);
} }
} }

View File

@@ -1,13 +1,16 @@
#nullable enable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Data.Payouts.LightningLike;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Services.Reporting; namespace BTCPayServer.Services.Reporting;
@@ -17,12 +20,12 @@ public class PayoutsReportProvider : ReportProvider
private readonly PullPaymentHostedService _pullPaymentHostedService; private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings; private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly DisplayFormatter _displayFormatter; private readonly DisplayFormatter _displayFormatter;
private readonly PaymentMethodHandlerDictionary _handlers; private readonly PayoutMethodHandlerDictionary _handlers;
public PayoutsReportProvider( public PayoutsReportProvider(
PullPaymentHostedService pullPaymentHostedService, PullPaymentHostedService pullPaymentHostedService,
DisplayFormatter displayFormatter, DisplayFormatter displayFormatter,
PaymentMethodHandlerDictionary handlers, PayoutMethodHandlerDictionary handlers,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings) BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
{ {
_displayFormatter = displayFormatter; _displayFormatter = displayFormatter;
@@ -51,23 +54,27 @@ public class PayoutsReportProvider : ReportProvider
data.Add(payout.Date); data.Add(payout.Date);
data.Add(payout.GetPayoutSource(_btcPayNetworkJsonSerializerSettings)); data.Add(payout.GetPayoutSource(_btcPayNetworkJsonSerializerSettings));
data.Add(payout.State.ToString()); data.Add(payout.State.ToString());
if (PaymentMethodId.TryParse(payout.PaymentMethodId, out var pmi)) string? payoutCurrency;
if (PayoutMethodId.TryParse(payout.PaymentMethodId, out var pmi))
{ {
var handler = _handlers.TryGet(pmi); var handler = _handlers.TryGet(pmi);
if (handler is ILightningPaymentHandler) if (handler is LightningLikePayoutHandler)
data.Add("Lightning"); data.Add("Lightning");
else if (handler is BitcoinLikePaymentHandler) else if (handler is BitcoinLikePayoutHandler)
data.Add("On-Chain"); data.Add("On-Chain");
else else
data.Add(pmi.ToString()); data.Add(pmi.ToString());
payoutCurrency = handler?.Currency;
} }
else else
continue; continue;
var ppBlob = payout.PullPaymentData?.GetBlob(); var ppBlob = payout.PullPaymentData?.GetBlob();
var currency = ppBlob?.Currency ?? pmi.CryptoCode; var currency = ppBlob?.Currency ?? payoutCurrency;
data.Add(pmi.CryptoCode); if (currency is null)
data.Add(blob.CryptoAmount.HasValue ? _displayFormatter.ToFormattedAmount(blob.CryptoAmount.Value, pmi.CryptoCode) : null); continue;
data.Add(payoutCurrency);
data.Add(blob.CryptoAmount.HasValue && payoutCurrency is not null ? _displayFormatter.ToFormattedAmount(blob.CryptoAmount.Value, payoutCurrency) : null);
data.Add(currency); data.Add(currency);
data.Add(_displayFormatter.ToFormattedAmount(blob.Amount, currency)); data.Add(_displayFormatter.ToFormattedAmount(blob.Amount, currency));
data.Add(blob.Destination); data.Add(blob.Destination);

View File

@@ -26,9 +26,9 @@
if (Model.AvailablePaymentMethods != null && Model.AvailablePaymentMethods.Any()) if (Model.AvailablePaymentMethods != null && Model.AvailablePaymentMethods.Any())
{ {
<div class="form-group"> <div class="form-group">
<label asp-for="SelectedPaymentMethod" class="form-label"></label> <label asp-for="SelectedPayoutMethod" class="form-label"></label>
<select asp-items="Model.AvailablePaymentMethods" asp-for="SelectedPaymentMethod" class="form-select"></select> <select asp-items="Model.AvailablePaymentMethods" asp-for="SelectedPayoutMethod" class="form-select"></select>
<span asp-validation-for="SelectedPaymentMethod" class="text-danger"></span> <span asp-validation-for="SelectedPayoutMethod" class="text-danger"></span>
</div> </div>
<button id="ok" type="submit" class="btn btn-primary w-100">Next</button> <button id="ok" type="submit" class="btn btn-primary w-100">Next</button>
@@ -36,7 +36,7 @@
break; break;
case RefundSteps.SelectRate: case RefundSteps.SelectRate:
<input type="hidden" asp-for="SelectedPaymentMethod"/> <input type="hidden" asp-for="SelectedPayoutMethod"/>
<input type="hidden" asp-for="CryptoAmountThen"/> <input type="hidden" asp-for="CryptoAmountThen"/>
<input type="hidden" asp-for="FiatAmount"/> <input type="hidden" asp-for="FiatAmount"/>
<input type="hidden" asp-for="OverpaidAmount"/> <input type="hidden" asp-for="OverpaidAmount"/>

View File

@@ -48,7 +48,7 @@
@if (await processorsView.Factory.CanRemove()) @if (await processorsView.Factory.CanRemove())
{ {
<span>-</span> <span>-</span>
<a asp-action="Remove" asp-route-storeId="@storeId" asp-route-id="@conf.Value.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The @Html.Encode(processorsView.Factory.FriendlyName) for @Html.Encode(conf.Key.CryptoCode) will be removed from your store.">Remove</a> <a asp-action="Remove" asp-route-storeId="@storeId" asp-route-id="@conf.Value.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The @Html.Encode(processorsView.Factory.FriendlyName) (@Html.Encode(conf.Key.ToString())) will be removed from your store.">Remove</a>
} }
} }
</td> </td>

View File

@@ -48,14 +48,18 @@
</button> </button>
} }
<input class="form-control form-control-lg font-monospace" asp-for="Destination" placeholder="Enter destination to claim funds" required style="font-size:.9rem;height:42px;"> <input class="form-control form-control-lg font-monospace" asp-for="Destination" placeholder="Enter destination to claim funds" required style="font-size:.9rem;height:42px;">
@if (Model.PaymentMethods.Length == 1) @if (Model.BitcoinOnly)
{
<span class="input-group-text">BTC</span>
}
else if (Model.PayoutMethodIds.Length == 1)
{ {
<input type="hidden" asp-for="SelectedPaymentMethod"> <input type="hidden" asp-for="SelectedPayoutMethod">
<span class="input-group-text">@Model.PaymentMethods.First().ToString()</span> <span class="input-group-text">@Model.PayoutMethodIds.First().ToString()</span>
} }
else if (!Model.BitcoinOnly) else
{ {
<select class="form-select w-auto" asp-for="SelectedPaymentMethod" asp-items="Model.PaymentMethods.Select(id => new SelectListItem(id.ToString(), id.ToString()))"></select> <select class="form-select w-auto" asp-for="SelectedPayoutMethod" asp-items="Model.PayoutMethodIds.Select(id => new SelectListItem(id.ToString(), id.ToString()))"></select>
} }
<button type="button" class="btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan destination with camera" id="scandestination-button"> <button type="button" class="btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan destination with camera" id="scandestination-button">
<vc:icon symbol="scan-qr"/> <vc:icon symbol="scan-qr"/>

View File

@@ -51,17 +51,17 @@
</div> </div>
</div> </div>
<div class="form-group mb-4"> <div class="form-group mb-4">
<label asp-for="PaymentMethods" class="form-label"></label> <label asp-for="PayoutMethods" class="form-label"></label>
@foreach (var item in Model.PaymentMethodItems) @foreach (var item in Model.PayoutMethodsItem)
{ {
<div class="form-check mb-2"> <div class="form-check mb-2">
<label class="form-label"> <label class="form-label">
<input name="PaymentMethods" class="form-check-input" type="checkbox" value="@item.Value" @(item.Selected ? "checked" : "")> <input name="PayoutMethods" class="form-check-input" type="checkbox" value="@item.Value" @(item.Selected ? "checked" : "")>
@item.Text @item.Text
</label> </label>
</div> </div>
} }
<span asp-validation-for="PaymentMethods" class="text-danger mt-0"></span> <span asp-validation-for="PayoutMethods" class="text-danger mt-0"></span>
</div> </div>
</div> </div>
<div class="col-lg-9"> <div class="col-lg-9">

View File

@@ -1,23 +1,24 @@
@using BTCPayServer.Client.Models @using BTCPayServer.Client.Models
@using BTCPayServer.Payments @using BTCPayServer.Payments
@using BTCPayServer.Payouts
@using BTCPayServer.Views.Stores @using BTCPayServer.Views.Stores
@using BTCPayServer.Abstractions.Extensions @using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Client @using BTCPayServer.Client
@using BTCPayServer.PayoutProcessors @using BTCPayServer.PayoutProcessors
@model BTCPayServer.Models.WalletViewModels.PayoutsModel @model BTCPayServer.Models.WalletViewModels.PayoutsModel
@inject IEnumerable<IPayoutHandler> PayoutHandlers; @inject PayoutMethodHandlerDictionary PayoutHandlers;
@{ @{
var storeId = Context.GetRouteValue("storeId") as string; var storeId = Context.GetRouteValue("storeId") as string;
ViewData.SetActivePage(StoreNavPages.Payouts, $"Payouts{(string.IsNullOrEmpty(Model.PullPaymentName) ? string.Empty : " for pull payment " + Model.PullPaymentName)}", Context.GetStoreData().Id); ViewData.SetActivePage(StoreNavPages.Payouts, $"Payouts{(string.IsNullOrEmpty(Model.PullPaymentName) ? string.Empty : " for pull payment " + Model.PullPaymentName)}", Context.GetStoreData().Id);
Model.PaginationQuery ??= new Dictionary<string, object>(); Model.PaginationQuery ??= new Dictionary<string, object>();
Model.PaginationQuery.Add("pullPaymentId", Model.PullPaymentId); Model.PaginationQuery.Add("pullPaymentId", Model.PullPaymentId);
Model.PaginationQuery.Add("paymentMethodId", Model.PaymentMethodId); Model.PaginationQuery.Add("payoutMethodId", Model.PayoutMethodId);
Model.PaginationQuery.Add("payoutState", Model.PayoutState); Model.PaginationQuery.Add("payoutState", Model.PayoutState);
var stateActions = new List<(string Action, string Text)>(); var stateActions = new List<(string Action, string Text)>();
if (PaymentMethodId.TryParse(Model.PaymentMethodId, out var paymentMethodId)) if (PayoutMethodId.TryParse(Model.PayoutMethodId, out var payoutMethodId))
{ {
var payoutHandler = PayoutHandlers.FindPayoutHandler(paymentMethodId); var payoutHandler = PayoutHandlers.TryGet(payoutMethodId);
if (payoutHandler is null) if (payoutHandler is null)
return; return;
stateActions.AddRange(payoutHandler.GetPayoutSpecificActions().Where(pair => pair.Key == Model.PayoutState).SelectMany(pair => pair.Value)); stateActions.AddRange(payoutHandler.GetPayoutSpecificActions().Where(pair => pair.Key == Model.PayoutState).SelectMany(pair => pair.Value));
@@ -96,22 +97,22 @@
} }
<form method="post" id="Payouts"> <form method="post" id="Payouts">
<input type="hidden" asp-for="PaymentMethodId" /> <input type="hidden" asp-for="PayoutMethodId" />
<input type="hidden" asp-for="PayoutState" /> <input type="hidden" asp-for="PayoutState" />
<div class="d-flex justify-content-between mb-4"> <div class="d-flex justify-content-between mb-4">
<ul class="nav mb-1 gap-2"> <ul class="nav mb-1 gap-2">
@foreach (var state in Model.PaymentMethods) @foreach (var state in Model.PayoutMethods)
{ {
<li class="nav-item py-0"> <li class="nav-item py-0">
<a asp-action="Payouts" asp-route-storeId="@Context.GetRouteValue("storeId")" <a asp-action="Payouts" asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-payoutState="@Model.PayoutState" asp-route-payoutState="@Model.PayoutState"
asp-route-paymentMethodId="@state.ToString()" asp-route-payoutMethodId="@state.ToString()"
asp-route-pullPaymentId="@Model.PullPaymentId" asp-route-pullPaymentId="@Model.PullPaymentId"
class="btcpay-pill position-relative me-0 @(state.ToString() == Model.PaymentMethodId ? "active" : "")" class="btcpay-pill position-relative me-0 @(state.ToString() == Model.PayoutMethodId ? "active" : "")"
id="@state.ToString()-view" id="@state.ToString()-view"
role="tab"> role="tab">
@state.ToString() @state.ToString()
@if (Model.PaymentMethodCount.TryGetValue(state.ToString(), out var count) && count > 0) @if (Model.PayoutMethodCount.TryGetValue(state.ToString(), out var count) && count > 0)
{ {
<span class="badge rounded-pill fw-semibold pe-0">@count</span> <span class="badge rounded-pill fw-semibold pe-0">@count</span>
} }
@@ -129,7 +130,7 @@
asp-route-storeId="@Context.GetRouteValue("storeId")" asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-payoutState="@state.Key" asp-route-payoutState="@state.Key"
asp-route-pullPaymentId="@Model.PullPaymentId" asp-route-pullPaymentId="@Model.PullPaymentId"
asp-route-paymentMethodId="@Model.PaymentMethodId" asp-route-payoutMethodId="@Model.PayoutMethodId"
class="nav-link @(state.Key == Model.PayoutState ? "active" : "")" role="tab"> class="nav-link @(state.Key == Model.PayoutState ? "active" : "")" role="tab">
@state.Key.GetStateString() @state.Key.GetStateString()
@if (state.Value > 0) @if (state.Value > 0)