Skip to content

Commit

Permalink
Rename BitcoinUrlBuilder to BitcoinUriBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
kristapsk committed Apr 12, 2023
1 parent f4e81ce commit f5575ab
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 223 deletions.
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
198 changes: 198 additions & 0 deletions NBitcoin/Payment/BitcoinUriBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#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));
scheme = network.UriScheme;
if (!uri.StartsWith($"{scheme}:", StringComparison.OrdinalIgnoreCase))
{
if (uri.StartsWith("bitcoin:", StringComparison.OrdinalIgnoreCase))
scheme = "bitcoin";
else
throw new FormatException("Invalid scheme");
}
Network = network;
uri = uri.Remove(0, $"{scheme}:".Length);
if (uri.StartsWith("//"))
uri = uri.Remove(0, 2);

var paramStart = uri.IndexOf('?');
string? address = null;
if (paramStart == -1)
address = uri;
else
{
address = uri.Substring(0, paramStart);
uri = uri.Remove(0, 1); //remove ?
}
if (address != String.Empty)
{
Address = Network.Parse<BitcoinAddress>(address, network);
}
uri = uri.Remove(0, address.Length);

Dictionary<string, string> parameters;
try
{
parameters = UriHelper.DecodeQueryParameters(uri);
}
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

0 comments on commit f5575ab

Please sign in to comment.