diff --git a/modules/ecashaddrjs/README.md b/modules/ecashaddrjs/README.md index ce2e8bdaf..736a0feb5 100644 --- a/modules/ecashaddrjs/README.md +++ b/modules/ecashaddrjs/README.md @@ -1,137 +1,151 @@ # eCashAddr.js: The eCash address format for Node.js and web browsers. [![NPM](https://nodei.co/npm/ecashaddrjs.png?downloads=true)](https://nodei.co/npm/ecashaddrjs/) JavaScript implementation for CashAddr address format for eCash. Compliant with the original CashAddr [specification](https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md) which improves upon [BIP 173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki). ## Installation ### Using NPM ```bsh $ npm install --save ecashaddrjs ``` ### Manually You may also download the distribution file manually and place it within your third-party scripts directory: [dist/cashaddrjs.min.js](https://unpkg.com/ecashaddrjs/dist/cashaddrjs.min.js). ## Usage Convert a `bitcoincash:` prefixed address to an `ecash:` prefixed address ### In Node.js ```javascript const ecashaddr = require('ecashaddrjs'); const bitcoincashAddress = 'bitcoincash:qpadrekpz6gjd8w0zfedmtqyld0r2j4qmuj6vnmhp6'; const { prefix, type, hash } = ecashaddr.decode(bitcoincashAddress); console.log(prefix); // 'bitcoincash' console.log(type); // 'P2PKH' console.log(hash); // Uint8Array [ 118, 160, ..., 115 ] console.log(ecashaddr.encode('ecash', type, hash)); // 'ecash:qpadrekpz6gjd8w0zfedmtqyld0r2j4qmuthccqd8d' console.log(ecashaddr.isValidCashAddress(bitcoincashAddress)); // true console.log(ecashaddr.isValidCashAddress(bitcoincashAddress), 'bitcoincash'); // true console.log(ecashaddr.isValidCashAddress(bitcoincashAddress), 'ecash'); // false +// getOutputScriptFromAddress +// p2pkh +console.log( + ecashaddr.getOutputScriptFromAddress( + 'ecash:qplkmuz3rx480u6vc4xgc0qxnza42p0e7vll6p90wr', + ), +); // 76a9144e532257c01b310b3b5c1fd947c79a72addf852388ac +// p2sh +console.log( + ecashaddr.getOutputScriptFromAddress( + 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07', + ), +); // a914d37c4c809fe9840e7bfa77b86bd47163f6fb6c6087 ``` ### Working with chronik-client in Node.js [chronik](https://www.npmjs.com/package/chronik-client) is the reference indexer for eCash. It queries the blockchain using address hash160 and type parameters. The `type` and `hash` parameters can be returned in a format ready for chronik by calling `cashaddr.decode(address, true)` ```javascript const ecashaddr = require('ecashaddrjs'); const { ChronikClient } = require('chronik-client'); const chronik = new ChronikClient('https://chronik.be.cash/xec'); const chronikQueryAddress = 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035'; const { prefix, type, hash } = ecashaddr.decode(chronikQueryAddress, true); console.log(prefix); // 'ecash' console.log(type); // 'p2pkh' (instead of 'P2PKH', returned without the 'true' flag) console.log(hash); // '95e79f51d4260bc0dc3ba7fb77c7be92d0fbdd1d' (instead of Uint8Array [ 149, 241, ..., 29 ], returned without the 'true' flag) console.log(ecashaddr.encode('ecash', type, hash)); // encode supports chronik output inputs // 'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035' // use chronik client to get a page of address tx history const history = await chronik .script(type, hash) .history(/*page=*/ 0, /*page_size=*/ 10); ``` ### React ```javascript import cashaddr from 'ecashaddrjs'; function convertBitcoincashToEcash(bitcoincashAddress) { /* NOTE This function assumes input parameter 'bitcoincashAddress' is a valid bitcoincash: address cashaddr.decode() will throw an error if 'bitcoincashAddress' lacks a prefix */ const { prefix, type, hash } = cashaddr.decode(bitcoincashAddress); const ecashAddress = cashaddr.encode('ecash', type, hash); return ecashAddress; } ``` ### Browser ```html ``` #### Script Tag You may include a script tag in your HTML and the `ecashaddr` module will be defined globally on subsequent scripts. ```html ... ... ``` #### jsFiddle https://jsfiddle.net/zghd6c2y/ #### Change Log 1.1.0 - Support decoding prefixless addresses\ 1.1.1 - Updated README to point to Bitcoin ABC monorepo\ 1.1.2 - Updated `repository` field in `package.json` to Bitcoin ABC monorepo\ 1.1.3 - Support string input and output for `hash`\ 1.2.0 - Support lowercase input and output of address types, support encoding outputScript to address, support getting type and hash from an outputScript with new exported function `getTypeAndHashFromOutputScript`\ 1.3.0 - Add `toLegacy` function to convert cashaddress format to legacy address\ 1.4.0 - Add `isValidCashAddress` function to validate cash addresses by prefix\ -1.4.1-6 - Fix repo README link for npmjs page +1.4.1-6 - Fix repo README link for npmjs page\ +1.5.0 - Add `getOutputScriptFromAddress` function to get outputScript from address diff --git a/modules/ecashaddrjs/package.json b/modules/ecashaddrjs/package.json index 39e5687ca..c9e4222cc 100644 --- a/modules/ecashaddrjs/package.json +++ b/modules/ecashaddrjs/package.json @@ -1,61 +1,61 @@ { "name": "ecashaddrjs", - "version": "1.4.6", + "version": "1.5.0", "description": "eCash cashaddr address format support for Node.js and web browsers.", "main": "src/cashaddr.js", "browser": "dist/cashaddrjs.min.js", "files": [ "src/", "dist/" ], "scripts": { "build": "webpack", "prepublish": "npm run build", "test": "mocha", "coverage": "nyc mocha", "junit": "mocha test --reporter mocha-junit-reporter" }, "repository": { "type": "git", "url": "git+https://github.com/Bitcoin-ABC/bitcoin-abc.git", "directory": "modules/ecashaddrjs" }, "keywords": [ "ecash", "bitcoin", "cash", "cashaddr", "address", "format", "node", "browser" ], "author": "Bitcoin ABC, Emilio Almansi", "license": "MIT", "bugs": { "url": "https://github.com/Bitcoin-ABC/bitcoin-abc/issues" }, "homepage": "https://github.com/Bitcoin-ABC/bitcoin-abc/tree/master/modules/ecashaddrjs#readme", "devDependencies": { "@babel/cli": "^7.21.0", "@babel/core": "^7.21.3", "@babel/preset-env": "^7.20.2", "babel-loader": "^9.1.2", "buffer": "^6.0.3", "chai": "^4.3.7", "debug": "^4.3.4", "eslint": "^8.37.0", "jsdoc": "^4.0.2", "mocha": "^10.2.0", "mocha-junit-reporter": "^2.2.0", "mocha-suppress-logs": "^0.3.1", "nyc": "^15.1.0", "random-js": "^2.1.0", "webpack": "^5.76.2", "webpack-cli": "^5.0.1" }, "dependencies": { "big-integer": "1.6.36", "bs58check": "^3.0.1" } } diff --git a/modules/ecashaddrjs/src/cashaddr.js b/modules/ecashaddrjs/src/cashaddr.js index 22091417e..460c8afcd 100644 --- a/modules/ecashaddrjs/src/cashaddr.js +++ b/modules/ecashaddrjs/src/cashaddr.js @@ -1,584 +1,605 @@ /** * @license * https://reviews.bitcoinabc.org * Copyright (c) 2017-2020 Emilio Almansi * Copyright (c) 2023 Bitcoin ABC * 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 bs58check = require('bs58check'); var convertBits = require('./convertBits'); var validation = require('./validation'); var validate = validation.validate; /** * Encoding and decoding of the new Cash Address format for eCash.
* Compliant with the original cashaddr specification: * {@link https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md} * @module cashaddr */ /** * Encodes a hash from a given type into an eCash address with the given prefix. * * @static * @param {string} prefix Cash address prefix. E.g.: 'ecash'. * @param {string} type Type of address to generate. Either 'P2PKH' or 'P2SH'. * @param {Uint8Array or string} 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 || typeof hash === 'string', 'Invalid hash: ' + hash + '. Must be string or Uint8Array.', ); if (typeof hash === 'string') { hash = stringToUint8Array(hash); } var prefixData = concat(prefixToUint5Array(prefix), new Uint8Array(1)); var versionByte = getTypeBits(type) + getHashSizeBits(hash); var payloadData = toUint5Array(concat(new Uint8Array([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.: 'ecash:qpm2qsznhks23z7629mms6s4cwef74vcwva87rkuu2'. * @param {returnHashAsString} bool User may ask for the hash160 be returned as a string instead of a uint8array * @returns {object} * @throws {ValidationError} */ function decode(address, chronikReady = false) { validate( typeof address === 'string' && hasSingleCase(address), 'Invalid address: ' + address + '.', ); var pieces = address.toLowerCase().split(':'); // if there is no prefix, it might still be valid let prefix, payload; if (pieces.length === 1) { // Check and see if it has a valid checksum for accepted prefixes let hasValidChecksum = false; for (let i = 0; i < VALID_PREFIXES.length; i += 1) { const testedPrefix = VALID_PREFIXES[i]; const prefixlessPayload = base32.decode(pieces[0]); hasValidChecksum = validChecksum(testedPrefix, prefixlessPayload); if (hasValidChecksum) { // Here's your prefix prefix = testedPrefix; payload = prefixlessPayload; // Stop testing other prefixes break; } } validate( hasValidChecksum, `Prefixless address ${address} does not have valid checksum for any valid prefix (${VALID_PREFIXES.join( ', ', )})`, ); } else { validate(pieces.length === 2, 'Invalid address: ' + address + '.'); prefix = pieces[0]; payload = base32.decode(pieces[1]); validate( validChecksum(prefix, payload), 'Invalid checksum: ' + address + '.', ); } var payloadData = fromUint5Array(payload.subarray(0, -8)); var versionByte = payloadData[0]; var hash = payloadData.subarray(1); validate( getHashSize(versionByte) === hash.length * 8, 'Invalid hash size: ' + address + '.', ); var type = getType(versionByte); return { prefix: prefix, type: chronikReady ? type.toLowerCase() : type, hash: chronikReady ? uint8arraytoString(hash) : hash, }; } /** * Error thrown when encoding or decoding fail due to invalid input. * * @constructor ValidationError * @param {string} message Error description. */ var ValidationError = validation.ValidationError; /** * All valid address prefixes. * * @private */ var VALID_PREFIXES = [ 'ecash', 'bitcoincash', 'simpleledger', 'etoken', 'ectest', 'ecregtest', 'bchtest', 'bchreg', ]; /** * Valid mainnet prefixes * * @private */ var VALID_PREFIXES_MAINNET = ['ecash', 'bitcoincash', 'simpleledger', 'etoken']; /** * Checks whether a string is a valid prefix; ie., it has a single letter case * and is one of 'ecash', 'ectest', 'etoken', etc * * @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 Cash address prefix. E.g.: 'ecash'. * @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': case 'P2PKH': return 0; case 'p2sh': 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 Cash address prefix. E.g.: 'ecash'. * @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(); } /** * Returns a uint8array for a given string input * * @private * @param {string} string Input string. * @returns {Uint8Array} */ function stringToUint8Array(string) { const buffer = Buffer.from(string, 'hex'); const arrayBuffer = new ArrayBuffer(buffer.length); const uint8Array = new Uint8Array(arrayBuffer); for (let i = 0; i < uint8Array.length; i += 1) { uint8Array[i] = buffer[i]; } return uint8Array; } /** * Returns a uint8array for a given string input * * @private * @param {Uint8Array} uint8Array Input string. * @returns {string} */ function uint8arraytoString(uint8Array) { let buffer = []; for (let i = 0; i < uint8Array.length; i += 1) { buffer.push(uint8Array[i]); } const hexBuffer = Buffer.from(buffer, 'hex'); const string = hexBuffer.toString('hex'); return string; } /** * Get type and hash from an outputScript * * Supported outputScripts: * * P2PKH: 76a91488ac * P2SH: a91487 * * Validates for supported outputScript and hash length * * * @private * @param {string} outputScript an ecash tx outputScript * @returns {object} * @throws {ValidationError} */ function getTypeAndHashFromOutputScript(outputScript) { const p2pkhPrefix = '76a914'; const p2pkhSuffix = '88ac'; const p2shPrefix = 'a914'; const p2shSuffix = '87'; let hash, type; // If outputScript begins with '76a914' and ends with '88ac' if ( outputScript.slice(0, p2pkhPrefix.length) === p2pkhPrefix && outputScript.slice(-1 * p2pkhSuffix.length) === p2pkhSuffix ) { // We have type p2pkh type = 'P2PKH'; // hash is the string in between '76a194' and '88ac' hash = outputScript.substring( outputScript.indexOf(p2pkhPrefix) + p2pkhPrefix.length, outputScript.lastIndexOf(p2pkhSuffix), ); // If outputScript begins with 'a914' and ends with '87' } else if ( outputScript.slice(0, p2shPrefix.length) === p2shPrefix && outputScript.slice(-1 * p2shSuffix.length) === p2shSuffix ) { // We have type p2sh type = 'P2SH'; // hash is the string in between 'a914' and '87' hash = outputScript.substring( outputScript.indexOf(p2shPrefix) + p2shPrefix.length, outputScript.lastIndexOf(p2shSuffix), ); } else { // Throw validation error if outputScript not of these two types throw new ValidationError('Unsupported outputScript: ' + outputScript); } // Throw validation error if hash is of invalid size // Per spec, valid hash sizes in bytes const VALID_SIZES = [20, 24, 28, 32, 40, 48, 56, 64]; if (!VALID_SIZES.includes(hash.length / 2)) { throw new ValidationError( 'Invalid hash size in outputScript: ' + outputScript, ); } return { type, hash }; } /** * Encodes a given outputScript into an eCash address using the optionally specified prefix. * * @static * @param {string} outputScript an ecash tx outputScript * @param {string} prefix Cash address prefix. E.g.: 'ecash'. * @returns {string} * @throws {ValidationError} */ function encodeOutputScript(outputScript, prefix = 'ecash') { // Get type and hash from outputScript const { type, hash } = getTypeAndHashFromOutputScript(outputScript); // The encode function validates hash for correct length return encode(prefix, type, hash); } /** * Converts an ecash address to legacy format * * @static * @param {string} cashaddress a valid p2pkh or p2sh ecash address * @returns {string} * @throws {ValidationError} */ function toLegacy(cashaddress) { const { prefix, type, hash } = decode(cashaddress); const isMainnet = VALID_PREFIXES_MAINNET.includes(prefix); // Get correct version byte for legacy format let versionByte; switch (type) { case 'P2PKH': versionByte = isMainnet ? 0 : 111; break; case 'P2SH': versionByte = isMainnet ? 5 : 196; break; default: throw new ValidationError('Unsupported address type: ' + type); } var buffer = Buffer.alloc(1 + hash.length); buffer[0] = versionByte; buffer.set(hash, 1); return bs58check.encode(buffer); } /** * Return true for a valid cashaddress * Prefixless addresses with valid checksum are also valid * * @static * @param {string} testedAddress a string tested for cashaddress validity * @param {string} optionalPrefix cashaddr prefix * @returns {bool} * @throws {ValidationError} */ function isValidCashAddress(cashaddress, optionalPrefix = false) { try { const { prefix } = decode(cashaddress); if (optionalPrefix) { return prefix === optionalPrefix; } return true; } catch (err) { return false; } } +/** + * Return true for a valid cashaddress + * Prefixless addresses with valid checksum are also valid + * + * @static + * @param {string} address a valid p2pkh or p2sh cash address + * @returns {string} the outputScript associated with this address and type + * @throws {ValidationError} if decode fails + */ +function getOutputScriptFromAddress(address) { + const { type, hash } = decode(address, true); + let registrationOutputScript; + if (type === 'p2pkh') { + registrationOutputScript = `76a914${hash}88ac`; + } else { + registrationOutputScript = `a914${hash}87`; + } + return registrationOutputScript; +} + module.exports = { encode: encode, decode: decode, uint8arraytoString: uint8arraytoString, encodeOutputScript: encodeOutputScript, getTypeAndHashFromOutputScript: getTypeAndHashFromOutputScript, toLegacy: toLegacy, isValidCashAddress: isValidCashAddress, + getOutputScriptFromAddress: getOutputScriptFromAddress, ValidationError: ValidationError, }; diff --git a/modules/ecashaddrjs/test/cashaddr.js b/modules/ecashaddrjs/test/cashaddr.js index cc4653e62..f4556af1c 100644 --- a/modules/ecashaddrjs/test/cashaddr.js +++ b/modules/ecashaddrjs/test/cashaddr.js @@ -1,652 +1,691 @@ /** * @license * https://reviews.bitcoinabc.org * Copyright (c) 2017-2020 Emilio Almansi * Copyright (c) 2023 Bitcoin ABC * Distributed under the MIT software license, see the accompanying * file LICENSE or http://www.opensource.org/licenses/mit-license.php. */ 'use strict'; const { assert } = require('chai'); const cashaddr = require('../src/cashaddr'); const { Random, MersenneTwister19937 } = require('random-js'); describe('cashaddr', () => { const NETWORKS = ['ecash', 'ectest', 'etoken']; const ADDRESS_TYPES = ['P2PKH', 'P2SH']; const VALID_SIZES = [20, 24, 28, 32, 40, 48, 56, 64]; const TEST_HASHES = [ new Uint8Array([ 118, 160, 64, 83, 189, 160, 168, 139, 218, 81, 119, 184, 106, 21, 195, 178, 159, 85, 152, 115, ]), new Uint8Array([ 203, 72, 18, 50, 41, 156, 213, 116, 49, 81, 172, 75, 45, 99, 174, 25, 142, 123, 176, 169, ]), new Uint8Array([ 1, 31, 40, 228, 115, 201, 95, 64, 19, 215, 213, 62, 197, 251, 195, 180, 45, 248, 237, 16, ]), ]; const TEST_HASHES_STRINGS = [ '76a04053bda0a88bda5177b86a15c3b29f559873', 'cb481232299cd5743151ac4b2d63ae198e7bb0a9', '011f28e473c95f4013d7d53ec5fbc3b42df8ed10', ]; const TEST_P2PKH_OUTPUTSCRIPTS = [ '76a91476a04053bda0a88bda5177b86a15c3b29f55987388ac', '76a914cb481232299cd5743151ac4b2d63ae198e7bb0a988ac', '76a914011f28e473c95f4013d7d53ec5fbc3b42df8ed1088ac', ]; const EXPECTED_P2PKH_OUTPUTS = [ 'ecash:qpm2qsznhks23z7629mms6s4cwef74vcwva87rkuu2', 'ecash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4ykdcjcn6n', 'ecash:qqq3728yw0y47sqn6l2na30mcw6zm78dzq653y7pv5', ]; const EXPECTED_P2PKH_OUTPUTS_LEGACY = [ '1BpEi6DfDAUFd7GtittLSdBeYJvcoaVggu', '1KXrWXciRDZUpQwQmuM1DbwsKDLYAYsVLR', '16w1D5WRVKJuZUsSRzdLp9w3YGcgoxDXb', ]; const TEST_P2SH_OUTPUTSCRIPTS = [ 'a91476a04053bda0a88bda5177b86a15c3b29f55987387', 'a914cb481232299cd5743151ac4b2d63ae198e7bb0a987', 'a914011f28e473c95f4013d7d53ec5fbc3b42df8ed1087', ]; const EXPECTED_P2SH_OUTPUTS = [ 'ecash:ppm2qsznhks23z7629mms6s4cwef74vcwv2zrv3l8h', 'ecash:pr95sy3j9xwd2ap32xkykttr4cvcu7as4ypg9alspw', 'ecash:pqq3728yw0y47sqn6l2na30mcw6zm78dzqd3vtezhf', ]; const EXPECTED_P2SH_OUTPUTS_LEGACY = [ '3CWFddi6m4ndiGyKqzYvsFYagqDLPVMTzC', '3LDsS579y7sruadqu11beEJoTjdFiFCdX4', '31nwvkZwyPdgzjBJZXfDmSWsC4ZLKpYyUw', ]; const EXPECTED_P2PKH_OUTPUTS_TESTNET = [ 'ectest:qpm2qsznhks23z7629mms6s4cwef74vcwvmvqr33lm', 'ectest:qr95sy3j9xwd2ap32xkykttr4cvcu7as4ysxxjl7ez', 'ectest:qqq3728yw0y47sqn6l2na30mcw6zm78dzqul0yev09', ]; const EXPECTED_P2PKH_OUTPUTS_TESTNET_LEGACY = [ 'mrLC19Je2BuWQDkWSTriGYPyQJXKkkBmCx', 'mz3ooahhEEzjbXR2VUKP3XACBCwF5zhQBy', 'mfctJGAVEWkZgfxV9zy1AjNFuXsKi2VXB8', ]; const EXPECTED_P2SH_OUTPUTS_TESTNET = [ 'ectest:ppm2qsznhks23z7629mms6s4cwef74vcwvvfavkjyx', 'ectest:pr95sy3j9xwd2ap32xkykttr4cvcu7as4y8rmacazl', 'ectest:pqq3728yw0y47sqn6l2na30mcw6zm78dzqt6jt705c', ]; const EXPECTED_P2SH_OUTPUTS_TESTNET_LEGACY = [ '2N44ThNe8NXHyv4bsX8AoVCXquBRW94Ls7W', '2NBn5Vp3BaaPD7NGPa8dUGBJ4g5qRXq92wG', '2MsM9zVVyar93CWorEfH6PPW8QQmW3s1uh6', ]; const ALL_VALID_MAINNET_ADDRESSES = EXPECTED_P2PKH_OUTPUTS.concat( EXPECTED_P2SH_OUTPUTS, ); const ALL_VALID_TESTNET_ADDRESSES = EXPECTED_P2PKH_OUTPUTS_TESTNET.concat( EXPECTED_P2SH_OUTPUTS_TESTNET, ); const ALL_VALID_ADDRESSES = ALL_VALID_MAINNET_ADDRESSES.concat( ALL_VALID_TESTNET_ADDRESSES, ); const random = new Random(MersenneTwister19937.seed(42)); function getRandomHash(size) { const hash = new Uint8Array(size); for (let i = 0; i < size; ++i) { hash[i] = random.integer(0, 255); } return hash; } describe('#encode()', () => { it('should fail on an invalid prefix', () => { assert.throws(() => { cashaddr.encode( 'some invalid prefix', ADDRESS_TYPES[0], new Uint8Array([]), ); }, cashaddr.ValidationError); }); it('should fail on a prefix with mixed letter case', () => { assert.throws(() => { cashaddr.encode('EcAsH', ADDRESS_TYPES[0], new Uint8Array([])); }, cashaddr.ValidationError); }); it('should fail on an invalid type', () => { assert.throws(() => { cashaddr.encode( NETWORKS[0], 'some invalid type', new Uint8Array([]), ); }, cashaddr.ValidationError); }); it('should fail on hashes of invalid length', () => { for (const size of VALID_SIZES) { const hash = getRandomHash(size - 1); assert.throws(() => { cashaddr.encode(NETWORKS[0], ADDRESS_TYPES[0], hash); }, cashaddr.ValidationError); } }); it('should encode test hashes on mainnet correctly with uint8Array hash input', () => { for (const index in TEST_HASHES) { assert.equal( cashaddr.encode('ecash', 'P2PKH', TEST_HASHES[index]), EXPECTED_P2PKH_OUTPUTS[index], ); assert.equal( cashaddr.encode('ecash', 'P2SH', TEST_HASHES[index]), EXPECTED_P2SH_OUTPUTS[index], ); } }); it('should encode test hashes on mainnet correctly with string input for hash', () => { for (const index in TEST_HASHES) { assert.equal( cashaddr.encode( 'ecash', 'P2PKH', TEST_HASHES_STRINGS[index], ), EXPECTED_P2PKH_OUTPUTS[index], ); assert.equal( cashaddr.encode( 'ecash', 'P2SH', TEST_HASHES_STRINGS[index], ), EXPECTED_P2SH_OUTPUTS[index], ); } }); it('should encode test hashes on testnet correctly', () => { for (const index in TEST_HASHES) { assert.equal( cashaddr.encode('ectest', 'P2PKH', TEST_HASHES[index]), EXPECTED_P2PKH_OUTPUTS_TESTNET[index], ); assert.equal( cashaddr.encode('ectest', 'P2SH', TEST_HASHES[index]), EXPECTED_P2SH_OUTPUTS_TESTNET[index], ); } }); }); describe('#getTypeAndHashFromOutputScript()', () => { it('should get type and hash from outputScripts on mainnet correctly', () => { for (const index in TEST_HASHES_STRINGS) { assert.deepEqual( cashaddr.getTypeAndHashFromOutputScript( TEST_P2PKH_OUTPUTSCRIPTS[index], ), { type: 'P2PKH', hash: TEST_HASHES_STRINGS[index] }, ); assert.deepEqual( cashaddr.getTypeAndHashFromOutputScript( TEST_P2SH_OUTPUTSCRIPTS[index], ), { type: 'P2SH', hash: TEST_HASHES_STRINGS[index] }, ); } }); it('should fail on unsupported outputScripts', () => { assert.throws(() => { // missing initial a cashaddr.getTypeAndHashFromOutputScript( '91476a04053bda0a88bda5177b86a15c3b29f55987387', ); }, cashaddr.ValidationError); assert.throws(() => { // p2pkh prefix and p2sh suffix cashaddr.getTypeAndHashFromOutputScript( '76a91476a04053bda0a88bda5177b86a15c3b29f55987387', ); }, cashaddr.ValidationError); assert.throws(() => { // some random string cashaddr.getTypeAndHashFromOutputScript( 'chronikWouldNeverReturnThis', ); }, cashaddr.ValidationError); assert.throws(() => { // Invalid hash length of 21 bytes (20 and 24 are valid) cashaddr.getTypeAndHashFromOutputScript( 'a91476a04053bda0a88bda5177b86a15c3b29f5598737387', ); }, cashaddr.ValidationError); }); }); describe('#encodeOutputScript()', () => { it('should encode outputScripts on mainnet correctly', () => { for (const index in TEST_HASHES) { assert.equal( cashaddr.encodeOutputScript( TEST_P2PKH_OUTPUTSCRIPTS[index], ), EXPECTED_P2PKH_OUTPUTS[index], ); assert.equal( cashaddr.encodeOutputScript(TEST_P2SH_OUTPUTSCRIPTS[index]), EXPECTED_P2SH_OUTPUTS[index], ); } }); it('should encode outputScripts to testnet prefix correctly', () => { for (const index in TEST_HASHES) { assert.equal( cashaddr.encodeOutputScript( TEST_P2PKH_OUTPUTSCRIPTS[index], 'ectest', ), EXPECTED_P2PKH_OUTPUTS_TESTNET[index], ); assert.equal( cashaddr.encodeOutputScript( TEST_P2SH_OUTPUTSCRIPTS[index], 'ectest', ), EXPECTED_P2SH_OUTPUTS_TESTNET[index], ); } }); it('should fail on unsupported outputScripts', () => { assert.throws(() => { // missing initial a cashaddr.encodeOutputScript( '91476a04053bda0a88bda5177b86a15c3b29f55987387', ); }, cashaddr.ValidationError); assert.throws(() => { // p2pkh prefix and p2sh suffix cashaddr.encodeOutputScript( '76a91476a04053bda0a88bda5177b86a15c3b29f55987387', ); }, cashaddr.ValidationError); assert.throws(() => { // some random string cashaddr.encodeOutputScript('chronikWouldNeverReturnThis'); }, cashaddr.ValidationError); assert.throws(() => { // Invalid hash length of 21 bytes (20 and 24 are valid) cashaddr.encodeOutputScript( 'a91476a04053bda0a88bda5177b86a15c3b29f5598737387', ); }, cashaddr.ValidationError); }); }); describe('#decode()', () => { it('should fail when the version byte is invalid', () => { assert.throws(() => { cashaddr.decode( 'ecash:zpm2qsznhks23z7629mms6s4cwef74vcwv6ddac6re', ); }, cashaddr.ValidationError); }); it('should fail when given an address with mixed letter case', () => { assert.throws(() => { cashaddr.decode( 'ecash:QPM2QSZNHKS23Z7629MMS6s4cwef74vcwvA87RKUU2', ); }, cashaddr.ValidationError); assert.throws(() => { cashaddr.decode( 'eCASH:qpm2qsznhks23z7629mms6s4cwef74vcwva87rkuu2', ); }, cashaddr.ValidationError); assert.throws(() => { cashaddr.decode( 'Ecash:QPM2QSZNHKS23Z7629MMS6s4cwef74vcwvA87RKUU2', ); }, cashaddr.ValidationError); }); it('should decode a valid address regardless of letter case', () => { assert.deepEqual( cashaddr.decode( 'ecash:qpm2qsznhks23z7629mms6s4cwef74vcwva87rkuu2', ).hash, cashaddr.decode( 'ECASH:QPM2QSZNHKS23Z7629MMS6S4CWEF74VCWVA87RKUU2', ).hash, ); }); it('should decode valid addresses correctly and return hash160 as a string if user so specifies for P2PKH addresses', () => { for (const index in EXPECTED_P2PKH_OUTPUTS) { assert.equal( cashaddr.decode(EXPECTED_P2PKH_OUTPUTS[index], true).hash, TEST_HASHES_STRINGS[index], ); } }); it('should decode valid addresses correctly and return hash160 as a string if user so specifies for P2SH addresses', () => { for (const index in EXPECTED_P2SH_OUTPUTS) { assert.equal( cashaddr.decode(EXPECTED_P2SH_OUTPUTS[index], true).hash, TEST_HASHES_STRINGS[index], ); } }); it('should accept prefixless input if checksum is valid', () => { assert.deepEqual( cashaddr.decode('qpm2qsznhks23z7629mms6s4cwef74vcwva87rkuu2') .hash, cashaddr.decode( 'ecash:qpm2qsznhks23z7629mms6s4cwef74vcwva87rkuu2', ).hash, ); }); it('should reject prefixless input if checksum is invalid', () => { assert.throws(() => { cashaddr.decode( 'qpm2qsznhks23z7629mms6s4cwef74vcwvINVALIDCHECKSUM', ); }, cashaddr.ValidationError); }); it('should reject any input that has two prefixes for some reason', () => { assert.throws(() => { cashaddr.decode( 'ecash:bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwva87rkuu2', ); }, cashaddr.ValidationError); }); it('should fail when decoding for a different network', () => { for (const network of NETWORKS) { for (const anotherNetwork of NETWORKS) { if (network !== anotherNetwork) { const hash = getRandomHash(20); assert.throws(() => { const address = cashaddr.encode( network, ADDRESS_TYPES[0], hash, ); const invalidAddress = [ anotherNetwork, address.split(':')[1], ].join(':'); cashaddr.decode(invalidAddress); }, cashaddr.ValidationError); } } } }); }); describe('#encode() #decode()', () => { it('should encode and decode all sizes correctly', () => { for (const size of VALID_SIZES) { const hash = getRandomHash(size); const address = cashaddr.encode( NETWORKS[0], ADDRESS_TYPES[0], hash, ); const { prefix, type, hash: actualHash, } = cashaddr.decode(address); assert.equal(prefix, NETWORKS[0]); assert.equal(type, ADDRESS_TYPES[0]); assert.deepEqual(actualHash, hash); } }); it('should encode and decode all types and networks', () => { for (const type of ADDRESS_TYPES) { for (const network of NETWORKS) { const hash = getRandomHash(20); const address = cashaddr.encode(network, type, hash); const { prefix, type: actualType, hash: actualHash, } = cashaddr.decode(address); assert.equal(prefix, network); assert.equal(actualType, type); assert.deepEqual(actualHash, hash); } } }); it('should encode and decode all types and networks if type input is lowercase', () => { for (const type of ADDRESS_TYPES) { for (const network of NETWORKS) { const hash = getRandomHash(20); const address = cashaddr.encode( network, type.toLowerCase(), hash, ); const { prefix, type: actualType, hash: actualHash, } = cashaddr.decode(address); assert.equal(prefix, network); assert.equal(actualType, type); assert.deepEqual(actualHash, hash); } } }); it('should encode and decode all types and networks if type input is lowercase and desired return data is formatted for chronik', () => { for (const type of ADDRESS_TYPES) { for (const network of NETWORKS) { const hash = getRandomHash(20); const address = cashaddr.encode( network, type.toLowerCase(), hash, ); const { prefix, type: actualType, hash: actualHash, } = cashaddr.decode(address, true); assert.equal(prefix, network); assert.equal(actualType, type.toLowerCase()); assert.deepEqual( actualHash, cashaddr.uint8arraytoString(hash), ); } } }); it('should encode and decode many random hashes', () => { const NUM_TESTS = 1000; for (let i = 0; i < NUM_TESTS; ++i) { for (const type of ADDRESS_TYPES) { const hash = getRandomHash(20); const address = cashaddr.encode(NETWORKS[0], type, hash); const { prefix, type: actualType, hash: actualHash, } = cashaddr.decode(address); assert.equal(prefix, NETWORKS[0]); assert.equal(actualType, type); assert.deepEqual(actualHash, hash); } } }); }); describe('#toLegacy()', () => { it('should fail when the version byte is invalid and cashaddr.decode throws an error', () => { assert.throws(() => { cashaddr.toLegacy( 'ecash:zpm2qsznhks23z7629mms6s4cwef74vcwv6ddac6re', ); }, cashaddr.ValidationError); }); it('should fail when given input that is not a valid cashaddress', () => { assert.throws(() => { cashaddr.toLegacy('1BpEi6DfDAUFd7GtittLSdBeYJvcoaVggu'); }, cashaddr.ValidationError); }); it('should convert mainnet p2pkh ecash addresses to expected legacy format', () => { for (const index in EXPECTED_P2PKH_OUTPUTS) { assert.equal( cashaddr.toLegacy(EXPECTED_P2PKH_OUTPUTS[index]), EXPECTED_P2PKH_OUTPUTS_LEGACY[index], ); } }); it('should convert testnet p2pkh ecash addresses to expected legacy format', () => { for (const index in EXPECTED_P2PKH_OUTPUTS_TESTNET) { assert.equal( cashaddr.toLegacy(EXPECTED_P2PKH_OUTPUTS_TESTNET[index]), EXPECTED_P2PKH_OUTPUTS_TESTNET_LEGACY[index], ); } }); it('should convert mainnet p2sh ecash addresses to expected legacy format', () => { for (const index in EXPECTED_P2SH_OUTPUTS) { assert.equal( cashaddr.toLegacy(EXPECTED_P2SH_OUTPUTS[index]), EXPECTED_P2SH_OUTPUTS_LEGACY[index], ); } }); it('should convert testnet p2sh ecash addresses to expected legacy format', () => { for (const index in EXPECTED_P2SH_OUTPUTS_TESTNET) { assert.equal( cashaddr.toLegacy(EXPECTED_P2SH_OUTPUTS_TESTNET[index]), EXPECTED_P2SH_OUTPUTS_TESTNET_LEGACY[index], ); } }); }); describe('#isValidCashAddress()', () => { it('returns false for address with invalid version byte', () => { assert.equal( cashaddr.isValidCashAddress( 'ecash:zpm2qsznhks23z7629mms6s4cwef74vcwv6ddac6re', ), false, ); }); it('returns false for a legacy address', () => { assert.equal( cashaddr.isValidCashAddress( EXPECTED_P2PKH_OUTPUTS_TESTNET_LEGACY[0], ), false, ); }); it('returns true for mainnet and testnet p2pkh and p2sh cashaddresses', () => { for (const index in ALL_VALID_ADDRESSES) { assert.equal( cashaddr.isValidCashAddress(ALL_VALID_ADDRESSES[index]), true, ); } }); it('returns true for all mainnet and testnet p2pkh and p2sh cashaddresses if prefixless but with correct checksum', () => { for (const index in ALL_VALID_ADDRESSES) { const thisPrefixedAddress = ALL_VALID_ADDRESSES[index]; const thisPrefixlessAddrress = thisPrefixedAddress.slice( thisPrefixedAddress.indexOf(':') + 1, ); assert.equal( cashaddr.isValidCashAddress(thisPrefixlessAddrress), true, ); } }); it('returns true for prefixless ecash: checksummed addresses against specified ecash: prefix type', () => { for (const index in ALL_VALID_MAINNET_ADDRESSES) { const thisPrefixedAddress = ALL_VALID_MAINNET_ADDRESSES[index]; const thisPrefixlessAddrress = thisPrefixedAddress.slice( thisPrefixedAddress.indexOf(':') + 1, ); assert.equal( cashaddr.isValidCashAddress( thisPrefixlessAddrress, 'ecash', ), true, ); } }); it('returns true for mainnet p2pkh and p2sh cashaddresses if ecash prefix is specified', () => { for (const index in ALL_VALID_MAINNET_ADDRESSES) { assert.equal( cashaddr.isValidCashAddress( ALL_VALID_MAINNET_ADDRESSES[index], 'ecash', ), true, ); } }); it('returns true for testnet p2pkh cashaddresses if testnet prefix is specified', () => { for (const index in ALL_VALID_TESTNET_ADDRESSES) { assert.equal( cashaddr.isValidCashAddress( ALL_VALID_TESTNET_ADDRESSES[index], 'ectest', ), true, ); } }); it('returns false for testnet p2pkh cashaddresses if mainnet prefix is specified', () => { for (const index in ALL_VALID_TESTNET_ADDRESSES) { assert.equal( cashaddr.isValidCashAddress( ALL_VALID_TESTNET_ADDRESSES[index], 'ecash', ), false, ); } }); }); + describe('#getOutputScriptFromAddress()', () => { + it('should get outputScripts from address on mainnet correctly', () => { + for (const index in EXPECTED_P2PKH_OUTPUTS) { + assert.equal( + cashaddr.getOutputScriptFromAddress( + EXPECTED_P2PKH_OUTPUTS[index], + ), + TEST_P2PKH_OUTPUTSCRIPTS[index], + ); + assert.equal( + cashaddr.getOutputScriptFromAddress( + EXPECTED_P2SH_OUTPUTS[index], + ), + TEST_P2SH_OUTPUTSCRIPTS[index], + ); + } + }); + it('should get outputScripts from testnet addresses correctly', () => { + for (const index in TEST_HASHES) { + assert.equal( + cashaddr.getOutputScriptFromAddress( + EXPECTED_P2PKH_OUTPUTS_TESTNET[index], + ), + TEST_P2PKH_OUTPUTSCRIPTS[index], + ); + assert.equal( + cashaddr.getOutputScriptFromAddress( + EXPECTED_P2SH_OUTPUTS_TESTNET[index], + ), + TEST_P2SH_OUTPUTSCRIPTS[index], + ); + } + }); + it('should fail on invalid addresses', () => { + assert.throws(() => { + cashaddr.getOutputScriptFromAddress('notAnAddress'); + }, cashaddr.ValidationError); + }); + }); });