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); bytesList.put(prefix);
} }
const filter = signingFieldsOnly ? (f) => f.isSigningField : undefined; const filter = signingFieldsOnly ? (f) => f.isSigningField : undefined;
coreTypes.STObject.from(object).toBytesSink(bytesList, filter); coreTypes.STObject.from(object, filter).toBytesSink(bytesList);
if (suffix) { if (suffix) {
bytesList.put(suffix); bytesList.put(suffix);
} }

View File

@@ -1,5 +1,6 @@
import { serializeUIntN } from "../utils/bytes-utils"; import { serializeUIntN } from "../utils/bytes-utils";
import * as enums from "./definitions.json"; import * as enums from "./definitions.json";
import { SerializedType } from "../types/serialized-type";
const TYPE_WIDTH = 2; const TYPE_WIDTH = 2;
const LEDGER_ENTRY_WIDTH = 2; const LEDGER_ENTRY_WIDTH = 2;
@@ -46,6 +47,10 @@ class Bytes {
toBytesSink(sink): void { toBytesSink(sink): void {
sink.put(this.bytes); sink.put(this.bytes);
} }
toBytes(): Uint8Array {
return this.bytes;
}
} }
/* /*
@@ -88,7 +93,7 @@ interface FieldInstance {
readonly ordinal: number; readonly ordinal: number;
readonly name: string; readonly name: string;
readonly header: Buffer; readonly header: Buffer;
readonly associatedType: any; readonly associatedType: typeof SerializedType;
} }
function buildField([name, info]: [string, FieldInfo]): FieldInstance { function buildField([name, info]: [string, FieldInfo]): FieldInstance {
@@ -103,7 +108,7 @@ function buildField([name, info]: [string, FieldInfo]): FieldInstance {
ordinal: (typeOrdinal << 16) | info.nth, ordinal: (typeOrdinal << 16) | info.nth,
type: new Bytes(info.type, typeOrdinal, TYPE_WIDTH), type: new Bytes(info.type, typeOrdinal, TYPE_WIDTH),
header: field, 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 * as assert from "assert";
import { Field, FieldInstance } from "../enums"; import { Field, FieldInstance } from "../enums";
import { SerializedType } from "../types/serialized-type";
/** /**
* BinaryParser is used to compute fields and values from a HexString * BinaryParser is used to compute fields and values from a HexString
@@ -43,10 +44,7 @@ class BinaryParser {
* @return The bytes * @return The bytes
*/ */
read(n: number): Buffer { read(n: number): Buffer {
assert( assert(n <= this.bytes.byteLength);
n <= this.bytes.byteLength,
n + " greater than " + this.bytes.byteLength
);
const slice = this.bytes.slice(0, n); const slice = this.bytes.slice(0, n);
this.skip(n); this.skip(n);
@@ -156,7 +154,7 @@ class BinaryParser {
* @param type The type that you want to read from the BinaryParser * @param type The type that you want to read from the BinaryParser
* @return The instance of that type 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); return type.fromParser(this);
} }
@@ -166,7 +164,7 @@ class BinaryParser {
* @param field The field that you wan to get the type of * @param field The field that you wan to get the type of
* @return The type associated with the given field * @return The type associated with the given field
*/ */
typeForField(field: FieldInstance) { typeForField(field: FieldInstance): typeof SerializedType {
return field.associatedType; return field.associatedType;
} }
@@ -176,14 +174,14 @@ class BinaryParser {
* @param field The field that you want to get the associated value for * @param field The field that you want to get the associated value for
* @return The value associated with the given field * @return The value associated with the given field
*/ */
readFieldValue(field: FieldInstance) { readFieldValue(field: FieldInstance): SerializedType {
const type = this.typeForField(field); const type = this.typeForField(field);
if (!type) { if (!type) {
throw new Error(`unsupported: (${field.name}, ${field.type.name})`); throw new Error(`unsupported: (${field.name}, ${field.type.name})`);
} }
const sizeHint = field.isVariableLengthEncoded const sizeHint = field.isVariableLengthEncoded
? this.readVariableLengthLength() ? this.readVariableLengthLength()
: null; : undefined;
const value = type.fromParser(this, sizeHint); const value = type.fromParser(this, sizeHint);
if (value === undefined) { if (value === undefined) {
throw new Error( throw new Error(
@@ -198,7 +196,7 @@ class BinaryParser {
* *
* @return The field and value * @return The field and value
*/ */
readFieldAndValue() { readFieldAndValue(): [FieldInstance, SerializedType] {
const field = this.readField(); const field = this.readField();
return [field, this.readFieldValue(field)]; return [field, this.readFieldValue(field)];
} }

View File

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

View File

@@ -1,225 +1,218 @@
import { makeClass } from "../utils/make-class"; import { Decimal } from "decimal.js";
const _ = require("lodash"); import { SerializedType } from "./serialized-type";
const assert = require("assert"); import { BinaryParser } from "../serdes/binary-parser";
const Decimal = require("decimal.js"); import { Currency } from "./currency";
const { SerializedType } = require("./serialized-type"); import { AccountID } from "./account-id";
const { bytesToHex } = require("../utils/bytes-utils");
const { Currency } = require("./currency");
const { AccountID } = require("./account-id");
const { UInt64 } = require("./uint-64");
/**
* Constants for validating amounts
*/
const MIN_IOU_EXPONENT = -96; const MIN_IOU_EXPONENT = -96;
const MAX_IOU_EXPONENT = 80; const MAX_IOU_EXPONENT = 80;
const MAX_IOU_PRECISION = 16; const MAX_IOU_PRECISION = 16;
const MIN_IOU_MANTISSA = "1000" + "0000" + "0000" + "0000"; // 16 digits const MAX_DROPS = new Decimal("1e17");
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 MIN_XRP = new Decimal("1e-6"); 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({ Decimal.config({
toExpPos: MAX_IOU_EXPONENT + MAX_IOU_PRECISION, toExpPos: MAX_IOU_EXPONENT + MAX_IOU_PRECISION,
toExpNeg: MIN_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. * Interface for JSON objects that represent amounts
This must be an integer number, with the absolute value not exceeding \ */
${MAX_NETWORK_DROPS} interface AmountObject {
value: string;
IOU values must have a maximum precision of ${MAX_IOU_PRECISION} significant \ currency: string;
digits. They are serialized as\na canonicalised mantissa and exponent. issuer: string;
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);
} }
function raiseIllegalAmountError(value) { /**
throw new Error( * Class for serializing/Deserializing Amounts
`${value.toString()} is an illegal amount\n` + AMOUNT_PARAMETERS_DESCRIPTION */
class Amount extends SerializedType {
static defaultAmount: Amount = new Amount(
Buffer.from("4000000000000000", "hex")
); );
}
const parsers = { constructor(bytes: Buffer) {
string(str) { super(bytes ?? Amount.defaultAmount.bytes);
// Using /^\d+$/ here fixes #31
if (!str.match(/^\d+$/)) {
raiseIllegalAmountError(str);
} }
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( /**
{ * Construct an amount from an IOU or string amount
Amount(value, currency, issuer, validate = true) { *
this.value = value || new Decimal("0"); * @param value An Amount, object representing an IOU, or a string representing an integer amount
this.currency = currency || Currency.XRP; * @returns An Amount object
this.issuer = issuer || null; */
if (validate) { static from(value: Amount | AmountObject | string): Amount {
this.assertValueIsValid(); if (value instanceof Amount) {
}
},
mixins: SerializedType,
statics: {
from(value) {
if (value instanceof this) {
return value; return value;
} }
const parser = parsers[typeof value];
if (parser) { const amount = Buffer.alloc(8);
return new this(...parser(value)); if (typeof value === "string") {
Amount.assertXrpIsValid(value);
const number = BigInt(value);
amount.writeBigUInt64BE(number);
amount[0] |= 0x40;
return new Amount(amount);
} else if (typeof value === "object") {
const number = new Decimal(value.value);
Amount.assertIouIsValid(number);
if (number.isZero()) {
amount[0] |= 0x80;
} else {
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;
} }
throw new Error(`unsupported value: ${value}`);
}, const exponent = number.e - 15;
fromParser(parser) { 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 mantissa = parser.read(8);
const currency = Currency.fromParser(parser);
const issuer = AccountID.fromParser(parser);
const b1 = mantissa[0]; const b1 = mantissa[0];
const b2 = mantissa[1]; const b2 = mantissa[1];
const isIOU = b1 & 0x80;
const isPositive = b1 & 0x40; const isPositive = b1 & 0x40;
const sign = isPositive ? "" : "-"; const sign = isPositive ? "" : "-";
if (isIOU) {
mantissa[0] = 0;
const currency = parser.readType(Currency);
const issuer = parser.readType(AccountID);
const exponent = ((b1 & 0x3f) << 2) + ((b2 & 0xff) >> 6) - 97; const exponent = ((b1 & 0x3f) << 2) + ((b2 & 0xff) >> 6) - 97;
mantissa[0] = 0;
mantissa[1] &= 0x3f; mantissa[1] &= 0x3f;
// decimal.js won't accept e notation with hex const value = new Decimal(`${sign}0x${mantissa.toString("hex")}`).times(
const value = new Decimal(`${sign}0x${bytesToHex(mantissa)}`).times( `1e${exponent}`
"1e" + exponent
); );
return new this(value, currency, issuer, false); Amount.assertIouIsValid(value);
return {
issuer: issuer.toJSON(),
currency: currency.toJSON(),
value: value.toString(),
};
}
} }
mantissa[0] &= 0x3f; /**
const drops = new Decimal(`${sign}0x${bytesToHex(mantissa)}`); * Validate XRP amount
const xrpValue = drops.dividedBy(DROPS_PER_XRP); *
return new this(xrpValue, Currency.XRP, null, false); * @param amount String representing XRP amount
}, * @returns void, but will throw if invalid amount
}, */
assertValueIsValid() { private static assertXrpIsValid(amount: string): void {
// zero is always a valid amount value if (amount.indexOf(".") !== -1) {
if (!this.isZero()) { throw new Error("XRP amounts must be integer");
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 decimal = new Decimal(amount);
const p = this.value.precision(); if (!decimal.isZero()) {
const e = this.exponent(); 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 ( if (
p > MAX_IOU_PRECISION || p > MAX_IOU_PRECISION ||
e > MAX_IOU_EXPONENT || e > MAX_IOU_EXPONENT ||
e < MIN_IOU_EXPONENT e < MIN_IOU_EXPONENT
) { ) {
raiseIllegalAmountError(this.value); throw new Error("Decimal precision out of range");
}
this.verifyNoDecimal(decimal);
} }
} }
}
},
isNative() {
return this.currency.isNative();
},
mantissa() {
// This is a tertiary fix for #31
const integerNumberString = this.verifyNoDecimal();
return UInt64.from(BigInt(integerNumberString)); /**
}, * Ensure that the value after being multiplied by the exponent does not contain a decimal.
verifyNoDecimal() { *
const integerNumberString = this.value * @param decimal a Decimal object
.times("1e" + -this.exponent()) * @returns a string of the object without a decimal
*/
private static verifyNoDecimal(decimal: Decimal): void {
const integerNumberString = decimal
.times(`1e${-(decimal.e - 15)}`)
.abs() .abs()
.toString(); .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) { if (integerNumberString.indexOf(".") !== -1) {
mantissa[0] |= notNegative ? 0x40 : 0; throw new Error("Decimal place found in integerNumberString");
sink.put(mantissa);
} 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;
} }
sink.put(mantissa);
this.currency.toBytesSink(sink); /**
this.issuer.toBytesSink(sink); * 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;
} }
}, }
toJSON() {
const valueString = this.valueString();
if (this.isNative()) {
return valueString;
}
return {
value: valueString,
currency: this.currency.toJSON(),
issuer: this.issuer.toJSON(),
};
},
},
undefined
);
export { Amount }; export { Amount };

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,26 @@
import { BytesList } from "../serdes/binary-serializer"; 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 * The base class for all binary-codec types
*/ */
class SerializedTypeClass { class SerializedType {
protected readonly bytes: Buffer = Buffer.alloc(0); protected readonly bytes: Buffer = Buffer.alloc(0);
constructor(bytes: Buffer) { constructor(bytes: Buffer) {
this.bytes = bytes ?? Buffer.alloc(0); 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 * Write the bytes representation of a SerializedType to a BytesList
* *
@@ -63,24 +73,24 @@ class SerializedTypeClass {
/** /**
* Base class for SerializedTypes that are comparable * Base class for SerializedTypes that are comparable
*/ */
class ComparableClass extends SerializedTypeClass { class Comparable extends SerializedType {
lt(other: ComparableClass): boolean { lt(other: Comparable): boolean {
return this.compareTo(other) < 0; return this.compareTo(other) < 0;
} }
eq(other: ComparableClass): boolean { eq(other: Comparable): boolean {
return this.compareTo(other) === 0; return this.compareTo(other) === 0;
} }
gt(other: ComparableClass): boolean { gt(other: Comparable): boolean {
return this.compareTo(other) > 0; return this.compareTo(other) > 0;
} }
gte(other: ComparableClass): boolean { gte(other: Comparable): boolean {
return this.compareTo(other) > -1; return this.compareTo(other) > -1;
} }
lte(other: ComparableClass): boolean { lte(other: Comparable): boolean {
return this.compareTo(other) < 1; return this.compareTo(other) < 1;
} }
@@ -90,71 +100,9 @@ class ComparableClass extends SerializedTypeClass {
* @param other The comparable object to compare this to * @param other The comparable object to compare this to
* @returns A number denoting the relationship of this and other * @returns A number denoting the relationship of this and other
*/ */
compareTo(other: ComparableClass): number { compareTo(other: Comparable): number {
throw new Error("cannot compare " + this + " and " + other); throw new Error(`cannot compare ${this} and ${other}`);
} }
} }
const Comparable = { export { SerializedType, 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,
};

View File

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

View File

@@ -1,79 +1,103 @@
import { makeClass } from "../utils/make-class";
import { Field } from "../enums"; import { Field } from "../enums";
const _ = require("lodash"); import { SerializedType } from "./serialized-type";
const { BinarySerializer } = require("../serdes/binary-serializer"); import { BinaryParser } from "../serdes/binary-parser";
const { SerializedType } = require("./serialized-type"); import { BinarySerializer, BytesList } from "../serdes/binary-serializer";
const STObject = makeClass( const OBJECT_END_MARKER = Buffer.from([0xe1]);
{ const OBJECT_END_MARKER_NAME = "ObjectEndMarker";
mixins: SerializedType, const OBJECT_FIELD_TYPE_NAME = "STObject";
statics: {
fromParser(parser, hint) { /**
const end = typeof hint === "number" ? parser.pos() + hint : null; * Class for Serializing/Deserializing objects
const so = new this(); */
while (!parser.end(end)) { 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(); const field = parser.readField();
if (field.name === "ObjectEndMarker") { if (field.name === OBJECT_END_MARKER_NAME) {
break; break;
} }
so[field.name] = parser.readFieldValue(field);
const associatedValue = parser.readFieldValue(field);
bytes.writeFieldAndValue(field, associatedValue);
if (field.type.name === OBJECT_FIELD_TYPE_NAME) {
bytes.put(OBJECT_END_MARKER);
} }
return so; }
},
from(value) { return new STObject(list.toBytes());
if (value instanceof this) { }
/**
* 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; return value;
} }
if (typeof value === "object") {
return _.transform( const list: BytesList = new BytesList();
value, const bytes: BinarySerializer = new BinarySerializer(list);
(so, val, key) => {
const field = Field[key]; let sorted = Object.keys(value)
if (field) { .map((f) => Field[f])
so[field.name] = field.associatedType.from(val); .filter((f) => f !== undefined && f.isSerialized)
} else { .sort((a, b) => {
so[key] = val; return a.ordinal - b.ordinal;
});
if (filter !== undefined) {
sorted = sorted.filter(filter);
} }
},
new this() sorted.forEach((field) => {
); const associatedValue = field.associatedType.from(value[field.name]);
}
throw new Error(`${value} is unsupported`); bytes.writeFieldAndValue(field, associatedValue);
}, if (field.type.name === OBJECT_FIELD_TYPE_NAME) {
}, bytes.put(OBJECT_END_MARKER);
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]));
} }
}); });
},
}, return new STObject(list.toBytes());
undefined }
);
/**
* 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 }; 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 * 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. * Base class for serializing and deserializing unsigned integers.
*/ */
abstract class UInt extends ComparableClass { abstract class UInt extends Comparable {
protected static width: number; protected static width: number;
constructor(bytes: Buffer) { 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 { BinaryParser } from "../serdes/binary-parser";
import { Hash256 } from "./hash-256"; import { Hash256 } from "./hash-256";
import { BytesList } from "../serdes/binary-serializer"; 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 for serializing and deserializing vectors of Hash256
*/ */
class Vector256 extends SerializedTypeClass { class Vector256 extends SerializedType {
constructor(bytes: Buffer) { constructor(bytes: Buffer) {
super(bytes); super(bytes);
} }

View File

@@ -24,7 +24,7 @@ function amountErrorTests () {
describe('Amount', function () { describe('Amount', function () {
it('can be parsed from', function () { it('can be parsed from', function () {
expect(Amount.from('1000000') instanceof Amount).toBe(true) expect(Amount.from('1000000') instanceof Amount).toBe(true)
expect(Amount.from('1000000').valueString()).toEqual('1000000') expect(Amount.from('1000000').toJSON()).toEqual('1000000')
const fixture = { const fixture = {
value: '1', value: '1',
issuer: '0000000000000000000000000000000000000000', issuer: '0000000000000000000000000000000000000000',

View File

@@ -16,13 +16,13 @@ describe('ripple-binary-codec', function () {
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
test(`${name}[${testN}] can encode ${truncateForDisplay(json(t.json))} to ${truncateForDisplay(t.binary)}`, test(`${name}[${testN}] can encode ${truncateForDisplay(json(t.json))} to ${truncateForDisplay(t.binary)}`,
() => { () => {
expect(t.binary).toEqual(encode(t.json)) expect(encode(t.json)).toEqual(t.binary)
}) })
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
test(`${name}[${testN}] can decode ${truncateForDisplay(t.binary)} to ${truncateForDisplay(json(t.json))}`, test(`${name}[${testN}] can decode ${truncateForDisplay(t.binary)} to ${truncateForDisplay(json(t.json))}`,
() => { () => {
const decoded = decode(t.binary) const decoded = decode(t.binary)
expect(t.json).toEqual(decoded) expect(decoded).toEqual(t.json)
}) })
}) })
}) })

View File

@@ -138,19 +138,19 @@ function transactionParsingTests () {
{ {
const [field, value] = readField() const [field, value] = readField()
expect(field).toEqual(Field.TakerPays) expect(field).toEqual(Field.TakerPays)
expect(value.currency.isNative()).toEqual(true) expect(value.isNative()).toEqual(true)
expect(value.currency.toJSON()).toEqual('XRP') expect(value.toJSON()).toEqual('98957503520')
} }
{ {
const [field, value] = readField() const [field, value] = readField()
expect(field).toEqual(Field.TakerGets) expect(field).toEqual(Field.TakerGets)
expect(value.currency.isNative()).toEqual(false) expect(value.isNative()).toEqual(false)
expect(value.issuer.toJSON()).toEqual(tx_json.TakerGets.issuer) expect(value.toJSON().issuer).toEqual(tx_json.TakerGets.issuer)
} }
{ {
const [field, value] = readField() const [field, value] = readField()
expect(field).toEqual(Field.Fee) expect(field).toEqual(Field.Fee)
expect(value.currency.isNative()).toEqual(true) expect(value.isNative()).toEqual(true)
} }
{ {
const [field, value] = readField() const [field, value] = readField()
@@ -197,9 +197,11 @@ function amountParsingTests () {
const value = parser.readType(Amount) const value = parser.readType(Amount)
// May not actually be in canonical form. The fixtures are to be used // May not actually be in canonical form. The fixtures are to be used
// also for json -> binary; // also for json -> binary;
assertEqualAmountJSON(toJSON(value), (f.test_json)) const json = toJSON(value)
assertEqualAmountJSON(json, (f.test_json))
if (f.exponent) { if (f.exponent) {
expect(value.exponent()).toEqual(f.exponent) const exponent = new Decimal(json.value);
expect(exponent.e-15).toEqual(f.exponent)
} }
}) })
}) })

View File

@@ -65,10 +65,7 @@ describe('encoding and decoding tx_json', function () {
}) })
expect(() => { expect(() => {
encode(my_tx) encode(my_tx)
}).toThrow({ }).toThrow()
name: 'Error',
message: amount_parameters_message('1000.001')
})
}) })
test('throws when Fee is invalid', function () { test('throws when Fee is invalid', function () {
const my_tx = Object.assign({}, tx_json, { const my_tx = Object.assign({}, tx_json, {
@@ -77,10 +74,7 @@ describe('encoding and decoding tx_json', function () {
}) })
expect(() => { expect(() => {
encode(my_tx) encode(my_tx)
}).toThrow({ }).toThrow()
name: 'Error',
message: amount_parameters_message('10.123')
})
}) })
test('throws when Amount and Fee are invalid', function () { test('throws when Amount and Fee are invalid', function () {
const my_tx = Object.assign({}, tx_json, { const my_tx = Object.assign({}, tx_json, {
@@ -89,10 +83,7 @@ describe('encoding and decoding tx_json', function () {
}) })
expect(() => { expect(() => {
encode(my_tx) encode(my_tx)
}).toThrow({ }).toThrow()
name: 'Error',
message: amount_parameters_message('1000.789')
})
}) })
test('throws when Amount is a number instead of a string-encoded integer', test('throws when Amount is a number instead of a string-encoded integer',
function () { function () {
@@ -101,11 +92,9 @@ describe('encoding and decoding tx_json', function () {
}) })
expect(() => { expect(() => {
encode(my_tx) encode(my_tx)
}).toThrow({ }).toThrow()
name: 'Error',
message: 'unsupported value: 1000.789'
})
}) })
test('throws when Fee is a number instead of a string-encoded integer', test('throws when Fee is a number instead of a string-encoded integer',
function () { function () {
const my_tx = Object.assign({}, tx_json, { const my_tx = Object.assign({}, tx_json, {
@@ -113,9 +102,6 @@ describe('encoding and decoding tx_json', function () {
}) })
expect(() => { expect(() => {
encode(my_tx) encode(my_tx)
}).toThrow({ }).toThrow()
name: 'Error',
message: 'unsupported value: 1234.56'
})
}) })
}) })

View File

@@ -27,7 +27,7 @@ describe('SerializedType interfaces', () => {
expect(Value.from(newJSON).toJSON()).toEqual(newJSON) expect(Value.from(newJSON).toJSON()).toEqual(newJSON)
}) })
describe(`${name} supports all methods of the SerializedType mixin`, () => { describe(`${name} supports all methods of the SerializedType mixin`, () => {
_.keys(SerializedType).forEach(k => { _.keys(SerializedType.prototype).forEach(k => {
test(`new ${name}.prototype.${k} !== undefined`, () => { test(`new ${name}.prototype.${k} !== undefined`, () => {
expect(Value.prototype[k]).not.toBe(undefined) expect(Value.prototype[k]).not.toBe(undefined)
}) })