mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 01:43:50 +01:00
Support accepting 0 amount bolt 11 invoices for payouts (#4014)
* Support accepting 0 amount bolt 11 invoices for payouts * add test * handle validation better * fix case when we just want pp to provide amt * Update BTCPayServer/HostedServices/PullPaymentHostedService.cs * Update BTCPayServer/HostedServices/PullPaymentHostedService.cs * Update BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs * Update UILightningLikePayoutController.cs * fix null * fix payments of payouts on cln * add comment * bump lightning lib --------- Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
This commit is contained in:
parent
36ea17a6b7
commit
95a0614ae1
@ -3586,7 +3586,17 @@ namespace BTCPayServer.Tests
|
|||||||
sourceLink = "https://chocolate.com"
|
sourceLink = "https://chocolate.com"
|
||||||
}).ToString());
|
}).ToString());
|
||||||
|
|
||||||
|
customerInvoice = await tester.CustomerLightningD.CreateInvoice(LightMoney.FromUnit(10, LightMoneyUnit.Satoshi),
|
||||||
|
Guid.NewGuid().ToString(), TimeSpan.FromDays(40));
|
||||||
|
var payout2 = await adminClient.CreatePayout(admin.StoreId,
|
||||||
|
new CreatePayoutThroughStoreRequest()
|
||||||
|
{
|
||||||
|
Approved = true,
|
||||||
|
Amount = new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC),
|
||||||
|
PaymentMethod = "BTC_LightningNetwork",
|
||||||
|
Destination = customerInvoice.BOLT11
|
||||||
|
});
|
||||||
|
Assert.Equal(payout2.Amount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Timeout = 60 * 2 * 1000)]
|
[Fact(Timeout = 60 * 2 * 1000)]
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.28" />
|
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.29" />
|
||||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||||
|
@ -324,20 +324,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
return this.CreateValidationError(ModelState);
|
return this.CreateValidationError(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Amount is null && destination.destination.Amount != null)
|
var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, request.Amount, paymentMethodId.CryptoCode, ppBlob.Currency);
|
||||||
|
if (amtError.error is not null)
|
||||||
{
|
{
|
||||||
request.Amount = destination.destination.Amount;
|
ModelState.AddModelError(nameof(request.Amount), amtError.error );
|
||||||
}
|
|
||||||
else if (request.Amount != null && destination.destination.Amount != null && request.Amount != destination.destination.Amount)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(nameof(request.Amount), $"Amount is implied in destination ({destination.destination.Amount}) that does not match the payout amount provided {request.Amount})");
|
|
||||||
return this.CreateValidationError(ModelState);
|
|
||||||
}
|
|
||||||
if (request.Amount is { } v && (v < ppBlob.MinimumClaim || v == 0.0m))
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(nameof(request.Amount), $"Amount too small (should be at least {ppBlob.MinimumClaim})");
|
|
||||||
return this.CreateValidationError(ModelState);
|
return this.CreateValidationError(ModelState);
|
||||||
}
|
}
|
||||||
|
request.Amount = amtError.amount;
|
||||||
var result = await _pullPaymentService.Claim(new ClaimRequest()
|
var result = await _pullPaymentService.Claim(new ClaimRequest()
|
||||||
{
|
{
|
||||||
Destination = destination.destination,
|
Destination = destination.destination,
|
||||||
@ -395,15 +388,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
return this.CreateValidationError(ModelState);
|
return this.CreateValidationError(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Amount is null && destination.destination.Amount != null)
|
var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, request.Amount);
|
||||||
|
if (amtError.error is not null)
|
||||||
{
|
{
|
||||||
request.Amount = destination.destination.Amount;
|
ModelState.AddModelError(nameof(request.Amount), amtError.error );
|
||||||
}
|
|
||||||
else if (request.Amount != null && destination.destination.Amount != null && request.Amount != destination.destination.Amount)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(nameof(request.Amount), $"Amount is implied in destination ({destination.destination.Amount}) that does not match the payout amount provided {request.Amount})");
|
|
||||||
return this.CreateValidationError(ModelState);
|
return this.CreateValidationError(ModelState);
|
||||||
}
|
}
|
||||||
|
request.Amount = amtError.amount;
|
||||||
if (request.Amount is { } v && (v < ppBlob?.MinimumClaim || v == 0.0m))
|
if (request.Amount is { } v && (v < ppBlob?.MinimumClaim || v == 0.0m))
|
||||||
{
|
{
|
||||||
var minimumClaim = ppBlob?.MinimumClaim is decimal val ? val : 0.0m;
|
var minimumClaim = ppBlob?.MinimumClaim is decimal val ? val : 0.0m;
|
||||||
|
@ -200,20 +200,14 @@ namespace BTCPayServer.Controllers
|
|||||||
return await ViewPullPayment(pullPaymentId);
|
return await ViewPullPayment(pullPaymentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vm.ClaimedAmount == 0)
|
var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, vm.ClaimedAmount == 0? null: vm.ClaimedAmount, paymentMethodId.CryptoCode, ppBlob.Currency);
|
||||||
|
if (amtError.error is not null)
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(vm.ClaimedAmount), "Amount is required");
|
ModelState.AddModelError(nameof(vm.ClaimedAmount), amtError.error );
|
||||||
}
|
}
|
||||||
else
|
else if (amtError.amount is not null)
|
||||||
{
|
{
|
||||||
var amount = ppBlob.Currency == "SATS" ? new Money(vm.ClaimedAmount, MoneyUnit.Satoshi).ToUnit(MoneyUnit.BTC) : vm.ClaimedAmount;
|
vm.ClaimedAmount = amtError.amount.Value;
|
||||||
if (destination.destination.Amount != null && amount != destination.destination.Amount)
|
|
||||||
{
|
|
||||||
var implied = _displayFormatter.Currency(destination.destination.Amount.Value, paymentMethodId.CryptoCode, DisplayFormatter.CurrencyFormat.Symbol);
|
|
||||||
var provided = _displayFormatter.Currency(vm.ClaimedAmount, ppBlob.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
|
||||||
ModelState.AddModelError(nameof(vm.ClaimedAmount),
|
|
||||||
$"Amount implied in destination ({implied}) does not match the payout amount provided ({provided}).");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
|
@ -6,5 +6,6 @@ namespace BTCPayServer.Data
|
|||||||
{
|
{
|
||||||
public string? Id { get; }
|
public string? Id { get; }
|
||||||
decimal? Amount { get; }
|
decimal? Amount { get; }
|
||||||
|
bool IsExplicitAmountMinimum => false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,5 +23,6 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||||||
public uint256 PaymentHash { get; }
|
public uint256 PaymentHash { get; }
|
||||||
public string Id => PaymentHash.ToString();
|
public string Id => PaymentHash.ToString();
|
||||||
public decimal? Amount { get; }
|
public decimal? Amount { get; }
|
||||||
|
public bool IsExplicitAmountMinimum => true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -264,7 +264,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||||||
PaymentMethodId pmi, CancellationToken cancellationToken)
|
PaymentMethodId pmi, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
|
var boltAmount = bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
|
||||||
if (boltAmount != payoutBlob.CryptoAmount)
|
if (boltAmount > payoutBlob.CryptoAmount)
|
||||||
{
|
{
|
||||||
|
|
||||||
payoutData.State = PayoutState.Cancelled;
|
payoutData.State = PayoutState.Cancelled;
|
||||||
@ -295,9 +295,8 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
|||||||
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
|
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
|
||||||
new PayInvoiceParams()
|
new PayInvoiceParams()
|
||||||
{
|
{
|
||||||
Amount = bolt11PaymentRequest.MinimumAmount == LightMoney.Zero
|
// CLN does not support explicit amount param if it is the same as the invoice amount
|
||||||
? new LightMoney((decimal)payoutBlob.CryptoAmount, LightMoneyUnit.BTC)
|
Amount = payoutBlob.CryptoAmount == bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC)? null: new LightMoney((decimal)payoutBlob.CryptoAmount, LightMoneyUnit.BTC)
|
||||||
: null
|
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
string message = null;
|
string message = null;
|
||||||
if (result.Result == PayResult.Ok)
|
if (result.Result == PayResult.Ok)
|
||||||
|
@ -834,6 +834,31 @@ namespace BTCPayServer.HostedServices
|
|||||||
|
|
||||||
public class ClaimRequest
|
public class ClaimRequest
|
||||||
{
|
{
|
||||||
|
public static (string error, decimal? amount) IsPayoutAmountOk(IClaimDestination destination, decimal? amount, string payoutCurrency = null, string ppCurrency = null)
|
||||||
|
{
|
||||||
|
return amount switch
|
||||||
|
{
|
||||||
|
null when destination.Amount is null && ppCurrency is null => ("Amount is not specified in destination or payout request", null),
|
||||||
|
null when destination.Amount is null => (null, null),
|
||||||
|
null when destination.Amount != null => (null,destination.Amount),
|
||||||
|
not null when destination.Amount is null => (null,amount),
|
||||||
|
not null when destination.Amount != null && amount != destination.Amount &&
|
||||||
|
destination.IsExplicitAmountMinimum &&
|
||||||
|
payoutCurrency == "BTC" && ppCurrency == "SATS" &&
|
||||||
|
new Money(amount.Value, MoneyUnit.Satoshi).ToUnit(MoneyUnit.BTC) < destination.Amount =>
|
||||||
|
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount",null),
|
||||||
|
not null when destination.Amount != null && amount != destination.Amount &&
|
||||||
|
destination.IsExplicitAmountMinimum &&
|
||||||
|
!(payoutCurrency == "BTC" && ppCurrency == "SATS") &&
|
||||||
|
amount < destination.Amount =>
|
||||||
|
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount",null),
|
||||||
|
not null when destination.Amount != null && amount != destination.Amount &&
|
||||||
|
!destination.IsExplicitAmountMinimum =>
|
||||||
|
($"Amount is implied in destination ({destination.Amount}) that does not match the payout amount provided {amount})", null),
|
||||||
|
_ => (null, amount)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static string GetErrorMessage(ClaimResult result)
|
public static string GetErrorMessage(ClaimResult result)
|
||||||
{
|
{
|
||||||
switch (result)
|
switch (result)
|
||||||
|
2
btcpayserver.sln.DotSettings
Normal file
2
btcpayserver.sln.DotSettings
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LNURL/@EntryIndexedValue">LNURL</s:String></wpf:ResourceDictionary>
|
Loading…
Reference in New Issue
Block a user