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

View File

@@ -3450,7 +3450,7 @@ namespace BTCPayServer.Tests
Assert.Equal(connStr, methods.FirstOrDefault(m => m.PaymentMethodId == "BTC-LN")?.Config["connectionString"].Value<string>());
methods = await adminClient.GetStorePaymentMethods(store.Id);
Assert.Null(methods.FirstOrDefault(m => m.PaymentMethodId == "BTC-LN").Config);
await this.AssertValidationError(["paymentMethodId"], () => adminClient.RemoveStorePaymentMethod(store.Id, "LOL"));
await this.AssertAPIError("paymentmethod-not-found", () => adminClient.RemoveStorePaymentMethod(store.Id, "LOL"));
await adminClient.RemoveStorePaymentMethod(store.Id, "BTC-LN");
// Alternative way of setting the connection string
@@ -3948,7 +3948,7 @@ namespace BTCPayServer.Tests
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, PaymentMethodId.Parse("BTC")));
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, Payouts.PayoutMethodId.Parse("BTC")));
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(4, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());

View File

@@ -2094,6 +2094,7 @@ namespace BTCPayServer.Tests
public async Task CanUsePullPaymentsViaUI()
{
using var s = CreateSeleniumTester();
s.Server.DeleteStore = false;
s.Server.ActivateLightning(LightningConnectionType.LndREST);
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
@@ -2274,7 +2275,7 @@ namespace BTCPayServer.Tests
s.GoToStore(newStore.storeId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
var paymentMethodOptions = s.Driver.FindElements(By.CssSelector("input[name='PaymentMethods']"));
var paymentMethodOptions = s.Driver.FindElements(By.CssSelector("input[name='PayoutMethods']"));
Assert.Equal(2, paymentMethodOptions.Count);
s.Driver.FindElement(By.Id("Name")).SendKeys("Lightning Test");
@@ -2287,7 +2288,7 @@ namespace BTCPayServer.Tests
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
// Bitcoin-only, SelectedPaymentMethod should not be displayed
s.Driver.ElementDoesNotExist(By.Id("SelectedPaymentMethod"));
s.Driver.ElementDoesNotExist(By.Id("SelectedPayoutMethod"));
var bolt = (await s.Server.CustomerLightningD.CreateInvoice(
payoutAmount,
@@ -3072,7 +3073,7 @@ namespace BTCPayServer.Tests
// Check that pull payment has lightning option
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), PaymentMethodId.Parse(Assert.Single(s.Driver.FindElements(By.CssSelector("input[name='PaymentMethods']"))).GetAttribute("value")));
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), PaymentMethodId.Parse(Assert.Single(s.Driver.FindElements(By.CssSelector("input[name='PayoutMethods']"))).GetAttribute("value")));
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
@@ -265,7 +266,7 @@ namespace BTCPayServer.Controllers
[HttpGet("invoices/{invoiceId}/refund")]
[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();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
@@ -290,24 +291,8 @@ namespace BTCPayServer.Controllers
new { pullPaymentId = ppId });
}
var paymentMethods = invoice.GetBlob().GetPaymentPrompts();
var pmis = paymentMethods.Select(method => method.PaymentMethodId).ToHashSet();
// 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 payoutMethodIds = _payoutHandlers.GetSupportedPayoutMethods(this.GetCurrentStore());
if (!payoutMethodIds.Any())
{
var vm = new RefundModel { Title = "No matching payment method" };
ModelState.AddModelError(nameof(vm.AvailablePaymentMethods),
@@ -315,18 +300,22 @@ namespace BTCPayServer.Controllers
return View("_RefundModal", vm);
}
var defaultRefund = invoice.Payments
.Select(p => p.GetBlob())
.Select(p => p.PaymentMethodId)
.FirstOrDefault(p => p != null && options.Contains(p));
// Find the most similar payment method to the one used for the invoice
var defaultRefund =
PaymentMethodId.GetSimilarities(
invoice.Payments.Select(o => o.GetPaymentMethodId()),
payoutMethodIds)
.OrderByDescending(o => o.similarity)
.Select(o => o.b)
.FirstOrDefault();
var refund = new RefundModel
{
Title = "Payment method",
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"),
SelectedPaymentMethod = defaultRefund?.ToString() ?? options.First().ToString()
SelectedPayoutMethod = defaultRefund?.ToString() ?? payoutMethodIds.First().ToString()
};
// Nothing to select, skip to next
@@ -351,31 +340,35 @@ namespace BTCPayServer.Controllers
return NotFound();
var store = GetCurrentStore();
var paymentMethodId = PaymentMethodId.Parse(model.SelectedPaymentMethod);
var pmi = PayoutMethodId.Parse(model.SelectedPayoutMethod);
var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true);
RateRules rules;
RateResult rateResult;
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
if (_handlers.TryGetValue(paymentMethodId, out var h) && h is LightningLikePaymentHandler lnh)
{
pms.TryGetValue(PaymentTypes.LNURL.GetPaymentMethodId(lnh.Network.CryptoCode), out paymentMethod);
}
ModelState.AddModelError(nameof(model.SelectedPayoutMethod), $"Invalid payout method");
return View("_RefundModal", model);
}
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);
}
var accounting = paymentMethod.Calculate();
decimal cryptoPaid = accounting.Paid;
decimal dueAmount = accounting.TotalDue;
var paymentMethodCurrency = paymentMethodId.CryptoCode;
var paymentMethodCurrency = paymentMethod.Currency;
var isPaidOver = invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver;
decimal? overpaidAmount = isPaidOver ? Math.Round(cryptoPaid - dueAmount, paymentMethod.Divisibility) : null;
@@ -421,7 +414,7 @@ namespace BTCPayServer.Controllers
createPullPayment = new CreatePullPayment
{
Name = $"Refund {invoice.Id}",
PaymentMethodIds = new[] { paymentMethodId },
PayoutMethodIds = new[] { pmi },
StoreId = invoice.StoreId,
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration
};

View File

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

View File

@@ -19,6 +19,7 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Plugins;
using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.PointOfSale;
@@ -48,12 +49,12 @@ namespace BTCPayServer
{
private readonly InvoiceRepository _invoiceRepository;
private readonly EventAggregator _eventAggregator;
private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController;
private readonly LinkGenerator _linkGenerator;
private readonly LightningAddressService _lightningAddressService;
private readonly LightningLikePayoutHandler _lightningLikePayoutHandler;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly IPluginHookService _pluginHookService;
@@ -62,13 +63,13 @@ namespace BTCPayServer
public UILNURLController(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
PayoutMethodHandlerDictionary payoutHandlers,
PaymentMethodHandlerDictionary handlers,
StoreRepository storeRepository,
AppService appService,
UIInvoiceController invoiceController,
LinkGenerator linkGenerator,
LightningAddressService lightningAddressService,
LightningLikePayoutHandler lightningLikePayoutHandler,
PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
IPluginHookService pluginHookService,
@@ -76,13 +77,13 @@ namespace BTCPayServer
{
_invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
_payoutHandlers = payoutHandlers;
_handlers = handlers;
_storeRepository = storeRepository;
_appService = appService;
_invoiceController = invoiceController;
_linkGenerator = linkGenerator;
_lightningAddressService = lightningAddressService;
_lightningLikePayoutHandler = lightningLikePayoutHandler;
_pullPaymentHostedService = pullPaymentHostedService;
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
_pluginHookService = pluginHookService;
@@ -102,9 +103,10 @@ namespace BTCPayServer
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);
if (!pp.IsRunning() || !pp.IsSupported(pmi))
if (!pp.IsRunning() || !pp.IsSupported(pmi) || !_payoutHandlers.TryGetValue(pmi, out var payoutHandler))
{
return NotFound();
}
@@ -126,7 +128,7 @@ namespace BTCPayServer
CurrentBalance = LightMoney.FromUnit(remaining, unit),
MinWithdrawable =
LightMoney.FromUnit(
Math.Min(await _lightningLikePayoutHandler.GetMinimumPayoutAmount(pmi, null), remaining),
Math.Min(await payoutHandler.GetMinimumPayoutAmount(null), remaining),
unit),
Tag = "withdrawRequest",
Callback = new Uri(Request.GetCurrentUrl()),
@@ -147,7 +149,7 @@ namespace BTCPayServer
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)" });
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)
{
return NotFound();
@@ -156,7 +158,7 @@ namespace BTCPayServer
var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest
{
Destination = new BoltInvoiceClaimDestination(pr, result),
PaymentMethodId = pmi,
PayoutMethodId = pmi,
PullPaymentId = pullPaymentId,
StoreId = pp.StoreId,
Value = result.MinimumAmount.ToDecimal(unit)
@@ -174,7 +176,7 @@ namespace BTCPayServer
lightningHandler.CreateLightningClient(pm);
var payResult = await UILightningLikePayoutController.TrypayBolt(client,
claimResponse.PayoutData.GetBlob(_btcPayNetworkJsonSerializerSettings),
claimResponse.PayoutData, result, pmi, cancellationToken);
claimResponse.PayoutData, result, payoutHandler.Currency, cancellationToken);
switch (payResult.Result)
{

View File

@@ -20,6 +20,7 @@ using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.NTag424;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
@@ -42,7 +43,7 @@ namespace BTCPayServer.Controllers
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly StoreRepository _storeRepository;
private readonly BTCPayServerEnvironment _env;
private readonly SettingsRepository _settingsRepository;
@@ -53,7 +54,7 @@ namespace BTCPayServer.Controllers
PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkProvider networkProvider,
BTCPayNetworkJsonSerializerSettings serializerSettings,
IEnumerable<IPayoutHandler> payoutHandlers,
PayoutMethodHandlerDictionary payoutHandlers,
StoreRepository storeRepository,
BTCPayServerEnvironment env,
SettingsRepository settingsRepository)
@@ -92,7 +93,7 @@ namespace BTCPayServer.Controllers
{
Entity = o,
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 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 supported = ppBlob.SupportedPaymentMethods;
PaymentMethodId paymentMethodId = null;
PayoutMethodId payoutMethodId = null;
IClaimDestination destination = null;
if (string.IsNullOrEmpty(vm.SelectedPaymentMethod))
IPayoutHandler payoutHandler = null;
if (string.IsNullOrEmpty(vm.SelectedPayoutMethod))
{
foreach (var pmId in supported)
{
var handler = _payoutHandlers.FindPayoutHandler(pmId);
var handler = _payoutHandlers.TryGet(pmId);
(IClaimDestination dst, string err) = handler == null
? (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)
{
paymentMethodId = pmId;
payoutMethodId = pmId;
destination = dst;
payoutHandler = handler;
break;
}
}
}
else
{
paymentMethodId = supported.FirstOrDefault(id => vm.SelectedPaymentMethod == id.ToString());
var payoutHandler = paymentMethodId is null ? null : _payoutHandlers.FindPayoutHandler(paymentMethodId);
destination = payoutHandler is null ? null : (await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken)).destination;
payoutMethodId = supported.FirstOrDefault(id => vm.SelectedPayoutMethod == id.ToString());
payoutHandler = payoutMethodId is null ? null : _payoutHandlers.TryGet(payoutMethodId);
destination = payoutHandler is null ? null : (await payoutHandler.ParseAndValidateClaimDestination(vm.Destination, ppBlob, cancellationToken)).destination;
}
if (destination is null)
@@ -255,8 +258,7 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.Destination), "Invalid destination or payment method");
return await ViewPullPayment(pullPaymentId);
}
var amtError = ClaimRequest.IsPayoutAmountOk(destination, vm.ClaimedAmount == 0 ? null : vm.ClaimedAmount, paymentMethodId.CryptoCode, ppBlob.Currency);
var amtError = ClaimRequest.IsPayoutAmountOk(destination, vm.ClaimedAmount == 0 ? null : vm.ClaimedAmount, payoutHandler.Currency, ppBlob.Currency);
if (amtError.error is not null)
{
ModelState.AddModelError(nameof(vm.ClaimedAmount), amtError.error);
@@ -276,7 +278,7 @@ namespace BTCPayServer.Controllers
Destination = destination,
PullPaymentId = pullPaymentId,
Value = vm.ClaimedAmount,
PaymentMethodId = paymentMethodId,
PayoutMethodId = payoutMethodId,
StoreId = pp.StoreId
});

View File

@@ -13,6 +13,7 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.Payouts;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
@@ -31,7 +32,7 @@ namespace BTCPayServer.Controllers
public class UIStorePullPaymentsController : Controller
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter;
private readonly PullPaymentHostedService _pullPaymentService;
@@ -50,7 +51,7 @@ namespace BTCPayServer.Controllers
}
public UIStorePullPaymentsController(BTCPayNetworkProvider btcPayNetworkProvider,
IEnumerable<IPayoutHandler> payoutHandlers,
PayoutMethodHandlerDictionary payoutHandlers,
CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter,
PullPaymentHostedService pullPaymentHostedService,
@@ -74,12 +75,12 @@ namespace BTCPayServer.Controllers
[HttpGet("stores/{storeId}/pull-payments/new")]
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> NewPullPayment(string storeId)
public IActionResult NewPullPayment(string storeId)
{
if (CurrentStore is null)
return NotFound();
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore);
var paymentMethods = _payoutHandlers.GetSupportedPayoutMethods(CurrentStore);
if (!paymentMethods.Any())
{
TempData.SetStatusMessageModel(new StatusMessageModel
@@ -96,7 +97,7 @@ namespace BTCPayServer.Controllers
Currency = CurrentStore.GetStoreBlob().DefaultCurrency,
CustomCSSLink = "",
EmbeddedCSS = "",
PaymentMethodItems =
PayoutMethodsItem =
paymentMethods.Select(id => new SelectListItem(id.ToString(), id.ToString(), true))
});
}
@@ -108,19 +109,19 @@ namespace BTCPayServer.Controllers
if (CurrentStore is null)
return NotFound();
var paymentMethodOptions = await _payoutHandlers.GetSupportedPaymentMethods(CurrentStore);
model.PaymentMethodItems =
var paymentMethodOptions = _payoutHandlers.GetSupportedPayoutMethods(CurrentStore);
model.PayoutMethodsItem =
paymentMethodOptions.Select(id => new SelectListItem(id.ToString(), id.ToString(), true));
model.Name ??= string.Empty;
model.Currency = model.Currency?.ToUpperInvariant()?.Trim() ?? String.Empty;
model.PaymentMethods ??= new List<string>();
if (!model.PaymentMethods.Any())
model.PayoutMethods ??= new List<string>();
if (!model.PayoutMethods.Any())
{
// 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
model.PaymentMethodItems =
model.PayoutMethodsItem =
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)
{
@@ -135,10 +136,10 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(model.Name), "The name should be maximum 50 characters.");
}
var selectedPaymentMethodIds = model.PaymentMethods.Select(PaymentMethodId.Parse).ToArray();
if (!selectedPaymentMethodIds.All(id => selectedPaymentMethodIds.Contains(id)))
var selectedPaymentMethodIds = model.PayoutMethods.Select(PayoutMethodId.Parse).ToArray();
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)
return View(model);
@@ -151,7 +152,7 @@ namespace BTCPayServer.Controllers
Amount = model.Amount,
Currency = model.Currency,
StoreId = storeId,
PaymentMethodIds = selectedPaymentMethodIds,
PayoutMethodIds = selectedPaymentMethodIds,
EmbeddedCSS = model.EmbeddedCSS,
CustomCSSLink = model.CustomCSSLink,
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())
{
TempData.SetStatusMessageModel(new StatusMessageModel
@@ -290,11 +291,11 @@ namespace BTCPayServer.Controllers
if (vm is null)
return NotFound();
vm.PaymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData());
vm.HasPayoutProcessor = await HasPayoutProcessor(storeId, vm.PaymentMethodId);
var paymentMethodId = PaymentMethodId.Parse(vm.PaymentMethodId);
vm.PayoutMethods = _payoutHandlers.GetSupportedPayoutMethods(HttpContext.GetStoreData());
vm.HasPayoutProcessor = await HasPayoutProcessor(storeId, vm.PayoutMethodId);
var payoutMethodId = PayoutMethodId.Parse(vm.PayoutMethodId);
var handler = _payoutHandlers
.FindPayoutHandler(paymentMethodId);
.TryGet(payoutMethodId);
var commandState = Enum.Parse<PayoutState>(vm.Command.Split("-").First());
var payoutIds = vm.GetSelectedPayouts(commandState);
if (payoutIds.Length == 0)
@@ -309,7 +310,7 @@ namespace BTCPayServer.Controllers
{
storeId = storeId,
pullPaymentId = vm.PullPaymentId,
paymentMethodId = paymentMethodId.ToString()
payoutMethodId = payoutMethodId.ToString()
});
}
@@ -331,7 +332,7 @@ namespace BTCPayServer.Controllers
await using var ctx = this._dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var payouts =
await GetPayoutsForPaymentMethod(paymentMethodId, ctx, payoutIds, storeId, cancellationToken);
await GetPayoutsForPaymentMethod(payoutMethodId, ctx, payoutIds, storeId, cancellationToken);
var failed = false;
for (int i = 0; i < payouts.Count; i++)
@@ -391,7 +392,7 @@ namespace BTCPayServer.Controllers
case "pay":
{
if (handler is { })
return await handler.InitiatePayment(paymentMethodId, payoutIds);
return await handler.InitiatePayment(payoutIds);
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Paying via this payment method is not supported",
@@ -405,7 +406,7 @@ namespace BTCPayServer.Controllers
await using var ctx = this._dbContextFactory.CreateContext();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var payouts =
await GetPayoutsForPaymentMethod(paymentMethodId, ctx, payoutIds, storeId, cancellationToken);
await GetPayoutsForPaymentMethod(payoutMethodId, ctx, payoutIds, storeId, cancellationToken);
for (int i = 0; i < payouts.Count; i++)
{
var payout = payouts[i];
@@ -426,7 +427,7 @@ namespace BTCPayServer.Controllers
{
storeId = storeId,
pullPaymentId = vm.PullPaymentId,
paymentMethodId = paymentMethodId.ToString()
payoutMethodId = payoutMethodId.ToString()
});
}
}
@@ -455,11 +456,11 @@ namespace BTCPayServer.Controllers
{
storeId = storeId,
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,
string storeId, CancellationToken cancellationToken)
{
@@ -469,7 +470,7 @@ namespace BTCPayServer.Controllers
IncludeStoreData = true,
Stores = new[] { storeId },
PayoutIds = payoutIds,
PaymentMethods = new[] { paymentMethodId.ToString() }
PayoutMethods = new[] { payoutMethodId.ToString() }
}, ctx, cancellationToken);
}
@@ -477,10 +478,10 @@ namespace BTCPayServer.Controllers
[HttpGet("stores/{storeId}/payouts")]
[Authorize(Policy = Policies.CanViewPayouts, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
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)
{
var paymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData());
var paymentMethods = _payoutHandlers.GetSupportedPayoutMethods(HttpContext.GetStoreData());
if (!paymentMethods.Any())
{
TempData.SetStatusMessageModel(new StatusMessageModel
@@ -491,17 +492,17 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
}
paymentMethodId ??= paymentMethods.First().ToString();
payoutMethodId ??= paymentMethods.First().ToString();
var vm = this.ParseListQuery(new PayoutsModel
{
PaymentMethods = paymentMethods,
PaymentMethodId = paymentMethodId,
PayoutMethods = paymentMethods,
PayoutMethodId = payoutMethodId,
PullPaymentId = pullPaymentId,
PayoutState = payoutState,
Skip = skip,
Count = count,
Payouts = new List<PayoutsModel.PayoutModel>(),
HasPayoutProcessor = await HasPayoutProcessor(storeId, paymentMethodId)
HasPayoutProcessor = await HasPayoutProcessor(storeId, payoutMethodId)
});
await using var ctx = _dbContextFactory.CreateContext();
var payoutRequest =
@@ -512,15 +513,15 @@ namespace BTCPayServer.Controllers
vm.PullPaymentName = (await ctx.PullPayments.FindAsync(pullPaymentId)).GetBlob().Name;
}
if (vm.PaymentMethodId != null)
{
var pmiStr = vm.PaymentMethodId;
payoutRequest = payoutRequest.Where(p => p.PaymentMethodId == pmiStr);
}
vm.PaymentMethodCount = (await payoutRequest.GroupBy(data => data.PaymentMethodId)
vm.PayoutMethodCount = (await payoutRequest.GroupBy(data => data.PaymentMethodId)
.Select(datas => new { datas.Key, Count = datas.Count() }).ToListAsync())
.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)
.Select(e => new { e.Key, Count = e.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 });
}
var pCurrency = _payoutHandlers.TryGet(PayoutMethodId.Parse(item.Payout.PaymentMethodId))?.Currency;
var m = new PayoutsModel.PayoutModel
{
PullPaymentId = item.PullPayment?.Id,
@@ -573,11 +576,11 @@ namespace BTCPayServer.Controllers
SourceLink = payoutSourceLink,
Date = item.Payout.Date,
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
};
var handler = _payoutHandlers
.FindPayoutHandler(item.Payout.GetPaymentMethodId());
.TryGet(item.Payout.GetPayoutMethodId());
var proofBlob = handler?.ParseProof(item.Payout);
m.ProofLink = proofBlob?.Link;
vm.Payouts.Add(m);
@@ -585,12 +588,15 @@ namespace BTCPayServer.Controllers
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(
new PayoutProcessorService.PayoutProcessorQuery { Stores = [storeId], PaymentMethods = [pmId] });
return _payoutProcessorFactories.Any(factory => factory.GetSupportedPaymentMethods().Contains(pmId)) && processors.Any();
new PayoutProcessorService.PayoutProcessorQuery { Stores = [storeId], PayoutMethodIds = [payoutMethodId] });
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.Bitcoin;
using BTCPayServer.Payments.PayJoin;
using BTCPayServer.Payouts;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
@@ -761,14 +762,14 @@ namespace BTCPayServer.Controllers
CreatePSBTResponse psbtResponse;
if (command == "schedule")
{
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(walletId.CryptoCode);
var pmi = PayoutTypes.CHAIN.GetPayoutMethodId(walletId.CryptoCode);
var claims =
vm.Outputs.Where(output => string.IsNullOrEmpty(output.PayoutId)).Select(output => new ClaimRequest()
{
Destination = new AddressClaimDestination(
BitcoinAddress.Create(output.DestinationAddress, network.NBitcoinNetwork)),
Value = output.Amount,
PaymentMethodId = pmi,
PayoutMethodId = pmi,
StoreId = walletId.StoreId,
PreApprove = true,
}).ToArray();

View File

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

View File

@@ -14,6 +14,7 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payouts;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Notifications;
@@ -31,10 +32,11 @@ using NewBlockEvent = BTCPayServer.Events.NewBlockEvent;
using PayoutData = BTCPayServer.Data.PayoutData;
using StoreData = BTCPayServer.Data.StoreData;
public class BitcoinLikePayoutHandler : IPayoutHandler
public class BitcoinLikePayoutHandler : IPayoutHandler, IHasNetwork
{
public string Currency { get; }
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly PaymentMethodHandlerDictionary _paymentHandlers;
private readonly ExplorerClientProvider _explorerClientProvider;
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
private readonly ApplicationDbContextFactory _dbContextFactory;
@@ -43,9 +45,15 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
private readonly EventAggregator _eventAggregator;
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 BitcoinLikePayoutHandler(BTCPayNetworkProvider btcPayNetworkProvider,
PayoutMethodId payoutMethodId,
BTCPayNetwork network,
PaymentMethodHandlerDictionary handlers,
WalletRepository walletRepository,
ExplorerClientProvider explorerClientProvider,
@@ -57,51 +65,46 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
TransactionLinkProviders transactionLinkProviders)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_handlers = handlers;
PayoutMethodId = payoutMethodId;
PaymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
Network = network;
_paymentHandlers = handlers;
WalletRepository = walletRepository;
_explorerClientProvider = explorerClientProvider;
_jsonSerializerSettings = jsonSerializerSettings;
_dbContextFactory = dbContextFactory;
_notificationSender = notificationSender;
Currency = network.CryptoCode;
this.Logs = logs;
_eventAggregator = eventAggregator;
_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)
{
var network = _handlers.GetNetwork(claimRequest.PaymentMethodId);
var explorerClient = _explorerClientProvider.GetExplorerClient(network);
var explorerClient = _explorerClientProvider.GetExplorerClient(Network);
if (claimRequest.Destination is IBitcoinLikeClaimDestination bitcoinLikeClaimDestination)
{
await explorerClient.TrackAsync(TrackedSource.Create(bitcoinLikeClaimDestination.Address));
await WalletRepository.AddWalletTransactionAttachment(
new WalletId(claimRequest.StoreId, network.CryptoCode),
new WalletId(claimRequest.StoreId, Network.CryptoCode),
bitcoinLikeClaimDestination.Address.ToString(),
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();
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
{
@@ -119,18 +122,16 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
{
if (payout?.Proof is null)
return null;
var paymentMethodId = payout.GetPaymentMethodId();
if (paymentMethodId is null)
return null;
var cryptoCode = _handlers.TryGetNetwork(paymentMethodId)?.CryptoCode;
if (cryptoCode is null)
var payoutMethodId = payout.GetPayoutMethodId();
if (payoutMethodId is null)
return null;
var cryptoCode = Network.CryptoCode;
ParseProofType(payout.Proof, out var raw, out var proofType);
if (proofType == PayoutTransactionOnChainBlob.Type)
{
var res = raw.ToObject<PayoutTransactionOnChainBlob>(
JsonSerializer.Create(_jsonSerializerSettings.GetSerializer(cryptoCode)));
JsonSerializer.Create(_jsonSerializerSettings.GetSerializer(payoutMethodId)));
if (res == null)
return null;
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?
.Consensus?
.ConsensusFactory?
@@ -226,8 +226,8 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
Stores = new[] { storeId },
PayoutIds = payoutIds
}, context)).Where(data =>
PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) &&
CanHandle(paymentMethodId))
PayoutMethodId.TryParse(data.PaymentMethodId, out var payoutMethodId) &&
payoutMethodId == PayoutMethodId)
.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)
{
@@ -251,8 +251,8 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
Stores = new[] { storeId },
PayoutIds = payoutIds
}, context)).Where(data =>
PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) &&
CanHandle(paymentMethodId))
PayoutMethodId.TryParse(data.PaymentMethodId, out var payoutMethodId) &&
payoutMethodId == PayoutMethodId)
.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)
{
@@ -273,25 +273,23 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
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();
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var pmi = paymentMethodId;
var payouts = await ctx.Payouts.Include(data => data.PullPaymentData)
.Where(data => payoutIds.Contains(data.Id)
&& pmi.ToString() == data.PaymentMethodId
&& PayoutMethodId.ToString() == data.PaymentMethodId
&& data.State == PayoutState.AwaitingPayment)
.ToListAsync();
var pullPaymentIds = payouts.Select(data => data.PullPaymentDataId).Distinct().Where(s => s != null).ToArray();
var storeId = payouts.First().StoreDataId;
var network = _handlers.GetNetwork(paymentMethodId);
List<string> bip21 = new List<string>();
foreach (var payout in payouts)
{
@@ -300,9 +298,9 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
continue;
}
var blob = payout.GetBlob(_jsonSerializerSettings);
if (payout.GetPaymentMethodId() != paymentMethodId)
if (payout.GetPayoutMethodId() != PayoutMethodId)
continue;
var claim = await ParseClaimDestination(paymentMethodId, blob.Destination, default);
var claim = await ParseClaimDestination(blob.Destination, default);
switch (claim.destination)
{
case UriClaimDestination uriClaimDestination:
@@ -312,17 +310,17 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
bip21.Add(newUri.Uri.ToString());
break;
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);
bip21.Add(bip21New.ToString());
break;
}
}
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
{
walletId = new WalletId(storeId, network.CryptoCode).ToString(),
walletId = new WalletId(storeId, Network.CryptoCode).ToString(),
pullPaymentId = pullPaymentIds.Length == 1 ? pullPaymentIds.First() : null
});
}
@@ -347,7 +345,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
}
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);
if (tx is null)
{
@@ -447,7 +445,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
return;
var derivationSchemeSettings = payout.StoreData
.GetDerivationSchemeSettings(_handlers, newTransaction.CryptoCode)?.AccountDerivation;
.GetDerivationSchemeSettings(_paymentHandlers, newTransaction.CryptoCode)?.AccountDerivation;
if (derivationSchemeSettings is null)
return;
@@ -494,7 +492,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
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.Threading;
using System.Threading.Tasks;
using BTCPayServer;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using Microsoft.AspNetCore.Mvc;
using PayoutData = BTCPayServer.Data.PayoutData;
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);
//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 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)
return res;
var res2 = ValidateClaimDestination(res.destination, pullPaymentBlob);
@@ -34,9 +39,8 @@ public interface IPayoutHandler
void StartBackgroundCheck(Action<Type[]> subscribe);
//allows you to process events that the main pull payment hosted service is subscribed to
Task BackgroundCheck(object o);
Task<decimal> GetMinimumPayoutAmount(PaymentMethodId paymentMethod, IClaimDestination claimDestination);
Task<decimal> GetMinimumPayoutAmount(IClaimDestination claimDestination);
Dictionary<PayoutState, List<(string Action, string Text)>> GetPayoutSpecificActions();
Task<StatusMessageModel> DoSpecificAction(string action, string[] payoutIds, string storeId);
Task<IEnumerable<PaymentMethodId>> GetSupportedPaymentMethods(StoreData storeData);
Task<IActionResult> InitiatePayment(PaymentMethodId paymentMethodId, string[] payoutIds);
Task<IActionResult> InitiatePayment(string[] payoutIds);
}

View File

@@ -6,45 +6,60 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using LNURL;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MimeKit;
using NBitcoin;
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 =
nameof(LightningLikePayoutHandlerOnionNamedClient);
public const string LightningLikePayoutHandlerClearnetNamedClient =
nameof(LightningLikePayoutHandlerClearnetNamedClient);
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly IHttpClientFactory _httpClientFactory;
private readonly UserService _userService;
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)
{
_handlers = handlers;
_paymentHandlers = paymentHandlers;
Network = network;
PayoutMethodId = payoutMethodId;
_options = options;
PaymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
_httpClientFactory = httpClientFactory;
_userService = userService;
_authorizationService = authorizationService;
}
public bool CanHandle(PaymentMethodId paymentMethod)
{
return _handlers.TryGetValue(paymentMethod, out var h) && h is ILightningPaymentHandler;
Currency = network.CryptoCode;
}
public Task TrackClaim(ClaimRequest claimRequest, PayoutData payoutData)
@@ -59,10 +74,9 @@ namespace BTCPayServer.Data.Payouts.LightningLike
: 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();
var network = ((IHasNetwork)_handlers[paymentMethodId]).Network;
try
{
string lnurlTag = null;
@@ -92,7 +106,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
}
var result =
BOLT11PaymentRequest.TryParse(destination, out var invoice, network.NBitcoinNetwork)
BOLT11PaymentRequest.TryParse(destination, out var invoice, Network.NBitcoinNetwork)
? new BoltInvoiceClaimDestination(destination, invoice)
: null;
@@ -144,7 +158,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
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));
}
@@ -159,34 +173,14 @@ namespace BTCPayServer.Data.Payouts.LightningLike
return Task.FromResult<StatusMessageModel>(null);
}
public async Task<IEnumerable<PaymentMethodId>> GetSupportedPaymentMethods(StoreData storeData)
public bool IsSupported(StoreData storeData)
{
var result = new List<PaymentMethodId>();
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;
return storeData.GetPaymentMethodConfig<LightningPaymentMethodConfig>(PaymentMethodId, _paymentHandlers, true)?.IsConfigured(Network, _options.Value) is true;
}
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",
"UILightningLikePayout", new { cryptoCode, payoutIds }));
}

View File

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

View File

@@ -5,6 +5,7 @@ using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Services;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
@@ -28,9 +29,9 @@ namespace BTCPayServer.Data
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)
@@ -44,13 +45,13 @@ namespace BTCPayServer.Data
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();
return result;
}
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)
@@ -83,10 +84,9 @@ namespace BTCPayServer.Data
}
}
public static async Task<List<PaymentMethodId>> GetSupportedPaymentMethods(
this IEnumerable<IPayoutHandler> payoutHandlers, StoreData storeData)
public static HashSet<PayoutMethodId> GetSupportedPayoutMethods(this PayoutMethodHandlerDictionary 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.JsonConverters;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using Newtonsoft.Json;
namespace BTCPayServer.Data
@@ -27,8 +28,8 @@ namespace BTCPayServer.Data
public TimeSpan BOLT11Expiration { get; set; }
[JsonProperty(ItemConverterType = typeof(PaymentMethodIdJsonConverter))]
public PaymentMethodId[] SupportedPaymentMethods { get; set; }
[JsonProperty(ItemConverterType = typeof(PayoutMethodIdJsonConverter))]
public PayoutMethodId[] SupportedPaymentMethods { get; set; }
public bool AutoApproveClaims { get; set; }

View File

@@ -1,5 +1,6 @@
using System.Linq;
using System.Text;
using BTCPayServer.Payouts;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
@@ -19,7 +20,7 @@ namespace BTCPayServer.Data
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);
}

View File

@@ -25,6 +25,7 @@ using BTCPayServer.NTag424;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
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());
}
#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)
{
var result = await explorerClientProvider.GetExplorerClient(psbt.Network.NetworkSet.CryptoCode).UpdatePSBTAsync(new UpdatePSBTRequest()
@@ -436,19 +430,23 @@ namespace BTCPayServer
var h = (BitcoinLikePaymentHandler)handlers[pmi];
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 &&
handlers.TryGetValue(paymentMethodId, out var value) &&
if (id is not null &&
handlers.TryGetValue(id, out var value) &&
value is IHasNetwork { Network: var n })
{
return n;
}
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)
{

View File

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

View File

@@ -70,6 +70,8 @@ using BTCPayServer.Services.Reporting;
using BTCPayServer.Services.WalletFileParsing;
using BTCPayServer.Payments.LNURLPay;
using System.Collections.Generic;
using BTCPayServer.Payouts;
@@ -369,10 +371,6 @@ namespace BTCPayServer.Hosting
services.AddReportProvider<PayoutsReportProvider>();
services.AddReportProvider<LegacyInvoiceExportReportProvider>();
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 =>
o.GetRequiredService<IEnumerable<IPaymentMethodBitpayAPIExtension>>().ToDictionary(o => o.PaymentMethodId, o => o));
@@ -398,6 +396,8 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
services.AddSingleton<PaymentMethodHandlerDictionary>();
services.AddSingleton<PaymentMethodViewProvider>();
services.AddSingleton<PayoutMethodHandlerDictionary>();
services.AddSingleton<NotificationManager>();
services.AddScoped<NotificationSender>();
@@ -580,6 +580,13 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
(IPaymentMethodBitpayAPIExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinPaymentMethodBitpayAPIExtension), new object[] { pmi }));
services.AddSingleton<IPaymentMethodViewExtension>(provider =>
(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)
{
@@ -596,6 +603,9 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
(IPaymentMethodViewExtension)ActivatorUtilities.CreateInstance(provider, typeof(LightningPaymentMethodViewExtension), new object[] { pmi }));
services.AddSingleton<IPaymentMethodBitpayAPIExtension>(provider =>
(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
{

View File

@@ -15,6 +15,7 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.Payouts;
using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Models;
@@ -47,7 +48,7 @@ namespace BTCPayServer.Hosting
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly SettingsRepository _Settings;
private readonly AppService _appService;
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
private readonly PayoutMethodHandlerDictionary _payoutHandlers;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly LightningAddressService _lightningAddressService;
private readonly ILogger<MigrationStartupTask> _logger;
@@ -62,7 +63,7 @@ namespace BTCPayServer.Hosting
IOptions<LightningNetworkOptions> lightningOptions,
SettingsRepository settingsRepository,
AppService appService,
IEnumerable<IPayoutHandler> payoutHandlers,
PayoutMethodHandlerDictionary payoutHandlers,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
LightningAddressService lightningAddressService,
ILogger<MigrationStartupTask> logger,
@@ -238,7 +239,7 @@ namespace BTCPayServer.Hosting
var processors = await ctx.PayoutProcessors.ToArrayAsync();
foreach (var processor in processors)
{
processor.PaymentMethod = processor.GetPaymentMethodId().ToString();
processor.PaymentMethod = processor.GetPayoutMethodId().ToString();
}
await ctx.SaveChangesAsync();
}
@@ -641,18 +642,18 @@ WHERE cte.""Id""=p.""Id""
await using var ctx = _DBContextFactory.CreateContext();
foreach (var payoutData in await ctx.Payouts.AsQueryable().ToArrayAsync())
{
var pmi = payoutData.GetPaymentMethodId();
var pmi = payoutData.GetPayoutMethodId();
if (pmi is null)
{
continue;
}
var handler = _payoutHandlers
.FindPayoutHandler(pmi);
.TryGet(pmi);
if (handler is null)
{
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;
}
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 SelectList AvailablePaymentMethods { get; set; }
[Display(Name = "Select the payment method used for refund")]
public string SelectedPaymentMethod { get; set; }
[Display(Name = "Select the payout method used for refund")]
public string SelectedPayoutMethod { get; set; }
public RefundSteps RefundStep { get; set; }
public string SelectedRefundOption { get; set; }
public decimal CryptoAmountNow { get; set; }

View File

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

View File

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

View File

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

View File

@@ -31,8 +31,9 @@ namespace BTCPayServer.Payments
/// <summary>
/// This class customize invoice creation by the creation of payment details for the PaymentMethod during invoice creation
/// </summary>
public interface IPaymentMethodHandler
public interface IPaymentMethodHandler : IHandler<PaymentMethodId>
{
PaymentMethodId IHandler<PaymentMethodId>.Id => PaymentMethodId;
PaymentMethodId PaymentMethodId { get; }
/// <summary>
/// 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 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)
{
var external = supportedPaymentMethod.GetExternalLightningUrl();
@@ -17,7 +20,7 @@ namespace BTCPayServer.Payments.Lightning
}
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");
return connectionString;
}

View File

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

View File

@@ -4,8 +4,6 @@ using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using BTCPayServer.Services.Altcoins.Monero.Payments;
using BTCPayServer.Services.Altcoins.Zcash.Payments;
namespace BTCPayServer.Payments
{
@@ -15,24 +13,53 @@ namespace BTCPayServer.Payments
/// </summary>
public class PaymentMethodId
{
public PaymentMethodId? FindNearest(IEnumerable<PaymentMethodId> others)
public T? FindNearest<T>(IEnumerable<T> others)
{
ArgumentNullException.ThrowIfNull(others);
return others.FirstOrDefault(f => f == this) ??
others.FirstOrDefault(f => f._CryptoCode == _CryptoCode);
return
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);
ArgumentNullException.ThrowIfNull(paymentType);
_CryptoCode = cryptoCode.ToUpperInvariant();
_Id = $"{_CryptoCode}-{paymentType}";
return from a in aItems
from b in bItems
select (a, b, CalculateDistance(a.ToString()!, b.ToString()!));
}
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 _CryptoCode;
public string CryptoCode => _CryptoCode;
public override bool Equals(object? obj)
{
@@ -93,6 +120,11 @@ namespace BTCPayServer.Payments
{
str ??= "";
str = str.Trim();
if (str.Length == 0)
{
paymentMethodId = null;
return false;
}
paymentMethodId = null;
var parts = str.Split(Separators, StringSplitOptions.RemoveEmptyEntries);
@@ -101,7 +133,7 @@ namespace BTCPayServer.Payments
{
if (LegacySupportedCryptos.Contains(cryptoCode))
{
paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode.ToUpperInvariant());
paymentMethodId = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode);
return true;
}
}
@@ -118,13 +150,14 @@ namespace BTCPayServer.Payments
paymentMethodId = type.GetPaymentMethodId(cryptoCode);
return true;
}
paymentMethodId = new PaymentMethodId(cryptoCode, paymentType);
paymentMethodId = new PaymentMethodId($"{cryptoCode.ToUpperInvariant()}-{paymentType}");
return true;
}
}
}
return false;
paymentMethodId = new PaymentMethodId(str);
return true;
}
private static PaymentType? GetPaymentType(string paymentType)

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
@@ -35,12 +36,14 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
private readonly IOptions<LightningNetworkOptions> _options;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly LightningLikePayoutHandler _payoutHandler;
public BTCPayNetwork Network => _payoutHandler.Network;
private readonly PaymentMethodHandlerDictionary _handlers;
public LightningAutomatedPayoutProcessor(
PayoutMethodId payoutMethodId,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
LightningClientFactoryService lightningClientFactoryService,
IEnumerable<IPayoutHandler> payoutHandlers,
PayoutMethodHandlerDictionary payoutHandlers,
UserService userService,
ILoggerFactory logger, IOptions<LightningNetworkOptions> options,
StoreRepository storeRepository, PayoutProcessorData payoutProcessorSettings,
@@ -49,7 +52,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
IPluginHookService pluginHookService,
EventAggregator eventAggregator,
PullPaymentHostedService pullPaymentHostedService) :
base(logger, storeRepository, payoutProcessorSettings, applicationDbContextFactory,
base(PaymentTypes.LN.GetPaymentMethodId(GetPayoutHandler(payoutHandlers, payoutMethodId).Network.CryptoCode), logger, storeRepository, payoutProcessorSettings, applicationDbContextFactory,
handlers, pluginHookService, eventAggregator)
{
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
@@ -57,16 +60,18 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
_userService = userService;
_options = options;
_pullPaymentHostedService = pullPaymentHostedService;
_payoutHandler = (LightningLikePayoutHandler)payoutHandlers.FindPayoutHandler(PaymentMethodId);
_payoutHandler = GetPayoutHandler(payoutHandlers, payoutMethodId);
_handlers = handlers;
}
private static LightningLikePayoutHandler GetPayoutHandler(PayoutMethodHandlerDictionary payoutHandlers, PayoutMethodId payoutMethodId)
{
return (LightningLikePayoutHandler)payoutHandlers[payoutMethodId];
}
private async Task HandlePayout(PayoutData payoutData, ILightningClient lightningClient)
{
if (payoutData.State != PayoutState.AwaitingPayment)
return;
if (!_handlers.TryGetValue(PaymentMethodId, out var handler) || handler is not IHasNetwork { Network: var network })
return;
var res = await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest()
{
State = PayoutState.InProgress, PayoutId = payoutData.Id, Proof = null
@@ -77,7 +82,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
}
var blob = payoutData.GetBlob(_btcPayNetworkJsonSerializerSettings);
var claim = await _payoutHandler.ParseClaimDestination(PaymentMethodId, blob.Destination, CancellationToken);
var claim = await _payoutHandler.ParseClaimDestination(blob.Destination, CancellationToken);
try
{
switch (claim.destination)
@@ -85,7 +90,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
case LNURLPayClaimDestinaton lnurlPayClaimDestinaton:
var lnurlResult = await UILightningLikePayoutController.GetInvoiceFromLNURL(payoutData,
_payoutHandler, blob,
lnurlPayClaimDestinaton, network.NBitcoinNetwork, CancellationToken);
lnurlPayClaimDestinaton, Network.NBitcoinNetwork, CancellationToken);
if (lnurlResult.Item2 is null)
{
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)
{
if (!_handlers.TryGetValue(PaymentMethodId, out var handler) || handler is not IHasNetwork { Network: var network })
return false;
var processorBlob = GetBlob(PayoutProcessorSettings);
var lightningSupportedPaymentMethod = (LightningPaymentMethodConfig)paymentMethodConfig;
if (lightningSupportedPaymentMethod.IsInternalNode &&
@@ -129,7 +132,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
}
var client =
lightningSupportedPaymentMethod.CreateLightningClient(network, _options.Value,
lightningSupportedPaymentMethod.CreateLightningClient(Network, _options.Value,
_lightningClientFactoryService);
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,
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.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Data.Payouts.LightningLike;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
@@ -13,38 +15,37 @@ namespace BTCPayServer.PayoutProcessors.Lightning;
public class LightningAutomatedPayoutSenderFactory : IPayoutProcessorFactory
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly PayoutMethodHandlerDictionary _handlers;
private readonly IServiceProvider _serviceProvider;
private readonly LinkGenerator _linkGenerator;
private readonly PayoutMethodId[] _supportedPayoutMethods;
public LightningAutomatedPayoutSenderFactory(BTCPayNetworkProvider btcPayNetworkProvider, IServiceProvider serviceProvider, LinkGenerator linkGenerator)
public LightningAutomatedPayoutSenderFactory(
PayoutMethodHandlerDictionary handlers,
IServiceProvider serviceProvider,
LinkGenerator linkGenerator)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_handlers = handlers;
_serviceProvider = serviceProvider;
_linkGenerator = linkGenerator;
_supportedPayoutMethods = _handlers.OfType<LightningLikePayoutHandler>().Select(n => n.PayoutMethodId).ToArray();
}
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",
"UILightningAutomatedPayoutProcessors", new
{
storeId,
cryptoCode = paymentMethodId.CryptoCode
cryptoCode = network.CryptoCode
}, request.Scheme, request.Host, request.PathBase);
}
public string Processor => ProcessorName;
public static string ProcessorName => nameof(LightningAutomatedPayoutSenderFactory);
public IEnumerable<PaymentMethodId> GetSupportedPaymentMethods()
{
return _btcPayNetworkProvider.GetAll().OfType<BTCPayNetwork>()
.Where(network => network.SupportLightning)
.Select(network =>
PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode));
}
public IEnumerable<PayoutMethodId> GetSupportedPayoutMethods() => _supportedPayoutMethods;
public Task<IHostedService> ConstructProcessor(PayoutProcessorData settings)
{
@@ -52,8 +53,8 @@ public class LightningAutomatedPayoutSenderFactory : IPayoutProcessorFactory
{
throw new NotSupportedException("This processor cannot handle the provided requirements");
}
return Task.FromResult<IHostedService>(ActivatorUtilities.CreateInstance<LightningAutomatedPayoutProcessor>(_serviceProvider, settings));
var payoutMethodId = settings.GetPayoutMethodId();
return Task.FromResult<IHostedService>(ActivatorUtilities.CreateInstance<LightningAutomatedPayoutProcessor>(_serviceProvider, settings, payoutMethodId));
}
}

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
@@ -15,41 +16,39 @@ namespace BTCPayServer.PayoutProcessors.OnChain;
public class OnChainAutomatedPayoutSenderFactory : EventHostedServiceBase, IPayoutProcessorFactory
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly PayoutMethodHandlerDictionary _handlers;
private readonly IServiceProvider _serviceProvider;
private readonly LinkGenerator _linkGenerator;
private readonly PayoutMethodId[] _supportedPayoutMethods;
public string FriendlyName { get; } = "Automated Bitcoin Sender";
public OnChainAutomatedPayoutSenderFactory(EventAggregator eventAggregator,
public OnChainAutomatedPayoutSenderFactory(
PayoutMethodHandlerDictionary handlers,
EventAggregator eventAggregator,
ILogger<OnChainAutomatedPayoutSenderFactory> logger,
BTCPayNetworkProvider btcPayNetworkProvider,
IServiceProvider serviceProvider, LinkGenerator linkGenerator) : base(eventAggregator, logger)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
_handlers = handlers;
_serviceProvider = serviceProvider;
_linkGenerator = linkGenerator;
_supportedPayoutMethods = _handlers.OfType<BitcoinLikePayoutHandler>().Select(c => c.PayoutMethodId).ToArray();
}
public string Processor => ProcessorName;
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",
"UIOnChainAutomatedPayoutProcessors", new
{
storeId,
cryptoCode = paymentMethodId.CryptoCode
cryptoCode = network.CryptoCode
}, request.Scheme, request.Host, request.PathBase);
}
public IEnumerable<PaymentMethodId> GetSupportedPaymentMethods()
{
return _btcPayNetworkProvider.GetAll().OfType<BTCPayNetwork>()
.Where(network => !network.ReadonlyWallet && network.WalletSupported)
.Select(network =>
PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode));
}
public IEnumerable<PayoutMethodId> GetSupportedPayoutMethods() => _supportedPayoutMethods;
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");
}
return Task.FromResult<IHostedService>(ActivatorUtilities.CreateInstance<OnChainAutomatedPayoutProcessor>(_serviceProvider, settings));
var payoutMethodId = settings.GetPayoutMethodId();
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.Data;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -33,14 +34,14 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller
_onChainAutomatedPayoutSenderFactory = onChainAutomatedPayoutSenderFactory;
_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}")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Configure(string storeId, string cryptoCode)
{
var id = GetPaymentMethodId(cryptoCode);
if (!_onChainAutomatedPayoutSenderFactory.GetSupportedPaymentMethods().Any(i => id == i))
var id = GetPayoutMethod(cryptoCode);
if (!_onChainAutomatedPayoutSenderFactory.GetSupportedPayoutMethods().Any(i => id == i))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
@@ -64,9 +65,9 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller
{
Stores = new[] { storeId },
Processors = new[] { _onChainAutomatedPayoutSenderFactory.Processor },
PaymentMethods = new[]
PayoutMethodIds = new[]
{
PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)
PayoutTypes.CHAIN.GetPayoutMethodId(cryptoCode)
}
}))
.FirstOrDefault();
@@ -81,8 +82,8 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller
{
if (!ModelState.IsValid)
return View(automatedTransferBlob);
var id = GetPaymentMethodId(cryptoCode);
if (!_onChainAutomatedPayoutSenderFactory.GetSupportedPaymentMethods().Any(i => id == i))
var id = GetPayoutMethod(cryptoCode);
if (!_onChainAutomatedPayoutSenderFactory.GetSupportedPayoutMethods().Any(i => id == i))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
@@ -97,16 +98,16 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller
{
Stores = new[] { storeId },
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
PaymentMethods = new[]
PayoutMethodIds = new[]
{
PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode)
PayoutTypes.CHAIN.GetPayoutMethodId(cryptoCode)
}
}))
.FirstOrDefault();
activeProcessor ??= new PayoutProcessorData();
activeProcessor.HasTypedBlob<OnChainAutomatedPayoutBlob>().SetBlob(automatedTransferBlob.ToBlob());
activeProcessor.StoreId = storeId;
activeProcessor.PaymentMethod = PaymentTypes.CHAIN.GetPaymentMethodId(cryptoCode).ToString();
activeProcessor.PaymentMethod = PayoutTypes.CHAIN.GetPayoutMethodId(cryptoCode).ToString();
activeProcessor.Processor = _onChainAutomatedPayoutSenderFactory.Processor;
var tcs = new TaskCompletionSource();
_eventAggregator.Publish(new PayoutProcessorUpdated()

View File

@@ -8,6 +8,7 @@ using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
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 };
PaymentMethods = new[] { paymentMethodId };
PayoutMethodIds = new[] { payoutMethodId };
}
public string[] Stores { 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)
@@ -72,9 +73,9 @@ public class PayoutProcessorService : EventHostedServiceBase
{
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));
}

View File

@@ -2,6 +2,7 @@ using BTCPayServer.Data;
using BTCPayServer.Payments;
using BTCPayServer.PayoutProcessors.Lightning;
using BTCPayServer.PayoutProcessors.OnChain;
using BTCPayServer.Payouts;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.PayoutProcessors;
@@ -18,8 +19,8 @@ public static class PayoutProcessorsExtensions
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.Data;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -45,9 +46,9 @@ public class UIPayoutProcessorsController : Controller
return View(_payoutProcessorFactories.Select(factory =>
{
var conf = activeProcessors.FirstOrDefault(datas => datas.Key == factory.Processor)
?.ToDictionary(data => data.GetPaymentMethodId(), data => data) ??
new Dictionary<PaymentMethodId, PayoutProcessorData>();
foreach (PaymentMethodId supportedPaymentMethod in factory.GetSupportedPaymentMethods())
?.ToDictionary(data => data.GetPayoutMethodId(), data => data) ??
new Dictionary<PayoutMethodId, PayoutProcessorData>();
foreach (var supportedPaymentMethod in factory.GetSupportedPayoutMethods())
{
conf.TryAdd(supportedPaymentMethod, null);
}
@@ -80,7 +81,7 @@ public class UIPayoutProcessorsController : Controller
public class StorePayoutProcessorsView
{
public Dictionary<PaymentMethodId, PayoutProcessorData> Configured { get; set; }
public Dictionary<PayoutMethodId, PayoutProcessorData> Configured { 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.Collections.Generic;
using System.Linq;
using BTCPayServer.Payouts;
using NBitcoin;
using Newtonsoft.Json;
@@ -37,15 +38,17 @@ namespace BTCPayServer.Services
{
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>();
public JsonSerializerSettings GetSerializer(string cryptoCode)
readonly Dictionary<PayoutMethodId, JsonSerializerSettings> _Serializers = new Dictionary<PayoutMethodId, JsonSerializerSettings>();
public JsonSerializerSettings GetSerializer(PayoutMethodId payoutMethodId)
{
ArgumentNullException.ThrowIfNull(cryptoCode);
_Serializers.TryGetValue(cryptoCode, out var serializer);
ArgumentNullException.ThrowIfNull(payoutMethodId);
_Serializers.TryGetValue(payoutMethodId, out var serializer);
return serializer;
}
}

View File

@@ -10,41 +10,10 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Invoices
{
public class PaymentMethodHandlerDictionary : IEnumerable<IPaymentMethodHandler>
public class PaymentMethodHandlerDictionary : HandlersDictionary<PaymentMethodId, IPaymentMethodHandler>
{
private readonly Dictionary<PaymentMethodId, IPaymentMethodHandler> _mappedHandlers =
new Dictionary<PaymentMethodId, IPaymentMethodHandler>();
public PaymentMethodHandlerDictionary(IEnumerable<IPaymentMethodHandler> paymentMethodHandlers)
public PaymentMethodHandlerDictionary(IEnumerable<IPaymentMethodHandler> paymentMethodHandlers) : base(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)

View File

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

View File

@@ -44,7 +44,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
};
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIStorePullPaymentsController.Payouts),
"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.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Data.Payouts.LightningLike;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Services.Reporting;
@@ -17,12 +20,12 @@ public class PayoutsReportProvider : ReportProvider
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly DisplayFormatter _displayFormatter;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly PayoutMethodHandlerDictionary _handlers;
public PayoutsReportProvider(
PullPaymentHostedService pullPaymentHostedService,
DisplayFormatter displayFormatter,
PaymentMethodHandlerDictionary handlers,
PayoutMethodHandlerDictionary handlers,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
{
_displayFormatter = displayFormatter;
@@ -51,23 +54,27 @@ public class PayoutsReportProvider : ReportProvider
data.Add(payout.Date);
data.Add(payout.GetPayoutSource(_btcPayNetworkJsonSerializerSettings));
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);
if (handler is ILightningPaymentHandler)
if (handler is LightningLikePayoutHandler)
data.Add("Lightning");
else if (handler is BitcoinLikePaymentHandler)
else if (handler is BitcoinLikePayoutHandler)
data.Add("On-Chain");
else
data.Add(pmi.ToString());
payoutCurrency = handler?.Currency;
}
else
continue;
var ppBlob = payout.PullPaymentData?.GetBlob();
var currency = ppBlob?.Currency ?? pmi.CryptoCode;
data.Add(pmi.CryptoCode);
data.Add(blob.CryptoAmount.HasValue ? _displayFormatter.ToFormattedAmount(blob.CryptoAmount.Value, pmi.CryptoCode) : null);
var currency = ppBlob?.Currency ?? payoutCurrency;
if (currency is 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(_displayFormatter.ToFormattedAmount(blob.Amount, currency));
data.Add(blob.Destination);

View File

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

View File

@@ -48,7 +48,7 @@
@if (await processorsView.Factory.CanRemove())
{
<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>

View File

@@ -48,14 +48,18 @@
</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;">
@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">
<span class="input-group-text">@Model.PaymentMethods.First().ToString()</span>
<input type="hidden" asp-for="SelectedPayoutMethod">
<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">
<vc:icon symbol="scan-qr"/>

View File

@@ -51,17 +51,17 @@
</div>
</div>
<div class="form-group mb-4">
<label asp-for="PaymentMethods" class="form-label"></label>
@foreach (var item in Model.PaymentMethodItems)
<label asp-for="PayoutMethods" class="form-label"></label>
@foreach (var item in Model.PayoutMethodsItem)
{
<div class="form-check mb-2">
<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
</label>
</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 class="col-lg-9">

View File

@@ -1,23 +1,24 @@
@using BTCPayServer.Client.Models
@using BTCPayServer.Payments
@using BTCPayServer.Payouts
@using BTCPayServer.Views.Stores
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Client
@using BTCPayServer.PayoutProcessors
@model BTCPayServer.Models.WalletViewModels.PayoutsModel
@inject IEnumerable<IPayoutHandler> PayoutHandlers;
@inject PayoutMethodHandlerDictionary PayoutHandlers;
@{
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);
Model.PaginationQuery ??= new Dictionary<string, object>();
Model.PaginationQuery.Add("pullPaymentId", Model.PullPaymentId);
Model.PaginationQuery.Add("paymentMethodId", Model.PaymentMethodId);
Model.PaginationQuery.Add("payoutMethodId", Model.PayoutMethodId);
Model.PaginationQuery.Add("payoutState", Model.PayoutState);
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)
return;
stateActions.AddRange(payoutHandler.GetPayoutSpecificActions().Where(pair => pair.Key == Model.PayoutState).SelectMany(pair => pair.Value));
@@ -96,22 +97,22 @@
}
<form method="post" id="Payouts">
<input type="hidden" asp-for="PaymentMethodId" />
<input type="hidden" asp-for="PayoutMethodId" />
<input type="hidden" asp-for="PayoutState" />
<div class="d-flex justify-content-between mb-4">
<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">
<a asp-action="Payouts" asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-payoutState="@Model.PayoutState"
asp-route-paymentMethodId="@state.ToString()"
asp-route-payoutMethodId="@state.ToString()"
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"
role="tab">
@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>
}
@@ -129,7 +130,7 @@
asp-route-storeId="@Context.GetRouteValue("storeId")"
asp-route-payoutState="@state.Key"
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">
@state.Key.GetStateString()
@if (state.Value > 0)