refactored src/types (#86)

Final refactor of the src/types directory. Refactored Amount and STObject classes, and finalized SerializedType and Comparable classes.
This commit is contained in:
Nathan Nichols
2020-07-20 11:02:59 -05:00
parent 2bfb8fc191
commit 383ab88d62
19 changed files with 384 additions and 424 deletions

View File

@@ -19,7 +19,7 @@ function serializeObject(object, opts = <any>{}) {
bytesList.put(prefix);
}
const filter = signingFieldsOnly ? (f) => f.isSigningField : undefined;
coreTypes.STObject.from(object).toBytesSink(bytesList, filter);
coreTypes.STObject.from(object, filter).toBytesSink(bytesList);
if (suffix) {
bytesList.put(suffix);
}

View File

@@ -1,5 +1,6 @@
import { serializeUIntN } from "../utils/bytes-utils";
import * as enums from "./definitions.json";
import { SerializedType } from "../types/serialized-type";
const TYPE_WIDTH = 2;
const LEDGER_ENTRY_WIDTH = 2;
@@ -46,6 +47,10 @@ class Bytes {
toBytesSink(sink): void {
sink.put(this.bytes);
}
toBytes(): Uint8Array {
return this.bytes;
}
}
/*
@@ -88,7 +93,7 @@ interface FieldInstance {
readonly ordinal: number;
readonly name: string;
readonly header: Buffer;
readonly associatedType: any;
readonly associatedType: typeof SerializedType;
}
function buildField([name, info]: [string, FieldInfo]): FieldInstance {
@@ -103,7 +108,7 @@ function buildField([name, info]: [string, FieldInfo]): FieldInstance {
ordinal: (typeOrdinal << 16) | info.nth,
type: new Bytes(info.type, typeOrdinal, TYPE_WIDTH),
header: field,
associatedType: undefined, // For later assignment in ./types/index.js
associatedType: SerializedType, // For later assignment in ./types/index.js
};
}

View File

@@ -1,5 +1,6 @@
import * as assert from "assert";
import { Field, FieldInstance } from "../enums";
import { SerializedType } from "../types/serialized-type";
/**
* BinaryParser is used to compute fields and values from a HexString
@@ -43,10 +44,7 @@ class BinaryParser {
* @return The bytes
*/
read(n: number): Buffer {
assert(
n <= this.bytes.byteLength,
n + " greater than " + this.bytes.byteLength
);
assert(n <= this.bytes.byteLength);
const slice = this.bytes.slice(0, n);
this.skip(n);
@@ -156,7 +154,7 @@ class BinaryParser {
* @param type The type that you want to read from the BinaryParser
* @return The instance of that type read from the BinaryParser
*/
readType(type) {
readType(type: typeof SerializedType): SerializedType {
return type.fromParser(this);
}
@@ -166,7 +164,7 @@ class BinaryParser {
* @param field The field that you wan to get the type of
* @return The type associated with the given field
*/
typeForField(field: FieldInstance) {
typeForField(field: FieldInstance): typeof SerializedType {
return field.associatedType;
}
@@ -176,14 +174,14 @@ class BinaryParser {
* @param field The field that you want to get the associated value for
* @return The value associated with the given field
*/
readFieldValue(field: FieldInstance) {
readFieldValue(field: FieldInstance): SerializedType {
const type = this.typeForField(field);
if (!type) {
throw new Error(`unsupported: (${field.name}, ${field.type.name})`);
}
const sizeHint = field.isVariableLengthEncoded
? this.readVariableLengthLength()
: null;
: undefined;
const value = type.fromParser(this, sizeHint);
if (value === undefined) {
throw new Error(
@@ -198,7 +196,7 @@ class BinaryParser {
*
* @return The field and value
*/
readFieldAndValue() {
readFieldAndValue(): [FieldInstance, SerializedType] {
const field = this.readField();
return [field, this.readFieldValue(field)];
}

View File

@@ -1,5 +1,6 @@
import * as assert from "assert";
import { FieldInstance } from "../enums";
import { SerializedType } from "../types/serialized-type";
/**
* Bytes list is a collection of buffer objects
@@ -61,7 +62,7 @@ class BinarySerializer {
*
* @param value a SerializedType value
*/
write(value): void {
write(value: SerializedType): void {
value.toBytesSink(this.sink);
}
@@ -80,7 +81,7 @@ class BinarySerializer {
* @param type the type to write
* @param value a value of that type
*/
writeType(type, value): void {
writeType(type: typeof SerializedType, value: SerializedType): void {
this.write(type.from(value));
}
@@ -124,9 +125,10 @@ class BinarySerializer {
* @param field field to write to BinarySerializer
* @param value value to write to BinarySerializer
*/
writeFieldAndValue(field: FieldInstance, value): void {
writeFieldAndValue(field: FieldInstance, value: SerializedType): void {
const associatedValue = field.associatedType.from(value);
assert(associatedValue.toBytesSink, field.name);
this.sink.put(field.header);
if (field.isVariableLengthEncoded) {
@@ -141,7 +143,7 @@ class BinarySerializer {
*
* @param value length encoded value to write to BytesList
*/
public writeLengthEncoded(value): void {
public writeLengthEncoded(value: SerializedType): void {
const bytes = new BytesList();
value.toBytesSink(bytes);
this.put(this.encodeVariableLength(bytes.getLength()));

View File

@@ -1,225 +1,218 @@
import { makeClass } from "../utils/make-class";
const _ = require("lodash");
const assert = require("assert");
const Decimal = require("decimal.js");
const { SerializedType } = require("./serialized-type");
const { bytesToHex } = require("../utils/bytes-utils");
const { Currency } = require("./currency");
const { AccountID } = require("./account-id");
const { UInt64 } = require("./uint-64");
import { Decimal } from "decimal.js";
import { SerializedType } from "./serialized-type";
import { BinaryParser } from "../serdes/binary-parser";
import { Currency } from "./currency";
import { AccountID } from "./account-id";
/**
* Constants for validating amounts
*/
const MIN_IOU_EXPONENT = -96;
const MAX_IOU_EXPONENT = 80;
const MAX_IOU_PRECISION = 16;
const MIN_IOU_MANTISSA = "1000" + "0000" + "0000" + "0000"; // 16 digits
const MAX_IOU_MANTISSA = "9999" + "9999" + "9999" + "9999"; // ..
const MAX_IOU = new Decimal(`${MAX_IOU_MANTISSA}e${MAX_IOU_EXPONENT}`);
const MIN_IOU = new Decimal(`${MIN_IOU_MANTISSA}e${MIN_IOU_EXPONENT}`);
const DROPS_PER_XRP = new Decimal("1e6");
const MAX_NETWORK_DROPS = new Decimal("1e17");
const MAX_DROPS = new Decimal("1e17");
const MIN_XRP = new Decimal("1e-6");
const MAX_XRP = MAX_NETWORK_DROPS.dividedBy(DROPS_PER_XRP);
// Never use exponential form
/**
* decimal.js configuration for Amount IOUs
*/
Decimal.config({
toExpPos: MAX_IOU_EXPONENT + MAX_IOU_PRECISION,
toExpNeg: MIN_IOU_EXPONENT - MAX_IOU_PRECISION,
});
const AMOUNT_PARAMETERS_DESCRIPTION = `
Native values must be described in drops, a million of which equal one XRP.
This must be an integer number, with the absolute value not exceeding \
${MAX_NETWORK_DROPS}
IOU values must have a maximum precision of ${MAX_IOU_PRECISION} significant \
digits. They are serialized as\na canonicalised mantissa and exponent.
The valid range for a mantissa is between ${MIN_IOU_MANTISSA} and \
${MAX_IOU_MANTISSA}
The exponent must be >= ${MIN_IOU_EXPONENT} and <= ${MAX_IOU_EXPONENT}
Thus the largest serializable IOU value is:
${MAX_IOU.toString()}
And the smallest:
${MIN_IOU.toString()}
`;
function isDefined(val) {
return !_.isUndefined(val);
/**
* Interface for JSON objects that represent amounts
*/
interface AmountObject {
value: string;
currency: string;
issuer: string;
}
function raiseIllegalAmountError(value) {
throw new Error(
`${value.toString()} is an illegal amount\n` + AMOUNT_PARAMETERS_DESCRIPTION
/**
* Class for serializing/Deserializing Amounts
*/
class Amount extends SerializedType {
static defaultAmount: Amount = new Amount(
Buffer.from("4000000000000000", "hex")
);
}
const parsers = {
string(str) {
// Using /^\d+$/ here fixes #31
if (!str.match(/^\d+$/)) {
raiseIllegalAmountError(str);
constructor(bytes: Buffer) {
super(bytes ?? Amount.defaultAmount.bytes);
}
/**
* Construct an amount from an IOU or string amount
*
* @param value An Amount, object representing an IOU, or a string representing an integer amount
* @returns An Amount object
*/
static from(value: Amount | AmountObject | string): Amount {
if (value instanceof Amount) {
return value;
}
return [new Decimal(str).dividedBy(DROPS_PER_XRP), Currency.XRP];
},
object(object) {
assert(isDefined(object.currency), "currency must be defined");
assert(isDefined(object.issuer), "issuer must be defined");
return [
new Decimal(object.value),
Currency.from(object.currency),
AccountID.from(object.issuer),
];
},
};
const Amount = makeClass(
{
Amount(value, currency, issuer, validate = true) {
this.value = value || new Decimal("0");
this.currency = currency || Currency.XRP;
this.issuer = issuer || null;
if (validate) {
this.assertValueIsValid();
}
},
mixins: SerializedType,
statics: {
from(value) {
if (value instanceof this) {
return value;
}
const parser = parsers[typeof value];
if (parser) {
return new this(...parser(value));
}
throw new Error(`unsupported value: ${value}`);
},
fromParser(parser) {
const mantissa = parser.read(8);
const b1 = mantissa[0];
const b2 = mantissa[1];
const amount = Buffer.alloc(8);
if (typeof value === "string") {
Amount.assertXrpIsValid(value);
const isIOU = b1 & 0x80;
const isPositive = b1 & 0x40;
const sign = isPositive ? "" : "-";
const number = BigInt(value);
amount.writeBigUInt64BE(number);
if (isIOU) {
mantissa[0] = 0;
const currency = parser.readType(Currency);
const issuer = parser.readType(AccountID);
const exponent = ((b1 & 0x3f) << 2) + ((b2 & 0xff) >> 6) - 97;
mantissa[1] &= 0x3f;
// decimal.js won't accept e notation with hex
const value = new Decimal(`${sign}0x${bytesToHex(mantissa)}`).times(
"1e" + exponent
);
return new this(value, currency, issuer, false);
}
amount[0] |= 0x40;
mantissa[0] &= 0x3f;
const drops = new Decimal(`${sign}0x${bytesToHex(mantissa)}`);
const xrpValue = drops.dividedBy(DROPS_PER_XRP);
return new this(xrpValue, Currency.XRP, null, false);
},
},
assertValueIsValid() {
// zero is always a valid amount value
if (!this.isZero()) {
if (this.isNative()) {
const abs = this.value.abs();
if (abs.lt(MIN_XRP) || abs.gt(MAX_XRP)) {
// value is in XRP scale, but show the value in canonical json form
raiseIllegalAmountError(this.value.times(DROPS_PER_XRP));
}
this.verifyNoDecimal(this.value); // This is a secondary fix for #31
} else {
const p = this.value.precision();
const e = this.exponent();
if (
p > MAX_IOU_PRECISION ||
e > MAX_IOU_EXPONENT ||
e < MIN_IOU_EXPONENT
) {
raiseIllegalAmountError(this.value);
}
}
}
},
isNative() {
return this.currency.isNative();
},
mantissa() {
// This is a tertiary fix for #31
const integerNumberString = this.verifyNoDecimal();
return new Amount(amount);
} else if (typeof value === "object") {
const number = new Decimal(value.value);
Amount.assertIouIsValid(number);
return UInt64.from(BigInt(integerNumberString));
},
verifyNoDecimal() {
const integerNumberString = this.value
.times("1e" + -this.exponent())
.abs()
.toString();
// Ensure that the value (after being multiplied by the exponent)
// does not contain a decimal. From the bn.js README:
// "decimals are not supported in this library."
// eslint-disable-next-line max-len
// https://github.com/indutny/bn.js/blob/9cb459f044853b46615464eea1a3ddfc7006463b/README.md
if (integerNumberString.indexOf(".") !== -1) {
raiseIllegalAmountError(integerNumberString);
}
return integerNumberString;
},
isZero() {
return this.value.isZero();
},
exponent() {
return this.isNative() ? -6 : this.value.e - 15;
},
valueString() {
return (this.isNative()
? this.value.times(DROPS_PER_XRP)
: this.value
).toString();
},
toBytesSink(sink) {
const isNative = this.isNative();
const notNegative = !this.value.isNegative();
const mantissa = this.mantissa().toBytes();
if (isNative) {
mantissa[0] |= notNegative ? 0x40 : 0;
sink.put(mantissa);
if (number.isZero()) {
amount[0] |= 0x80;
} else {
mantissa[0] |= 0x80;
if (!this.isZero()) {
if (notNegative) {
mantissa[0] |= 0x40;
}
const exponent = this.value.e - 15;
const exponentByte = 97 + exponent;
mantissa[0] |= exponentByte >>> 2;
mantissa[1] |= (exponentByte & 0x03) << 6;
const integerNumberString = number
.times(`1e${-(number.e - 15)}`)
.abs()
.toString();
amount.writeBigUInt64BE(BigInt(integerNumberString));
amount[0] |= 0x80;
if (number.gt(new Decimal(0))) {
amount[0] |= 0x40;
}
sink.put(mantissa);
this.currency.toBytesSink(sink);
this.issuer.toBytesSink(sink);
}
},
toJSON() {
const valueString = this.valueString();
if (this.isNative()) {
return valueString;
const exponent = number.e - 15;
const exponentByte = 97 + exponent;
amount[0] |= exponentByte >>> 2;
amount[1] |= (exponentByte & 0x03) << 6;
}
const currency = Currency.from(value.currency).toBytes();
const issuer = AccountID.from(value.issuer).toBytes();
return new Amount(Buffer.concat([amount, currency, issuer]));
}
throw new Error("Invalid type to construct an Amount");
}
/**
* Read an amount from a BinaryParser
*
* @param parser BinaryParser to read the Amount from
* @returns An Amount object
*/
static fromParser(parser: BinaryParser): Amount {
const isXRP = parser.peek() & 0x80;
const numBytes = isXRP ? 48 : 8;
return new Amount(parser.read(numBytes));
}
/**
* Get the JSON representation of this Amount
*
* @returns the JSON interpretation of this.bytes
*/
toJSON(): AmountObject | string {
if (this.isNative()) {
const bytes = this.bytes;
const isPositive = bytes[0] & 0x40;
const sign = isPositive ? "" : "-";
bytes[0] &= 0x3f;
return `${sign}${bytes.readBigUInt64BE().toString()}`;
} else {
const parser = new BinaryParser(this.toString());
const mantissa = parser.read(8);
const currency = Currency.fromParser(parser);
const issuer = AccountID.fromParser(parser);
const b1 = mantissa[0];
const b2 = mantissa[1];
const isPositive = b1 & 0x40;
const sign = isPositive ? "" : "-";
const exponent = ((b1 & 0x3f) << 2) + ((b2 & 0xff) >> 6) - 97;
mantissa[0] = 0;
mantissa[1] &= 0x3f;
const value = new Decimal(`${sign}0x${mantissa.toString("hex")}`).times(
`1e${exponent}`
);
Amount.assertIouIsValid(value);
return {
value: valueString,
currency: this.currency.toJSON(),
issuer: this.issuer.toJSON(),
issuer: issuer.toJSON(),
currency: currency.toJSON(),
value: value.toString(),
};
},
},
undefined
);
}
}
/**
* Validate XRP amount
*
* @param amount String representing XRP amount
* @returns void, but will throw if invalid amount
*/
private static assertXrpIsValid(amount: string): void {
if (amount.indexOf(".") !== -1) {
throw new Error("XRP amounts must be integer");
}
const decimal = new Decimal(amount);
if (!decimal.isZero()) {
if (decimal.lt(MIN_XRP) || decimal.gt(MAX_DROPS)) {
throw new Error("Invalid XRP amount");
}
}
}
/**
* Validate IOU.value amount
*
* @param decimal Decimal.js object representing IOU.value
* @returns void, but will throw if invalid amount
*/
private static assertIouIsValid(decimal: Decimal): void {
if (!decimal.isZero()) {
const p = decimal.precision();
const e = decimal.e - 15;
if (
p > MAX_IOU_PRECISION ||
e > MAX_IOU_EXPONENT ||
e < MIN_IOU_EXPONENT
) {
throw new Error("Decimal precision out of range");
}
this.verifyNoDecimal(decimal);
}
}
/**
* Ensure that the value after being multiplied by the exponent does not contain a decimal.
*
* @param decimal a Decimal object
* @returns a string of the object without a decimal
*/
private static verifyNoDecimal(decimal: Decimal): void {
const integerNumberString = decimal
.times(`1e${-(decimal.e - 15)}`)
.abs()
.toString();
if (integerNumberString.indexOf(".") !== -1) {
throw new Error("Decimal place found in integerNumberString");
}
}
/**
* Test if this amount is in units of Native Currency(XRP)
*
* @returns true if Native (XRP)
*/
private isNative(): boolean {
return (this.bytes[0] & 0x80) === 0;
}
}
export { Amount };

View File

@@ -1,10 +1,10 @@
import { SerializedTypeClass } from "./serialized-type";
import { SerializedType } from "./serialized-type";
import { BinaryParser } from "../serdes/binary-parser";
/**
* Variable length encoded type
*/
class Blob extends SerializedTypeClass {
class Blob extends SerializedType {
constructor(bytes: Buffer) {
super(bytes);
}

View File

@@ -1,10 +1,10 @@
import { ComparableClass } from "./serialized-type";
import { Comparable } from "./serialized-type";
import { BinaryParser } from "../serdes/binary-parser";
/**
* Base class defining how to encode and decode hashes
*/
class Hash extends ComparableClass {
class Hash extends Comparable {
static readonly width: number;
constructor(bytes: Buffer) {

View File

@@ -7,18 +7,18 @@ import {
import { AccountID } from "./account-id";
import { Amount } from "./amount";
import { Blob } from "./blob";
const { Currency } = require("./currency");
const { Hash128 } = require("./hash-128");
const { Hash160 } = require("./hash-160");
const { Hash256 } = require("./hash-256");
const { PathSet } = require("./path-set");
const { STArray } = require("./st-array");
const { STObject } = require("./st-object");
const { UInt16 } = require("./uint-16");
const { UInt32 } = require("./uint-32");
const { UInt64 } = require("./uint-64");
const { UInt8 } = require("./uint-8");
const { Vector256 } = require("./vector-256");
import { Currency } from "./currency";
import { Hash128 } from "./hash-128";
import { Hash160 } from "./hash-160";
import { Hash256 } from "./hash-256";
import { PathSet } from "./path-set";
import { STArray } from "./st-array";
import { STObject } from "./st-object";
import { UInt16 } from "./uint-16";
import { UInt32 } from "./uint-32";
import { UInt64 } from "./uint-64";
import { UInt8 } from "./uint-8";
import { Vector256 } from "./vector-256";
const coreTypes = {
AccountID,

View File

@@ -1,7 +1,7 @@
import { AccountID } from "./account-id";
import { Currency } from "./currency";
import { BinaryParser } from "../serdes/binary-parser";
import { SerializedTypeClass } from "./serialized-type";
import { SerializedType } from "./serialized-type";
/**
* Constants for separating Paths in a PathSet
@@ -28,7 +28,7 @@ interface HopObject {
/**
* Serialize and Deserialize a Hop
*/
class Hop extends SerializedTypeClass {
class Hop extends SerializedType {
/**
* Create a Hop from a HopObject
*
@@ -123,7 +123,7 @@ class Hop extends SerializedTypeClass {
/**
* Class for serializing/deserializing Paths
*/
class Path extends SerializedTypeClass {
class Path extends SerializedType {
/**
* construct a Path from an array of Hops
*
@@ -184,7 +184,7 @@ class Path extends SerializedTypeClass {
/**
* Deserialize and Serialize the PathSet type
*/
class PathSet extends SerializedTypeClass {
class PathSet extends SerializedType {
/**
* Construct a PathSet from an Array of Arrays representing paths
*

View File

@@ -1,16 +1,26 @@
import { BytesList } from "../serdes/binary-serializer";
const { bytesToHex, slice } = require("../utils/bytes-utils");
import { BinaryParser } from "../serdes/binary-parser";
/**
* The base class for all binary-codec types
*/
class SerializedTypeClass {
class SerializedType {
protected readonly bytes: Buffer = Buffer.alloc(0);
constructor(bytes: Buffer) {
this.bytes = bytes ?? Buffer.alloc(0);
}
static fromParser(parser: BinaryParser, hint?: number): SerializedType {
throw new Error("fromParser not implemented");
return this.fromParser(parser, hint);
}
static from(value: any): SerializedType {
throw new Error("from not implemented");
return this.from(value);
}
/**
* Write the bytes representation of a SerializedType to a BytesList
*
@@ -63,24 +73,24 @@ class SerializedTypeClass {
/**
* Base class for SerializedTypes that are comparable
*/
class ComparableClass extends SerializedTypeClass {
lt(other: ComparableClass): boolean {
class Comparable extends SerializedType {
lt(other: Comparable): boolean {
return this.compareTo(other) < 0;
}
eq(other: ComparableClass): boolean {
eq(other: Comparable): boolean {
return this.compareTo(other) === 0;
}
gt(other: ComparableClass): boolean {
gt(other: Comparable): boolean {
return this.compareTo(other) > 0;
}
gte(other: ComparableClass): boolean {
gte(other: Comparable): boolean {
return this.compareTo(other) > -1;
}
lte(other: ComparableClass): boolean {
lte(other: Comparable): boolean {
return this.compareTo(other) < 1;
}
@@ -90,71 +100,9 @@ class ComparableClass extends SerializedTypeClass {
* @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);
compareTo(other: Comparable): number {
throw new Error(`cannot compare ${this} and ${other}`);
}
}
const Comparable = {
lt(other) {
return this.compareTo(other) < 0;
},
eq(other) {
return this.compareTo(other) === 0;
},
gt(other) {
return this.compareTo(other) > 0;
},
gte(other) {
return this.compareTo(other) > -1;
},
lte(other) {
return this.compareTo(other) < 1;
},
};
const SerializedType = {
toBytesSink(sink) {
sink.put(this._bytes);
},
toHex() {
return bytesToHex(this.toBytes());
},
toBytes() {
if (this._bytes) {
return slice(this._bytes);
}
const bl = new BytesList();
this.toBytesSink(bl);
return bl.toBytes();
},
toJSON() {
return this.toHex();
},
toString() {
return this.toHex();
},
};
function ensureArrayLikeIs(Type, arrayLike) {
return {
withChildren(Child) {
if (arrayLike instanceof Type) {
return arrayLike;
}
const obj = new Type();
for (let i = 0; i < arrayLike.length; i++) {
obj.push(Child.from(arrayLike[i]));
}
return obj;
},
};
}
export {
ensureArrayLikeIs,
SerializedType,
SerializedTypeClass,
Comparable,
ComparableClass,
};
export { SerializedType, Comparable };

View File

@@ -1,14 +1,16 @@
import { SerializedTypeClass } from "./serialized-type";
import { SerializedType } from "./serialized-type";
import { STObject } from "./st-object";
import { BinaryParser } from "../serdes/binary-parser";
const ARRAY_END_MARKER = 0xf1;
const OBJECT_END_MARKER = 0xe1;
const ARRAY_END_MARKER = Buffer.from([0xf1]);
const ARRAY_END_MARKER_NAME = "ArrayEndMarker";
const OBJECT_END_MARKER = Buffer.from([0xe1]);
/**
* Class for serializing and deserializing Arrays of Objects
*/
class STArray extends SerializedTypeClass {
class STArray extends SerializedType {
/**
* Construct an STArray from a BinaryParser
*
@@ -20,18 +22,18 @@ class STArray extends SerializedTypeClass {
while (!parser.end()) {
const field = parser.readField();
if (field.name === "ArrayEndMarker") {
if (field.name === ARRAY_END_MARKER_NAME) {
break;
}
bytes.push(
field.header,
parser.readFieldValue(field).toBytes(),
Buffer.from([OBJECT_END_MARKER])
OBJECT_END_MARKER
);
}
bytes.push(Buffer.from([ARRAY_END_MARKER]));
bytes.push(ARRAY_END_MARKER);
return new STArray(Buffer.concat(bytes));
}
@@ -51,7 +53,7 @@ class STArray extends SerializedTypeClass {
bytes.push(STObject.from(obj).toBytes());
});
bytes.push(Buffer.from([ARRAY_END_MARKER]));
bytes.push(ARRAY_END_MARKER);
return new STArray(Buffer.concat(bytes));
}
@@ -67,7 +69,7 @@ class STArray extends SerializedTypeClass {
while (!arrayParser.end()) {
const field = arrayParser.readField();
if (field.name === "ArrayEndMarker") {
if (field.name === ARRAY_END_MARKER_NAME) {
break;
}

View File

@@ -1,79 +1,103 @@
import { makeClass } from "../utils/make-class";
import { Field } from "../enums";
const _ = require("lodash");
const { BinarySerializer } = require("../serdes/binary-serializer");
const { SerializedType } = require("./serialized-type");
import { SerializedType } from "./serialized-type";
import { BinaryParser } from "../serdes/binary-parser";
import { BinarySerializer, BytesList } from "../serdes/binary-serializer";
const STObject = makeClass(
{
mixins: SerializedType,
statics: {
fromParser(parser, hint) {
const end = typeof hint === "number" ? parser.pos() + hint : null;
const so = new this();
while (!parser.end(end)) {
const field = parser.readField();
if (field.name === "ObjectEndMarker") {
break;
}
so[field.name] = parser.readFieldValue(field);
}
return so;
},
from(value) {
if (value instanceof this) {
return value;
}
if (typeof value === "object") {
return _.transform(
value,
(so, val, key) => {
const field = Field[key];
if (field) {
so[field.name] = field.associatedType.from(val);
} else {
so[key] = val;
}
},
new this()
);
}
throw new Error(`${value} is unsupported`);
},
},
fieldKeys() {
return Object.keys(this)
.map((k) => Field[k])
.filter(Boolean);
},
toJSON() {
// Otherwise seemingly result will have same prototype as `this`
const accumulator = {}; // of only `own` properties
return _.transform(
this,
(result, value, key) => {
result[key] = value && value.toJSON ? value.toJSON() : value;
},
accumulator
);
},
toBytesSink(sink, filter = () => true) {
const serializer = new BinarySerializer(sink);
const fields = this.fieldKeys();
const sorted = _.sortBy(fields, "ordinal");
sorted.filter(filter).forEach((field) => {
const value = this[field.name];
if (!field.isSerialized) {
return;
}
serializer.writeFieldAndValue(field, value);
if (field.type.name === "STObject") {
serializer.put(Buffer.from([0xe1]));
}
const OBJECT_END_MARKER = Buffer.from([0xe1]);
const OBJECT_END_MARKER_NAME = "ObjectEndMarker";
const OBJECT_FIELD_TYPE_NAME = "STObject";
/**
* Class for Serializing/Deserializing objects
*/
class STObject extends SerializedType {
/**
* Construct a STObject from a BinaryParser
*
* @param parser BinaryParser to read STObject from
* @returns A STObject object
*/
static fromParser(parser: BinaryParser): STObject {
const list: BytesList = new BytesList();
const bytes: BinarySerializer = new BinarySerializer(list);
while (!parser.end()) {
const field = parser.readField();
if (field.name === OBJECT_END_MARKER_NAME) {
break;
}
const associatedValue = parser.readFieldValue(field);
bytes.writeFieldAndValue(field, associatedValue);
if (field.type.name === OBJECT_FIELD_TYPE_NAME) {
bytes.put(OBJECT_END_MARKER);
}
}
return new STObject(list.toBytes());
}
/**
* Construct a STObject from a JSON object
*
* @param value An object to include
* @param filter optional, denote which field to include in serialized object
* @returns a STObject object
*/
static from(
value: STObject | object,
filter?: (...any) => boolean
): STObject {
if (value instanceof STObject) {
return value;
}
const list: BytesList = new BytesList();
const bytes: BinarySerializer = new BinarySerializer(list);
let sorted = Object.keys(value)
.map((f) => Field[f])
.filter((f) => f !== undefined && f.isSerialized)
.sort((a, b) => {
return a.ordinal - b.ordinal;
});
},
},
undefined
);
if (filter !== undefined) {
sorted = sorted.filter(filter);
}
sorted.forEach((field) => {
const associatedValue = field.associatedType.from(value[field.name]);
bytes.writeFieldAndValue(field, associatedValue);
if (field.type.name === OBJECT_FIELD_TYPE_NAME) {
bytes.put(OBJECT_END_MARKER);
}
});
return new STObject(list.toBytes());
}
/**
* Get the JSON interpretation of this.bytes
*
* @returns a JSON object
*/
toJSON(): object {
const objectParser = new BinaryParser(this.toString());
const accumulator = {};
while (!objectParser.end()) {
const field = objectParser.readField();
if (field.name === OBJECT_END_MARKER_NAME) {
break;
}
accumulator[field.name] = objectParser.readFieldValue(field).toJSON();
}
return accumulator;
}
}
export { STObject };

View File

@@ -1,4 +1,4 @@
import { ComparableClass } from "./serialized-type";
import { Comparable } from "./serialized-type";
/**
* Compare numbers and bigints n1 and n2
@@ -14,7 +14,7 @@ function compare(n1: number | bigint, n2: number | bigint): number {
/**
* Base class for serializing and deserializing unsigned integers.
*/
abstract class UInt extends ComparableClass {
abstract class UInt extends Comparable {
protected static width: number;
constructor(bytes: Buffer) {

View File

@@ -1,4 +1,4 @@
import { SerializedTypeClass } from "./serialized-type";
import { SerializedType } from "./serialized-type";
import { BinaryParser } from "../serdes/binary-parser";
import { Hash256 } from "./hash-256";
import { BytesList } from "../serdes/binary-serializer";
@@ -6,7 +6,7 @@ import { BytesList } from "../serdes/binary-serializer";
/**
* Class for serializing and deserializing vectors of Hash256
*/
class Vector256 extends SerializedTypeClass {
class Vector256 extends SerializedType {
constructor(bytes: Buffer) {
super(bytes);
}