diff --git a/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs b/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs index 8ef85c0ef..c5b96ec26 100644 --- a/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs +++ b/BTCPayServer.Common/Altcoins/Liquid/ElementsLikeBtcPayNetwork.cs @@ -1,6 +1,7 @@ #if ALTCOINS using System.Collections.Generic; using System.Linq; +using BTCPayServer.Common; using NBitcoin; using NBXplorer; using NBXplorer.Models; @@ -33,13 +34,15 @@ namespace BTCPayServer output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId)); } - public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue) + public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue) { //precision 0: 10 = 0.00000010 //precision 2: 10 = 0.00001000 //precision 8: 10 = 10 - var money = cryptoInfoDue is null? null: new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC); - return $"{base.GenerateBIP21(cryptoInfoAddress, money)}{(money is null? "?": "&")}assetid={AssetId}"; + var money = cryptoInfoDue is null ? null : new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC); + var builder = base.GenerateBIP21(cryptoInfoAddress, money); + builder.QueryParams.Add("assetid", AssetId.ToString()); + return builder; } } } diff --git a/BTCPayServer.Common/BTCPayNetwork.cs b/BTCPayServer.Common/BTCPayNetwork.cs index f69015a42..21ae3e888 100644 --- a/BTCPayServer.Common/BTCPayNetwork.cs +++ b/BTCPayServer.Common/BTCPayNetwork.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using BTCPayServer.Common; using NBitcoin; using NBXplorer; using NBXplorer.Models; @@ -121,9 +122,15 @@ namespace BTCPayServer }); } - public virtual string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue) + public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue) { - return $"{UriScheme}:{cryptoInfoAddress}{(cryptoInfoDue is null? string.Empty: $"?amount={cryptoInfoDue.ToString(false, true)}")}"; + var builder = new PaymentUrlBuilder(UriScheme); + builder.Host = cryptoInfoAddress; + if (cryptoInfoDue != null && cryptoInfoDue != Money.Zero) + { + builder.QueryParams.Add("amount", cryptoInfoDue.ToString(false, true)); + } + return builder; } public virtual List FilterValidTransactions(List transactionInformationSet) diff --git a/BTCPayServer.Common/PaymentUrlBuilder.cs b/BTCPayServer.Common/PaymentUrlBuilder.cs new file mode 100644 index 000000000..e61fa2ad6 --- /dev/null +++ b/BTCPayServer.Common/PaymentUrlBuilder.cs @@ -0,0 +1,30 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BTCPayServer.Common +{ + public class PaymentUrlBuilder + { + public PaymentUrlBuilder(string uriScheme) + { + UriScheme = uriScheme; + } + public string UriScheme { get; set; } + public Dictionary QueryParams { get; set; } = new Dictionary(); + public string? Host { get; set; } + public override string ToString() + { + StringBuilder builder = new StringBuilder($"{UriScheme}:{Host}"); + if (QueryParams.Count != 0) + { + var parts = QueryParams.Select(q => Uri.EscapeDataString(q.Key) + "=" + System.Web.NBitcoin.HttpUtility.UrlEncode(q.Value)) + .ToArray(); + builder.Append($"?{string.Join('&', parts)}"); + } + return builder.ToString(); + } + } +} diff --git a/BTCPayServer/Controllers/GreenField/StoreOnChainWalletsController.cs b/BTCPayServer/Controllers/GreenField/StoreOnChainWalletsController.cs index 493e79566..893135033 100644 --- a/BTCPayServer/Controllers/GreenField/StoreOnChainWalletsController.cs +++ b/BTCPayServer/Controllers/GreenField/StoreOnChainWalletsController.cs @@ -127,17 +127,16 @@ namespace BTCPayServer.Controllers.GreenField return BadRequest(); } - var bip21 = network.GenerateBIP21(kpi.Address.ToString(), null); + var bip21 = network.GenerateBIP21(kpi.Address?.ToString(), null); var allowedPayjoin = derivationScheme.IsHotWallet && Store.GetStoreBlob().PayJoinEnabled; if (allowedPayjoin) { - bip21 += - $"?{PayjoinClient.BIP21EndpointKey}={Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint", new {cryptoCode}))}"; + bip21.QueryParams.Add(PayjoinClient.BIP21EndpointKey, Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint", new { cryptoCode }))); } return Ok(new OnChainWalletAddressData() { - Address = kpi.Address.ToString(), - PaymentLink = bip21, + Address = kpi.Address?.ToString(), + PaymentLink = bip21.ToString(), KeyPath = kpi.KeyPath }); } diff --git a/BTCPayServer/Controllers/WalletsController.PullPayments.cs b/BTCPayServer/Controllers/WalletsController.PullPayments.cs index 6d0ba94d1..ae025dd7e 100644 --- a/BTCPayServer/Controllers/WalletsController.PullPayments.cs +++ b/BTCPayServer/Controllers/WalletsController.PullPayments.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Models; using BTCPayServer.Client.Models; +using BTCPayServer.Common; using BTCPayServer.Data; using BTCPayServer.HostedServices; using BTCPayServer.ModelBinders; @@ -285,7 +286,7 @@ namespace BTCPayServer.Controllers continue; } var blob = payout.GetBlob(_jsonSerializerSettings); - bip21.Add(network.GenerateBIP21(payout.Destination, new Money(blob.CryptoAmount.Value, MoneyUnit.BTC))); + bip21.Add(network.GenerateBIP21(payout.Destination, new Money(blob.CryptoAmount.Value, MoneyUnit.BTC)).ToString()); } if(bip21.Any()) diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index b1ca982b9..39ff9102b 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -368,18 +368,17 @@ namespace BTCPayServer.Controllers return NotFound(); var address = _walletReceiveService.Get(walletId)?.Address; var allowedPayjoin = paymentMethod.IsHotWallet && CurrentStore.GetStoreBlob().PayJoinEnabled; - var bip21 = address is null ? null : network.GenerateBIP21(address.ToString(), null); + var bip21 = network.GenerateBIP21(address?.ToString(), null); if (allowedPayjoin) { - bip21 += - $"?{PayjoinClient.BIP21EndpointKey}={Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint", new {walletId.CryptoCode}))}"; + bip21.QueryParams.Add(PayjoinClient.BIP21EndpointKey, Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint", new { walletId.CryptoCode }))); } return View(new WalletReceiveViewModel() { CryptoCode = walletId.CryptoCode, Address = address?.ToString(), CryptoImage = GetImage(paymentMethod.PaymentId, network), - PaymentLink = bip21 + PaymentLink = bip21.ToString() }); } diff --git a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs index 87ac2c394..729dfb03c 100644 --- a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs +++ b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs @@ -79,9 +79,9 @@ namespace BTCPayServer.Payments if ((paymentMethodDetails as BitcoinLikeOnChainPaymentMethod)?.PayjoinEnabled is true && serverUri != null) { - bip21 += $"&{PayjoinClient.BIP21EndpointKey}={serverUri.WithTrailingSlash()}{network.CryptoCode}/{PayjoinClient.BIP21EndpointKey}"; + bip21.QueryParams.Add(PayjoinClient.BIP21EndpointKey, $"{serverUri.WithTrailingSlash()}{network.CryptoCode}/{PayjoinClient.BIP21EndpointKey}"); } - return bip21; + return bip21.ToString(); } public override string InvoiceViewPaymentPartialName { get; } = "Bitcoin/ViewBitcoinLikePaymentData";