Refactor ./src/types Hash and derived types (#82)

Refactored Hash and derived types.
This commit is contained in:
Nathan Nichols
2020-07-09 16:04:29 -05:00
parent 8ac03699aa
commit 2b8fba0c8a
9 changed files with 403 additions and 217 deletions

View File

@@ -1,45 +1,58 @@
import { makeClass } from "../utils/make-class";
const { decodeAccountID, encodeAccountID } = require("ripple-address-codec");
const { Hash160 } = require("./hash-160");
import { decodeAccountID, encodeAccountID } from "ripple-address-codec";
import { Hash160 } from "./hash-160";
const AccountID = makeClass(
{
AccountID(bytes) {
Hash160.call(this, bytes);
},
inherits: Hash160,
statics: {
from(value) {
return value instanceof this
? value
: /^r/.test(value)
? this.fromBase58(value)
: new this(value);
},
cache: {},
fromCache(base58) {
let cached = this.cache[base58];
if (!cached) {
cached = this.cache[base58] = this.fromBase58(base58);
}
return cached;
},
fromBase58(value) {
const acc = new this(decodeAccountID(value));
acc._toBase58 = value;
return acc;
},
},
toJSON() {
return this.toBase58();
},
cached: {
toBase58() {
return encodeAccountID(this._bytes);
},
},
},
undefined
);
/**
* Class defining how to encode and decode an AccountID
*/
class AccountID extends Hash160 {
static readonly defaultAccountID: AccountID = new AccountID(Buffer.alloc(20));
constructor(bytes: Buffer) {
super(bytes ?? AccountID.defaultAccountID.bytes);
}
/**
* Defines how to construct an AccountID
*
* @param value either an existing AccountID, a hex-string, or a base58 r-Address
* @returns an AccountID object
*/
static from(value: AccountID | string): AccountID {
if (value instanceof this) {
return value;
}
return /^r/.test(value)
? this.fromBase58(value)
: new AccountID(Buffer.from(value, "hex"));
}
/**
* Defines how to build an AccountID from a base58 r-Address
*
* @param value a base58 r-Address
* @returns an AccountID object
*/
static fromBase58(value: string): AccountID {
return new AccountID(decodeAccountID(value));
}
/**
* Overload of toJSON
*
* @returns the base58 string for this AccountID
*/
toJSON(): string {
return this.toBase58();
}
/**
* Defines how to encode AccountID into a base58 address
*
* @returns the base58 string defined by this.bytes
*/
toBase58(): string {
return encodeAccountID(this.bytes);
}
}
export { AccountID };

View File

@@ -1,30 +1,34 @@
import { makeClass } from "../utils/make-class";
import { parseBytes } from "../utils/bytes-utils";
import { SerializedType } from "./serialized-type";
import { SerializedTypeClass } from "./serialized-type";
import { BinaryParser } from "../serdes/binary-parser";
const Blob = makeClass(
{
mixins: SerializedType,
Blob(bytes) {
if (bytes) {
this._bytes = parseBytes(bytes, Uint8Array);
} else {
this._bytes = new Uint8Array(0);
}
},
statics: {
fromParser(parser, hint) {
return new this(parser.read(hint));
},
from(value) {
if (value instanceof this) {
return value;
}
return new this(value);
},
},
},
undefined
);
/**
* Variable length encoded type
*/
class Blob extends SerializedTypeClass {
constructor(bytes: Buffer) {
super(bytes);
}
/**
* Defines how to read a Blob from a BinaryParser
*
* @param parser The binary parser to read the Blob from
* @param hint The length of the blob, computed by readVariableLengthLength() and passed in
* @returns A Blob object
*/
static fromParser(parser: BinaryParser, hint: number): Blob {
return new Blob(parser.read(hint));
}
/**
* Create a Blob object from a hex-string
*
* @param value existing Blob object or a hex-string
* @returns A Blob object
*/
static from(value: Blob | string): Blob {
return value instanceof Blob ? value : new Blob(Buffer.from(value, "hex"));
}
}
export { Blob };

View File

@@ -1,12 +1,12 @@
import { makeClass } from "../utils/make-class";
const _ = require("lodash");
const { slice } = require("../utils/bytes-utils");
const { Hash160 } = require("./hash-160");
import { Hash160 } from "./hash-160";
const ISO_REGEX = /^[A-Z0-9]{3}$/;
const HEX_REGEX = /^[A-F0-9]{40}$/;
function isoToBytes(iso) {
const bytes = new Uint8Array(20);
/**
* Convert an ISO code to a currency bytes representation
*/
function isoToBytes(iso: string): Buffer {
const bytes = Buffer.alloc(20);
if (iso !== "XRP") {
const isoBytes = iso.split("").map((c) => c.charCodeAt(0));
bytes.set(isoBytes, 12);
@@ -14,80 +14,123 @@ function isoToBytes(iso) {
return bytes;
}
function isISOCode(val) {
return val.length === 3; // ISO_REGEX.test(val);
/**
* Tests if ISO is a valid iso code
*/
function isIsoCode(iso: string): boolean {
return ISO_REGEX.test(iso);
}
function isHex(val) {
return HEX_REGEX.test(val);
/**
* Tests if hex is a valid hex-string
*/
function isHex(hex: string): boolean {
return HEX_REGEX.test(hex);
}
function isStringRepr(val) {
return _.isString(val) && (isISOCode(val) || isHex(val));
/**
* Tests if a string is a valid representation of a currency
*/
function isStringRepresentation(input: string): boolean {
return isIsoCode(input) || isHex(input);
}
function isBytesArray(val) {
return val.length === 20;
/**
* Tests if a Buffer is a valid representation of a currency
*/
function isBytesArray(bytes: Buffer): boolean {
return bytes.byteLength === 20;
}
function isValidRepr(val) {
return isStringRepr(val) || isBytesArray(val);
/**
* Ensures that a value is a valid representation of a currency
*/
function isValidRepresentation(input: Buffer | string): boolean {
return input instanceof Buffer
? isBytesArray(input)
: isStringRepresentation(input);
}
function bytesFromRepr(val) {
if (isValidRepr(val)) {
// We assume at this point that we have an object with a length, either 3,
// 20 or 40.
return val.length === 3 ? isoToBytes(val) : val;
/**
* Generate bytes from a string or buffer representation of a currency
*/
function bytesFromRepresentation(input: string): Buffer {
if (!isValidRepresentation(input)) {
throw new Error(`Unsupported Currency representation: ${input}`);
}
throw new Error(`Unsupported Currency repr: ${val}`);
return input.length === 3 ? isoToBytes(input) : Buffer.from(input, "hex");
}
const $uper = Hash160.prototype;
const Currency = makeClass(
{
inherits: Hash160,
getters: ["isNative", "iso"],
statics: {
init() {
this.XRP = new this(new Uint8Array(20));
},
from(val) {
return val instanceof this ? val : new this(bytesFromRepr(val));
},
},
Currency(bytes) {
Hash160.call(this, bytes);
this.classify();
},
classify() {
// We only have a non null iso() property available if the currency can be
// losslessly represented by the 3 letter iso code. If none is available a
// hex encoding of the full 20 bytes is the canonical representation.
let onlyISO = true;
/**
* Class defining how to encode and decode Currencies
*/
class Currency extends Hash160 {
static readonly XRP = new Currency(Buffer.alloc(20));
private readonly _iso?: string;
private readonly _isNative: boolean;
const bytes = this._bytes;
const code = slice(this._bytes, 12, 15, Array);
const iso = code.map((c) => String.fromCharCode(c)).join("");
constructor(byteBuf: Buffer) {
super(byteBuf ?? Currency.XRP.bytes);
for (let i = bytes.length - 1; i >= 0; i--) {
if (bytes[i] !== 0 && !(i === 12 || i === 13 || i === 14)) {
onlyISO = false;
break;
}
let onlyISO = true;
const bytes = this.bytes;
const code = this.bytes.slice(12, 15);
const iso = code.toString();
for (let i = bytes.length - 1; i >= 0; i--) {
if (bytes[i] !== 0 && !(i === 12 || i === 13 || i === 14)) {
onlyISO = false;
break;
}
const lossLessISO = onlyISO && iso !== "XRP" && ISO_REGEX.test(iso);
this._isNative = onlyISO && _.isEqual(code, [0, 0, 0]);
this._iso = this._isNative ? "XRP" : lossLessISO ? iso : null;
},
toJSON() {
if (this.iso()) {
return this.iso();
}
return $uper.toJSON.call(this);
},
},
undefined
);
}
const lossLessISO = onlyISO && iso !== "XRP" && ISO_REGEX.test(iso);
this._isNative = onlyISO && code.toString("hex") === "000000";
this._iso = this._isNative ? "XRP" : lossLessISO ? iso : undefined;
}
/**
* Tells if this currency is native
*
* @returns true if native, false if not
*/
isNative(): boolean {
return this._isNative;
}
/**
* Return the ISO code of this currency
*
* @returns ISO code if it exists, else undefined
*/
iso(): string | undefined {
return this._iso;
}
/**
* Constructs a Currency object
*
* @param val Currency object or a string representation of a currency
*/
static from(val: Currency | string): Currency {
return val instanceof this
? val
: new Currency(bytesFromRepresentation(val));
}
/**
* Gets the JSON representation of a currency
*
* @returns JSON representation
*/
toJSON(): string {
const iso = this.iso();
if (iso !== undefined) {
return iso;
}
return this.bytes.toString("hex").toUpperCase();
}
}
export { Currency };

View File

@@ -1,12 +1,15 @@
import { makeClass } from "../utils/make-class";
import { Hash } from "./hash";
const Hash128 = makeClass(
{
inherits: Hash,
statics: { width: 16 },
},
undefined
);
/**
* Hash with a width of 128 bits
*/
class Hash128 extends Hash {
static readonly width = 16;
static readonly ZERO_128: Hash128 = new Hash128(Buffer.alloc(Hash128.width));
constructor(bytes: Buffer) {
super(bytes ?? Hash128.ZERO_128.bytes);
}
}
export { Hash128 };

View File

@@ -1,12 +1,15 @@
import { makeClass } from "../utils/make-class";
const { Hash } = require("./hash");
import { Hash } from "./hash";
const Hash160 = makeClass(
{
inherits: Hash,
statics: { width: 20 },
},
undefined
);
/**
* Hash with a width of 160 bits
*/
class Hash160 extends Hash {
static readonly width = 20;
static readonly ZERO_160: Hash160 = new Hash160(Buffer.alloc(Hash160.width));
constructor(bytes: Buffer) {
super(bytes ?? Hash160.ZERO_160.bytes);
}
}
export { Hash160 };

View File

@@ -1,17 +1,15 @@
import { makeClass } from "../utils/make-class";
import { Hash } from "./hash";
const Hash256 = makeClass(
{
inherits: Hash,
statics: {
width: 32,
init() {
this.ZERO_256 = new this(new Uint8Array(this.width));
},
},
},
undefined
);
/**
* Hash with a width of 256 bits
*/
class Hash256 extends Hash {
static readonly width = 32;
static readonly ZERO_256 = new Hash256(Buffer.alloc(Hash256.width));
constructor(bytes: Buffer) {
super(bytes ?? Hash256.ZERO_256.bytes);
}
}
export { Hash256 };

View File

@@ -1,48 +1,70 @@
import * as assert from "assert";
import { makeClass } from "../utils/make-class";
import { Comparable, SerializedType } from "./serialized-type";
import { compareBytes, parseBytes } from "../utils/bytes-utils";
import { ComparableClass } from "./serialized-type";
import { BinaryParser } from "../serdes/binary-parser";
const Hash = makeClass(
{
Hash(bytes) {
const width = this.constructor.width;
this._bytes = bytes
? parseBytes(bytes, Uint8Array)
: new Uint8Array(width);
assert.equal(this._bytes.length, width);
},
mixins: [Comparable, SerializedType],
statics: {
width: NaN,
from(value) {
if (value instanceof this) {
return value;
}
return new this(parseBytes(value));
},
fromParser(parser, hint) {
return new this(parser.read(hint || this.width));
},
},
compareTo(other) {
return compareBytes(this._bytes, this.constructor.from(other)._bytes);
},
toString() {
return this.toHex();
},
nibblet(depth) {
const byteIx = depth > 0 ? (depth / 2) | 0 : 0;
let b = this._bytes[byteIx];
if (depth % 2 === 0) {
b = (b & 0xf0) >>> 4;
} else {
b = b & 0x0f;
}
return b;
},
},
undefined
);
/**
* Base class defining how to encode and decode hashes
*/
class Hash extends ComparableClass {
static readonly width: number;
constructor(bytes: Buffer) {
super(bytes);
}
/**
* Construct a Hash object from an existing Hash object or a hex-string
*
* @param value A hash object or hex-string of a hash
*/
static from(value: Hash | string): Hash {
return value instanceof this ? value : new this(Buffer.from(value, "hex"));
}
/**
* Read a Hash object from a BinaryParser
*
* @param parser BinaryParser to read the hash from
* @param hint length of the bytes to read, optional
*/
static fromParser(parser: BinaryParser, hint?: number): Hash {
return new this(parser.read(hint ?? this.width));
}
/**
* Overloaded operator for comparing two hash objects
*
* @param other The Hash to compare this to
*/
compareTo(other: Hash): number {
return Buffer.compare(
this.bytes,
(this.constructor as typeof Hash).from(other).bytes
);
}
/**
* @returns the hex-string representation of this Hash
*/
toString(): string {
return this.toHex();
}
/**
* Returns four bits at the specified depth within a hash
*
* @param depth The depth of the four bits
* @returns The number represented by the four bits
*/
nibblet(depth: number): number {
const byteIx = depth > 0 ? (depth / 2) | 0 : 0;
let b = this.bytes[byteIx];
if (depth % 2 === 0) {
b = (b & 0xf0) >>> 4;
} else {
b = b & 0x0f;
}
return b;
}
}
export { Hash };

View File

@@ -1,6 +1,100 @@
import { BytesList } from "../serdes/binary-serializer";
const { bytesToHex, slice } = require("../utils/bytes-utils");
/**
* The base class for all binary-codec types
*/
class SerializedTypeClass {
protected readonly bytes: Buffer = Buffer.alloc(0);
constructor(bytes: Buffer) {
this.bytes = bytes ?? Buffer.alloc(0);
}
/**
* Write the bytes representation of a SerializedType to a BytesList
*
* @param list The BytesList to write SerializedType bytes to
*/
toBytesSink(list: BytesList): void {
list.put(this.bytes);
}
/**
* Get the hex representation of a SerializedType's bytes
*
* @returns hex String of this.bytes
*/
toHex(): string {
return this.toBytes().toString("hex").toUpperCase();
}
/**
* Get the bytes representation of a SerializedType
*
* @returns A buffer of the bytes
*/
toBytes(): Buffer {
if (this.bytes) {
return this.bytes;
}
const bytes = new BytesList();
this.toBytesSink(bytes);
return bytes.toBytes();
}
/**
* Return the JSON representation of a SerializedType
*
* @returns any type, if not overloaded returns hexString representation of bytes
*/
toJSON(): any {
return this.toHex();
}
/**
* @returns hexString representation of this.bytes
*/
toString(): string {
return this.toHex();
}
}
/**
* Base class for SerializedTypes that are comparable
*/
class ComparableClass extends SerializedTypeClass {
lt(other: ComparableClass): boolean {
return this.compareTo(other) < 0;
}
eq(other: ComparableClass): boolean {
return this.compareTo(other) === 0;
}
gt(other: ComparableClass): boolean {
return this.compareTo(other) > 0;
}
gte(other: ComparableClass): boolean {
return this.compareTo(other) > -1;
}
lte(other: ComparableClass): boolean {
return this.compareTo(other) < 1;
}
/**
* Overload this method to define how two Comparable SerializedTypes are compared
*
* @param other The comparable object to compare this to
* @returns A number denoting the relationship of this and other
*/
compareTo(other: ComparableClass): number {
throw new Error("cannot compare " + this + " and " + other);
}
}
const Comparable = {
lt(other) {
return this.compareTo(other) < 0;
@@ -57,4 +151,10 @@ function ensureArrayLikeIs(Type, arrayLike) {
};
}
export { ensureArrayLikeIs, SerializedType, Comparable };
export {
ensureArrayLikeIs,
SerializedType,
SerializedTypeClass,
Comparable,
ComparableClass,
};

View File

@@ -1,8 +1,8 @@
const { coreTypes } = require('../dist/types')
const { Hash160, Hash256, Currency, AccountID } = coreTypes
const { Hash160, Hash256, AccountID, Currency } = coreTypes
describe('Hash160', function () {
test('has a static width membmer', function () {
test('has a static width member', function () {
expect(Hash160.width).toBe(20)
})
test('inherited by subclasses', function () {
@@ -39,16 +39,16 @@ describe('Hash256', function () {
describe('Currency', function () {
test('Will have a null iso() for dodgy XRP ', function () {
const bad = Currency.from('0000000000000000000000005852500000000000')
expect(bad.iso()).toBeNull()
const bad = Currency.from('0000000000000000000000005852500000000000',)
expect(bad.iso()).toBeUndefined()
expect(bad.isNative()).toBe(false)
})
test('can be constructed from an Array', function () {
const xrp = Currency.from(new Uint8Array(20))
test('can be constructed from a Buffer', function () {
const xrp = new Currency(Buffer.alloc(20))
expect(xrp.iso()).toBe('XRP')
})
test('throws on invalid reprs', function () {
expect(() => Currency.from(new Uint8Array(19))).toThrow()
expect(() => Currency.from(Buffer.alloc(19))).toThrow()
expect(() => Currency.from(1)).toThrow()
expect(() => Currency.from(
'00000000000000000000000000000000000000m')).toThrow()