Merge pull request #2060 from btcpayserver/feat/unified-qr

Unified QR code, BIP21 with lightning fallback
This commit is contained in:
rockstardev 2020-11-14 12:11:26 -06:00 committed by GitHub
commit 6c7433b2f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 297 additions and 378 deletions

View File

@ -30,14 +30,21 @@ namespace BTCPayServer.Client.Models
public double PaymentTolerance { get; set; } = 0;
public bool AnyoneCanCreateInvoice { get; set; }
public bool RequiresRefundEmail { get; set; }
public bool LightningAmountInSatoshi { get; set; }
public bool LightningPrivateRouteHints { get; set; }
public bool OnChainWithLnInvoiceFallback { get; set; }
public bool RedirectAutomatically { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool ShowRecommendedFee { get; set; } = true;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public int RecommendedFeeBlockTarget { get; set; } = 1;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string DefaultLang { get; set; } = "en";
public bool LightningAmountInSatoshi { get; set; }
public string CustomLogo { get; set; }
@ -45,16 +52,13 @@ namespace BTCPayServer.Client.Models
public string HtmlTitle { get; set; }
public bool RedirectAutomatically { get; set; }
public bool RequiresRefundEmail { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never;
public bool PayJoinEnabled { get; set; }
public bool LightningPrivateRouteHints { get; set; }
[JsonExtensionData]

View File

@ -770,30 +770,31 @@ namespace BTCPayServer.Tests
BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.00005m));
});
await tester.ExplorerNode.GenerateAsync(1);
await Task.Delay(100); // wait a bit for payment to process before fetching new invoice
var newInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
var oldBolt11= invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
Assert.NotEqual(newBolt11,oldBolt11);
var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
var oldBolt11 = invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
Assert.NotEqual(newBolt11, oldBolt11);
Assert.Equal(newInvoice.BtcDue.GetValue(), BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC));
Logs.Tester.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue()} via lightning" );
Logs.Tester.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue()} via lightning");
var evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
{
await tester.SendLightningPaymentAsync(newInvoice);
}, evt => evt.InvoiceId == invoice.Id);
var fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId);
Assert.Contains(fetchedInvoice.Status, new []{InvoiceStatus.Complete, InvoiceStatus.Confirmed});
Assert.Contains(fetchedInvoice.Status, new[] { InvoiceStatus.Complete, InvoiceStatus.Confirmed });
Assert.Equal(InvoiceExceptionStatus.None, fetchedInvoice.ExceptionStatus);
Logs.Tester.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice " );
Logs.Tester.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice ");
evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
{
await tester.SendLightningPaymentAsync(invoice);
}, evt => evt.InvoiceId == invoice.Id);
Assert.Equal(evt.InvoiceId, invoice.Id);
fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId);
Assert.Equal( 3,fetchedInvoice.Payments.Count);
Assert.Equal(3, fetchedInvoice.Payments.Count);
}
[Fact(Timeout = 60 * 2 * 1000)]
@ -1508,7 +1509,7 @@ namespace BTCPayServer.Tests
);
}
}
// [Fact(Timeout = TestTimeout)]
[Fact()]
[Trait("Integration", "Integration")]
@ -1527,9 +1528,9 @@ namespace BTCPayServer.Tests
BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest),
Money.Coins(0.01m));
});
var payments = Assert.IsType<InvoiceDetailsModel>(
Assert.IsType<ViewResult>(await user.GetController<InvoiceController>().Invoice(invoice.Id)).Model)
.Payments;
@ -1990,7 +1991,60 @@ namespace BTCPayServer.Tests
};
var criteriaCompat = store.GetPaymentMethodCriteria(tester.NetworkProvider, blob);
Assert.Single(criteriaCompat);
Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", BitcoinPaymentType.Instance) ));
Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", BitcoinPaymentType.Instance)));
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanSetUnifiedQrCode()
{
using (var tester = ServerTester.Create())
{
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit);
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var invoice = user.BitPay.CreateInvoice(
new Invoice()
{
Price = 5.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
// validate that invoice data model doesn't have lightning string initially
var res = await user.GetController<InvoiceController>().Checkout(invoice.Id);
var paymentMethodFirst = Assert.IsType<PaymentModel>(
Assert.IsType<ViewResult>(res).Model
);
Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR);
// enable unified QR code in settings
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model
);
vm.OnChainWithLnInvoiceFallback = true;
Assert.IsType<RedirectToActionResult>(
user.GetController<StoresController>().CheckoutExperience(vm).Result
);
// validate that QR code now has both onchain and offchain payment urls
res = await user.GetController<InvoiceController>().Checkout(invoice.Id);
var paymentMethodSecond = Assert.IsType<PaymentModel>(
Assert.IsType<ViewResult>(res).Model
);
Assert.Contains("&lightning=", paymentMethodSecond.InvoiceBitcoinUrlQR);
Assert.StartsWith("BITCOIN:", paymentMethodSecond.InvoiceBitcoinUrlQR);
var split = paymentMethodSecond.InvoiceBitcoinUrlQR.Split('?')[0];
Assert.True($"BITCOIN:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split);
}
}
@ -2030,7 +2084,7 @@ namespace BTCPayServer.Tests
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
//test backward compat
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
var blob = store.GetStoreBlob();
@ -2044,8 +2098,8 @@ namespace BTCPayServer.Tests
};
var criteriaCompat = store.GetPaymentMethodCriteria(tester.NetworkProvider, blob);
Assert.Single(criteriaCompat);
Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && !methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", LightningPaymentType.Instance) ));
Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && !methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", LightningPaymentType.Instance)));
}
}

View File

@ -120,21 +120,22 @@ namespace BTCPayServer.Controllers.GreenField
//we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
NetworkFeeMode = storeBlob.NetworkFeeMode,
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
RedirectAutomatically = storeBlob.RedirectAutomatically,
ShowRecommendedFee = storeBlob.ShowRecommendedFee,
RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget,
DefaultLang = storeBlob.DefaultLang,
MonitoringExpiration = storeBlob.MonitoringExpiration,
InvoiceExpiration = storeBlob.InvoiceExpiration,
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
CustomLogo = storeBlob.CustomLogo,
CustomCSS = storeBlob.CustomCSS,
HtmlTitle = storeBlob.HtmlTitle,
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate,
PaymentTolerance = storeBlob.PaymentTolerance,
RedirectAutomatically = storeBlob.RedirectAutomatically,
PayJoinEnabled = storeBlob.PayJoinEnabled,
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints
PayJoinEnabled = storeBlob.PayJoinEnabled
};
}
@ -155,21 +156,22 @@ namespace BTCPayServer.Controllers.GreenField
//we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
blob.NetworkFeeMode = restModel.NetworkFeeMode;
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
blob.OnChainWithLnInvoiceFallback = restModel.OnChainWithLnInvoiceFallback;
blob.RedirectAutomatically = restModel.RedirectAutomatically;
blob.ShowRecommendedFee = restModel.ShowRecommendedFee;
blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget;
blob.DefaultLang = restModel.DefaultLang;
blob.MonitoringExpiration = restModel.MonitoringExpiration;
blob.InvoiceExpiration = restModel.InvoiceExpiration;
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
blob.CustomLogo = restModel.CustomLogo;
blob.CustomCSS = restModel.CustomCSS;
blob.HtmlTitle = restModel.HtmlTitle;
blob.AnyoneCanInvoice = restModel.AnyoneCanCreateInvoice;
blob.LightningDescriptionTemplate = restModel.LightningDescriptionTemplate;
blob.PaymentTolerance = restModel.PaymentTolerance;
blob.RedirectAutomatically = restModel.RedirectAutomatically;
blob.PayJoinEnabled = restModel.PayJoinEnabled;
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
model.SetStoreBlob(blob);
}

View File

@ -215,7 +215,7 @@ namespace BTCPayServer.Controllers
if (paymentMethodCriteria.Value != null)
{
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, paymentMethodCriteria.Value.Currency));
}
}
}
}
@ -305,7 +305,9 @@ namespace BTCPayServer.Controllers
}).ToArray());
}
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetworkBase network, InvoiceEntity entity, StoreData store, InvoiceLogs logs)
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair,
IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetworkBase network, InvoiceEntity entity,
StoreData store, InvoiceLogs logs)
{
try
{
@ -317,12 +319,14 @@ namespace BTCPayServer.Controllers
{
return null;
}
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity;
paymentMethod.Network = network;
var paymentMethod = new PaymentMethod
{
ParentEntity = entity,
Network = network,
Rate = rate.BidAsk.Bid,
PreferOnion = Uri.TryCreate(entity.ServerUrl, UriKind.Absolute, out var u) && u.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase)
};
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.BidAsk.Bid;
paymentMethod.PreferOnion = Uri.TryCreate(entity.ServerUrl, UriKind.Absolute, out var u) && u.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
using (logs.Measure($"{logPrefix} Payment method details creation"))
{
@ -339,7 +343,7 @@ namespace BTCPayServer.Controllers
{
var amount = paymentMethod.Calculate().Due.GetValue(network as BTCPayNetwork);
var limitValueCrypto = criteria.Value.Value / currentRateToCrypto.BidAsk.Bid;
if (amount < limitValueCrypto && criteria.Above)
{
logs.Write($"{logPrefix} invoice amount below accepted value for payment method", InvoiceEventData.EventSeverity.Error);
@ -369,7 +373,7 @@ namespace BTCPayServer.Controllers
}
catch (Exception ex)
{
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})", InvoiceEventData.EventSeverity.Error);
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex})", InvoiceEventData.EventSeverity.Error);
}
return null;
}

View File

@ -375,16 +375,19 @@ namespace BTCPayServer.Controllers
Type = criteria.Above ? PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan : PaymentMethodCriteriaViewModel.CriteriaType.LessThan,
Value = criteria.Value?.ToString() ?? ""
}).ToList();
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints;
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
vm.ShowRecommendedFee = storeBlob.ShowRecommendedFee;
vm.RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget;
vm.CustomCSS = storeBlob.CustomCSS;
vm.CustomLogo = storeBlob.CustomLogo;
vm.HtmlTitle = storeBlob.HtmlTitle;
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.ShowRecommendedFee = storeBlob.ShowRecommendedFee;
vm.RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget;
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints;
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
return View(vm);
}
@ -450,16 +453,20 @@ namespace BTCPayServer.Controllers
blob.LightningMaxValue = null;
blob.OnChainMinValue = null;
#pragma warning restore 612
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = model.LightningPrivateRouteHints;
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;
blob.RedirectAutomatically = model.RedirectAutomatically;
blob.ShowRecommendedFee = model.ShowRecommendedFee;
blob.RecommendedFeeBlockTarget = model.RecommendedFeeBlockTarget;
blob.CustomLogo = model.CustomLogo;
blob.CustomCSS = model.CustomCSS;
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
blob.DefaultLang = model.DefaultLang;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.ShowRecommendedFee = model.ShowRecommendedFee;
blob.RecommendedFeeBlockTarget = model.RecommendedFeeBlockTarget;
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = model.LightningPrivateRouteHints;
blob.RedirectAutomatically = model.RedirectAutomatically;
if (CurrentStore.SetStoreBlob(blob))
{
needUpdate = true;

View File

@ -31,22 +31,17 @@ namespace BTCPayServer.Data
[Obsolete("Use NetworkFeeMode instead")]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool? NetworkFeeDisabled
{
get; set;
}
public bool? NetworkFeeDisabled { get; set; }
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public NetworkFeeMode NetworkFeeMode
{
get;
set;
}
public NetworkFeeMode NetworkFeeMode { get; set; }
public bool RequiresRefundEmail { get; set; }
public bool LightningAmountInSatoshi { get; set; }
public bool LightningPrivateRouteHints { get; set; }
public bool OnChainWithLnInvoiceFallback { get; set; }
public bool RedirectAutomatically { get; set; }
public bool ShowRecommendedFee { get; set; }
public int RecommendedFeeBlockTarget { get; set; }
CurrencyPair[] _DefaultCurrencyPairs;
@ -81,11 +76,7 @@ namespace BTCPayServer.Data
[DefaultValue(typeof(TimeSpan), "00:15:00")]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
public TimeSpan InvoiceExpiration
{
get;
set;
}
public TimeSpan InvoiceExpiration { get; set; }
public decimal Spread { get; set; } = 0.0m;
@ -106,8 +97,6 @@ namespace BTCPayServer.Data
[Obsolete]
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue LightningMaxValue { get; set; }
public bool LightningAmountInSatoshi { get; set; }
public bool LightningPrivateRouteHints { get; set; }
public string CustomCSS { get; set; }
public string CustomLogo { get; set; }
@ -185,10 +174,10 @@ namespace BTCPayServer.Data
public Dictionary<string, string> WalletKeyPathRoots { get; set; }
public EmailSettings EmailSettings { get; set; }
public bool RedirectAutomatically { get; set; }
public bool PayJoinEnabled { get; set; }
public StoreHints Hints { get; set; }
public class StoreHints
{
public bool Wallet { get; set; }
@ -229,7 +218,7 @@ namespace BTCPayServer.Data
public CurrencyValue Value { get; set; }
public bool Above { get; set; }
}
public class RateRule_Obsolete
{
public RateRule_Obsolete()

View File

@ -31,6 +31,31 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Default payment method on checkout")]
public string DefaultPaymentMethod { get; set; }
[Display(Name = "Requires a refund email")]
public bool RequiresRefundEmail { get; set; }
[Display(Name = "Display lightning payment amounts in Satoshis")]
public bool LightningAmountInSatoshi { get; set; }
[Display(Name = "Add hop hints for private channels to the lightning invoice")]
public bool LightningPrivateRouteHints { get; set; }
[Display(Name = "Include lightning invoice fallback to on-chain BIP21 payment url")]
public bool OnChainWithLnInvoiceFallback { get; set; }
[Display(Name = "Redirect invoice to redirect url automatically after paid")]
public bool RedirectAutomatically { get; set; }
[Display(Name = "Show recommended fee")]
public bool ShowRecommendedFee { get; set; }
[Display(Name = "Recommended fee confirmation target blocks")]
[Range(1, double.PositiveInfinity)]
public int RecommendedFeeBlockTarget { get; set; }
[Display(Name = "Default language on checkout")]
public string DefaultLang { get; set; }
@ -42,25 +67,6 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Custom HTML title to display on Checkout page")]
public string HtmlTitle { get; set; }
[Display(Name = "Requires a refund email")]
public bool RequiresRefundEmail { get; set; }
[Display(Name = "Show recommended fee")]
public bool ShowRecommendedFee { get; set; }
[Display(Name = "Recommended fee confirmation target blocks")]
[Range(1, double.PositiveInfinity)]
public int RecommendedFeeBlockTarget { get; set; }
[Display(Name = "Display lightning payment amounts in Satoshis")]
public bool LightningAmountInSatoshi { get; set; }
[Display(Name = "Add hop hints for private channels to the lightning invoice")]
public bool LightningPrivateRouteHints { get; set; }
[Display(Name = "Redirect invoice to redirect url automatically after paid")]
public bool RedirectAutomatically { get; set; }
public List<PaymentMethodCriteriaViewModel> PaymentMethodCriteria { get; set; }
}

View File

@ -46,8 +46,7 @@ namespace BTCPayServer.Payments.Bitcoin
public Task<KeyPathInformation> ReserveAddress;
}
public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse,
StoreBlob storeBlob)
public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse, StoreBlob storeBlob)
{
var paymentMethodId = new PaymentMethodId(model.CryptoCode, PaymentTypes.BTCLike);
@ -55,9 +54,33 @@ namespace BTCPayServer.Payments.Bitcoin
var network = _networkProvider.GetNetwork<BTCPayNetwork>(model.CryptoCode);
model.IsLightning = false;
model.PaymentMethodName = GetPaymentMethodName(network);
model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21;
model.InvoiceBitcoinUrlQR = cryptoInfo.PaymentUrls.BIP21;
var lightningFallback = "";
if (storeBlob.OnChainWithLnInvoiceFallback)
{
var lightningInfo = invoiceResponse.CryptoInfo.FirstOrDefault(a =>
a.GetpaymentMethodId() == new PaymentMethodId(model.CryptoCode, PaymentTypes.LightningLike));
if (!String.IsNullOrEmpty(lightningInfo?.PaymentUrls?.BOLT11))
lightningFallback = "&" + lightningInfo.PaymentUrls.BOLT11.Replace("lightning:", "lightning=", StringComparison.OrdinalIgnoreCase);
}
model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21 + lightningFallback;
// We're trying to make as many characters uppercase to make QR smaller
// Ref: https://github.com/btcpayserver/btcpayserver/pull/2060#issuecomment-723828348
model.InvoiceBitcoinUrlQR = cryptoInfo.PaymentUrls.BIP21
.Replace("bitcoin:", "BITCOIN:", StringComparison.OrdinalIgnoreCase)
+ lightningFallback.ToUpperInvariant().Replace("LIGHTNING=", "lightning=", StringComparison.OrdinalIgnoreCase);
if (bech32Prefixes.Any(a => model.BtcAddress.StartsWith(a, StringComparison.OrdinalIgnoreCase)))
{
model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrlQR.Replace(
$"BITCOIN:{model.BtcAddress}", $"BITCOIN:{model.BtcAddress.ToUpperInvariant()}",
StringComparison.OrdinalIgnoreCase
);
}
}
private static string[] bech32Prefixes = new[] { "bc1", "tb1", "bcrt1" };
public override string GetCryptoImage(PaymentMethodId paymentMethodId)
{

View File

@ -12,15 +12,13 @@ namespace BTCPayServer.Payments
public class BitcoinPaymentType : PaymentType
{
public static BitcoinPaymentType Instance { get; } = new BitcoinPaymentType();
private BitcoinPaymentType()
{
}
private BitcoinPaymentType() { }
public override string ToPrettyString() => "On-Chain";
public override string GetId() => "BTCLike";
public override string GetBadge() => "🔗";
public override string ToStringNormalized() => "OnChain";
public override string GetBadge() => "🔗";
public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str)
{
@ -72,8 +70,8 @@ namespace BTCPayServer.Payments
public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails,
Money cryptoInfoDue, string serverUri)
{
var bip21 = ((BTCPayNetwork)network).GenerateBIP21(paymentMethodDetails.GetPaymentDestination(), cryptoInfoDue);
var bip21 = ((BTCPayNetwork)network).GenerateBIP21(paymentMethodDetails.GetPaymentDestination(), cryptoInfoDue);
if ((paymentMethodDetails as BitcoinLikeOnChainPaymentMethod)?.PayjoinEnabled is true)
{
bip21 += $"&{PayjoinClient.BIP21EndpointKey}={serverUri.WithTrailingSlash()}{network.CryptoCode}/{PayjoinClient.BIP21EndpointKey}";

View File

@ -11,18 +11,13 @@ namespace BTCPayServer.Payments
{
public static LightningPaymentType Instance { get; } = new LightningPaymentType();
private LightningPaymentType()
{
}
private LightningPaymentType() { }
public override string ToPrettyString() => "Off-Chain";
public override string GetId() => "LightningLike";
public override string GetBadge() => "⚡";
public override string GetBadge() => "⚡";
public override string ToStringNormalized() => "LightningNetwork";
public override string ToStringNormalized()
{
return "LightningNetwork";
}
public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str)
{
return ((BTCPayNetwork)network)?.ToObject<LightningLikePaymentData>(str);
@ -35,7 +30,7 @@ namespace BTCPayServer.Payments
public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str)
{
return JsonConvert.DeserializeObject<Payments.Lightning.LightningLikePaymentMethodDetails>(str);
return JsonConvert.DeserializeObject<LightningLikePaymentMethodDetails>(str);
}
public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details)
@ -57,8 +52,10 @@ namespace BTCPayServer.Payments
public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails,
Money cryptoInfoDue, string serverUri)
{
return
$"lightning:{paymentMethodDetails.GetPaymentDestination().ToUpperInvariant().Replace("LIGHTNING:", "", StringComparison.InvariantCultureIgnoreCase)}";
var lnInvoiceTrimmedOfScheme = paymentMethodDetails.GetPaymentDestination().ToLowerInvariant()
.Replace("lightning:", "", StringComparison.InvariantCultureIgnoreCase);
return $"lightning:{lnInvoiceTrimmedOfScheme}";
}
public override string InvoiceViewPaymentPartialName { get; } = "ViewLightningLikePaymentData";

View File

@ -36,73 +36,34 @@ namespace BTCPayServer.Services.Invoices
}
public string OrderId { get; set; }
[JsonProperty(PropertyName = "buyerName")]
public string BuyerName
{
get; set;
}
public string BuyerName { get; set; }
[JsonProperty(PropertyName = "buyerEmail")]
public string BuyerEmail
{
get; set;
}
public string BuyerEmail { get; set; }
[JsonProperty(PropertyName = "buyerCountry")]
public string BuyerCountry
{
get; set;
}
public string BuyerCountry { get; set; }
[JsonProperty(PropertyName = "buyerZip")]
public string BuyerZip
{
get; set;
}
public string BuyerZip { get; set; }
[JsonProperty(PropertyName = "buyerState")]
public string BuyerState
{
get; set;
}
public string BuyerState { get; set; }
[JsonProperty(PropertyName = "buyerCity")]
public string BuyerCity
{
get; set;
}
public string BuyerCity { get; set; }
[JsonProperty(PropertyName = "buyerAddress2")]
public string BuyerAddress2
{
get; set;
}
public string BuyerAddress2 { get; set; }
[JsonProperty(PropertyName = "buyerAddress1")]
public string BuyerAddress1
{
get; set;
}
public string BuyerAddress1 { get; set; }
[JsonProperty(PropertyName = "buyerPhone")]
public string BuyerPhone
{
get; set;
}
public string BuyerPhone { get; set; }
[JsonProperty(PropertyName = "itemDesc")]
public string ItemDesc
{
get; set;
}
public string ItemDesc { get; set; }
[JsonProperty(PropertyName = "itemCode")]
public string ItemCode
{
get; set;
}
public string ItemCode { get; set; }
[JsonProperty(PropertyName = "physical")]
public bool? Physical
{
get; set;
}
public bool? Physical { get; set; }
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal? TaxIncluded
{
get; set;
}
public decimal? TaxIncluded { get; set; }
public string PosData { get; set; }
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; }
@ -122,133 +83,64 @@ namespace BTCPayServer.Services.Invoices
class BuyerInformation
{
[JsonProperty(PropertyName = "buyerName")]
public string BuyerName
{
get; set;
}
public string BuyerName { get; set; }
[JsonProperty(PropertyName = "buyerEmail")]
public string BuyerEmail
{
get; set;
}
public string BuyerEmail { get; set; }
[JsonProperty(PropertyName = "buyerCountry")]
public string BuyerCountry
{
get; set;
}
public string BuyerCountry { get; set; }
[JsonProperty(PropertyName = "buyerZip")]
public string BuyerZip
{
get; set;
}
public string BuyerZip { get; set; }
[JsonProperty(PropertyName = "buyerState")]
public string BuyerState
{
get; set;
}
public string BuyerState { get; set; }
[JsonProperty(PropertyName = "buyerCity")]
public string BuyerCity
{
get; set;
}
public string BuyerCity { get; set; }
[JsonProperty(PropertyName = "buyerAddress2")]
public string BuyerAddress2
{
get; set;
}
public string BuyerAddress2 { get; set; }
[JsonProperty(PropertyName = "buyerAddress1")]
public string BuyerAddress1
{
get; set;
}
public string BuyerAddress1 { get; set; }
[JsonProperty(PropertyName = "buyerPhone")]
public string BuyerPhone
{
get; set;
}
public string BuyerPhone { get; set; }
}
class ProductInformation
{
[JsonProperty(PropertyName = "itemDesc")]
public string ItemDesc
{
get; set;
}
public string ItemDesc { get; set; }
[JsonProperty(PropertyName = "itemCode")]
public string ItemCode
{
get; set;
}
public string ItemCode { get; set; }
[JsonProperty(PropertyName = "physical")]
public bool Physical
{
get; set;
}
public bool Physical { get; set; }
[JsonProperty(PropertyName = "price")]
public decimal Price
{
get; set;
}
public decimal Price { get; set; }
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal TaxIncluded
{
get; set;
}
public decimal TaxIncluded { get; set; }
[JsonProperty(PropertyName = "currency")]
public string Currency
{
get; set;
}
public string Currency { get; set; }
}
[JsonIgnore]
public BTCPayNetworkProvider Networks { get; set; }
public const int InternalTagSupport_Version = 1;
public const int GreenfieldInvoices_Version = 2;
public const int Lastest_Version = 2;
public int Version { get; set; }
public string Id
{
get; set;
}
public string StoreId
{
get; set;
}
public string Id { get; set; }
public string StoreId { get; set; }
public SpeedPolicy SpeedPolicy
{
get; set;
}
public SpeedPolicy SpeedPolicy { get; set; }
[Obsolete("Use GetPaymentMethod(network) instead")]
public decimal Rate
{
get; set;
}
public DateTimeOffset InvoiceTime
{
get; set;
}
public DateTimeOffset ExpirationTime
{
get; set;
}
public decimal Rate { get; set; }
public DateTimeOffset InvoiceTime { get; set; }
public DateTimeOffset ExpirationTime { get; set; }
[Obsolete("Use GetPaymentMethod(network).GetPaymentMethodDetails().GetDestinationAddress() instead")]
public string DepositAddress
{
get; set;
}
public InvoiceMetadata Metadata
{
get;
set;
}
public string DepositAddress { get; set; }
public InvoiceMetadata Metadata { get; set; }
public decimal Price { get; set; }
public string Currency { get; set; }
@ -267,18 +159,10 @@ namespace BTCPayServer.Services.Invoices
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy
{
get;
set;
}
public string DerivationStrategy { get; set; }
[Obsolete("Use GetPaymentMethodFactories() instead")]
public string DerivationStrategies
{
get;
set;
}
public string DerivationStrategies { get; set; }
public IEnumerable<T> GetSupportedPaymentMethod<T>(PaymentMethodId paymentMethodId) where T : ISupportedPaymentMethod
{
return
@ -335,28 +219,18 @@ namespace BTCPayServer.Services.Invoices
}
[JsonIgnore]
public InvoiceStatus Status
{
get;
set;
}
public InvoiceStatus Status { get; set; }
[JsonProperty(PropertyName = "status")]
[Obsolete("Use Status instead")]
public string StatusString => InvoiceState.ToString(Status);
[JsonIgnore]
public InvoiceExceptionStatus ExceptionStatus
{
get; set;
}
public InvoiceExceptionStatus ExceptionStatus { get; set; }
[JsonProperty(PropertyName = "exceptionStatus")]
[Obsolete("Use ExceptionStatus instead")]
public string ExceptionStatusString => InvoiceState.ToString(ExceptionStatus);
[Obsolete("Use GetPayments instead")]
public List<PaymentEntity> Payments
{
get; set;
}
public List<PaymentEntity> Payments { get; set; }
#pragma warning disable CS0618
public List<PaymentEntity> GetPayments()
@ -372,22 +246,10 @@ namespace BTCPayServer.Services.Invoices
return GetPayments(network.CryptoCode);
}
#pragma warning restore CS0618
public bool Refundable
{
get;
set;
}
public string RefundMail
{
get;
set;
}
public bool Refundable { get; set; }
public string RefundMail { get; set; }
[JsonProperty("redirectURL")]
public string RedirectURLTemplate
{
get;
set;
}
public string RedirectURLTemplate { get; set; }
[JsonIgnore]
public Uri RedirectURL => FillPlaceholdersUri(RedirectURLTemplate);
@ -401,65 +263,29 @@ namespace BTCPayServer.Services.Invoices
return null;
}
public bool RedirectAutomatically
{
get;
set;
}
public bool RedirectAutomatically { get; set; }
[Obsolete("Use GetPaymentMethod(network).GetTxFee() instead")]
public Money TxFee
{
get;
set;
}
public bool FullNotifications
{
get;
set;
}
public string NotificationEmail
{
get;
set;
}
public Money TxFee { get; set; }
public bool FullNotifications { get; set; }
public string NotificationEmail { get; set; }
[JsonProperty("notificationURL")]
public string NotificationURLTemplate
{
get;
set;
}
public string NotificationURLTemplate { get; set; }
[JsonIgnore]
public Uri NotificationURL => FillPlaceholdersUri(NotificationURLTemplate);
public string ServerUrl
{
get;
set;
}
public string ServerUrl { get; set; }
[Obsolete("Use Set/GetPaymentMethod() instead")]
[JsonProperty(PropertyName = "cryptoData")]
public JObject PaymentMethod { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public DateTimeOffset MonitoringExpiration
{
get;
set;
}
public HistoricalAddressInvoiceData[] HistoricalAddresses
{
get;
set;
}
public DateTimeOffset MonitoringExpiration { get; set; }
public HistoricalAddressInvoiceData[] HistoricalAddresses { get; set; }
public HashSet<string> AvailableAddressHashes
{
get;
set;
}
public HashSet<string> AvailableAddressHashes { get; set; }
public bool ExtendedNotifications { get; set; }
public List<InvoiceEventData> Events { get; internal set; }
public double PaymentTolerance { get; set; }
@ -559,7 +385,7 @@ namespace BTCPayServer.Services.Invoices
{
var minerInfo = new MinerFeeInfo();
minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod) details).FeeRate
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate
.GetFee(1).Satoshi;
dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo);
cryptoInfo.PaymentUrls = new InvoicePaymentUrls()
@ -1181,7 +1007,7 @@ namespace BTCPayServer.Services.Invoices
{
return null;
}
paymentData.Network = Network;
if (paymentData is BitcoinLikePaymentData bitcoin)
{
@ -1229,9 +1055,10 @@ namespace BTCPayServer.Services.Invoices
PaymentType paymentType;
if (string.IsNullOrEmpty(CryptoPaymentDataType))
{
paymentType = BitcoinPaymentType.Instance;;
paymentType = BitcoinPaymentType.Instance;
;
}
else if(!PaymentTypes.TryParse(CryptoPaymentDataType, out paymentType))
else if (!PaymentTypes.TryParse(CryptoPaymentDataType, out paymentType))
{
return null;
}

View File

@ -1,4 +1,4 @@
@using BTCPayServer.Payments
@using BTCPayServer.Payments
@model CheckoutExperienceViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
@ -36,9 +36,9 @@
@for (var index = 0; index < Model.PaymentMethodCriteria.Count; index++)
{
var criteria = Model.PaymentMethodCriteria[index];
<tr >
<tr>
<td class="border-right-0 border-left-0 pt-3">
<input type="hidden" asp-for="PaymentMethodCriteria[index].PaymentMethod"/>
<input type="hidden" asp-for="PaymentMethodCriteria[index].PaymentMethod" />
@PaymentMethodId.Parse(criteria.PaymentMethod).ToPrettyString()
</td>
<td class="border-right-0 border-left-0 ">
@ -49,43 +49,46 @@
</td>
<td class="border-right-0 border-left-0">
<input placeholder="6.15 USD" asp-for="PaymentMethodCriteria[index].Value" class="form-control my-0" style="max-width: 20ch;"/>
<input placeholder="6.15 USD" asp-for="PaymentMethodCriteria[index].Value" class="form-control my-0" style="max-width: 20ch;" />
</td>
</tr>
}
</table>
}
<div class="form-group mb-4">
<div class="form-group">
<div class="form-check">
<input asp-for="RequiresRefundEmail" type="checkbox" class="form-check-input"/>
<input asp-for="RequiresRefundEmail" type="checkbox" class="form-check-input" />
<label asp-for="RequiresRefundEmail" class="form-check-label"></label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="LightningAmountInSatoshi" type="checkbox" class="form-check-input"/>
<input asp-for="LightningAmountInSatoshi" type="checkbox" class="form-check-input" />
<label asp-for="LightningAmountInSatoshi" class="form-check-label"></label>
<span asp-validation-for="LightningAmountInSatoshi" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="LightningPrivateRouteHints" type="checkbox" class="form-check-input"/>
<input asp-for="LightningPrivateRouteHints" type="checkbox" class="form-check-input" />
<label asp-for="LightningPrivateRouteHints" class="form-check-label"></label>
<span asp-validation-for="LightningPrivateRouteHints" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="RedirectAutomatically" type="checkbox" class="form-check-input"/>
<input asp-for="OnChainWithLnInvoiceFallback" type="checkbox" class="form-check-input" />
<label asp-for="OnChainWithLnInvoiceFallback" class="form-check-label"></label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="RedirectAutomatically" type="checkbox" class="form-check-input" />
<label asp-for="RedirectAutomatically" class="form-check-label"></label>
<span asp-validation-for="RedirectAutomatically" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="ShowRecommendedFee" type="checkbox" class="form-check-input"/>
<input asp-for="ShowRecommendedFee" type="checkbox" class="form-check-input" />
<label asp-for="ShowRecommendedFee" class="form-check-label"></label>
<p class="form-text text-muted">Fee will be shown for BTC and LTC onchain payments only.</p>
</div>
@ -103,19 +106,19 @@
</div>
<div class="form-group">
<label asp-for="HtmlTitle"></label>
<input asp-for="HtmlTitle" class="form-control"/>
<input asp-for="HtmlTitle" class="form-control" />
<span asp-validation-for="HtmlTitle" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomLogo"></label>
<a href="https://docs.btcpayserver.org/Theme/#checkout-page-themes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="CustomLogo" class="form-control"/>
<input asp-for="CustomLogo" class="form-control" />
<span asp-validation-for="CustomLogo" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomCSS"></label>
<a href="https://docs.btcpayserver.org/Theme/#checkout-page-themes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="CustomCSS" class="form-control"/>
<input asp-for="CustomCSS" class="form-control" />
<span asp-validation-for="CustomCSS" class="text-danger"></span>
<p class="form-text text-muted">
Bundled Themes:

View File

@ -320,6 +320,31 @@
"default": false,
"description": "If true, then no authentication is needed to create invoices on this store."
},
"requiresRefundEmail": {
"type": "boolean",
"default": false,
"description": "If true, the checkout page will ask to enter an email address before accessing payment information."
},
"lightningAmountInSatoshi": {
"type": "boolean",
"default": false,
"description": "If true, lightning payment methods show amount in satoshi in the checkout page."
},
"lightningPrivateRouteHints": {
"type": "boolean",
"default": false,
"description": "Should private route hints be included in the lightning payment of the checkout page."
},
"onChainWithLnInvoiceFallback": {
"type": "boolean",
"default": false,
"description": "Include lightning invoice fallback to on-chain BIP21 payment url."
},
"redirectAutomatically": {
"type": "boolean",
"default": false,
"description": "After successfull payment, should the checkout page redirect the user automatically to the redirect URL of the invoice?"
},
"showRecommendedFee": {
"type": "boolean",
"default": true
@ -335,11 +360,6 @@
"default": "en",
"description": "The default language to use in the checkout page. (The different translations available are listed [here](https://github.com/btcpayserver/btcpayserver/tree/master/BTCPayServer/wwwroot/locales)"
},
"lightningAmountInSatoshi": {
"type": "boolean",
"default": false,
"description": "If true, lightning payment methods show amount in satoshi in the checkout page."
},
"customLogo": {
"type": "string",
"nullable": true,
@ -355,16 +375,6 @@
"nullable": true,
"description": "The HTML title of the checkout page (when you over the tab in your browser)"
},
"redirectAutomatically": {
"type": "boolean",
"default": false,
"description": "After successfull payment, should the checkout page redirect the user automatically to the redirect URL of the invoice?"
},
"requiresRefundEmail": {
"type": "boolean",
"default": false,
"description": "If true, the checkout page will ask to enter an email address before accessing payment information."
},
"networkFeeMode": {
"$ref": "#/components/schemas/NetworkFeeMode"
},
@ -372,11 +382,6 @@
"type": "boolean",
"default": false,
"description": "If true, payjoin will be proposed in the checkout page if possible. ([More information](https://docs.btcpayserver.org/Payjoin/))"
},
"lightningPrivateRouteHints": {
"type": "boolean",
"default": false,
"description": "Should private route hints be included in the lightning payment of the checkout page."
}
}
},