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);
+ });
+ });
});