226 lines
8.2 KiB
Java
226 lines
8.2 KiB
Java
package com.tangem.wallet.bch;
|
|
|
|
import java.math.BigInteger;
|
|
import java.util.Arrays;
|
|
|
|
|
|
/**
|
|
* Copyright (c) 2018 Tobias Brandt
|
|
*
|
|
* Distributed under the MIT software license, see the accompanying file LICENSE
|
|
* or http://www.opensource.org/licenses/mit-license.php.
|
|
*/
|
|
|
|
|
|
public class CashAddr {
|
|
|
|
public static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
|
|
private static final char[] CHARS = CHARSET.toCharArray();
|
|
|
|
|
|
public static final String SEPARATOR = ":";
|
|
|
|
public static final String MAIN_NET_PREFIX = "bitcoincash";
|
|
|
|
public static final String TEST_NET_PREFIX = "bchtest";
|
|
|
|
public static final String ASSUMED_DEFAULT_PREFIX = MAIN_NET_PREFIX;
|
|
|
|
private static final BigInteger[] POLYMOD_GENERATORS = new BigInteger[] { new BigInteger("98f2bc8e61", 16),
|
|
new BigInteger("79b76d99e2", 16), new BigInteger("f33e5fb3c4", 16), new BigInteger("ae2eabe2a8", 16),
|
|
new BigInteger("1e4f43e470", 16) };
|
|
|
|
private static final BigInteger POLYMOD_AND_CONSTANT = new BigInteger("07ffffffff", 16);
|
|
|
|
public static String toCashAddress(BitcoinCashAddressType addressType, byte[] hash) {
|
|
String prefixString = MAIN_NET_PREFIX;
|
|
byte[] prefixBytes = getPrefixBytes(prefixString);
|
|
byte[] payloadBytes = concatenateByteArrays(new byte[] { addressType.getVersionByte() }, hash);
|
|
payloadBytes = convertBits(payloadBytes, 8, 5, false);
|
|
byte[] allChecksumInput = concatenateByteArrays(
|
|
concatenateByteArrays(concatenateByteArrays(prefixBytes, new byte[] { 0 }), payloadBytes),
|
|
new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 });
|
|
byte[] checksumBytes = calculateChecksumBytesPolymod(allChecksumInput);
|
|
checksumBytes = convertBits(checksumBytes, 8, 5, true);
|
|
String cashAddress = BitcoinCashBase32.encode(concatenateByteArrays(payloadBytes, checksumBytes));
|
|
//return prefixString + SEPARATOR + cashAddress;
|
|
return cashAddress;
|
|
}
|
|
|
|
public static BitcoinCashAddressDecodedParts decodeCashAddress(String bitcoinCashAddress) {
|
|
if (!isValidCashAddress(bitcoinCashAddress)) {
|
|
throw new RuntimeException("Address wasn't valid: " + bitcoinCashAddress);
|
|
}
|
|
|
|
BitcoinCashAddressDecodedParts decoded = new BitcoinCashAddressDecodedParts();
|
|
String[] addressParts = bitcoinCashAddress.split(SEPARATOR);
|
|
if (addressParts.length == 2) {
|
|
decoded.setPrefix(addressParts[0]);
|
|
} else {
|
|
decoded.setPrefix(MAIN_NET_PREFIX);
|
|
}
|
|
|
|
byte[] addressData = BitcoinCashBase32.decode(addressParts[addressParts.length - 1]);
|
|
addressData = Arrays.copyOfRange(addressData, 0, addressData.length - 8);
|
|
addressData = BitcoinCashBitArrayConverter.convertBits(addressData, 5, 8, true);
|
|
byte versionByte = addressData[0];
|
|
byte[] hash = Arrays.copyOfRange(addressData, 1, addressData.length);
|
|
|
|
decoded.setAddressType(getAddressTypeFromVersionByte(versionByte));
|
|
decoded.setHash(hash);
|
|
|
|
return decoded;
|
|
}
|
|
|
|
private static BitcoinCashAddressType getAddressTypeFromVersionByte(byte versionByte) {
|
|
for (BitcoinCashAddressType addressType : BitcoinCashAddressType.values()) {
|
|
if (addressType.getVersionByte() == versionByte) {
|
|
return addressType;
|
|
}
|
|
}
|
|
|
|
throw new RuntimeException("Unknown version byte: " + versionByte);
|
|
}
|
|
|
|
|
|
|
|
public static boolean isValidCashAddress(String bitcoinCashAddress ) {
|
|
try {
|
|
if (!isSingleCase(bitcoinCashAddress))
|
|
return false;
|
|
|
|
bitcoinCashAddress = bitcoinCashAddress.toLowerCase();
|
|
String prefix;
|
|
|
|
if (bitcoinCashAddress.contains(SEPARATOR)) {
|
|
String[] split = bitcoinCashAddress.split(SEPARATOR);
|
|
prefix = split[0];
|
|
if (!prefix.equals(MAIN_NET_PREFIX)) {return false;} //for now we use main net only
|
|
bitcoinCashAddress = split[1];
|
|
} else {
|
|
prefix = MAIN_NET_PREFIX;
|
|
}
|
|
if (!bitcoinCashAddress.startsWith("q")) {return false;} //for now we use P2PKH addresses only
|
|
|
|
byte[] checksumData = concatenateByteArrays(
|
|
concatenateByteArrays(getPrefixBytes(prefix ), new byte[] { 0x00 }),
|
|
BitcoinCashBase32.decode(bitcoinCashAddress));
|
|
|
|
byte[] calculateChecksumBytesPolymod = calculateChecksumBytesPolymod(checksumData);
|
|
return new BigInteger(calculateChecksumBytesPolymod).compareTo(BigInteger.ZERO) == 0;
|
|
} catch (RuntimeException re) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static boolean isSingleCase(String bitcoinCashAddress) {
|
|
if (bitcoinCashAddress.equals(bitcoinCashAddress.toLowerCase())) {
|
|
return true;
|
|
}
|
|
if (bitcoinCashAddress.equals(bitcoinCashAddress.toUpperCase())) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param checksumInput
|
|
* @return Returns a 40 bits checksum in form of 5 8-bit arrays. This still has
|
|
* to me mapped to 5-bit array representation
|
|
*/
|
|
private static byte[] calculateChecksumBytesPolymod(byte[] checksumInput) {
|
|
BigInteger c = BigInteger.ONE;
|
|
|
|
for (int i = 0; i < checksumInput.length; i++) {
|
|
byte c0 = c.shiftRight(35).byteValue();
|
|
c = c.and(POLYMOD_AND_CONSTANT).shiftLeft(5)
|
|
.xor(new BigInteger(String.format("%02x", checksumInput[i]), 16));
|
|
|
|
if ((c0 & 0x01) != 0)
|
|
c = c.xor(POLYMOD_GENERATORS[0]);
|
|
if ((c0 & 0x02) != 0)
|
|
c = c.xor(POLYMOD_GENERATORS[1]);
|
|
if ((c0 & 0x04) != 0)
|
|
c = c.xor(POLYMOD_GENERATORS[2]);
|
|
if ((c0 & 0x08) != 0)
|
|
c = c.xor(POLYMOD_GENERATORS[3]);
|
|
if ((c0 & 0x10) != 0)
|
|
c = c.xor(POLYMOD_GENERATORS[4]);
|
|
}
|
|
|
|
byte[] checksum = c.xor(BigInteger.ONE).toByteArray();
|
|
if (checksum.length == 5) {
|
|
return checksum;
|
|
} else {
|
|
byte[] newChecksumArray = new byte[5];
|
|
|
|
System.arraycopy(checksum, Math.max(0, checksum.length - 5), newChecksumArray,
|
|
Math.max(0, 5 - checksum.length), Math.min(5, checksum.length));
|
|
|
|
return newChecksumArray;
|
|
}
|
|
|
|
}
|
|
|
|
private static byte[] getPrefixBytes(String prefixString ) {
|
|
byte[] prefixBytes = new byte[prefixString.length()];
|
|
|
|
char[] charArray = prefixString.toCharArray();
|
|
for (int i = 0; i < charArray.length; i++) {
|
|
prefixBytes[i] = (byte) (charArray[i] & 0x1f);
|
|
}
|
|
|
|
return prefixBytes;
|
|
}
|
|
|
|
private static byte[] concatenateByteArrays(byte[] first, byte[] second) {
|
|
byte[] concatenatedBytes = new byte[first.length + second.length];
|
|
|
|
System.arraycopy(first, 0, concatenatedBytes, 0, first.length);
|
|
System.arraycopy(second, 0, concatenatedBytes, first.length, second.length);
|
|
|
|
return concatenatedBytes;
|
|
}
|
|
|
|
private static byte[] convertBits(byte[] bytes8Bits, int from, int to, boolean strictMode) {
|
|
//Copyright (c) 2017 Pieter Wuille
|
|
|
|
int length = (int) (strictMode ? Math.floor((double) bytes8Bits.length * from / to)
|
|
: Math.ceil((double) bytes8Bits.length * from / to));
|
|
int mask = ((1 << to) - 1) & 0xff;
|
|
byte[] result = new byte[length];
|
|
int index = 0;
|
|
int accumulator = 0;
|
|
int bits = 0;
|
|
for (int i = 0; i < bytes8Bits.length; i++) {
|
|
byte value = bytes8Bits[i];
|
|
accumulator = (((accumulator & 0xff) << from) | (value & 0xff));
|
|
bits += from;
|
|
while (bits >= to) {
|
|
bits -= to;
|
|
result[index] = (byte) ((accumulator >> bits) & mask);
|
|
++index;
|
|
}
|
|
}
|
|
if (!strictMode) {
|
|
if (bits > 0) {
|
|
result[index] = (byte) ((accumulator << (to - bits)) & mask);
|
|
++index;
|
|
}
|
|
} else {
|
|
if (!(bits < from && ((accumulator << (to - bits)) & mask) == 0)) {
|
|
throw new RuntimeException("Strict mode was used but input couldn't be converted without padding");
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|