libnexa-ts
Version:
A pure and powerful Nexa SDK library.
251 lines (228 loc) • 8.46 kB
text/typescript
import Base32 from "../../encoding/base32";
import BufferUtils from "../../utils/buffer.utils";
import ValidationUtils from "../../utils/validation.utils";
export enum AddressType {
PayToPublicKeyHash = 'P2PKH',
PayToScriptTemplate = 'P2ST',
GroupIdAddress = 'GROUP'
}
interface AddressParts {
prefix: string;
type: AddressType;
data: Uint8Array;
}
export default class AddressFormatter {
/** @see encodeAddress */
public static encode(prefix: string, type: AddressType, data: Uint8Array): string {
return this.encodeAddress({ prefix, type, data })
}
/**
* Encodes a hash from a given type into a Nexa address with the given prefix.
*
* @param address Object contains Network prefix (E.g.: 'nexa'), Type of address to generate and data to encode.
*/
public static encodeAddress(address: AddressParts): string {
ValidationUtils.validateArgument(typeof address.prefix === 'string' && this.isValidPrefix(address.prefix), 'Invalid prefix: ' + address.prefix + '.');
ValidationUtils.validateArgument(typeof address.type === 'string', 'Invalid type: ' + address.type + '.');
ValidationUtils.validateArgument(BufferUtils.isBuffer(address.data), 'Invalid data: ' + address.data + '.');
let eight0 = [0,0,0,0, 0,0,0,0];
let prefixData = this.prefixToArray(address.prefix).concat([0]);
let versionByte = this.getTypeBits(address.type);
let payloadData = this.convertBits(BufferUtils.concat([Uint8Array.from([versionByte]), address.data]), 8, 5);
let checksumData = prefixData.concat(payloadData).concat(eight0);
let payload = payloadData.concat(this.checksumToArray(this.polymod(checksumData)));
return address.prefix + ':' + Base32.encode(payload);
}
/**
* Decodes the given address into its constituting prefix, type and data. See {@link encodeAddress}.
*
* @param address Address to decode. E.g.: 'nexa:qpm2qsznhks23z7629mms6s4cwef74vcwvgpsey0xy'.
*/
public static decode(address: string): AddressParts {
ValidationUtils.validateArgument(typeof address === 'string' && this.hasSingleCase(address), 'Invalid address: ' + address + '.');
let pieces = address.toLowerCase().split(':');
ValidationUtils.validateState(pieces.length === 2, 'Missing prefix: ' + address + '.');
let prefix = pieces[0];
let payload = Base32.decode(pieces[1]);
ValidationUtils.validateState(this.validChecksum(prefix, payload), 'Invalid checksum: ' + address + '.');
let convertedBits = this.convertBits(payload.slice(0, -8), 5, 8, true);
let versionByte = convertedBits.shift();
let hash = convertedBits;
// no length limits in nexa: validate(getHashSize(versionByte) === hash.length * 8, 'Invalid hash size: ' + address + '.');
let type = this.getType(versionByte!);
return {
prefix: prefix,
type: type,
data: Uint8Array.from(hash),
};
}
/**
* Checks whether a string is a valid prefix; ie., it has a single letter case.
* and is only letters.
*
* @param prefix
*/
private static isValidPrefix(prefix: string): boolean {
return this.hasSingleCase(prefix) && /^[A-Za-z]+$/.test(prefix);
}
/**
* Derives an array from the given prefix to be used in the computation
* of the address' checksum.
*
* @param prefix Network prefix. E.g.: 'nexa'.
*/
private static prefixToArray(prefix: string): number[] {
let result: number[] = [];
for (let i = 0; i < prefix.length; ++i) {
result.push(prefix.charCodeAt(i) & 31);
}
return result;
}
/**
* Returns an array representation of the given checksum to be encoded
* within the address' payload.
*
* @param checksum Computed checksum.
*/
private static checksumToArray(checksum: number): number[] {
let result: number[] = [];
for (let i = 0; i < 8; ++i) {
result.push(checksum & 31);
checksum /= 32;
}
return result.reverse();
}
/**
* Returns the bit representation of the given type within the version byte.
*
* @param type Address type. Either 'P2PKH' or 'P2SH'.
*/
private static getTypeBits(type: AddressType): number {
switch (type) {
case AddressType.PayToPublicKeyHash:
return 0;
case AddressType.PayToScriptTemplate:
return 19<<3;
case AddressType.GroupIdAddress:
return 11<<3;
default:
throw new TypeError('Invalid type: ' + type + '.');
}
}
/**
* Retrieves the address type from its bit representation within the
* version byte.
*
* @param versionByte
*/
private static getType(versionByte: number): AddressType {
switch (versionByte & 248) {
case 0:
return AddressType.PayToPublicKeyHash;
case 19<<3:
return AddressType.PayToScriptTemplate;
case 11<<3:
return AddressType.GroupIdAddress;
default:
throw new Error('Invalid address type in version byte: ' + versionByte + '.');
}
}
/**
* Returns true if, and only if, the given string contains either uppercase
* or lowercase letters, but not both.
*
* @param string Input string.
*/
private static hasSingleCase(string: string): boolean {
return string === string.toLowerCase() || string === string.toUpperCase();
}
/**
* Verify that the payload has not been corrupted by checking that the
* checksum is valid.
*
* @param prefix Network prefix. E.g.: 'nexa'.
* @param payload Array of 5-bit integers containing the address' payload.
*/
private static validChecksum(prefix: string, payload: number[]): boolean {
let prefixData = this.prefixToArray(prefix).concat([0]);
return this.polymod(prefixData.concat(payload)) === 0;
}
/**
* Computes a checksum from the given input data as specified for the CashAddr
* format: https://github.com/Bitcoin-UAHF/spec/blob/master/cashaddr.md.
*
* @param data Array of 5-bit integers over which the checksum is to be computed.
*/
private static polymod(data: number[]): number {
let GENERATOR1 = [0x98, 0x79, 0xf3, 0xae, 0x1e];
let GENERATOR2 = [0xf2bc8e61, 0xb76d99e2, 0x3e5fb3c4, 0x2eabe2a8, 0x4f43e470];
// Treat c as 8 bits + 32 bits
let c0 = 0, c1 = 1, C = 0;
for (let j = 0; j < data.length; j++) {
// Set C to c shifted by 35
C = c0 >>> 3;
// 0x[07]ffffffff
c0 &= 0x07;
// Shift as a whole number
c0 <<= 5;
c0 |= c1 >>> 27;
// 0xffffffff >>> 5
c1 &= 0x07ffffff;
c1 <<= 5;
// xor the last 5 bits
c1 ^= data[j];
for (let i = 0; i < GENERATOR1.length; ++i) {
if (C & (1 << i)) {
c0 ^= GENERATOR1[i];
c1 ^= GENERATOR2[i];
}
}
}
c1 ^= 1;
// Negative numbers -> large positive numbers
if (c1 < 0) {
c1 ^= 1 << 31;
c1 += (1 << 30) * 2;
}
// Unless bitwise operations are used,
// numbers are consisting of 52 bits, except
// the sign bit. The result is max 40 bits,
// so it fits perfectly in one number!
return c0 * (1 << 30) * 4 + c1;
}
/**
* Converts an array of integers made up of `from` bits into an
* array of integers made up of `to` bits. The output array is
* zero-padded if necessary, unless strict mode is true.
* Original by Pieter Wuille: https://github.com/sipa/bech32.
*
* @param data Array of integers made up of `from` bits.
* @param from Length in bits of elements in the input array.
* @param to Length in bits of elements in the output array.
* @param strict Require the conversion to be completed without padding.
*/
private static convertBits(data: Uint8Array | number[], from: number, to: number, strict = false): number[] {
let accumulator = 0;
let bits = 0;
let result = [];
let mask = (1 << to) - 1;
for (let i=0; i<data.length; i++) {
let value = data[i];
ValidationUtils.validateArgument(!(value < 0 || (value >> from) !== 0), 'value ' + value);
accumulator = (accumulator << from) | value;
bits += from;
while (bits >= to) {
bits -= to;
result.push((accumulator >> bits) & mask);
}
}
if (!strict) {
if (bits > 0) {
result.push((accumulator << (to - bits)) & mask);
}
} else {
ValidationUtils.validateState(!(bits >= from || ((accumulator << (to - bits)) & mask)), 'Conversion requires padding but strict mode was used');
}
return result;
}
}