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
76 changes: 58 additions & 18 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 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
}

/**
Expand All @@ -37,22 +48,36 @@ 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<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
84 changes: 84 additions & 0 deletions packages/ripple-binary-codec/test/issue.test.ts
Original file line number Diff line number Diff line change
@@ -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'))
})
})
Loading