diff --git a/NBitcoin.Tests/PaymentTests.cs b/NBitcoin.Tests/PaymentTests.cs index 5ede2dbe4..8e1c036ca 100644 --- a/NBitcoin.Tests/PaymentTests.cs +++ b/NBitcoin.Tests/PaymentTests.cs @@ -23,69 +23,71 @@ public class PaymentTests [Trait("UnitTest", "UnitTest")] public void CanParsePaymentUrl() { - Assert.Equal("bitcoin:", new BitcoinUrlBuilder(Network.Main).Uri.ToString()); + Assert.Equal("bitcoin:", new BitcoinUriBuilder(Network.Main).Uri.ToString()); - var url = CreateBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha"); - Assert.Equal("129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha", url.Address.ToString()); + var uri = CreateBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha"); + Assert.Equal("129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha", uri.Address.ToString()); - url = CreateBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=0.06"); - Assert.Equal("129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha", url.Address.ToString()); - Assert.Equal(Money.Parse("0.06"), url.Amount); + uri = CreateBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=0.06"); + Assert.Equal("129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha", uri.Address.ToString()); + Assert.Equal(Money.Parse("0.06"), uri.Amount); - url = new BitcoinUrlBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=0.06&label=Tom%20%26%20Jerry", Network.Main); - Assert.Equal("129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha", url.Address.ToString()); - Assert.Equal(Money.Parse("0.06"), url.Amount); - Assert.Equal("Tom & Jerry", url.Label); - Assert.Equal(url.ToString(), new BitcoinUrlBuilder(url.ToString(), Network.Main).ToString()); + uri = new BitcoinUriBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=0.06&label=Tom%20%26%20Jerry", Network.Main); + Assert.Equal("129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha", uri.Address.ToString()); + Assert.Equal(Money.Parse("0.06"), uri.Amount); + Assert.Equal("Tom & Jerry", uri.Label); + Assert.Equal(uri.ToString(), new BitcoinUriBuilder(uri.ToString(), Network.Main).ToString()); //Request 50 BTC with message: - url = new BitcoinUrlBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz", Network.Main); - Assert.Equal(Money.Parse("50"), url.Amount); - Assert.Equal("Luke-Jr", url.Label); - Assert.Equal("Donation for project xyz", url.Message); - Assert.Equal(url.ToString(), new BitcoinUrlBuilder(url.ToString(), Network.Main).ToString()); + uri = new BitcoinUriBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz", Network.Main); + Assert.Equal(Money.Parse("50"), uri.Amount); + Assert.Equal("Luke-Jr", uri.Label); + Assert.Equal("Donation for project xyz", uri.Message); + Assert.Equal(uri.ToString(), new BitcoinUriBuilder(uri.ToString(), Network.Main).ToString()); //Some future version that has variables which are (currently) not understood and required and thus invalid: - url = new BitcoinUrlBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz&unknownparam=lol", Network.Main); + uri = new BitcoinUriBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz&unknownparam=lol", Network.Main); //Some future version that has variables which are (currently) not understood but not required and thus valid: - Assert.Throws(() => new BitcoinUrlBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz&req-unknownparam=lol", Network.Main)); - Assert.Throws(() => new BitcoinUrlBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&amount=50", Network.Main)); + Assert.Throws(() => new BitcoinUriBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz&req-unknownparam=lol", Network.Main)); + Assert.Throws(() => new BitcoinUriBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&amount=50", Network.Main)); - url = new BitcoinUrlBuilder("bitcoin:mq7se9wy2egettFxPbmn99cK8v5AFq55Lx?amount=0.11&r=https://merchant.com/pay.php?h%3D2a8628fc2fbe", Network.TestNet); - Assert.Equal("bitcoin:mq7se9wy2egettFxPbmn99cK8v5AFq55Lx?amount=0.11&r=https://merchant.com/pay.php?h%3d2a8628fc2fbe", url.ToString()); - Assert.Equal(url.ToString(), new BitcoinUrlBuilder(url.ToString(), Network.TestNet).ToString()); + uri = new BitcoinUriBuilder("bitcoin:mq7se9wy2egettFxPbmn99cK8v5AFq55Lx?amount=0.11&r=https://merchant.com/pay.php?h%3D2a8628fc2fbe", Network.TestNet); + Assert.Equal("bitcoin:mq7se9wy2egettFxPbmn99cK8v5AFq55Lx?amount=0.11&r=https://merchant.com/pay.php?h%3d2a8628fc2fbe", uri.ToString()); + Assert.Equal(uri.ToString(), new BitcoinUriBuilder(uri.ToString(), Network.TestNet).ToString()); //Support no address - url = new BitcoinUrlBuilder("bitcoin:?r=https://merchant.com/pay.php?h%3D2a8628fc2fbe", Network.Main); - Assert.Equal(url.ToString(), new BitcoinUrlBuilder(url.ToString(), Network.Main).ToString()); + uri = new BitcoinUriBuilder("bitcoin:?r=https://merchant.com/pay.php?h%3D2a8628fc2fbe", Network.Main); + Assert.Equal(uri.ToString(), new BitcoinUriBuilder(uri.ToString(), Network.Main).ToString()); //Support shitcoins - url = new BitcoinUrlBuilder("litecoin:LeLAhU5S7vbVxL4rsT69eMoMrpgV9SNbns", Altcoins.Litecoin.Instance.Mainnet); - Assert.Equal(url.ToString(), new BitcoinUrlBuilder(url.ToString(), Altcoins.Litecoin.Instance.Mainnet).ToString()); - Assert.Equal("litecoin:LeLAhU5S7vbVxL4rsT69eMoMrpgV9SNbns", url.ToString()); + uri = new BitcoinUriBuilder("litecoin:LeLAhU5S7vbVxL4rsT69eMoMrpgV9SNbns", Altcoins.Litecoin.Instance.Mainnet); + Assert.Equal(uri.ToString(), new BitcoinUriBuilder(uri.ToString(), Altcoins.Litecoin.Instance.Mainnet).ToString()); + Assert.Equal("litecoin:LeLAhU5S7vbVxL4rsT69eMoMrpgV9SNbns", uri.ToString()); // Old verison of BitcoinUrl was only supporting bitcoin: to not break existing code, we should support this - url = new BitcoinUrlBuilder("bitcoin:LeLAhU5S7vbVxL4rsT69eMoMrpgV9SNbns", Altcoins.Litecoin.Instance.Mainnet); - Assert.Equal(url.ToString(), new BitcoinUrlBuilder(url.ToString(), Altcoins.Litecoin.Instance.Mainnet).ToString()); - Assert.Equal("bitcoin:LeLAhU5S7vbVxL4rsT69eMoMrpgV9SNbns", url.ToString()); + #pragma warning disable 0618 + uri = new BitcoinUrlBuilder("bitcoin:LeLAhU5S7vbVxL4rsT69eMoMrpgV9SNbns", Altcoins.Litecoin.Instance.Mainnet); + Assert.Equal(uri.ToString(), new BitcoinUrlBuilder(uri.ToString(), Altcoins.Litecoin.Instance.Mainnet).ToString()); + Assert.Equal("bitcoin:LeLAhU5S7vbVxL4rsT69eMoMrpgV9SNbns", uri.ToString()); + #pragma warning restore 0618 } [Fact] [Trait("UnitTest", "UnitTest")] - public void BitcoinUrlKeepUnknownParameter() + public void BitcoinUriKeepUnknownParameter() { - BitcoinUrlBuilder url = new BitcoinUrlBuilder("bitcoin:?r=https://merchant.com/pay.php?h%3D2a8628fc2fbe&idontknow=test", Network.Main); + BitcoinUriBuilder uri = new BitcoinUriBuilder("bitcoin:?r=https://merchant.com/pay.php?h%3D2a8628fc2fbe&idontknow=test", Network.Main); - Assert.Equal("test", url.UnknownParameters["idontknow"]); - Assert.Equal("https://merchant.com/pay.php?h=2a8628fc2fbe", url.UnknownParameters["r"]); + Assert.Equal("test", uri.UnknownParameters["idontknow"]); + Assert.Equal("https://merchant.com/pay.php?h=2a8628fc2fbe", uri.UnknownParameters["r"]); } - private BitcoinUrlBuilder CreateBuilder(string uri) + private BitcoinUriBuilder CreateBuilder(string uri) { - var builder = new BitcoinUrlBuilder(uri, Network.Main); + var builder = new BitcoinUriBuilder(uri, Network.Main); Assert.Equal(builder.Uri.ToString(), uri); - builder = new BitcoinUrlBuilder(new Uri(uri, UriKind.Absolute), Network.Main); + builder = new BitcoinUriBuilder(new Uri(uri, UriKind.Absolute), Network.Main); Assert.Equal(builder.ToString(), uri); return builder; } diff --git a/NBitcoin/Payment/BitcoinUriBuilder.cs b/NBitcoin/Payment/BitcoinUriBuilder.cs new file mode 100644 index 000000000..3e928997a --- /dev/null +++ b/NBitcoin/Payment/BitcoinUriBuilder.cs @@ -0,0 +1,184 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +#if !NOHTTPCLIENT +using System.Net.Http; +using System.Net.Http.Headers; +#endif +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Web.NBitcoin; +using System.Runtime.ExceptionServices; + +namespace NBitcoin.Payment +{ + /// + /// https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + /// + public class BitcoinUriBuilder + { + public BitcoinUriBuilder(Network network) + { + Network = network; + scheme = network.UriScheme; + } + public BitcoinUriBuilder(Uri uri, Network network) + : this(uri.AbsoluteUri, network) + { + if (uri == null) + throw new ArgumentNullException(nameof(uri)); + } + + public Network Network { get; } + string scheme; + + public BitcoinUriBuilder(string uri, Network network) + { + if (uri == null) + throw new ArgumentNullException(nameof(uri)); + if (network == null) + throw new ArgumentNullException(nameof(network)); + + var parsedUri = new Uri(uri, UriKind.Absolute); + scheme = + parsedUri.Scheme.Equals(network.UriScheme, StringComparison.OrdinalIgnoreCase) ? network.UriScheme : + parsedUri.Scheme.Equals("bitcoin", StringComparison.OrdinalIgnoreCase) ? "bitcoin" : + throw new FormatException("Invalid scheme"); + + Network = network; + + if (parsedUri.AbsolutePath is { Length: > 0 } address) + { + Address = Network.Parse(address, network); + } + + Dictionary parameters; + try + { + parameters = UriHelper.DecodeQueryParameters(parsedUri.GetComponents(UriComponents.Query, UriFormat.UriEscaped)); + } + catch (ArgumentException) + { + throw new FormatException("A URI parameter is duplicated"); + } + if (parameters.ContainsKey("amount")) + { + Amount = Money.Parse(parameters["amount"]); + parameters.Remove("amount"); + } + if (parameters.ContainsKey("label")) + { + Label = parameters["label"]; + parameters.Remove("label"); + } + if (parameters.ContainsKey("message")) + { + Message = parameters["message"]; + parameters.Remove("message"); + } + _UnknownParameters = parameters; + var reqParam = parameters.Keys.FirstOrDefault(k => k.StartsWith("req-", StringComparison.OrdinalIgnoreCase)); + if (reqParam != null) + throw new FormatException("Non compatible required parameter " + reqParam); + } + + private readonly Dictionary _UnknownParameters = new Dictionary(); + public IReadOnlyDictionary UnknownParameters + { + get + { + return _UnknownParameters; + } + } + [Obsolete("Use UnknownParameters property")] + public Dictionary UnknowParameters + { + get + { + return _UnknownParameters; + } + } + + public BitcoinAddress? Address + { + get; + set; + } + public Money? Amount + { + get; + set; + } + public string? Label + { + get; + set; + } + public string? Message + { + get; + set; + } + public Uri Uri + { + get + { + Dictionary parameters = new Dictionary(); + StringBuilder builder = new StringBuilder(); + builder.Append($"{scheme}:"); + if (Address != null) + { + builder.Append(Address.ToString()); + } + + if (Amount != null) + { + parameters.Add("amount", Amount.ToString(false, true)); + } + if (Label != null) + { + parameters.Add("label", Label.ToString()); + } + if (Message != null) + { + parameters.Add("message", Message.ToString()); + } + + foreach (var kv in UnknownParameters) + { + parameters.Add(kv.Key, kv.Value); + } + + WriteParameters(parameters, builder); + + return new System.Uri(builder.ToString(), UriKind.Absolute); + } + } + + private static void WriteParameters(Dictionary parameters, StringBuilder builder) + { + bool first = true; + foreach (var parameter in parameters) + { + if (first) + { + first = false; + builder.Append("?"); + } + else + builder.Append("&"); + builder.Append(parameter.Key); + builder.Append("="); + builder.Append(System.Web.NBitcoin.HttpUtility.UrlEncode(parameter.Value)); + } + } + + public override string ToString() + { + return Uri.AbsoluteUri; + } + } +} diff --git a/NBitcoin/Payment/BitcoinUrlBuilder.cs b/NBitcoin/Payment/BitcoinUrlBuilder.cs index 817546863..f2b97d090 100644 --- a/NBitcoin/Payment/BitcoinUrlBuilder.cs +++ b/NBitcoin/Payment/BitcoinUrlBuilder.cs @@ -1,191 +1,33 @@ #nullable enable using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -#if !NOHTTPCLIENT -using System.Net.Http; -using System.Net.Http.Headers; -#endif -using System.Text; -using System.Threading.Tasks; -using System.Web; -using System.Web.NBitcoin; -using System.Runtime.ExceptionServices; namespace NBitcoin.Payment { - /// - /// https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki - /// - public class BitcoinUrlBuilder + [Obsolete("Use BitcoinUriBuilder instead")] + public class BitcoinUrlBuilder : BitcoinUriBuilder { - [Obsolete("Use BitcoinUrlBuilder(Network) instead")] - public BitcoinUrlBuilder() + [Obsolete("Use BitcoinUriBuilder(Network) instead")] + public BitcoinUrlBuilder() : + base(Bitcoin.Instance.Mainnet) { - Network = Bitcoin.Instance.Mainnet; - scheme = "bitcoin"; - } - public BitcoinUrlBuilder(Network network) - { - Network = network; - scheme = network.UriScheme; - } - public BitcoinUrlBuilder(Uri uri, Network network) - : this(uri.AbsoluteUri, network) - { - if (uri == null) - throw new ArgumentNullException(nameof(uri)); - } - - public Network Network { get; } - string scheme; - - public BitcoinUrlBuilder(string uri, Network network) - { - if (uri == null) - throw new ArgumentNullException(nameof(uri)); - if (network == null) - throw new ArgumentNullException(nameof(network)); - - var parsedUri = new Uri(uri, UriKind.Absolute); - - scheme = - parsedUri.Scheme.Equals(network.UriScheme, StringComparison.OrdinalIgnoreCase) ? network.UriScheme : - parsedUri.Scheme.Equals("bitcoin", StringComparison.OrdinalIgnoreCase) ? "bitcoin" : - throw new FormatException("Invalid scheme"); - - Network = network; - - if (parsedUri.AbsolutePath is { Length: > 0 } address) - { - Address = Network.Parse(address, network); - } - - Dictionary parameters; - try - { - parameters = UriHelper.DecodeQueryParameters(parsedUri.GetComponents(UriComponents.Query, UriFormat.UriEscaped)); - } - catch (ArgumentException) - { - throw new FormatException("A URI parameter is duplicated"); - } - if (parameters.ContainsKey("amount")) - { - Amount = Money.Parse(parameters["amount"]); - parameters.Remove("amount"); - } - if (parameters.ContainsKey("label")) - { - Label = parameters["label"]; - parameters.Remove("label"); - } - if (parameters.ContainsKey("message")) - { - Message = parameters["message"]; - parameters.Remove("message"); - } - _UnknownParameters = parameters; - var reqParam = parameters.Keys.FirstOrDefault(k => k.StartsWith("req-", StringComparison.OrdinalIgnoreCase)); - if (reqParam != null) - throw new FormatException("Non compatible required parameter " + reqParam); - } - - private readonly Dictionary _UnknownParameters = new Dictionary(); - public IReadOnlyDictionary UnknownParameters - { - get - { - return _UnknownParameters; - } - } - [Obsolete("Use UnknownParameters property")] - public Dictionary UnknowParameters - { - get - { - return _UnknownParameters; - } } - public BitcoinAddress? Address - { - get; - set; - } - public Money? Amount - { - get; - set; - } - public string? Label + [Obsolete("Use BitcoinUriBuilder(network) instead")] + public BitcoinUrlBuilder(Network network) : + base(network) { - get; - set; - } - public string? Message - { - get; - set; - } - public Uri Uri - { - get - { - Dictionary parameters = new Dictionary(); - StringBuilder builder = new StringBuilder(); - builder.Append($"{scheme}:"); - if (Address != null) - { - builder.Append(Address.ToString()); - } - - if (Amount != null) - { - parameters.Add("amount", Amount.ToString(false, true)); - } - if (Label != null) - { - parameters.Add("label", Label.ToString()); - } - if (Message != null) - { - parameters.Add("message", Message.ToString()); - } - - foreach (var kv in UnknownParameters) - { - parameters.Add(kv.Key, kv.Value); - } - - WriteParameters(parameters, builder); - - return new System.Uri(builder.ToString(), UriKind.Absolute); - } } - private static void WriteParameters(Dictionary parameters, StringBuilder builder) + [Obsolete("Use BitcoinUriBuilder(uri, network) instead")] + public BitcoinUrlBuilder(Uri uri, Network network) : + base(uri, network) { - bool first = true; - foreach (var parameter in parameters) - { - if (first) - { - first = false; - builder.Append("?"); - } - else - builder.Append("&"); - builder.Append(parameter.Key); - builder.Append("="); - builder.Append(System.Web.NBitcoin.HttpUtility.UrlEncode(parameter.Value)); - } } - public override string ToString() + [Obsolete("Use BitcoinUriBuilder(uri, network) instead")] + public BitcoinUrlBuilder(string uri, Network network) : + base(uri, network) { - return Uri.AbsoluteUri; } } }