Network.js

/* eslint-disable no-unused-vars */
import { PublicKey } from './CryptoTypes.js';
import { NetworkTimestamp, NetworkTimestampDatetimeConverter } from './NetworkTimestamp.js';
/* eslint-enable no-unused-vars */
import Ripemd160 from 'ripemd160';

const BASE32_RFC4648_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

/**
 * Represents a network.
 * @template {{bytes: Uint8Array}} TAddress
 * @template {NetworkTimestamp} TNetworkTimestamp
 */
export class Network {
	/**
	 * Creates a new network with the specified name and identifier byte.
	 * @param {string} name Network name.
	 * @param {number} identifier Network identifier byte.
	 * @param {NetworkTimestampDatetimeConverter} datetimeConverter Network timestamp datetime converter associated with this network.
	 * @param {function} addressHasher Gets the primary hasher to use in the public key to address conversion.
	 * @param {function} createAddress Creates an encoded address from an address without checksum and checksum bytes.
	 * @param {AddressConstructable} AddressClass Address class associated with this network.
	 * @param {Constructable} NetworkTimestampClass Network timestamp class associated with this network.
	 */
	constructor(name, identifier, datetimeConverter, addressHasher, createAddress, AddressClass, NetworkTimestampClass) {
		/**
		 * Network name.
		 * @type string
		 */
		this.name = name;

		/**
		 * Network identifier byte.
		 * @type number
		 */
		this.identifier = identifier;

		/**
		 * Network timestamp datetime converter associated with this network.
		 * @type NetworkTimestampDatetimeConverter
		 */
		this.datetimeConverter = datetimeConverter;

		/**
		 * @private
		 */
		this._addressHasher = addressHasher;

		/**
		 * @private
		 */
		this._createAddress = createAddress;

		/**
		 * @private
		 */
		this._AddressClass = AddressClass;

		/**
		 * @private
		 */
		this._NetworkTimestampClass = NetworkTimestampClass;
	}

	/**
	 * Converts a public key to an address.
	 * @param {PublicKey} publicKey Public key to convert.
	 * @returns {TAddress} Address corresponding to the public key input.
	 */
	publicKeyToAddress(publicKey) {
		const partOneHashBuilder = this._addressHasher();
		partOneHashBuilder.update(publicKey.bytes);
		const partOneHash = partOneHashBuilder.digest();

		const partTwoHash = new Ripemd160().update(Buffer.from(partOneHash)).digest();

		const version = new Uint8Array([this.identifier, ...partTwoHash]);

		const partThreeHashBuilder = this._addressHasher();
		partThreeHashBuilder.update(version);
		const checksum = partThreeHashBuilder.digest().subarray(0, 4);

		return this._createAddress(version, checksum);
	}

	/**
	 * Checks if an address string is valid and belongs to this network.
	 * @param {string} addressString Address to check.
	 * @returns {boolean} \c true if address is valid and belongs to this network.
	 */
	isValidAddressString(addressString) {
		if (this._AddressClass.ENCODED_SIZE !== addressString.length)
			return false;

		for (let i = 0; i < addressString.length; ++i) {
			if (-1 === BASE32_RFC4648_ALPHABET.indexOf(addressString[i]))
				return false;
		}

		return this.isValidAddress(new this._AddressClass(addressString));
	}

	/**
	 * Checks if an address is valid and belongs to this network.
	 * @param {TAddress} address Address to check.
	 * @returns {boolean} \c true if address is valid and belongs to this network.
	 */
	isValidAddress(address) {
		if (address.bytes[0] !== this.identifier)
			return false;

		const hashBuilder = this._addressHasher();
		hashBuilder.update(address.bytes.subarray(0, 1 + 20));

		const checkSumFromAddress = address.bytes.subarray(1 + 20);
		const calculatedChecksum = hashBuilder.digest().subarray(0, checkSumFromAddress.length);

		for (let i = 0; i < checkSumFromAddress.length; ++i) {
			if (checkSumFromAddress[i] !== calculatedChecksum[i])
				return false;
		}

		return true;
	}

	/**
	 * Converts a network timestamp to a datetime.
	 * @param {TNetworkTimestamp} referenceNetworkTimestamp Reference network timestamp to convert.
	 * @returns {Date} Datetime representation of the reference network timestamp.
	 */
	toDatetime(referenceNetworkTimestamp) {
		return this.datetimeConverter.toDatetime(Number(referenceNetworkTimestamp.timestamp));
	}

	/**
	 * Converts a datetime to a network timestamp.
	 * @param {Date} referenceDatetime Reference datetime to convert.
	 * @returns {TNetworkTimestamp} Network timestamp representation of the reference datetime.
	 */
	fromDatetime(referenceDatetime) {
		return new this._NetworkTimestampClass(this.datetimeConverter.toDifference(referenceDatetime));
	}

	/**
	 * Returns string representation of this object.
	 * @returns {string} String representation of this object
	 */
	toString() {
		return this.name;
	}
}

/**
 * Provides utility functions for finding a network.
 */
export class NetworkLocator {
	/**
	 * Finds a network with a specified name within a list of networks.
	 * @template {Network<any, any>} TNetwork
	 * @param {Array<TNetwork>} networks List of networks to search.
	 * @param {Array<string>|string} singleOrMultipleNames Names for which to search.
	 * @returns {TNetwork} First network with a name in the supplied list.
	 */
	static findByName(networks, singleOrMultipleNames) {
		const names = Array.isArray(singleOrMultipleNames) ? singleOrMultipleNames : [singleOrMultipleNames];
		const matchingNetwork = networks.find(network => names.some(name => name === network.name));
		if (undefined === matchingNetwork)
			throw RangeError(`no network found with name '${names.join(', ')}'`);

		return matchingNetwork;
	}

	/**
	 * Finds a network with a specified identifier within a list of networks.
	 * @template {Network<any, any>} TNetwork
	 * @param {Array<TNetwork>} networks List of networks to search.
	 * @param {Array<number>|number} singleOrMultipleIdentifiers Identifiers for which to search.
	 * @returns {TNetwork} First network with an identifier in the supplied list.
	 */
	static findByIdentifier(networks, singleOrMultipleIdentifiers) {
		const identifiers = Array.isArray(singleOrMultipleIdentifiers) ? singleOrMultipleIdentifiers : [singleOrMultipleIdentifiers];
		const matchingNetwork = networks.find(network => identifiers.some(identifier => identifier === network.identifier));
		if (undefined === matchingNetwork)
			throw RangeError(`no network found with identifier '${identifiers.join(', ')}'`);

		return matchingNetwork;
	}
}

// region type declarations

/**
 * Constructable class type.
 * @class
 * @typedef {{new(...args: any[]): object}} Constructable
 */

/**
 * Address class type.
 * @class
 * @typedef {{new(...args: any[]): object, ENCODED_SIZE: number}} AddressConstructable
 */

// endregion