Skip to content

Commit 006ef0f

Browse files
authored
Fix serialization of Issue type when asset is MPT (XRPLF#3090)
* WIP commit * WIP commit * fix serialization for Issue * update HISTORY * fix flaking oracle tests * export VaultFlags and some code comments * fix fromParser parsing
1 parent e3ba5a8 commit 006ef0f

File tree

14 files changed

+664
-135
lines changed

14 files changed

+664
-135
lines changed

packages/ripple-binary-codec/HISTORY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
### Fixed
6+
* Fix serialization/deserialization issues in `Issue` serialized type for MPTIssue.
7+
58
## 2.5.0 (2025-07-29)
69

710
### Added

packages/ripple-binary-codec/src/types/issue.ts

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { concat } from '@xrplf/isomorphic/utils'
1+
import { bytesToHex, concat } from '@xrplf/isomorphic/utils'
22
import { BinaryParser } from '../serdes/binary-parser'
33

44
import { AccountID } from './account-id'
55
import { Currency } from './currency'
66
import { JsonObject, SerializedType } from './serialized-type'
77
import { Hash192 } from './hash-192'
8+
import { readUInt32BE, writeUInt32BE } from '../utils'
89

910
interface XRPIssue extends JsonObject {
1011
currency: string
@@ -35,21 +36,23 @@ function isIssueObject(arg): arg is IssueObject {
3536
return isXRP || isIOU || isMPT
3637
}
3738

39+
const MPT_WIDTH = 44
40+
const NO_ACCOUNT = AccountID.from('0000000000000000000000000000000000000001')
41+
3842
/**
39-
* Class for serializing/Deserializing Amounts
43+
* Class for serializing/Deserializing Issue
4044
*/
4145
class Issue extends SerializedType {
42-
static readonly ZERO_ISSUED_CURRENCY: Issue = new Issue(new Uint8Array(20))
46+
static readonly XRP_ISSUE: Issue = new Issue(new Uint8Array(20))
4347

4448
constructor(bytes: Uint8Array) {
45-
super(bytes ?? Issue.ZERO_ISSUED_CURRENCY.bytes)
49+
super(bytes ?? Issue.XRP_ISSUE.bytes)
4650
}
4751

4852
/**
49-
* Construct an amount from an IOU or string amount
53+
* Construct Issue from XRPIssue, IOUIssue or MPTIssue
5054
*
51-
* @param value An Amount, object representing an IOU, MPTAmount, or a string
52-
* representing an integer amount
55+
* @param value An object representing an XRPIssue, IOUIssue or MPTIssue
5356
* @returns An Issue object
5457
*/
5558
static from<T extends Issue | IssueObject>(value: T): Issue {
@@ -76,45 +79,69 @@ class Issue extends SerializedType {
7679
const mptIssuanceIdBytes = Hash192.from(
7780
value.mpt_issuance_id.toString(),
7881
).toBytes()
79-
return new Issue(mptIssuanceIdBytes)
82+
const issuerAccount = mptIssuanceIdBytes.slice(4)
83+
const sequence = Number(readUInt32BE(mptIssuanceIdBytes.slice(0, 4), 0)) // sequence is in Big-endian format in mpt_issuance_id
84+
85+
// Convert to Little-endian
86+
const sequenceBuffer = new Uint8Array(4)
87+
new DataView(sequenceBuffer.buffer).setUint32(0, sequence, true)
88+
89+
return new Issue(
90+
concat([issuerAccount, NO_ACCOUNT.toBytes(), sequenceBuffer]),
91+
)
8092
}
8193
}
8294

83-
throw new Error('Invalid type to construct an Amount')
95+
throw new Error('Invalid type to construct an Issue')
8496
}
8597

8698
/**
87-
* Read an amount from a BinaryParser
99+
* Read Issue from a BinaryParser
88100
*
89-
* @param parser BinaryParser to read the Amount from
90-
* @param hint The number of bytes to consume from the parser.
91-
* For an MPT amount, pass 24 (the fixed length for Hash192).
101+
* @param parser BinaryParser to read the Issue from
92102
*
93103
* @returns An Issue object
94104
*/
95-
static fromParser(parser: BinaryParser, hint?: number): Issue {
96-
if (hint === Hash192.width) {
97-
const mptBytes = parser.read(Hash192.width)
98-
return new Issue(mptBytes)
105+
static fromParser(parser: BinaryParser): Issue {
106+
// XRP
107+
const currencyOrAccount = parser.read(20)
108+
if (new Currency(currencyOrAccount).toJSON() === 'XRP') {
109+
return new Issue(currencyOrAccount)
99110
}
100-
const currency = parser.read(20)
101-
if (new Currency(currency).toJSON() === 'XRP') {
102-
return new Issue(currency)
111+
112+
// MPT
113+
const issuerAccountId = new AccountID(parser.read(20))
114+
if (NO_ACCOUNT.toHex() === issuerAccountId.toHex()) {
115+
const sequence = parser.read(4)
116+
return new Issue(
117+
concat([currencyOrAccount, NO_ACCOUNT.toBytes(), sequence]),
118+
)
103119
}
104-
const currencyAndIssuer = [currency, parser.read(20)]
105-
return new Issue(concat(currencyAndIssuer))
120+
121+
// IOU
122+
return new Issue(concat([currencyOrAccount, issuerAccountId.toBytes()]))
106123
}
107124

108125
/**
109-
* Get the JSON representation of this Amount
126+
* Get the JSON representation of this IssueObject
110127
*
111128
* @returns the JSON interpretation of this.bytes
112129
*/
113130
toJSON(): IssueObject {
114-
// If the buffer is exactly 24 bytes, treat it as an MPT amount.
115-
if (this.toBytes().length === Hash192.width) {
131+
// If the buffer is exactly 44 bytes, treat it as an MPTIssue.
132+
if (this.toBytes().length === MPT_WIDTH) {
133+
const issuerAccount = this.toBytes().slice(0, 20)
134+
const sequence = new DataView(this.toBytes().slice(40).buffer).getUint32(
135+
0,
136+
true,
137+
)
138+
139+
// sequence part of mpt_issuance_id should be in Big-endian
140+
const sequenceBuffer = new Uint8Array(4)
141+
writeUInt32BE(sequenceBuffer, sequence, 0)
142+
116143
return {
117-
mpt_issuance_id: this.toHex().toUpperCase(),
144+
mpt_issuance_id: bytesToHex(concat([sequenceBuffer, issuerAccount])),
118145
}
119146
}
120147

packages/ripple-binary-codec/src/types/st-object.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,7 @@ class STObject extends SerializedType {
151151
? STArray.from(xAddressDecoded[field.name], definitions)
152152
: field.type.name === 'UInt64'
153153
? UInt64.from(xAddressDecoded[field.name], field.name)
154-
: field.associatedType?.from
155-
? field.associatedType.from(xAddressDecoded[field.name])
156-
: (() => {
157-
throw new Error(
158-
`Type ${field.type.name} for field ${field.name} is missing associatedType.from`,
159-
)
160-
})()
154+
: field.associatedType.from(xAddressDecoded[field.name])
161155

162156
if (associatedValue == undefined) {
163157
throw new TypeError(

0 commit comments

Comments
 (0)