diff --git a/src/com/google/bitcoin/core/ECKey.java b/src/com/google/bitcoin/core/ECKey.java
index 81af7f72..be9ad483 100644
--- a/src/com/google/bitcoin/core/ECKey.java
+++ b/src/com/google/bitcoin/core/ECKey.java
@@ -181,15 +181,21 @@ public class ECKey implements Serializable {
public String toString() {
StringBuffer b = new StringBuffer();
b.append("pub:").append(Utils.bytesToHexString(pub));
- if (priv != null) {
- b.append(" priv:").append(Utils.bytesToHexString(priv.toByteArray()));
- }
if (creationTimeSeconds != 0) {
b.append(" timestamp:" + creationTimeSeconds);
}
return b.toString();
}
+ public String toStringWithPrivate() {
+ StringBuffer b = new StringBuffer();
+ b.append(toString());
+ if (priv != null) {
+ b.append(" priv:").append(Utils.bytesToHexString(priv.toByteArray()));
+ }
+ return b.toString();
+ }
+
/**
* Returns the address that corresponds to the public part of this ECKey. Note that an address is derived from
* the RIPEMD-160 hash of the public key and is not the public key itself (which is too large to be convenient).
diff --git a/src/com/google/bitcoin/core/Utils.java b/src/com/google/bitcoin/core/Utils.java
index b8bd24af..7d476a87 100644
--- a/src/com/google/bitcoin/core/Utils.java
+++ b/src/com/google/bitcoin/core/Utils.java
@@ -275,6 +275,26 @@ public class Utils {
BigInteger cents = value.remainder(COIN);
return String.format("%s%d.%02d", negative ? "-" : "", coins.intValue(), cents.intValue() / 1000000);
}
+
+ /**
+ *
+ * Returns the given value as a plain string denominated in BTC.
+ * The result is unformatted with no trailing zeroes.
+ * For instance, an input value of BigInteger.valueOf(150000) nanocoin gives an output string of "0.0015" BTC
+ *
+ *
+ * @param value The value in nanocoins to convert to a string (denominated in BTC)
+ * @throws IllegalArgumentException
+ * If the input value is null
+ */
+ public static String bitcoinValueToPlainString(BigInteger value) {
+ if (value == null) {
+ throw new IllegalArgumentException("Value cannot be null");
+ }
+
+ BigDecimal valueInBTC = new BigDecimal(value).divide(new BigDecimal(Utils.COIN));
+ return valueInBTC.toPlainString();
+ }
/**
* MPI encoded numbers are produced by the OpenSSL BN_bn2mpi function. They consist of
diff --git a/src/com/google/bitcoin/uri/BitcoinURI.java b/src/com/google/bitcoin/uri/BitcoinURI.java
new file mode 100644
index 00000000..d0b9c8e2
--- /dev/null
+++ b/src/com/google/bitcoin/uri/BitcoinURI.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright 2012 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.google.bitcoin.uri;
+
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLDecoder;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.bitcoin.core.Address;
+import com.google.bitcoin.core.AddressFormatException;
+import com.google.bitcoin.core.NetworkParameters;
+import com.google.bitcoin.core.Utils;
+
+/**
+ *
+ * Provides a standard implementation of a Bitcoin URI with support for the
+ * following:
+ *
+ *
+ * - URLEncoded URIs (as passed in by IE on the command line)
+ * - BIP21 names (including the "req-" prefix handling requirements)
+ *
+ * Accepted formats
+ *
+ * The following input forms are accepted
+ *
+ *
+ * - {@code bitcoin:}
+ * - {@code bitcoin:?=&=} with multiple
+ * additional name/value pairs
+ *
+ *
+ * The name/value pairs are processed as follows:
+ *
+ *
+ * - URL encoding is stripped and treated as UTF-8
+ * - names prefixed with {@code req-} are treated as required and if unknown
+ * or conflicting cause a parse exception
+ * - Unknown names not prefixed with {@code req-} are added to a Map, accessible
+ * by parameter name
+ * - Known names not prefixed with {@code req-} are processed unless they are
+ * malformed
+ *
+ *
+ * The following names are known and have the following formats
+ *
+ *
+ * - {@code amount} decimal value to 8 dp (e.g. 0.12345678) Note that the
+ * exponent notation is not supported any more
+ * - {@code label} any URL encoded alphanumeric
+ * - {@code message} any URL encoded alphanumeric
+ *
+ *
+ * @author Andreas Schildbach (initial code)
+ * @author Jim Burton (enhancements for MultiBit)
+ * @author Gary Rowe (BIP21 support)
+ * @see BIP 0021
+ */
+public class BitcoinURI {
+ /**
+ * Provides logging for this class
+ */
+ private static final Logger log = LoggerFactory.getLogger(BitcoinURI.class);
+
+ // Not worth turning into an enum
+ private static final String FIELD_MESSAGE = "message";
+ private static final String FIELD_LABEL = "label";
+ private static final String FIELD_AMOUNT = "amount";
+ private static final String FIELD_ADDRESS = "address";
+
+ public static final String BITCOIN_SCHEME = "bitcoin";
+ private static final String ENCODED_SPACE_CHARACTER = "%20";
+ private static final String AMPERSAND_SEPARATOR = "&";
+ private static final String QUESTION_MARK_SEPARATOR = "?";
+ private static final String COLON_SEPARATOR = ":";
+
+ /**
+ * Contains all the parameters in the order in which they were processed
+ */
+ private final Map parameterMap = new LinkedHashMap();
+
+ /**
+ * @param networkParameters
+ * The BitCoinJ network parameters that determine which network
+ * the URI is from
+ * @param input
+ * The raw URI data to be parsed (see class comments for accepted
+ * formats)
+ * @throws BitcoinURIParseException
+ * If the input fails Bitcoin URI syntax and semantic checks
+ */
+ public BitcoinURI(NetworkParameters networkParameters, String input) {
+ // Basic validation
+ if (networkParameters == null) {
+ throw new IllegalArgumentException("NetworkParameters cannot be null");
+ }
+ if (input == null) {
+ throw new IllegalArgumentException("Input cannot be null");
+ }
+
+ log.debug("Attempting to parse '{}' for {}", input, networkParameters.port == 8333 ? "prodNet" : "testNet");
+
+ // URI validation
+ if (!input.startsWith(BITCOIN_SCHEME)) {
+ throw new BitcoinURIParseException("Bad scheme - expecting '" + BITCOIN_SCHEME + "'");
+ }
+
+ // Attempt to form the URI (fail fast syntax checking to official standards)
+ URI uri;
+ try {
+ uri = new URI(input);
+ } catch (URISyntaxException e) {
+ throw new BitcoinURIParseException("Bad URI syntax", e);
+ }
+
+ // URI is formed as bitcoin:?
+
+ // Remove the bitcoin scheme
+ // (Note: getSchemeSpecificPart() is not used as it unescapes the label and parse then fails.
+ // For instance with : bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=0.06&label=Tom%20%26%20Jerry
+ // the & (%26) in Tom and Jerry gets interpreted as a separator and the label then gets parsed as 'Tom ' instead of 'Tom & Jerry')
+ String schemeSpecificPart = "";
+ if (uri.toString().startsWith(BITCOIN_SCHEME + COLON_SEPARATOR)) {
+ schemeSpecificPart = uri.toString().substring(BITCOIN_SCHEME.length() + 1);
+ }
+
+ // Split off the address from the rest of the query parameters
+ String[] addressSplitTokens = schemeSpecificPart.split("\\?");
+ if (addressSplitTokens.length == 0 || "".equals(addressSplitTokens[0])) {
+ throw new BitcoinURIParseException("Missing address");
+ }
+ String addressToken = addressSplitTokens[0];
+
+ String[] nameValuePairTokens;
+ if (addressSplitTokens.length == 1) {
+ // only an address is specified - use an empty '=' token array
+ nameValuePairTokens = new String[] {};
+ } else {
+ if (addressSplitTokens.length == 2) {
+ // split into '=' tokens
+ nameValuePairTokens = addressSplitTokens[1].split("&");
+ } else {
+ throw new BitcoinURIParseException("Too many question marks in URI '" + input + "'");
+ }
+ }
+
+ // Attempt to parse the rest of the URI parameters
+ parseParameters(networkParameters, addressToken, nameValuePairTokens);
+ }
+
+ /**
+ * @param networkParameters
+ * The network parameters
+ * @param nameValuePairTokens
+ * The tokens representing the name value pairs (assumed to be
+ * separated by '=' e.g. 'amount=0.2')
+ */
+ private void parseParameters(NetworkParameters networkParameters, String addressToken, String[] nameValuePairTokens) {
+ // Attempt to parse the addressToken as a Bitcoin address for this network
+ try {
+ Address address = new Address(networkParameters, addressToken);
+ putWithValidation(FIELD_ADDRESS, address);
+ } catch (final AddressFormatException e) {
+ throw new BitcoinURIParseException("Bad address", e);
+ }
+
+ // Attempt to decode the rest of the tokens into a parameter map
+ for (int i = 0; i < nameValuePairTokens.length; i++) {
+
+ String[] tokens = nameValuePairTokens[i].split("=");
+ if (tokens.length != 2 || "".equals(tokens[0])) {
+ throw new BitcoinURIParseException("Malformed Bitcoin URI - cannot parse name value pair '" + nameValuePairTokens[i] + "'");
+ }
+
+ String nameToken = tokens[0].toLowerCase();
+ String valueToken = tokens[1];
+
+ // Parse the amount
+ if (FIELD_AMOUNT.equals(nameToken)) {
+ // Decode the amount (contains an optional decimal component to 8dp)
+ try {
+ BigInteger amount = Utils.toNanoCoins(valueToken);
+ putWithValidation(FIELD_AMOUNT, amount);
+ } catch (NumberFormatException e) {
+ throw new OptionalFieldValidationException("'" + valueToken + "' value is not a valid amount", e);
+ }
+ } else {
+ if (nameToken.startsWith("req-")) {
+ // A required parameter that we do not know about
+ throw new RequiredFieldValidationException("'" + nameToken + "' is required but not known, this URI is not valid");
+ } else {
+ // Known fields and unknown parameters that are optional
+ try {
+ putWithValidation(nameToken, URLDecoder.decode(valueToken, "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ // should not happen as UTF-8 is valid encoding
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+
+ // Note to the future : when you want to implement 'req-expires' have a look at commit 410a53791841 which had it in
+ }
+
+ /**
+ *
+ * Put the value against the key in the map checking for duplication.
+ * This avoids address field overwrite etc.
+ *
+ *
+ * @param key
+ * The key for the map
+ * @param value
+ * The value to store
+ */
+ private void putWithValidation(String key, Object value) {
+ if (parameterMap.containsKey(key)) {
+ throw new BitcoinURIParseException("'" + key + "' is duplicated, URI is invalid");
+ } else {
+ parameterMap.put(key, value);
+ }
+ }
+
+ /**
+ * @return The Bitcoin Address from the URI
+ */
+ public Address getAddress() {
+ return (Address) parameterMap.get(FIELD_ADDRESS);
+ }
+
+ /**
+ * @return The amount name encoded using a pure integer value based at
+ * 10,000,000 units is 1 BTC. May be null if no amount is specified
+ */
+ public BigInteger getAmount() {
+ return (BigInteger) parameterMap.get(FIELD_AMOUNT);
+ }
+
+ /**
+ * @return The label from the URI.
+ */
+ public String getLabel() {
+ return (String) parameterMap.get(FIELD_LABEL);
+ }
+
+ /**
+ * @return The message from the URI.
+ */
+ public String getMessage() {
+ return (String) parameterMap.get(FIELD_MESSAGE);
+ }
+
+ /**
+ * @param name The name of the parameter
+ * @return The parameter value, or null if not present
+ */
+ public Object getParameterByName(String name) {
+ return parameterMap.get(name);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder("BitcoinURI[");
+ boolean first = true;
+ for (Map.Entry entry : parameterMap.entrySet()) {
+ if (first) {
+ first = false;
+ } else {
+ builder.append(",");
+ }
+ builder.append("'").append(entry.getKey()).append("'=").append("'").append(entry.getValue().toString()).append("'");
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+
+ /**
+ *
+ * Simple Bitcoin URI builder using known good fields
+ *
+ *
+ * @param address
+ * The Bitcoin address
+ * @param amount
+ * The amount in nanocoins (decimal)
+ * @param label
+ * A label
+ * @param message
+ * A message
+ * @return A String containing the Bitcoin URI
+ */
+ public static String convertToBitcoinURI(Address address, BigInteger amount, String label, String message) {
+ if (address == null) {
+ throw new IllegalArgumentException("Missing address");
+ }
+
+ if (amount != null && amount.compareTo(BigInteger.ZERO) < 0) {
+ throw new IllegalArgumentException("Amount must be positive");
+ }
+
+ StringBuilder builder = new StringBuilder();
+ builder.append(BITCOIN_SCHEME).append(COLON_SEPARATOR).append(address.toString());
+
+ boolean questionMarkHasBeenOutput = false;
+
+ if (amount != null) {
+ builder.append(QUESTION_MARK_SEPARATOR).append(FIELD_AMOUNT).append("=");
+ builder.append(Utils.bitcoinValueToPlainString(amount));
+ questionMarkHasBeenOutput = true;
+ }
+
+ if (label != null && !"".equals(label)) {
+ if (questionMarkHasBeenOutput) {
+ builder.append(AMPERSAND_SEPARATOR);
+ } else {
+ builder.append(QUESTION_MARK_SEPARATOR);
+ questionMarkHasBeenOutput = true;
+ }
+ builder.append(FIELD_LABEL).append("=").append(encodeURLString(label));
+ }
+
+ if (message != null && !"".equals(message)) {
+ if (questionMarkHasBeenOutput) {
+ builder.append(AMPERSAND_SEPARATOR);
+ } else {
+ builder.append(QUESTION_MARK_SEPARATOR);
+ questionMarkHasBeenOutput = true;
+ }
+ builder.append(FIELD_MESSAGE).append("=").append(encodeURLString(message));
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ *
+ * Encode a string using URL encoding
+ *
+ *
+ * @param stringToEncode
+ * The string to URL encode
+ */
+ static String encodeURLString(String stringToEncode) {
+ try {
+ return java.net.URLEncoder.encode(stringToEncode, "UTF-8").replace("+", ENCODED_SPACE_CHARACTER);
+ } catch (UnsupportedEncodingException e) {
+ // should not happen - UTF-8 is a valid encoding
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/com/google/bitcoin/uri/BitcoinURIParseException.java b/src/com/google/bitcoin/uri/BitcoinURIParseException.java
new file mode 100644
index 00000000..51fa61da
--- /dev/null
+++ b/src/com/google/bitcoin/uri/BitcoinURIParseException.java
@@ -0,0 +1,24 @@
+package com.google.bitcoin.uri;
+
+/**
+ * Exception to provide the following to {@link BitcoinURI}:
+ *
+ * - Provision of parsing error messages
+ *
+ * This base exception acts as a general failure mode not attributable to a specific cause (other than
+ * that reported in the exception message). Since this is in English, it may not be worth reporting directly
+ * to the user other than as part of a "general failure to parse" response.
+ *
+ * @since 0.3.0
+ *
+ */
+public class BitcoinURIParseException extends RuntimeException {
+
+ public BitcoinURIParseException(String s) {
+ super(s);
+ }
+
+ public BitcoinURIParseException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+}
diff --git a/src/com/google/bitcoin/uri/OptionalFieldValidationException.java b/src/com/google/bitcoin/uri/OptionalFieldValidationException.java
new file mode 100644
index 00000000..3d84652b
--- /dev/null
+++ b/src/com/google/bitcoin/uri/OptionalFieldValidationException.java
@@ -0,0 +1,23 @@
+package com.google.bitcoin.uri;
+
+/**
+ * Exception to provide the following to {@link org.multibit.qrcode.BitcoinURI}:
+ *
+ * - Provision of parsing error messages
+ *
+ * This exception occurs when an optional field is detected (under the Bitcoin URI scheme) and fails
+ * to pass the associated test (such as {@code amount} not being a valid number).
+ *
+ * @since 0.3.0
+ *
+ */
+public class OptionalFieldValidationException extends BitcoinURIParseException {
+
+ public OptionalFieldValidationException(String s) {
+ super(s);
+ }
+
+ public OptionalFieldValidationException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+}
diff --git a/src/com/google/bitcoin/uri/RequiredFieldValidationException.java b/src/com/google/bitcoin/uri/RequiredFieldValidationException.java
new file mode 100644
index 00000000..85a6bb78
--- /dev/null
+++ b/src/com/google/bitcoin/uri/RequiredFieldValidationException.java
@@ -0,0 +1,24 @@
+package com.google.bitcoin.uri;
+
+/**
+ * Exception to provide the following to {@link BitcoinURI}:
+ *
+ * - Provision of parsing error messages
+ *
+ * This exception occurs when a required field is detected (under the BIP21 rules) and fails
+ * to pass the associated test (such as {@code req-expires} being out of date), or the required field is unknown
+ * to this version of the client in which case it should fail for security reasons.
+ *
+ * @since 0.3.0
+ *
+ */
+public class RequiredFieldValidationException extends BitcoinURIParseException {
+
+ public RequiredFieldValidationException(String s) {
+ super(s);
+ }
+
+ public RequiredFieldValidationException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+}
diff --git a/tests/com/google/bitcoin/core/UtilsTest.java b/tests/com/google/bitcoin/core/UtilsTest.java
index 8bd99b4a..d004dcbc 100644
--- a/tests/com/google/bitcoin/core/UtilsTest.java
+++ b/tests/com/google/bitcoin/core/UtilsTest.java
@@ -16,10 +16,15 @@
package com.google.bitcoin.core;
+import static com.google.bitcoin.core.Utils.CENT;
+import static com.google.bitcoin.core.Utils.COIN;
+import static com.google.bitcoin.core.Utils.bitcoinValueToFriendlyString;
+import static com.google.bitcoin.core.Utils.bitcoinValueToPlainString;
+import static com.google.bitcoin.core.Utils.toNanoCoins;
import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.fail;
+import static org.junit.Assert.assertTrue;
-import static com.google.bitcoin.core.Utils.*;
+import java.math.BigInteger;
import org.junit.Assert;
import org.junit.Test;
@@ -34,7 +39,7 @@ public class UtilsTest {
assertEquals(COIN.add(Utils.CENT), toNanoCoins("1.01"));
try {
toNanoCoins("2E-20");
- fail("should not have accepted fractional nanocoins");
+ org.junit.Assert.fail("should not have accepted fractional nanocoins");
} catch (ArithmeticException e) {
}
@@ -53,6 +58,44 @@ public class UtilsTest {
assertEquals("-1.23", bitcoinValueToFriendlyString(toNanoCoins(1, 23).negate()));
}
+ /**
+ * Test the bitcoinValueToPlainString amount formatter
+ */
+ @Test
+ public void testBitcoinValueToPlainString() {
+ // null argument check
+ try {
+ bitcoinValueToPlainString(null);
+ org.junit.Assert.fail("Expecting IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("Value cannot be null"));
+ }
+
+ assertEquals("0.0015", bitcoinValueToPlainString(BigInteger.valueOf(150000)));
+ assertEquals("1.23", bitcoinValueToPlainString(toNanoCoins("1.23")));
+ assertEquals("-1.23", bitcoinValueToPlainString(toNanoCoins("-1.23")));
+
+ assertEquals("0.1", bitcoinValueToPlainString(toNanoCoins("0.1")));
+ assertEquals("1.1", bitcoinValueToPlainString(toNanoCoins("1.1")));
+ assertEquals("21.12", bitcoinValueToPlainString(toNanoCoins("21.12")));
+ assertEquals("321.123", bitcoinValueToPlainString(toNanoCoins("321.123")));
+ assertEquals("4321.1234", bitcoinValueToPlainString(toNanoCoins("4321.1234")));
+ assertEquals("54321.12345", bitcoinValueToPlainString(toNanoCoins("54321.12345")));
+ assertEquals("654321.123456", bitcoinValueToPlainString(toNanoCoins("654321.123456")));
+ assertEquals("7654321.1234567", bitcoinValueToPlainString(toNanoCoins("7654321.1234567")));
+ assertEquals("87654321.12345678", bitcoinValueToPlainString(toNanoCoins("87654321.12345678")));
+
+ // check there are no trailing zeros
+ assertEquals("1", bitcoinValueToPlainString(toNanoCoins("1.0")));
+ assertEquals("2", bitcoinValueToPlainString(toNanoCoins("2.00")));
+ assertEquals("3", bitcoinValueToPlainString(toNanoCoins("3.000")));
+ assertEquals("4", bitcoinValueToPlainString(toNanoCoins("4.0000")));
+ assertEquals("5", bitcoinValueToPlainString(toNanoCoins("5.00000")));
+ assertEquals("6", bitcoinValueToPlainString(toNanoCoins("6.000000")));
+ assertEquals("7", bitcoinValueToPlainString(toNanoCoins("7.0000000")));
+ assertEquals("8", bitcoinValueToPlainString(toNanoCoins("8.00000000")));
+ }
+
@Test
public void testReverseBytes() {
Assert.assertArrayEquals(new byte[] {1,2,3,4,5}, Utils.reverseBytes(new byte[] {5,4,3,2,1}));
diff --git a/tests/com/google/bitcoin/uri/BitcoinURITest.java b/tests/com/google/bitcoin/uri/BitcoinURITest.java
new file mode 100644
index 00000000..381a628e
--- /dev/null
+++ b/tests/com/google/bitcoin/uri/BitcoinURITest.java
@@ -0,0 +1,448 @@
+/*
+ * Copyright 2012 the original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package com.google.bitcoin.uri;
+
+import static junit.framework.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.UnsupportedEncodingException;
+
+import org.junit.Test;
+
+import com.google.bitcoin.core.Address;
+import com.google.bitcoin.core.AddressFormatException;
+import com.google.bitcoin.core.NetworkParameters;
+import com.google.bitcoin.core.Utils;
+
+public class BitcoinURITest {
+
+ private BitcoinURI testObject = null;
+
+ private static final String PRODNET_GOOD_ADDRESS = "1KzTSfqjF2iKCduwz59nv2uqh1W2JsTxZH";
+
+ /**
+ * Tests conversion to Bitcoin URI
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ * @throws AddressFormatException
+ */
+ @Test
+ public void testConvertToBitcoinURI() throws BitcoinURIParseException, AddressFormatException {
+ Address goodAddress = new Address(NetworkParameters.prodNet(), PRODNET_GOOD_ADDRESS);
+
+ // simple example
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS + "?amount=12.34&label=Hello&message=AMessage", BitcoinURI.convertToBitcoinURI(goodAddress, Utils.toNanoCoins("12.34"), "Hello", "AMessage"));
+
+ // example with spaces, ampersand and plus
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS + "?amount=12.34&label=Hello%20World&message=Mess%20%26%20age%20%2B%20hope", BitcoinURI.convertToBitcoinURI(goodAddress, Utils.toNanoCoins("12.34"), "Hello World", "Mess & age + hope"));
+
+ // address null
+ try {
+ BitcoinURI.convertToBitcoinURI(null, Utils.toNanoCoins("0.1"), "hope", "glory");
+ fail("Expecting IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("address"));
+ }
+
+ // amount negative
+ try {
+ BitcoinURI.convertToBitcoinURI(goodAddress, Utils.toNanoCoins("-0.1"), "hope", "glory");
+ fail("Expecting IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("Amount must be positive"));
+ }
+
+ // no amount, label present, message present
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS + "?label=Hello&message=glory", BitcoinURI.convertToBitcoinURI(goodAddress, null, "Hello", "glory"));
+
+ // amount present, no label, message present
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS + "?amount=0.1&message=glory", BitcoinURI.convertToBitcoinURI(goodAddress, Utils.toNanoCoins("0.1"), null, "glory"));
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS + "?amount=0.1&message=glory", BitcoinURI.convertToBitcoinURI(goodAddress, Utils.toNanoCoins("0.1"), "", "glory"));
+
+ // amount present, label present, no message
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS + "?amount=12.34&label=Hello", BitcoinURI.convertToBitcoinURI(goodAddress,Utils.toNanoCoins("12.34"), "Hello", null));
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS + "?amount=12.34&label=Hello", BitcoinURI.convertToBitcoinURI(goodAddress, Utils.toNanoCoins("12.34"), "Hello", ""));
+
+ // amount present, no label, no message
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS + "?amount=1000", BitcoinURI.convertToBitcoinURI(goodAddress, Utils.toNanoCoins("1000"), null, null));
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS + "?amount=1000", BitcoinURI.convertToBitcoinURI(goodAddress, Utils.toNanoCoins("1000"), "", ""));
+
+ // no amount, label present, no message
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS + "?label=Hello", BitcoinURI.convertToBitcoinURI(goodAddress, null, "Hello", null));
+
+ // no amount, no label, message present
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS + "?message=Agatha", BitcoinURI.convertToBitcoinURI(goodAddress, null, null, "Agatha"));
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS + "?message=Agatha", BitcoinURI.convertToBitcoinURI(goodAddress, null, "", "Agatha"));
+
+ // no amount, no label, no message
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS, BitcoinURI.convertToBitcoinURI(goodAddress, null, null, null));
+ assertEquals("bitcoin:" + PRODNET_GOOD_ADDRESS, BitcoinURI.convertToBitcoinURI(goodAddress, null, "", ""));
+ }
+
+ /**
+ * Test the simplest well-formed URI
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ */
+ @Test
+ public void testGood_Simple() throws BitcoinURIParseException {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS);
+ assertNotNull(testObject);
+ assertNull("Unexpected amount", testObject.getAmount());
+ assertNull("Unexpected label", testObject.getLabel());
+ assertEquals("Unexpected label", 20, testObject.getAddress().getHash160().length);
+ }
+
+ /**
+ * Test missing constructor parameters
+ */
+ @Test
+ public void testBad_Constructor() {
+ try {
+ testObject = new BitcoinURI(null, "blimpcoin:" + PRODNET_GOOD_ADDRESS);
+ fail("Expecting IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("NetworkParameters"));
+ }
+
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), null);
+ fail("Expecting IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("Input"));
+ }
+ }
+
+ /**
+ * Test a broken URI (bad scheme)
+ */
+ @Test
+ public void testBad_Scheme() {
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), "blimpcoin:" + PRODNET_GOOD_ADDRESS);
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("Bad scheme"));
+ }
+ }
+
+ /**
+ * Test a broken URI (bad syntax)
+ */
+ @Test
+ public void testBad_BadSyntax() {
+ // Various illegal characters
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + "|" + PRODNET_GOOD_ADDRESS);
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("Bad URI syntax"));
+ }
+
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS + "\\");
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("Bad URI syntax"));
+ }
+
+ // Separator without field
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":");
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("Bad URI syntax"));
+ }
+ }
+
+ /**
+ * Test a broken URI (missing address)
+ */
+ @Test
+ public void testBad_Address() {
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME);
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("Missing address"));
+ }
+ }
+
+ /**
+ * Test a broken URI (bad address type)
+ */
+ @Test
+ public void testBad_IncorrectAddressType() {
+ try {
+ testObject = new BitcoinURI(NetworkParameters.testNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS);
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("Bad address"));
+ }
+ }
+
+ /**
+ * Handles a simple amount
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ */
+ @Test
+ public void testGood_Amount() throws BitcoinURIParseException {
+ // Test the decimal parsing
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?amount=9876543210.12345678");
+ assertEquals("987654321012345678", testObject.getAmount().toString());
+
+ // Test the decimal parsing
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?amount=.12345678");
+ assertEquals("12345678", testObject.getAmount().toString());
+
+ // Test the integer parsing
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?amount=9876543210");
+ assertEquals("987654321000000000", testObject.getAmount().toString());
+ }
+
+ /**
+ * Handles a simple label
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ */
+ @Test
+ public void testGood_Label() throws BitcoinURIParseException {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?label=Hello%20World");
+ assertEquals("Hello World", testObject.getLabel());
+ }
+
+ /**
+ * Handles a simple label with an embedded ampersand and plus
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ * @throws UnsupportedEncodingException
+ */
+ @Test
+ public void testGood_LabelWithAmpersandAndPlus() throws BitcoinURIParseException, UnsupportedEncodingException {
+ String testString = "Hello Earth & Mars + Venus";
+ String encodedLabel = BitcoinURI.encodeURLString(testString);
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS + "?label="
+ + encodedLabel);
+ assertEquals(testString, testObject.getLabel());
+ }
+
+ /**
+ * Handles a Russian label (Unicode test)
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ * @throws UnsupportedEncodingException
+ */
+ @Test
+ public void testGood_LabelWithRussian() throws BitcoinURIParseException, UnsupportedEncodingException {
+ // Moscow in Russian in Cyrillic
+ String moscowString = "\u041c\u043e\u0441\u043a\u0432\u0430";
+ String encodedLabel = BitcoinURI.encodeURLString(moscowString);
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS + "?label="
+ + encodedLabel);
+ assertEquals(moscowString, testObject.getLabel());
+ }
+
+ /**
+ * Handles a simple message
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ */
+ @Test
+ public void testGood_Message() throws BitcoinURIParseException {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?message=Hello%20World");
+ assertEquals("Hello World", testObject.getMessage());
+ }
+
+ /**
+ * Handles various well-formed combinations
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ */
+ @Test
+ public void testGood_Combinations() throws BitcoinURIParseException {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?amount=9876543210&label=Hello%20World&message=Be%20well");
+ assertEquals(
+ "BitcoinURI['address'='1KzTSfqjF2iKCduwz59nv2uqh1W2JsTxZH','amount'='987654321000000000','label'='Hello World','message'='Be well']",
+ testObject.toString());
+ }
+
+ /**
+ * Handles a badly formatted amount field
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ */
+ @Test
+ public void testBad_Amount() throws BitcoinURIParseException {
+ // Missing
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?amount=");
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("amount"));
+ }
+
+ // Non-decimal (BIP 21)
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?amount=12X4");
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("amount"));
+ }
+ }
+
+ /**
+ * Handles a badly formatted label field
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ */
+ @Test
+ public void testBad_Label() throws BitcoinURIParseException {
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?label=");
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("label"));
+ }
+ }
+
+ /**
+ * Handles a badly formatted message field
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ */
+ @Test
+ public void testBad_Message() throws BitcoinURIParseException {
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?message=");
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("message"));
+ }
+ }
+
+ /**
+ * Handles duplicated fields (sneaky address overwrite attack)
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ */
+ @Test
+ public void testBad_Duplicated() throws BitcoinURIParseException {
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?address=aardvark");
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("address"));
+ }
+ }
+
+ /**
+ * Handles case when there are too many equals
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ */
+ @Test
+ public void testBad_TooManyEquals() throws BitcoinURIParseException {
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?label=aardvark=zebra");
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("cannot parse name value pair"));
+ }
+ }
+
+ /**
+ * Handles case when there are too many question marks
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ */
+ @Test
+ public void testBad_TooManyQuestionMarks() throws BitcoinURIParseException {
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?label=aardvark?message=zebra");
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("Too many question marks"));
+ }
+ }
+
+ /**
+ * Handles unknown fields (required and not required)
+ *
+ * @throws BitcoinURIParseException
+ * If something goes wrong
+ */
+ @Test
+ public void testUnknown() throws BitcoinURIParseException {
+ // Unknown not required field
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?aardvark=true");
+ assertEquals("BitcoinURI['address'='1KzTSfqjF2iKCduwz59nv2uqh1W2JsTxZH','aardvark'='true']", testObject.toString());
+
+ assertEquals("true", (String) testObject.getParameterByName("aardvark"));
+
+ // Unknown not required field (isolated)
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?aardvark");
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("cannot parse name value pair"));
+ }
+
+ // Unknown and required field
+ try {
+ testObject = new BitcoinURI(NetworkParameters.prodNet(), BitcoinURI.BITCOIN_SCHEME + ":" + PRODNET_GOOD_ADDRESS
+ + "?req-aardvark=true");
+ fail("Expecting BitcoinURIParseException");
+ } catch (BitcoinURIParseException e) {
+ assertTrue(e.getMessage().contains("req-aardvark"));
+ }
+ }
+}