diff --git a/packages/ripple-binary-codec/src/types/account-id.ts b/packages/ripple-binary-codec/src/types/account-id.ts index 58d57688..70118a90 100644 --- a/packages/ripple-binary-codec/src/types/account-id.ts +++ b/packages/ripple-binary-codec/src/types/account-id.ts @@ -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 }; diff --git a/packages/ripple-binary-codec/src/types/blob.ts b/packages/ripple-binary-codec/src/types/blob.ts index 08b99c97..379b08b8 100644 --- a/packages/ripple-binary-codec/src/types/blob.ts +++ b/packages/ripple-binary-codec/src/types/blob.ts @@ -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 }; diff --git a/packages/ripple-binary-codec/src/types/currency.ts b/packages/ripple-binary-codec/src/types/currency.ts index c0ac3183..d409662d 100644 --- a/packages/ripple-binary-codec/src/types/currency.ts +++ b/packages/ripple-binary-codec/src/types/currency.ts @@ -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 }; diff --git a/packages/ripple-binary-codec/src/types/hash-128.ts b/packages/ripple-binary-codec/src/types/hash-128.ts index cb68841d..f8d60370 100644 --- a/packages/ripple-binary-codec/src/types/hash-128.ts +++ b/packages/ripple-binary-codec/src/types/hash-128.ts @@ -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 }; diff --git a/packages/ripple-binary-codec/src/types/hash-160.ts b/packages/ripple-binary-codec/src/types/hash-160.ts index 96fede82..bb4f673c 100644 --- a/packages/ripple-binary-codec/src/types/hash-160.ts +++ b/packages/ripple-binary-codec/src/types/hash-160.ts @@ -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 }; diff --git a/packages/ripple-binary-codec/src/types/hash-256.ts b/packages/ripple-binary-codec/src/types/hash-256.ts index abcfda8c..db4e4d68 100644 --- a/packages/ripple-binary-codec/src/types/hash-256.ts +++ b/packages/ripple-binary-codec/src/types/hash-256.ts @@ -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 }; diff --git a/packages/ripple-binary-codec/src/types/hash.ts b/packages/ripple-binary-codec/src/types/hash.ts index f08b6309..e0aa36ca 100644 --- a/packages/ripple-binary-codec/src/types/hash.ts +++ b/packages/ripple-binary-codec/src/types/hash.ts @@ -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 }; diff --git a/packages/ripple-binary-codec/src/types/serialized-type.ts b/packages/ripple-binary-codec/src/types/serialized-type.ts index ac7027bb..5d169c24 100644 --- a/packages/ripple-binary-codec/src/types/serialized-type.ts +++ b/packages/ripple-binary-codec/src/types/serialized-type.ts @@ -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, +}; diff --git a/packages/ripple-binary-codec/test/hash.test.js b/packages/ripple-binary-codec/test/hash.test.js index 5988b05a..41615a05 100644 --- a/packages/ripple-binary-codec/test/hash.test.js +++ b/packages/ripple-binary-codec/test/hash.test.js @@ -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()