diff --git a/packages/ripple-binary-codec/HISTORY.md b/packages/ripple-binary-codec/HISTORY.md index 88093c1ed6..60f31c559f 100644 --- a/packages/ripple-binary-codec/HISTORY.md +++ b/packages/ripple-binary-codec/HISTORY.md @@ -2,6 +2,9 @@ ## Unreleased +### Fixed +- add `MPTAmount` support in `Issue` (rippled internal type) + ## 2.3.0 (2025-2-13) ### Added diff --git a/packages/ripple-binary-codec/src/types/issue.ts b/packages/ripple-binary-codec/src/types/issue.ts index a7c22b62d9..5a84d9cb20 100644 --- a/packages/ripple-binary-codec/src/types/issue.ts +++ b/packages/ripple-binary-codec/src/types/issue.ts @@ -4,24 +4,35 @@ import { BinaryParser } from '../serdes/binary-parser' import { AccountID } from './account-id' import { Currency } from './currency' import { JsonObject, SerializedType } from './serialized-type' +import { Hash192 } from './hash-192' -/** - * Interface for JSON objects that represent amounts - */ -interface IssueObject extends JsonObject { +interface XRPIssue extends JsonObject { + currency: string +} + +interface IOUIssue extends JsonObject { currency: string - issuer?: string + issuer: string } +interface MPTIssue extends JsonObject { + mpt_issuance_id: string +} +/** + * Interface for JSON objects that represent issues + */ +type IssueObject = XRPIssue | IOUIssue | MPTIssue /** - * Type guard for AmountObject + * Type guard for Issue Object */ function isIssueObject(arg): arg is IssueObject { const keys = Object.keys(arg).sort() - if (keys.length === 1) { - return keys[0] === 'currency' - } - return keys.length === 2 && keys[0] === 'currency' && keys[1] === 'issuer' + const isXRP = keys.length === 1 && keys[0] === 'currency' + const isIOU = + keys.length === 2 && keys[0] === 'currency' && keys[1] === 'issuer' + const isMPT = keys.length === 1 && keys[0] === 'mpt_issuance_id' + + return isXRP || isIOU || isMPT } /** @@ -37,9 +48,9 @@ class Issue extends SerializedType { /** * Construct an amount from an IOU or string amount * - * @param value An Amount, object representing an IOU, or a string + * @param value An Amount, object representing an IOU, MPTAmount, or a string * representing an integer amount - * @returns An Amount object + * @returns An Issue object */ static from(value: T): Issue { if (value instanceof Issue) { @@ -47,12 +58,26 @@ class Issue extends SerializedType { } if (isIssueObject(value)) { - const currency = Currency.from(value.currency).toBytes() - if (value.issuer == null) { + if (value.currency) { + const currency = Currency.from(value.currency.toString()).toBytes() + + //IOU case + if (value.issuer) { + const issuer = AccountID.from(value.issuer.toString()).toBytes() + return new Issue(concat([currency, issuer])) + } + + //XRP case return new Issue(currency) } - const issuer = AccountID.from(value.issuer).toBytes() - return new Issue(concat([currency, issuer])) + + // MPT case + if (value.mpt_issuance_id) { + const mptIssuanceIdBytes = Hash192.from( + value.mpt_issuance_id.toString(), + ).toBytes() + return new Issue(mptIssuanceIdBytes) + } } throw new Error('Invalid type to construct an Amount') @@ -62,9 +87,16 @@ class Issue extends SerializedType { * Read an amount from a BinaryParser * * @param parser BinaryParser to read the Amount from - * @returns An Amount object + * @param hint The number of bytes to consume from the parser. + * For an MPT amount, pass 24 (the fixed length for Hash192). + * + * @returns An Issue object */ - static fromParser(parser: BinaryParser): Issue { + static fromParser(parser: BinaryParser, hint?: number): Issue { + if (hint === Hash192.width) { + const mptBytes = parser.read(Hash192.width) + return new Issue(mptBytes) + } const currency = parser.read(20) if (new Currency(currency).toJSON() === 'XRP') { return new Issue(currency) @@ -79,7 +111,15 @@ class Issue extends SerializedType { * @returns the JSON interpretation of this.bytes */ toJSON(): IssueObject { + // If the buffer is exactly 24 bytes, treat it as an MPT amount. + if (this.toBytes().length === Hash192.width) { + return { + mpt_issuance_id: this.toHex().toUpperCase(), + } + } + const parser = new BinaryParser(this.toString()) + const currency = Currency.fromParser(parser) as Currency if (currency.toJSON() === 'XRP') { return { currency: currency.toJSON() } diff --git a/packages/ripple-binary-codec/test/issue.test.ts b/packages/ripple-binary-codec/test/issue.test.ts new file mode 100644 index 0000000000..72ba9fef34 --- /dev/null +++ b/packages/ripple-binary-codec/test/issue.test.ts @@ -0,0 +1,84 @@ +import { BinaryParser } from '../src/binary' +import { Issue } from '../src/types/issue' + +describe('Issue type conversion functions', () => { + it(`test from value xrp`, () => { + const xrpJson = { currency: 'XRP' } + const xrpIssue = Issue.from(xrpJson) + expect(xrpIssue.toJSON()).toEqual(xrpJson) + }) + + it(`test from value issued currency`, () => { + const iouJson = { + currency: 'USD', + issuer: 'rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn', + } + const iouIssue = Issue.from(iouJson) + expect(iouIssue.toJSON()).toEqual(iouJson) + }) + + it(`test from value non-standard currency`, () => { + const iouJson = { + currency: '0123456789ABCDEF0123456789ABCDEF01234567', + issuer: 'rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn', + } + const iouIssue = Issue.from(iouJson) + expect(iouIssue.toJSON()).toEqual(iouJson) + }) + + it(`test from value mpt`, () => { + const mptJson = { + mpt_issuance_id: 'BAADF00DBAADF00DBAADF00DBAADF00DBAADF00DBAADF00D', + } + const mptIssue = Issue.from(mptJson) + expect(mptIssue.toJSON()).toEqual(mptJson) + }) + + it(`test from parser xrp`, () => { + const xrpJson = { currency: 'XRP' } + const xrpIssue = Issue.from(xrpJson) + const parser = new BinaryParser(xrpIssue.toHex()) + const parserIssue = Issue.fromParser(parser) + expect(parserIssue.toJSON()).toEqual(xrpJson) + }) + + it(`test from parser issued currency`, () => { + const iouJson = { + currency: 'EUR', + issuer: 'rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD', + } + const iouIssue = Issue.from(iouJson) + const parser = new BinaryParser(iouIssue.toHex()) + const parserIssue = Issue.fromParser(parser) + expect(parserIssue.toJSON()).toEqual(iouJson) + }) + + it(`test from parser non-standard currency`, () => { + const iouJson = { + currency: '0123456789ABCDEF0123456789ABCDEF01234567', + issuer: 'rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD', + } + const iouIssue = Issue.from(iouJson) + const parser = new BinaryParser(iouIssue.toHex()) + const parserIssue = Issue.fromParser(parser) + expect(parserIssue.toJSON()).toEqual(iouJson) + }) + + it(`test from parser mpt`, () => { + const mptJson = { + mpt_issuance_id: 'BAADF00DBAADF00DBAADF00DBAADF00DBAADF00DBAADF00D', + } + const mptIssue = Issue.from(mptJson) + const parser = new BinaryParser(mptIssue.toHex()) + const parserIssue = Issue.fromParser(parser, 24) + expect(parserIssue.toJSON()).toEqual(mptJson) + }) + + it(`throws with invalid input`, () => { + const invalidJson = { random: 123 } + expect(() => { + // @ts-expect-error -- need to test error message + Issue.from(invalidJson) + }).toThrow(new Error('Invalid type to construct an Amount')) + }) +})