Skip to content
3 changes: 3 additions & 0 deletions packages/ripple-binary-codec/HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

### Fixed
- add `MPTAmount` support in `Issue` (rippled internal type)

## 2.3.0 (2025-2-13)

### Added
Expand Down
70 changes: 55 additions & 15 deletions packages/ripple-binary-codec/src/types/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 XRPIssue extends JsonObject {
currency: string
}

interface IOUIssue extends JsonObject {
currency: string
issuer: string
}
interface MPTIssue extends JsonObject {
mpt_issuance_id: string
}
/**
* Interface for JSON objects that represent amounts
*/
interface IssueObject extends JsonObject {
currency: string
issuer?: string
}
type IssueObject = XRPIssue | IOUIssue | MPTIssue

/**
* Type guard for AmountObject
*/
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
}

/**
Expand All @@ -39,20 +50,34 @@ class Issue extends SerializedType {
*
* @param value An Amount, object representing an IOU, or a string
* representing an integer amount
* @returns An Amount object
* @returns An Issue object
*/
static from<T extends Issue | IssueObject>(value: T): Issue {
if (value instanceof Issue) {
return value
}

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')
Expand All @@ -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)
Expand All @@ -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() }
Expand Down
86 changes: 86 additions & 0 deletions packages/ripple-binary-codec/test/issue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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()).toMatchObject(xrpJson)
})

it(`test from value issued currency`, () => {
const iouJson = {
currency: 'USD',
issuer: 'rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn',
}
const iouIssue = Issue.from(iouJson)
expect(iouIssue.toJSON()).toMatchObject(iouJson)
})

it(`test from value nonstandard currency`, () => {
const iouJson = {
currency: '0123456789ABCDEF0123456789ABCDEF01234567',
issuer: 'rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn',
}
const iouIssue = Issue.from(iouJson)
expect(iouIssue.toJSON()).toMatchObject(iouJson)
})

it(`test from value mpt`, () => {
const mptJson = {
// value: '100', // MPT amounts must be an integer string (no decimal point)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@khancode saw value in the python tests, but wasn't sure whether to add it here based on our discussions. Seems like it should be here, but that would require me to change the isMPT logic in Issue to something different than what we discussed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check if value is included when Issue.from is normally called? If yes, then we should add it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue.from accepts both types in the input parameter.

static from<T extends Issue | IssueObject>(value: T): Issue

It should work either ways

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@khancode when you say normally called do you mean the implementations for IOU and XRP? there were no tests before so hard to tell but based off of the isIssueObject check it seems like those have no value param. However, in your python implementation there was no value parameter for IOU and XRP either, but you added it for MPT. I'm assuming this should be the same in JS so I should add value as a param

Copy link
Contributor

@khancode khancode Mar 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I meant the implementations normal call flow. Yes, it looks like in PY its design is different than JS. Just need to make sure we make it compatible with JS design.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like value is excluded by looking at the console.log inside of Issue.from so this looks good 👍

mpt_issuance_id: 'BAADF00DBAADF00DBAADF00DBAADF00DBAADF00DBAADF00D',
}
const mptIssue = Issue.from(mptJson)
expect(mptIssue.toJSON()).toMatchObject(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()).toMatchObject(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()).toMatchObject(iouJson)
})

it(`test from parser nonstandard 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()).toMatchObject(iouJson)
})

it(`test from parser mpt`, () => {
const mptJson = {
// value: '100', // MPT amounts must be an integer string (no decimal point)
mpt_issuance_id: 'BAADF00DBAADF00DBAADF00DBAADF00DBAADF00DBAADF00D',
}
const mptIssue = Issue.from(mptJson)
const parser = new BinaryParser(mptIssue.toHex())
const parserIssue = Issue.fromParser(parser, 24)
expect(parserIssue.toJSON()).toMatchObject(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'))
})
})
Loading