diff --git a/packages/ripple-binary-codec/src/enums/definitions.json b/packages/ripple-binary-codec/src/enums/definitions.json index fa251727..0ef9345e 100644 --- a/packages/ripple-binary-codec/src/enums/definitions.json +++ b/packages/ripple-binary-codec/src/enums/definitions.json @@ -2049,12 +2049,12 @@ "NFTokenCreateOffer": 27, "NFTokenCancelOffer": 28, "NFTokenAcceptOffer": 29, - "XchainDoorCreate": 30, - "XchainSeqnumCreate": 31, - "XchainTransfer": 32, - "XchainClaim": 33, - "XchainAccountCreate": 34, - "XchainAccountClaim": 35, + "XChainDoorCreate": 30, + "XChainSeqnumCreate": 31, + "XChainTransfer": 32, + "XChainClaim": 33, + "XChainAccountCreate": 34, + "XChainAccountClaim": 35, "EnableAmendment": 100, "SetFee": 101, diff --git a/packages/ripple-binary-codec/src/serdes/binary-parser.ts b/packages/ripple-binary-codec/src/serdes/binary-parser.ts index 534ef52f..52f0d650 100644 --- a/packages/ripple-binary-codec/src/serdes/binary-parser.ts +++ b/packages/ripple-binary-codec/src/serdes/binary-parser.ts @@ -7,7 +7,7 @@ import { Buffer } from 'buffer/' * BinaryParser is used to compute fields and values from a HexString */ class BinaryParser { - private bytes: Buffer + public bytes: Buffer /** * Initialize bytes to a hex string diff --git a/packages/ripple-binary-codec/src/types/blob.ts b/packages/ripple-binary-codec/src/types/blob.ts index 4d9fa693..7804ff27 100644 --- a/packages/ripple-binary-codec/src/types/blob.ts +++ b/packages/ripple-binary-codec/src/types/blob.ts @@ -14,7 +14,7 @@ class Blob extends SerializedType { * 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 + * @param hint The length of the blob, computed by readVariableLength() and passed in * @returns A Blob object */ static fromParser(parser: BinaryParser, hint: number): Blob { diff --git a/packages/ripple-binary-codec/src/types/index.ts b/packages/ripple-binary-codec/src/types/index.ts index 5efa199a..014aa12e 100644 --- a/packages/ripple-binary-codec/src/types/index.ts +++ b/packages/ripple-binary-codec/src/types/index.ts @@ -11,7 +11,9 @@ import { Currency } from './currency' import { Hash128 } from './hash-128' import { Hash160 } from './hash-160' import { Hash256 } from './hash-256' +import { IssuedCurrency } from './issued-currency' import { PathSet } from './path-set' +import { Sidechain } from './sidechain' import { STArray } from './st-array' import { STObject } from './st-object' import { UInt16 } from './uint-16' @@ -28,7 +30,9 @@ const coreTypes = { Hash128, Hash160, Hash256, + IssuedCurrency, PathSet, + Sidechain, STArray, STObject, UInt8, diff --git a/packages/ripple-binary-codec/src/types/issued-currency.ts b/packages/ripple-binary-codec/src/types/issued-currency.ts new file mode 100644 index 00000000..b4419224 --- /dev/null +++ b/packages/ripple-binary-codec/src/types/issued-currency.ts @@ -0,0 +1,114 @@ +import { BinaryParser } from '../serdes/binary-parser' + +import { AccountID } from './account-id' +import { Currency } from './currency' +import { JsonObject, SerializedType } from './serialized-type' +import { Buffer } from 'buffer/' + +/** + * Interface for JSON objects that represent amounts + */ +interface IssuedCurrencyObject extends JsonObject { + currency: string + issuer: string +} + +/** + * Type guard for AmountObject + */ +function isIssuedCurrencyObject(arg): arg is IssuedCurrencyObject { + const keys = Object.keys(arg).sort() + return keys.length === 2 && keys[0] === 'currency' && keys[1] === 'issuer' +} + +/** + * Class for serializing/Deserializing Amounts + */ +class IssuedCurrency extends SerializedType { + static readonly ZERO_ISSUED_CURRENCY: IssuedCurrency = new IssuedCurrency( + Buffer.alloc(20), + ) + + constructor(bytes: Buffer) { + super(bytes ?? IssuedCurrency.ZERO_ISSUED_CURRENCY.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: T, + ): IssuedCurrency { + if (value instanceof IssuedCurrency) { + return value + } + + if (typeof value === 'string') { + IssuedCurrency.assertXrpIsValid(value) + + const currency = Currency.from(value).toBytes() + + return new IssuedCurrency(currency) + } + + if (isIssuedCurrencyObject(value)) { + const currency = Currency.from(value.currency).toBytes() + const issuer = AccountID.from(value.issuer).toBytes() + return new IssuedCurrency(Buffer.concat([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): IssuedCurrency { + const currency = parser.read(20) + if (new Currency(currency).toJSON() === 'XRP') { + return new IssuedCurrency(currency) + } + const currencyAndIssuer = [currency, parser.read(20)] + return new IssuedCurrency(Buffer.concat(currencyAndIssuer)) + } + + /** + * Get the JSON representation of this Amount + * + * @returns the JSON interpretation of this.bytes + */ + toJSON(): IssuedCurrencyObject | string { + const parser = new BinaryParser(this.toString()) + const currency = Currency.fromParser(parser) as Currency + if (currency.toJSON() === 'XRP') { + return currency.toJSON() + } + const issuer = AccountID.fromParser(parser) as AccountID + + return { + currency: currency.toJSON(), + issuer: issuer.toJSON(), + } + } + + /** + * Validate XRP amount + * + * @param value String representing XRP amount + * @returns void, but will throw if invalid amount + */ + private static assertXrpIsValid(value: string): void { + if (value !== 'XRP') { + throw new Error(`${value} is an illegal amount`) + } + } +} + +export { IssuedCurrency, IssuedCurrencyObject } diff --git a/packages/ripple-binary-codec/src/types/sidechain.ts b/packages/ripple-binary-codec/src/types/sidechain.ts new file mode 100644 index 00000000..26a0b761 --- /dev/null +++ b/packages/ripple-binary-codec/src/types/sidechain.ts @@ -0,0 +1,114 @@ +import { BinaryParser } from '../serdes/binary-parser' + +import { AccountID } from './account-id' +import { JsonObject, SerializedType } from './serialized-type' +import { Buffer } from 'buffer/' +import { IssuedCurrency, IssuedCurrencyObject } from './issued-currency' + +/** + * Interface for JSON objects that represent amounts + */ +interface SidechainObject extends JsonObject { + dst_chain_door: string + dst_chain_issue: IssuedCurrencyObject | string + src_chain_door: string + src_chain_issue: IssuedCurrencyObject | string +} + +/** + * Type guard for AmountObject + */ +function isSidechainObject(arg): arg is SidechainObject { + const keys = Object.keys(arg).sort() + return ( + keys.length === 4 && + keys[0] === 'dst_chain_door' && + keys[1] === 'dst_chain_issue' && + keys[2] === 'src_chain_door' && + keys[3] === 'src_chain_issue' + ) +} + +/** + * Class for serializing/Deserializing Amounts + */ +class Sidechain extends SerializedType { + static readonly ZERO_SIDECHAIN: Sidechain = new Sidechain(Buffer.alloc(80)) + + constructor(bytes: Buffer) { + super(bytes ?? Sidechain.ZERO_SIDECHAIN.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: T): Sidechain { + if (value instanceof Sidechain) { + return value + } + + if (isSidechainObject(value)) { + const dst_chain_door = AccountID.from(value.dst_chain_door).toBytes() + const dst_chain_issue = IssuedCurrency.from( + value.dst_chain_issue, + ).toBytes() + const src_chain_door = AccountID.from(value.src_chain_door).toBytes() + const src_chain_issue = IssuedCurrency.from( + value.src_chain_issue, + ).toBytes() + return new Sidechain( + Buffer.concat([ + dst_chain_door, + dst_chain_issue, + src_chain_door, + src_chain_issue, + ]), + ) + } + + throw new Error('Invalid type to construct a Sidechain') + } + + /** + * Read an amount from a BinaryParser + * + * @param parser BinaryParser to read the Amount from + * @returns An Amount object + */ + static fromParser(parser: BinaryParser): Sidechain { + const bytes: Array = [] + + bytes.push(parser.read(AccountID.width)) + bytes.push(IssuedCurrency.fromParser(parser).toBytes()) + bytes.push(parser.read(AccountID.width)) + bytes.push(IssuedCurrency.fromParser(parser).toBytes()) + + return new Sidechain(Buffer.concat(bytes)) + } + + /** + * Get the JSON representation of this Amount + * + * @returns the JSON interpretation of this.bytes + */ + toJSON(): SidechainObject { + const parser = new BinaryParser(this.toString()) + const dst_chain_door = AccountID.fromParser(parser) as AccountID + const dst_chain_issue = IssuedCurrency.fromParser(parser) + const src_chain_door = AccountID.fromParser(parser) as AccountID + const src_chain_issue = IssuedCurrency.fromParser(parser) + + return { + dst_chain_door: dst_chain_door.toJSON(), + dst_chain_issue: dst_chain_issue.toJSON(), + src_chain_door: src_chain_door.toJSON(), + src_chain_issue: src_chain_issue.toJSON(), + } + } +} + +export { Sidechain, SidechainObject } diff --git a/packages/ripple-binary-codec/test/binary-serializer.test.js b/packages/ripple-binary-codec/test/binary-serializer.test.js index 41b5f9c9..50f042e3 100644 --- a/packages/ripple-binary-codec/test/binary-serializer.test.js +++ b/packages/ripple-binary-codec/test/binary-serializer.test.js @@ -105,6 +105,7 @@ let json_omitted = { } const NegativeUNL = require('./fixtures/negative-unl.json') +const XChainDoorCreate = require('./fixtures/xchain-door-create.json') function bytesListTest() { const list = new BytesList() @@ -220,7 +221,7 @@ function PaymentChannelTest() { }) } -function NegativeUNLTest() { +function negativeUNLTest() { test('can serialize NegativeUNL', () => { expect(encode(NegativeUNL.tx)).toEqual(NegativeUNL.binary) }) @@ -229,6 +230,15 @@ function NegativeUNLTest() { }) } +function sidechainTest() { + test('can serialize XChainDoorCreate', () => { + expect(encode(XChainDoorCreate.tx)).toEqual(XChainDoorCreate.binary) + }) + test('can deserialize XChainDoorCreate', () => { + expect(decode(XChainDoorCreate.binary)).toEqual(XChainDoorCreate.tx) + }) +} + function omitUndefinedTest() { test('omits fields with undefined value', () => { let encodedOmitted = encode(json_omitted) @@ -282,7 +292,8 @@ describe('Binary Serialization', function () { describe('SignerListSet', SignerListSetTest) describe('Escrow', EscrowTest) describe('PaymentChannel', PaymentChannelTest) - describe('NegativeUNLTest', NegativeUNLTest) + describe('NegativeUNLTest', negativeUNLTest) + describe('SidechainTest', sidechainTest) describe('OmitUndefined', omitUndefinedTest) describe('TicketTest', ticketTest) describe('NFToken', nfTokenTest) diff --git a/packages/ripple-binary-codec/test/fixtures/xchain-door-create.json b/packages/ripple-binary-codec/test/fixtures/xchain-door-create.json new file mode 100644 index 00000000..c4021e56 --- /dev/null +++ b/packages/ripple-binary-codec/test/fixtures/xchain-door-create.json @@ -0,0 +1,57 @@ +{ + "binary": "12001E22800000002023000000048114C48CAD01682D7A86296EF14523074D4852C02EA9F4EB130001811474A41942D90FDD8E4E8BB25A7E91843CFEDB9A5DE1EB1300018114C287E75E44FEB7AF3537173BB3A866A652C91502E1EB1300018114F5B6BA5BA9F91592A4B607E0397E47A298B95EA2E1EB13000181145720A5ABFA7D844BD615F4E62FA7C963E85B0C7DE1EB1300018114B7521887260F712472A8E5775EE6234042641C0CE1F10118CC86E58C9B58D4CF71CB8C1B41F21BB290CE13D4000000000000000000000000555344000000000027B6C49755570AD538DDD42EE417A4708F17EF76C48CAD01682D7A86296EF14523074D4852C02EA90000000000000000000000000000000000000000", + "tx": { + "Account" : "rJvExveLEL4jNDEeLKCVdxaSCN9cEBnEQC", + "Flags" : 2147483648, + "Sidechain" : + { + "dst_chain_door" : "rKeSSvHvaMZJp9ykaxutVwkhZgWuWMLnQt", + "dst_chain_issue" : { + "currency" : "USD", + "issuer" : "rhczJR49YsdxwtYTPvxeSc1Jjr7R748cHv" + }, + "src_chain_door" : "rJvExveLEL4jNDEeLKCVdxaSCN9cEBnEQC", + "src_chain_issue" : "XRP" + }, + "SignerEntries" : + [ + { + "SignerEntry" : + { + "Account" : "rBdjyperRHKTzdxnZhyN94MpjN2aknRX8G", + "SignerWeight" : 1 + } + }, + { + "SignerEntry" : + { + "Account" : "rJj2ty2MDGu7dtm1bvZMA5KuhzreNL2HHo", + "SignerWeight" : 1 + } + }, + { + "SignerEntry" : + { + "Account" : "rPQDTwG7tWYNzqjytf8YCYX6hZemGG9TTh", + "SignerWeight" : 1 + } + }, + { + "SignerEntry" : + { + "Account" : "r3AguhaYj2enNDz37mzJNskxcQKb3sAYjE", + "SignerWeight" : 1 + } + }, + { + "SignerEntry" : + { + "Account" : "rH5KrD1ocKBWq3Mf7WGy8tTtEi84M1uwGm", + "SignerWeight" : 1 + } + } + ], + "SignerQuorum" : 4, + "TransactionType" : "XChainDoorCreate" + } +}