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: + *

+ * + *

Accepted formats

+ *

+ * The following input forms are accepted + *

+ * + *

+ * The name/value pairs are processed as follows: + *

+ * + *

+ * The following names are known and have the following formats + *

+ * + * + * @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")); + } + } +}