package com.greenaddress.greenapi;
import android.util.Pair;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.VersionedChecksummedBytes;
import org.bitcoinj.core.WrongLengthException;
import org.bitcoinj.core.WrongNetworkException;
import org.bitcoinj.params.AbstractBitcoinNetParams;
import org.bitcoinj.uri.BitcoinURIParseException;
import org.bitcoinj.uri.OptionalFieldValidationException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import static com.google.common.base.Preconditions.checkNotNull;
public class ConfidentialAddress extends VersionedChecksummedBytes {
public ConfidentialAddress(final NetworkParameters params, final int version, final byte[] hash, final byte[] blindingPubKey)
throws WrongNetworkException, WrongLengthException {
super(4, getBytes(params, version, hash, blindingPubKey));
checkNotNull(params);
if (!isAcceptableVersion(params, version))
throw new WrongNetworkException(version, params.getAcceptableAddressCodes());
if (!isAcceptableLength(params, version, hash.length))
throw new WrongLengthException(hash.length);
}
protected ConfidentialAddress(final NetworkParameters params, final String address) throws AddressFormatException {
super(address);
checkNotNull(params);
if (version != 4)
throw new WrongNetworkException(version, params.getAcceptableAddressCodes());
final byte[] hash = bytes;
if (!isAcceptableLength(params, version, hash.length - 1 - 33)) // len - version - pubkey
throw new WrongLengthException(hash.length);
}
public static ConfidentialAddress fromBase58(final NetworkParameters params, final String base58) throws AddressFormatException {
return new ConfidentialAddress(params, base58);
}
private static boolean isAcceptableVersion(final NetworkParameters params, final int version) {
for (final int v : params.getAcceptableAddressCodes())
if (version == v)
return true;
return false;
}
private static boolean isAcceptableLength(final NetworkParameters params, final int version, final int length) {
switch (length) {
case 20: return (version != params.getP2WSHHeader());
default: return false;
}
}
public static ConfidentialAddress fromP2SHHash(final NetworkParameters params, final byte[] hash160, final byte[] blindingPubKey) {
try {
return new ConfidentialAddress(params, params.getP2SHHeader(), hash160, blindingPubKey);
} catch (final WrongNetworkException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
private static byte[] getBytes(final NetworkParameters params, final int version, final byte[] hash, final byte[] blindingPubKey) {
if (version == params.getAddressHeader() || version == params.getP2SHHeader()) {
final byte[] bytes = new byte[1 + 33 + 20]; // p2sh/p2pkh version + pubkey + hash
bytes[0] = (byte) version;
System.arraycopy(blindingPubKey, 0, bytes, 1, 33);
System.arraycopy(hash, 0, bytes, 34, 20);
return bytes;
}
throw new IllegalArgumentException("Could not figure out how to serialize address");
}
public Address getBitcoinAddress() {
if (bytes[0] == Network.NETWORK.getP2SHHeader())
return Address.fromP2SHHash(Network.NETWORK, Arrays.copyOfRange(bytes, 34, 54));
if (bytes[0] == Network.NETWORK.getP2WPKHHeader())
return Address.fromP2WPKHHash(Network.NETWORK, Arrays.copyOfRange(bytes, 34, 54));
throw new RuntimeException(); // Cannot happen.
}
public static Pair<String, Coin> parseBitcoinURI(final NetworkParameters params, final String input) throws BitcoinURIParseException {
checkNotNull(input);
final String scheme = params == null ? AbstractBitcoinNetParams.BITCOIN_SCHEME : params.getUriScheme();
// Attempt to form the URI (fail fast syntax checking to official standards).
final URI uri;
try {
uri = new URI(input);
} catch (final URISyntaxException e) {
throw new BitcoinURIParseException("Bad URI syntax", e);
}
// URI is formed as bitcoin:<address>?<query parameters>
// blockchain.info generates URIs of non-BIP compliant form bitcoin://address?....
// We support both until Ben fixes his code.
// 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')
final String blockchainInfoScheme = scheme + "://";
final String correctScheme = scheme + ':';
final String schemeSpecificPart;
if (input.startsWith(blockchainInfoScheme))
schemeSpecificPart = input.substring(blockchainInfoScheme.length());
else if (input.startsWith(correctScheme))
schemeSpecificPart = input.substring(correctScheme.length());
else
throw new BitcoinURIParseException("Unsupported URI scheme: " + uri.getScheme());
// Split off the address from the rest of the query parameters.
final String[] addressSplitTokens = schemeSpecificPart.split("\\?", 2);
if (addressSplitTokens.length == 0)
throw new BitcoinURIParseException("No data found after the bitcoin: prefix");
final String addressToken = addressSplitTokens[0]; // may be empty!
final String[] nameValuePairTokens;
if (addressSplitTokens.length == 1) {
// Only an address is specified - use an empty '<name>=<value>' token array.
nameValuePairTokens = new String[] {};
} else {
// Split into '<name>=<value>' tokens.
nameValuePairTokens = addressSplitTokens[1].split("&");
}
// Attempt to parse the rest of the URI parameters.
final Map<String, Object> parameterMap = new LinkedHashMap<>();
Coin value = null;
for (final String nameValuePairToken : nameValuePairTokens) {
final int sepIndex = nameValuePairToken.indexOf('=');
if (sepIndex == -1)
throw new BitcoinURIParseException("Malformed Bitcoin URI - no separator in '" +
nameValuePairToken + '\'');
if (sepIndex == 0)
throw new BitcoinURIParseException("Malformed Bitcoin URI - empty name '" +
nameValuePairToken + '\'');
final String nameToken = nameValuePairToken.substring(0, sepIndex).toLowerCase(Locale.US);
final String valueToken = nameValuePairToken.substring(sepIndex + 1);
// Parse the amount.
if ("amount".equals(nameToken)) {
// Decode the amount (contains an optional decimal component to 8dp).
try {
value = Coin.parseCoin(valueToken);
if (params != null && value.isGreaterThan(params.getMaxMoney()))
throw new BitcoinURIParseException("Max number of coins exceeded");
if (value.signum() < 0)
throw new ArithmeticException("Negative coins specified");
if (parameterMap.containsKey(nameToken))
throw new BitcoinURIParseException(String.format(Locale.US, "'%s' is duplicated, URI is invalid", nameToken));
else
parameterMap.put(nameToken, value);
} catch (final IllegalArgumentException e) {
throw new OptionalFieldValidationException(String.format(Locale.US, "'%s' is not a valid amount", valueToken), e);
} catch (final ArithmeticException e) {
throw new OptionalFieldValidationException(String.format(Locale.US, "'%s' has too many decimal places", valueToken), e);
}
}
}
if (addressToken.isEmpty())
throw new BitcoinURIParseException("No address found");
// Validate
ConfidentialAddress.fromBase58(params, addressToken);
// Return string if correct
return new Pair<>(addressToken, value);
}
public byte[] getBlindingPubKey() {
return Arrays.copyOfRange(bytes, 1, 34);
}
}