X-address compatibility (#90)

ripple-binary-codec can encode transactions with X-Addresses
This commit is contained in:
Nathan Nichols
2020-08-21 14:06:09 -05:00
parent d1b23a8b2d
commit 440e9922d7
6 changed files with 404 additions and 15 deletions

View File

@@ -1,6 +1,8 @@
import { decodeAccountID, encodeAccountID } from "ripple-address-codec";
import { Hash160 } from "./hash-160";
const HEX_REGEX = /^[A-F0-9]{40}$/;
/**
* Class defining how to encode and decode an AccountID
*/
@@ -27,9 +29,9 @@ class AccountID extends Hash160 {
return new AccountID();
}
return /^r/.test(value)
? this.fromBase58(value)
: new AccountID(Buffer.from(value, "hex"));
return HEX_REGEX.test(value)
? new AccountID(Buffer.from(value, "hex"))
: this.fromBase58(value);
}
throw new Error("Cannot construct AccountID from value given");

View File

@@ -1,11 +1,53 @@
import { Field, FieldInstance } from "../enums";
import { SerializedType, JsonObject } from "./serialized-type";
import {
xAddressToClassicAddress,
isValidXAddress,
} from "ripple-address-codec";
import { BinaryParser } from "../serdes/binary-parser";
import { BinarySerializer, BytesList } from "../serdes/binary-serializer";
const OBJECT_END_MARKER = Buffer.from([0xe1]);
const OBJECT_END_MARKER_NAME = "ObjectEndMarker";
const OBJECT_FIELD_TYPE_NAME = "STObject";
const OBJECT_END_MARKER_BYTE = Buffer.from([0xe1]);
const OBJECT_END_MARKER = "ObjectEndMarker";
const ST_OBJECT = "STObject";
const DESTINATION = "Destination";
const ACCOUNT = "Account";
const SOURCE_TAG = "SourceTag";
const DEST_TAG = "DestinationTag";
/**
* Break down an X-Address into an account and a tag
*
* @param field Name of field
* @param xAddress X-Address corresponding to the field
*/
function handleXAddress(field: string, xAddress: string): JsonObject {
const decoded = xAddressToClassicAddress(xAddress);
let tagName;
if (field === DESTINATION) tagName = DEST_TAG;
else if (field === ACCOUNT) tagName = SOURCE_TAG;
else if (decoded.tag !== false)
throw new Error(`${field} cannot have an associated tag`);
return decoded.tag !== false
? { [field]: decoded.classicAddress, [tagName]: decoded.tag }
: { [field]: decoded.classicAddress };
}
/**
* Validate that two objects don't both have the same tag fields
*
* @param obj1 First object to check for tags
* @param obj2 Second object to check for tags
* @throws When both objects have SourceTag or DestinationTag
*/
function checkForDuplicateTags(obj1: JsonObject, obj2: JsonObject): void {
if (!(obj1[SOURCE_TAG] === undefined || obj2[SOURCE_TAG] === undefined))
throw new Error("Cannot have Account X-Address and SourceTag");
if (!(obj1[DEST_TAG] === undefined || obj2[DEST_TAG] === undefined))
throw new Error("Cannot have Destination X-Address and DestinationTag");
}
/**
* Class for Serializing/Deserializing objects
@@ -23,15 +65,15 @@ class STObject extends SerializedType {
while (!parser.end()) {
const field = parser.readField();
if (field.name === OBJECT_END_MARKER_NAME) {
if (field.name === OBJECT_END_MARKER) {
break;
}
const associatedValue = parser.readFieldValue(field);
bytes.writeFieldAndValue(field, associatedValue);
if (field.type.name === OBJECT_FIELD_TYPE_NAME) {
bytes.put(OBJECT_END_MARKER);
if (field.type.name === ST_OBJECT) {
bytes.put(OBJECT_END_MARKER_BYTE);
}
}
@@ -56,7 +98,16 @@ class STObject extends SerializedType {
const list: BytesList = new BytesList();
const bytes: BinarySerializer = new BinarySerializer(list);
let sorted = Object.keys(value)
const xAddressDecoded = Object.entries(value).reduce((acc, [key, val]) => {
let handled: JsonObject | undefined = undefined;
if (isValidXAddress(val)) {
handled = handleXAddress(key, val);
checkForDuplicateTags(handled, value as JsonObject);
}
return Object.assign(acc, handled ?? { [key]: val });
}, {});
let sorted = Object.keys(xAddressDecoded)
.map((f: string): FieldInstance => Field[f] as FieldInstance)
.filter((f: FieldInstance): boolean => f !== undefined && f.isSerialized)
.sort((a, b) => {
@@ -68,11 +119,13 @@ class STObject extends SerializedType {
}
sorted.forEach((field) => {
const associatedValue = field.associatedType.from(value[field.name]);
const associatedValue = field.associatedType.from(
xAddressDecoded[field.name]
);
bytes.writeFieldAndValue(field, associatedValue);
if (field.type.name === OBJECT_FIELD_TYPE_NAME) {
bytes.put(OBJECT_END_MARKER);
if (field.type.name === ST_OBJECT) {
bytes.put(OBJECT_END_MARKER_BYTE);
}
});
@@ -90,7 +143,7 @@ class STObject extends SerializedType {
while (!objectParser.end()) {
const field = objectParser.readField();
if (field.name === OBJECT_END_MARKER_NAME) {
if (field.name === OBJECT_END_MARKER) {
break;
}
accumulator[field.name] = objectParser.readFieldValue(field).toJSON();