/* * This file is part of Bisq. * * Bisq is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * * Bisq 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 Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Bisq. If not, see <http://www.gnu.org/licenses/>. */ package bisq.asset.coins; import bisq.asset.AddressValidationResult; import bisq.asset.AddressValidator; import bisq.asset.Base58BitcoinAddressValidator; import bisq.asset.Coin; import bisq.asset.NetworkParametersAdapter; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.VersionedChecksummedBytes; import org.bitcoinj.params.Networks; import java.io.ByteArrayOutputStream; import java.util.Arrays; import java.util.Locale; import javax.annotation.Nullable; public class WorldMobileCoin extends Coin { public WorldMobileCoin() { super("WorldMobileCoin", "WMCC", new WorldMobileCoinAddressValidator()); } public static class WorldMobileCoinAddressValidator implements AddressValidator { private final Base58BitcoinAddressValidator base58BitcoinAddressValidator; private final NetworkParameters networkParameters; public WorldMobileCoinAddressValidator() { networkParameters = new WorldMobileCoinParams(); base58BitcoinAddressValidator = new Base58BitcoinAddressValidator(networkParameters); } public AddressValidationResult validate(String address) { if (!isLowerCase(address)) { return base58BitcoinAddressValidator.validate(address); } try { WitnessAddress.fromBech32(networkParameters, address); } catch (AddressFormatException ex) { return AddressValidationResult.invalidAddress(ex); } return AddressValidationResult.validAddress(); } private static boolean isLowerCase(String str) { char ch; for (int i = 0; i < str.length(); i++) { ch = str.charAt(i); if (Character.isDigit(ch)) continue; if (!Character.isLowerCase(ch)) return false; } return true; } } public static class WorldMobileCoinParams extends NetworkParametersAdapter { public WorldMobileCoinParams() { addressHeader = 0x49; p2shHeader = 0x4b; acceptableAddressCodes = new int[]{addressHeader, p2shHeader}; } } private static class WitnessAddress extends VersionedChecksummedBytes { static final int PROGRAM_LENGTH_PKH = 20; static final int PROGRAM_LENGTH_SH = 32; static final int PROGRAM_MIN_LENGTH = 2; static final int PROGRAM_MAX_LENGTH = 40; static final String WITNESS_ADDRESS_HRP = "wc"; private WitnessAddress(NetworkParameters params, byte[] data) throws AddressFormatException { super(params.getAddressHeader(), data); if (data.length < 1) throw new AddressFormatException("Zero data found"); final int version = getWitnessVersion(); if (version < 0 || version > 16) throw new AddressFormatException("Invalid script version: " + version); byte[] program = getWitnessProgram(); if (program.length < PROGRAM_MIN_LENGTH || program.length > PROGRAM_MAX_LENGTH) throw new AddressFormatException("Invalid length: " + program.length); if (version == 0 && program.length != PROGRAM_LENGTH_PKH && program.length != PROGRAM_LENGTH_SH) throw new AddressFormatException( "Invalid length for address version 0: " + program.length); } int getWitnessVersion() { return bytes[0] & 0xff; } byte[] getWitnessProgram() { return convertBits(bytes, 1, bytes.length - 1, 5, 8, false); } static WitnessAddress fromBech32(@Nullable NetworkParameters params, String bech32) throws AddressFormatException { Bech32.Bech32Data bechData = Bech32.decode(bech32); if (params == null) { for (NetworkParameters p : Networks.get()) { if (bechData.hrp.equals(WITNESS_ADDRESS_HRP)) return new WitnessAddress(p, bechData.data); } throw new AddressFormatException("Invalid Prefix: No network found for " + bech32); } else { if (bechData.hrp.equals(WITNESS_ADDRESS_HRP)) return new WitnessAddress(params, bechData.data); throw new AddressFormatException("Wrong Network: " + bechData.hrp); } } /** * Helper */ private static byte[] convertBits(final byte[] in, final int inStart, final int inLen, final int fromBits, final int toBits, final boolean pad) throws AddressFormatException { int acc = 0; int bits = 0; ByteArrayOutputStream out = new ByteArrayOutputStream(64); final int maxv = (1 << toBits) - 1; final int max_acc = (1 << (fromBits + toBits - 1)) - 1; for (int i = 0; i < inLen; i++) { int value = in[i + inStart] & 0xff; if ((value >>> fromBits) != 0) { throw new AddressFormatException( String.format("Input value '%X' exceeds '%d' bit size", value, fromBits)); } acc = ((acc << fromBits) | value) & max_acc; bits += fromBits; while (bits >= toBits) { bits -= toBits; out.write((acc >>> bits) & maxv); } } if (pad) { if (bits > 0) out.write((acc << (toBits - bits)) & maxv); } else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) { throw new AddressFormatException("Could not convert bits, invalid padding"); } return out.toByteArray(); } } private static class Bech32 { private static final byte[] CHARSET_REV = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 }; static class Bech32Data { final String hrp; final byte[] data; private Bech32Data(final String hrp, final byte[] data) { this.hrp = hrp; this.data = data; } } private static int polymod(final byte[] values) { int c = 1; for (byte v_i : values) { int c0 = (c >>> 25) & 0xff; c = ((c & 0x1ffffff) << 5) ^ (v_i & 0xff); if ((c0 & 1) != 0) c ^= 0x3b6a57b2; if ((c0 & 2) != 0) c ^= 0x26508e6d; if ((c0 & 4) != 0) c ^= 0x1ea119fa; if ((c0 & 8) != 0) c ^= 0x3d4233dd; if ((c0 & 16) != 0) c ^= 0x2a1462b3; } return c; } private static byte[] expandHrp(final String hrp) { int len = hrp.length(); byte ret[] = new byte[len * 2 + 1]; for (int i = 0; i < len; ++i) { int c = hrp.charAt(i) & 0x7f; ret[i] = (byte) ((c >>> 5) & 0x07); ret[i + len + 1] = (byte) (c & 0x1f); } ret[len] = 0; return ret; } private static boolean verifyChecksum(final String hrp, final byte[] values) { byte[] exp = expandHrp(hrp); byte[] combined = new byte[exp.length + values.length]; System.arraycopy(exp, 0, combined, 0, exp.length); System.arraycopy(values, 0, combined, exp.length, values.length); return polymod(combined) == 1; } public static Bech32Data decode(final String str) throws AddressFormatException { boolean lower = false, upper = false; int len = str.length(); if (len < 8) throw new AddressFormatException("Input too short: " + len); if (len > 90) throw new AddressFormatException("Input too long: " + len); for (int i = 0; i < len; ++i) { char c = str.charAt(i); if (c < 33 || c > 126) throw new AddressFormatException(invalidChar(c, i)); if (c >= 'a' && c <= 'z') { if (upper) throw new AddressFormatException(invalidChar(c, i)); lower = true; } if (c >= 'A' && c <= 'Z') { if (lower) throw new AddressFormatException(invalidChar(c, i)); upper = true; } } final int pos = str.lastIndexOf('1'); if (pos < 1) throw new AddressFormatException("Invalid Prefix: Missing human-readable part"); final int dataLen = len - 1 - pos; if (dataLen < 6) throw new AddressFormatException("Data part too short: " + dataLen); byte[] values = new byte[dataLen]; for (int i = 0; i < dataLen; ++i) { char c = str.charAt(i + pos + 1); if (CHARSET_REV[c] == -1) throw new AddressFormatException(invalidChar(c, i + pos + 1)); values[i] = CHARSET_REV[c]; } String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT); if (!verifyChecksum(hrp, values)) throw new AddressFormatException("Invalid Checksum"); return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6)); } private static String invalidChar(char c, int i) { return "Invalid character '" + Character.toString(c) + "' at position " + i; } } }