UNPKG

cashaddrjs-slp

Version:

SLP & Bitcoin Cash cashaddr address format support for Node.js and web browsers.

332 lines (310 loc) 8.91 kB
/** * @license * https://github.com/bitcoincashjs/cashaddr * Copyright (c) 2017-2018 Emilio Almansi * Distributed under the MIT software license, see the accompanying * file LICENSE or http://www.opensource.org/licenses/mit-license.php. */ 'use strict'; var base32 = require('./base32'); var bigInt = require('big-integer'); var convertBits = require('./convertBits'); var validation = require('./validation'); var validate = validation.validate; /** * Encoding and decoding of the new Cash Address format for Bitcoin Cash. <br /> * Compliant with the original cashaddr specification: * {@link https://github.com/Bitcoin-UAHF/spec/blob/master/cashaddr.md} * @module cashaddr */ /** * Encodes a hash from a given type into a Bitcoin Cash address with the given prefix. * * @static * @param {string} prefix Network prefix. E.g.: 'bitcoincash'. * @param {string} type Type of address to generate. Either 'P2PKH' or 'P2SH'. * @param {Uint8Array} hash Hash to encode represented as an array of 8-bit integers. * @returns {string} * @throws {ValidationError} */ function encode(prefix, type, hash) { validate(typeof prefix === 'string' && isValidPrefix(prefix), 'Invalid prefix: ' + prefix + '.'); validate(typeof type === 'string', 'Invalid type: ' + type + '.'); validate(hash instanceof Uint8Array, 'Invalid hash: ' + hash + '.'); var prefixData = concat(prefixToUint5Array(prefix), new Uint8Array(1)); var versionByte = getTypeBits(type) + getHashSizeBits(hash); var payloadData = toUint5Array(concat(Uint8Array.of(versionByte), hash)); var checksumData = concat(concat(prefixData, payloadData), new Uint8Array(8)); var payload = concat(payloadData, checksumToUint5Array(polymod(checksumData))); return prefix + ':' + base32.encode(payload); } /** * Decodes the given address into its constituting prefix, type and hash. See [#encode()]{@link encode}. * * @static * @param {string} address Address to decode. E.g.: 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a'. * @returns {object} * @throws {ValidationError} */ function decode(address) { validate(typeof address === 'string' && hasSingleCase(address), 'Invalid address: ' + address + '.'); var pieces = address.toLowerCase().split(':'); validate(pieces.length === 2, 'Missing prefix: ' + address + '.'); var prefix = pieces[0]; var payload = base32.decode(pieces[1]); validate(validChecksum(prefix, payload), 'Invalid checksum: ' + address + '.'); var payloadData = fromUint5Array(payload.slice(0, -8)); var versionByte = payloadData[0]; var hash = payloadData.slice(1); validate(getHashSize(versionByte) === hash.length * 8, 'Invalid hash size: ' + address + '.'); var type = getType(versionByte); return { prefix: prefix, type: type, hash: hash, }; } /** * Error thrown when encoding or decoding fail due to invalid input. * * @constructor ValidationError * @param {string} message Error description. */ var ValidationError = validation.ValidationError; /** * Valid address prefixes. * * @private */ var VALID_PREFIXES = ['bitcoincash', 'bchtest', 'bchreg', 'simpleledger', 'slptest', 'slpreg']; /** * Checks whether a string is a valid prefix; ie., it has a single letter case * and is one of 'bitcoincash', 'bchtest', or 'bchreg'. * * @private * @param {string} prefix * @returns {boolean} */ function isValidPrefix(prefix) { return hasSingleCase(prefix) && VALID_PREFIXES.indexOf(prefix.toLowerCase()) !== -1; } /** * Derives an array from the given prefix to be used in the computation * of the address' checksum. * * @private * @param {string} prefix Network prefix. E.g.: 'bitcoincash'. * @returns {Uint8Array} */ function prefixToUint5Array(prefix) { var result = new Uint8Array(prefix.length); for (var i = 0; i < prefix.length; ++i) { result[i] = prefix[i].charCodeAt(0) & 31; } return result; } /** * Returns an array representation of the given checksum to be encoded * within the address' payload. * * @private * @param {BigInteger} checksum Computed checksum. * @returns {Uint8Array} */ function checksumToUint5Array(checksum) { var result = new Uint8Array(8); for (var i = 0; i < 8; ++i) { result[7 - i] = checksum.and(31).toJSNumber(); checksum = checksum.shiftRight(5); } return result; } /** * Returns the bit representation of the given type within the version * byte. * * @private * @param {string} type Address type. Either 'P2PKH' or 'P2SH'. * @returns {number} * @throws {ValidationError} */ function getTypeBits(type) { switch (type) { case 'P2PKH': return 0; case 'P2SH': return 8; default: throw new ValidationError('Invalid type: ' + type + '.'); } } /** * Retrieves the address type from its bit representation within the * version byte. * * @private * @param {number} versionByte * @returns {string} * @throws {ValidationError} */ function getType(versionByte) { switch (versionByte & 120) { case 0: return 'P2PKH'; case 8: return 'P2SH'; default: throw new ValidationError('Invalid address type in version byte: ' + versionByte + '.'); } } /** * Returns the bit representation of the length in bits of the given * hash within the version byte. * * @private * @param {Uint8Array} hash Hash to encode represented as an array of 8-bit integers. * @returns {number} * @throws {ValidationError} */ function getHashSizeBits(hash) { switch (hash.length * 8) { case 160: return 0; case 192: return 1; case 224: return 2; case 256: return 3; case 320: return 4; case 384: return 5; case 448: return 6; case 512: return 7; default: throw new ValidationError('Invalid hash size: ' + hash.length + '.'); } } /** * Retrieves the the length in bits of the encoded hash from its bit * representation within the version byte. * * @private * @param {number} versionByte * @returns {number} */ function getHashSize(versionByte) { switch (versionByte & 7) { case 0: return 160; case 1: return 192; case 2: return 224; case 3: return 256; case 4: return 320; case 5: return 384; case 6: return 448; case 7: return 512; } } /** * Converts an array of 8-bit integers into an array of 5-bit integers, * right-padding with zeroes if necessary. * * @private * @param {Uint8Array} data * @returns {Uint8Array} */ function toUint5Array(data) { return convertBits(data, 8, 5); } /** * Converts an array of 5-bit integers back into an array of 8-bit integers, * removing extra zeroes left from padding if necessary. * Throws a {@link ValidationError} if input is not a zero-padded array of 8-bit integers. * * @private * @param {Uint8Array} data * @returns {Uint8Array} * @throws {ValidationError} */ function fromUint5Array(data) { return convertBits(data, 5, 8, true); } /** * Returns the concatenation a and b. * * @private * @param {Uint8Array} a * @param {Uint8Array} b * @returns {Uint8Array} * @throws {ValidationError} */ function concat(a, b) { var ab = new Uint8Array(a.length + b.length); ab.set(a); ab.set(b, a.length); return ab; } /** * Computes a checksum from the given input data as specified for the CashAddr * format: https://github.com/Bitcoin-UAHF/spec/blob/master/cashaddr.md. * * @private * @param {Uint8Array} data Array of 5-bit integers over which the checksum is to be computed. * @returns {BigInteger} */ function polymod(data) { var GENERATOR = [0x98f2bc8e61, 0x79b76d99e2, 0xf33e5fb3c4, 0xae2eabe2a8, 0x1e4f43e470]; var checksum = bigInt(1); for (var i = 0; i < data.length; ++i) { var value = data[i]; var topBits = checksum.shiftRight(35); checksum = checksum.and(0x07ffffffff).shiftLeft(5).xor(value); for (var j = 0; j < GENERATOR.length; ++j) { if (topBits.shiftRight(j).and(1).equals(1)) { checksum = checksum.xor(GENERATOR[j]); } } } return checksum.xor(1); } /** * Verify that the payload has not been corrupted by checking that the * checksum is valid. * * @private * @param {string} prefix Network prefix. E.g.: 'bitcoincash'. * @param {Uint8Array} payload Array of 5-bit integers containing the address' payload. * @returns {boolean} */ function validChecksum(prefix, payload) { var prefixData = concat(prefixToUint5Array(prefix), new Uint8Array(1)); var checksumData = concat(prefixData, payload); return polymod(checksumData).equals(0); } /** * Returns true if, and only if, the given string contains either uppercase * or lowercase letters, but not both. * * @private * @param {string} string Input string. * @returns {boolean} */ function hasSingleCase(string) { return string === string.toLowerCase() || string === string.toUpperCase(); } module.exports = { encode: encode, decode: decode, ValidationError: ValidationError, };