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

@@ -42,6 +42,11 @@ Encode a transaction object into a hex-string.
'1100612200000000240000000125000000072D0000000055DF530FB14C5304852F20080B0A8EEF3A6BDD044F41F4EBBD68B8B321145FE4FF6240000002540BE4008114D0F5430B66E06498D4CEEC816C7B3337F9982337'
```
#### X-Address Compatibility
* ripple-binary-codec handles X-addresses by looking for a few specific files (Account/SourceTag, Destination/DestinationTag).
* If other fields (in the future) must to support X-addresses with tags, this library will need to be updated.
* When decoding rippled binary, the output will always output classic address + tag, with no X-addresses. X-address support only applies when encoding to binary.
### encodeForSigning(json: object): string
Encode the transaction object for signing.

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();

View File

@@ -0,0 +1,188 @@
{
"transactions": [{
"rjson": {
"Account": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV",
"Destination": "rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj",
"TransactionType": "Payment",
"TxnSignature": "3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639",
"SigningPubKey": "034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E",
"Amount": "10000000000",
"DestinationTag": 1010,
"SourceTag": 84854,
"Fee": "10",
"Flags": 0,
"Sequence": 62
},
"xjson": {
"Account": "X7tFPvjMH7nDxP8nTGkeeggcUpCZj8UbyT2QoiRHGDfjqrB",
"Destination": "XVYmGpJqHS95ir411XvanwY1xt5Z2314WsamHPVgUNABUGV",
"TransactionType": "Payment",
"TxnSignature": "3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639",
"SigningPubKey": "034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E",
"Amount": "10000000000",
"Fee": "10",
"Flags": 0,
"Sequence": 62
}
},
{
"rjson": {
"Account": "r4DymtkgUAh2wqRxVfdd3Xtswzim6eC6c5",
"Amount": "199000000",
"Destination": "rsekGH9p9neiPxym2TMJhqaCzHFuokenTU",
"DestinationTag": 3663729509,
"Fee": "6335",
"Flags": 2147483648,
"LastLedgerSequence": 57313352,
"Sequence": 105791,
"SigningPubKey": "02053A627976CE1157461336AC65290EC1571CAAD1B327339980F7BF65EF776F83",
"TransactionType": "Payment",
"TxnSignature": "30440220086D3330CD6CE01D891A26BA0355D8D5A5D28A5C9A1D0C5E06E321C81A02318A0220027C3F6606E41FEA35103EDE5224CC489B6514ACFE27543185B0419DD02E301C"
},
"xjson": {
"Account": "r4DymtkgUAh2wqRxVfdd3Xtswzim6eC6c5",
"Amount": "199000000",
"Destination": "X7cBoj6a5xSEfPCr6AStN9YPhbMAA2yaN2XYWwRJKAKb3y5",
"Fee": "6335",
"Flags": 2147483648,
"LastLedgerSequence": 57313352,
"Sequence": 105791,
"SigningPubKey": "02053A627976CE1157461336AC65290EC1571CAAD1B327339980F7BF65EF776F83",
"TransactionType": "Payment",
"TxnSignature": "30440220086D3330CD6CE01D891A26BA0355D8D5A5D28A5C9A1D0C5E06E321C81A02318A0220027C3F6606E41FEA35103EDE5224CC489B6514ACFE27543185B0419DD02E301C"
}
},
{
"rjson": {
"Account": "rDsbeomae4FXwgQTJp9Rs64Qg9vDiTCdBv",
"Amount": "105302107",
"Destination": "r33hypJXDs47LVpmvta7hMW9pR8DYeBtkW",
"DestinationTag": 1658156118,
"Fee": "60000",
"Flags": 2147483648,
"LastLedgerSequence": 57313566,
"Sequence": 1113196,
"SigningPubKey": "03D847C2DBED3ABF0453F71DCD7641989136277218DF516AD49519C9693F32727E",
"TransactionType": "Payment",
"TxnSignature": "3045022100FCA10FBAC65EA60C115A970CD52E6A526B1F9DDB6C4F843DA3DE7A97DFF9492D022037824D0FC6F663FB08BE0F2812CBADE1F61836528D44945FC37F10CC03215111"
},
"xjson": {
"Account": "rDsbeomae4FXwgQTJp9Rs64Qg9vDiTCdBv",
"Amount": "105302107",
"Destination": "X7ikFY5asEwp6ikt2AJdTfBLALEs5JN35kkeqKVeT1GdvY1",
"Fee": "60000",
"Flags": 2147483648,
"LastLedgerSequence": 57313566,
"Sequence": 1113196,
"SigningPubKey": "03D847C2DBED3ABF0453F71DCD7641989136277218DF516AD49519C9693F32727E",
"TransactionType": "Payment",
"TxnSignature": "3045022100FCA10FBAC65EA60C115A970CD52E6A526B1F9DDB6C4F843DA3DE7A97DFF9492D022037824D0FC6F663FB08BE0F2812CBADE1F61836528D44945FC37F10CC03215111"
}
},
{
"rjson": {
"Account": "rDsbeomae4FXwgQTJp9Rs64Qg9vDiTCdBv",
"Amount": "3899911571",
"Destination": "rU2mEJSLqBRkYLVTv55rFTgQajkLTnT6mA",
"DestinationTag": 255406,
"Fee": "60000",
"Flags": 2147483648,
"LastLedgerSequence": 57313566,
"Sequence": 1113197,
"SigningPubKey": "03D847C2DBED3ABF0453F71DCD7641989136277218DF516AD49519C9693F32727E",
"TransactionType": "Payment",
"TxnSignature": "3044022077642D94BB3C49BF3CB4C804255EC830D2C6009EA4995E38A84602D579B8AAD702206FAD977C49980226E8B495BF03C8D9767380F1546BBF5A4FD47D604C0D2CCF9B"
},
"xjson": {
"Account": "rDsbeomae4FXwgQTJp9Rs64Qg9vDiTCdBv",
"Amount": "3899911571",
"Destination": "XVfH8gwNWVbB5Kft16jmTNgGTqgw1dzA8ZTBkNjSLw6JdXS",
"Fee": "60000",
"Flags": 2147483648,
"LastLedgerSequence": 57313566,
"Sequence": 1113197,
"SigningPubKey": "03D847C2DBED3ABF0453F71DCD7641989136277218DF516AD49519C9693F32727E",
"TransactionType": "Payment",
"TxnSignature": "3044022077642D94BB3C49BF3CB4C804255EC830D2C6009EA4995E38A84602D579B8AAD702206FAD977C49980226E8B495BF03C8D9767380F1546BBF5A4FD47D604C0D2CCF9B"
}
},
{
"rjson": {
"Account": "r4eEbLKZGbVSBHnSUBZW8i5XaMjGLdqT4a",
"Amount": "820370849",
"Destination": "rDhmyBh4JwDAtXyRZDarNgg52UcLLRoGje",
"DestinationTag": 2017780486,
"Fee": "6000",
"Flags": 2147483648,
"LastLedgerSequence": 57315579,
"Sequence": 234254,
"SigningPubKey": "038CF47114672A12B269AEE015BF7A8438609B994B0640E4B28B2F56E93D948B15",
"TransactionType": "Payment",
"TxnSignature": "3044022015004653B1CBDD5CCA1F7B38555F1B37FE3F811E9D5070281CCC6C8A93460D870220679E9899184901EA69750C8A9325768490B1B9C1A733842446727653FF3D1DC0"
},
"xjson": {
"Account": "r4eEbLKZGbVSBHnSUBZW8i5XaMjGLdqT4a",
"Amount": "820370849",
"Destination": "XV31huWNJQXsAJFwgE6rnC8uf8jRx4H4waq4MyGUxz5CXzS",
"Fee": "6000",
"Flags": 2147483648,
"LastLedgerSequence": 57315579,
"Sequence": 234254,
"SigningPubKey": "038CF47114672A12B269AEE015BF7A8438609B994B0640E4B28B2F56E93D948B15",
"TransactionType": "Payment",
"TxnSignature": "3044022015004653B1CBDD5CCA1F7B38555F1B37FE3F811E9D5070281CCC6C8A93460D870220679E9899184901EA69750C8A9325768490B1B9C1A733842446727653FF3D1DC0"
}
},
{
"rjson": {
"Account": "rsGeDwS4rpocUumu9smpXomzaaeG4Qyifz",
"Amount": "1500000000",
"Destination": "rDxfhNRgCDNDckm45zT5ayhKDC4Ljm7UoP",
"DestinationTag": 1000635172,
"Fee": "5000",
"Flags": 2147483648,
"Sequence": 55741075,
"SigningPubKey": "02ECB814477DF9D8351918878E235EE6AF147A2A5C20F1E71F291F0F3303357C36",
"SourceTag": 1000635172,
"TransactionType": "Payment",
"TxnSignature": "304402202A90972E21823214733082E1977F9EA2D6B5101902F108E7BDD7D128CEEA7AF3022008852C8DAD746A7F18E66A47414FABF551493674783E8EA7409C501D3F05F99A"
},
"xjson": {
"Account": "rsGeDwS4rpocUumu9smpXomzaaeG4Qyifz",
"Amount": "1500000000",
"Destination": "XVBkK1yLutMqFGwTm6hykn7YXGDUrjsZSkpzMgRveZrMbHs",
"Fee": "5000",
"Flags": 2147483648,
"Sequence": 55741075,
"SigningPubKey": "02ECB814477DF9D8351918878E235EE6AF147A2A5C20F1E71F291F0F3303357C36",
"SourceTag": 1000635172,
"TransactionType": "Payment",
"TxnSignature": "304402202A90972E21823214733082E1977F9EA2D6B5101902F108E7BDD7D128CEEA7AF3022008852C8DAD746A7F18E66A47414FABF551493674783E8EA7409C501D3F05F99A"
}
},
{
"rjson": {
"Account": "rHWcuuZoFvDS6gNbmHSdpb7u1hZzxvCoMt",
"Amount": "48918500000",
"Destination": "rEb8TK3gBgk5auZkwc6sHnwrGVJH8DuaLh",
"DestinationTag": 105959914,
"Fee": "10",
"Flags": 2147483648,
"Sequence": 32641,
"SigningPubKey": "02E98DA545CCCC5D14C82594EE9E6CCFCF5171108E2410B3E784183E1068D33429",
"TransactionType": "Payment",
"TxnSignature": "304502210091DCA7AF189CD9DC93BDE24DEAE87381FBF16789C43113EE312241D648982B2402201C6055FEFFF1F119640AAC0B32C4F37375B0A96033E0527A21C1366920D6A524"
},
"xjson": {
"Account": "rHWcuuZoFvDS6gNbmHSdpb7u1hZzxvCoMt",
"Amount": "48918500000",
"Destination": "XVH3aqvbYGhRhrD1FYSzGooNuxdzbG3VR2fuM47oqbXxQr7",
"Fee": "10",
"Flags": 2147483648,
"Sequence": 32641,
"SigningPubKey": "02E98DA545CCCC5D14C82594EE9E6CCFCF5171108E2410B3E784183E1068D33429",
"TransactionType": "Payment",
"TxnSignature": "304502210091DCA7AF189CD9DC93BDE24DEAE87381FBF16789C43113EE312241D648982B2402201C6055FEFFF1F119640AAC0B32C4F37375B0A96033E0527A21C1366920D6A524"
}
}]
}

View File

@@ -16,10 +16,14 @@ describe('Hash160', function () {
expect(h1.lt(h2)).toBe(true)
expect(h3.lt(h2)).toBe(true)
})
test('throws when constructed from invalid hash length', () => {
expect(() => Hash160.from('10000000000000000000000000000000000000')).toThrow('Invalid Hash length 19')
expect(() => Hash160.from('100000000000000000000000000000000000000000')).toThrow('Invalid Hash length 21')
})
})
describe('Hash256', function () {
test('has a static width membmer', function () {
test('has a static width member', function () {
expect(Hash256.width).toBe(32)
})
test('has a ZERO_256 member', function () {

View File

@@ -0,0 +1,137 @@
const { encode, decode } = require("./../dist/index");
const fixtures = require('./fixtures/x-codec-fixtures.json')
let json_x1 = {
OwnerCount: 0,
Account: "XVXdn5wEVm5G4UhEHWDPqjvdeH361P7BsapL4m2D2XnPSwT",
PreviousTxnLgrSeq: 7,
LedgerEntryType: "AccountRoot",
PreviousTxnID: "DF530FB14C5304852F20080B0A8EEF3A6BDD044F41F4EBBD68B8B321145FE4FF",
Flags: 0,
Sequence: 1,
Balance: "10000000000"
}
let json_r1 = {
OwnerCount: 0,
Account: 'rLs1MzkFWCxTbuAHgjeTZK4fcCDDnf2KRv',
PreviousTxnLgrSeq: 7,
LedgerEntryType: 'AccountRoot',
PreviousTxnID: 'DF530FB14C5304852F20080B0A8EEF3A6BDD044F41F4EBBD68B8B321145FE4FF',
Flags: 0,
Sequence: 1,
Balance: '10000000000',
SourceTag: 12345,
}
let json_null_x = {
"OwnerCount": 0,
"Account": "rLs1MzkFWCxTbuAHgjeTZK4fcCDDnf2KRv",
"Destination": "rLs1MzkFWCxTbuAHgjeTZK4fcCDDnf2KRv",
"Issuer": "XVXdn5wEVm5G4UhEHWDPqjvdeH361P4GETfNyyXGaoqBj71",
"PreviousTxnLgrSeq": 7,
"LedgerEntryType": "AccountRoot",
"PreviousTxnID": "DF530FB14C5304852F20080B0A8EEF3A6BDD044F41F4EBBD68B8B321145FE4FF",
"Flags": 0,
"Sequence": 1,
"Balance": "10000000000"
}
let json_invalid_x = {
"OwnerCount": 0,
"Account": "rLs1MzkFWCxTbuAHgjeTZK4fcCDDnf2KRv",
"Destination": "rLs1MzkFWCxTbuAHgjeTZK4fcCDDnf2KRv",
"Issuer": "XVXdn5wEVm5g4UhEHWDPqjvdeH361P4GETfNyyXGaoqBj71",
"PreviousTxnLgrSeq": 7,
"LedgerEntryType": "AccountRoot",
"PreviousTxnID": "DF530FB14C5304852F20080B0A8EEF3A6BDD044F41F4EBBD68B8B321145FE4FF",
"Flags": 0,
"Sequence": 1,
"Balance": "10000000000"
}
let json_null_r = {
"OwnerCount": 0,
"Account": "rLs1MzkFWCxTbuAHgjeTZK4fcCDDnf2KRv",
"Destination": "rLs1MzkFWCxTbuAHgjeTZK4fcCDDnf2KRv",
"Issuer": "rLs1MzkFWCxTbuAHgjeTZK4fcCDDnf2KRv",
"PreviousTxnLgrSeq": 7,
"LedgerEntryType": "AccountRoot",
"PreviousTxnID": "DF530FB14C5304852F20080B0A8EEF3A6BDD044F41F4EBBD68B8B321145FE4FF",
"Flags": 0,
"Sequence": 1,
"Balance": "10000000000"
}
let invalid_json_issuer_tagged = {
"OwnerCount": 0,
"Account": "rLs1MzkFWCxTbuAHgjeTZK4fcCDDnf2KRv",
"Destination": "rLs1MzkFWCxTbuAHgjeTZK4fcCDDnf2KRv",
"Issuer": "XVXdn5wEVm5G4UhEHWDPqjvdeH361P7BsapL4m2D2XnPSwT",
"PreviousTxnLgrSeq": 7,
"LedgerEntryType": "AccountRoot",
"PreviousTxnID": "DF530FB14C5304852F20080B0A8EEF3A6BDD044F41F4EBBD68B8B321145FE4FF",
"Flags": 0,
"Sequence": 1,
"Balance": "10000000000"
}
let invalid_json_x_and_tagged = {
OwnerCount: 0,
Account: 'XVXdn5wEVm5G4UhEHWDPqjvdeH361P7BsapL4m2D2XnPSwT',
PreviousTxnLgrSeq: 7,
LedgerEntryType: 'AccountRoot',
PreviousTxnID: 'DF530FB14C5304852F20080B0A8EEF3A6BDD044F41F4EBBD68B8B321145FE4FF',
Flags: 0,
Sequence: 1,
Balance: '10000000000',
SourceTag: 12345,
}
describe("X-Address Account is equivalent to a classic address w/ SourceTag", () => {
let encoded_x = encode(json_x1);
let encoded_r = encode(json_r1);
test("Can encode with x-Address", () => {
expect(encoded_x).toEqual(encoded_r);
})
test("decoded X-address is object w/ source and tag", () => {
let decoded_x = decode(encoded_x);
expect(decoded_x).toEqual(json_r1);
})
test("Encoding issuer X-Address w/ undefined destination tag", () => {
expect(encode(json_null_x)).toEqual(encode(json_null_r));
})
test("Throws when X-Address is invalid", () => {
expect(() => encode(json_invalid_x)).toThrow("checksum_invalid");
})
})
describe("Invalid X-Address behavior", () => {
test("X-Address with tag throws value for invalid field",() => {
expect(() => encode(invalid_json_issuer_tagged)).toThrow(new Error("Issuer cannot have an associated tag"))
})
test("Throws when Account has both X-Addr and Destination Tag", () => {
expect(() => encode(invalid_json_x_and_tagged)).toThrow(new Error("Cannot have Account X-Address and SourceTag"));
});
})
describe('ripple-binary-codec x-address test', function () {
function makeSuite (name, entries) {
describe(name, function () {
entries.forEach((t, testN) => {
test(`${name}[${testN}] encodes X-address json equivalent to classic address json`,
() => {
expect(encode(t.rjson)).toEqual(encode(t.xjson))
})
test(`${name}[${testN}] decodes X-address json equivalent to classic address json`, () => {
expect(decode(encode(t.xjson))).toEqual(t.rjson);
})
})
})
}
makeSuite('transactions', fixtures.transactions)
})