Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename BitcoinUrlBuilder to BitcoinUriBuilder #1168

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 39 additions & 37 deletions NBitcoin.Tests/PaymentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FormatException>(() => new BitcoinUrlBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz&req-unknownparam=lol", Network.Main));
Assert.Throws<FormatException>(() => new BitcoinUrlBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&amount=50", Network.Main));
Assert.Throws<FormatException>(() => new BitcoinUriBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz&req-unknownparam=lol", Network.Main));
Assert.Throws<FormatException>(() => 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;
}
Expand Down
184 changes: 184 additions & 0 deletions NBitcoin/Payment/BitcoinUriBuilder.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
/// </summary>
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<BitcoinAddress>(address, network);
}

Dictionary<string, string> 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<string, string> _UnknownParameters = new Dictionary<string, string>();
public IReadOnlyDictionary<string, string> UnknownParameters
{
get
{
return _UnknownParameters;
}
}
[Obsolete("Use UnknownParameters property")]
public Dictionary<string, string> 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<string, string> parameters = new Dictionary<string, string>();
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<string, string> 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;
}
}
}
Loading