|
2 | 2 |
|
3 | 3 | from xrpl.core.binarycodec import XRPLBinaryCodecException |
4 | 4 | from xrpl.core.binarycodec.binary_wrappers.binary_parser import BinaryParser |
| 5 | +from xrpl.core.binarycodec.types.account_id import AccountID |
| 6 | +from xrpl.core.binarycodec.types.currency import Currency |
5 | 7 | from xrpl.core.binarycodec.types.path_set import PathSet |
6 | 8 |
|
7 | 9 | buffer = ( |
@@ -108,3 +110,130 @@ def test_from_parser_to_json(self): |
108 | 110 | def test_raises_invalid_value_type(self): |
109 | 111 | invalid_value = 1 |
110 | 112 | self.assertRaises(XRPLBinaryCodecException, PathSet.from_value, invalid_value) |
| 113 | + |
| 114 | + # ── MPT PathSet serialization tests ── |
| 115 | + # |
| 116 | + # PathSet binary format reference (from rippled STPathSet.cpp): |
| 117 | + # |
| 118 | + # Each path step starts with a 1-byte type flag bitmask: |
| 119 | + # 0x01 = account (followed by 20-byte AccountID) |
| 120 | + # 0x10 = currency (followed by 20-byte Currency) |
| 121 | + # 0x20 = issuer (followed by 20-byte AccountID) |
| 122 | + # 0x40 = MPT (followed by 24-byte MPTID) |
| 123 | + # |
| 124 | + # Special marker bytes: |
| 125 | + # 0xFF = path boundary (separates alternative paths within a PathSet) |
| 126 | + # 0x00 = end of PathSet (terminates the entire PathSet) |
| 127 | + # |
| 128 | + # Currency (0x10) and MPT (0x40) are mutually exclusive within a |
| 129 | + # single path step — a step cannot carry both flags. |
| 130 | + |
| 131 | + def test_one_path_with_one_mpt_hop(self): |
| 132 | + mpt_issuance_id = "00000001B5F762798A53D543A014CAF8B297CFF8F2F937E8" |
| 133 | + path = [[{"mpt_issuance_id": mpt_issuance_id}]] |
| 134 | + |
| 135 | + pathset = PathSet.from_value(path) |
| 136 | + # "40" → type byte: MPT flag (0x40) |
| 137 | + # mpt_issuance_id → raw 24-byte MPTID |
| 138 | + # "00" → end of PathSet |
| 139 | + expected_hex = "40" + mpt_issuance_id + "00" |
| 140 | + self.assertEqual(str(pathset).upper(), expected_hex) |
| 141 | + |
| 142 | + # round-trip JSON equivalence |
| 143 | + self.assertEqual(pathset.to_json(), path) |
| 144 | + |
| 145 | + # deserialization via BinaryParser |
| 146 | + parser = BinaryParser(expected_hex) |
| 147 | + self.assertEqual(str(PathSet.from_parser(parser)), str(pathset)) |
| 148 | + |
| 149 | + def test_two_paths_with_mpt_hops(self): |
| 150 | + mpt_id_1 = "00000001B5F762798A53D543A014CAF8B297CFF8F2F937E8" |
| 151 | + mpt_id_2 = "000004C463C52827307480341125DA0577DEFC38405B0E3E" |
| 152 | + path = [ |
| 153 | + [{"mpt_issuance_id": mpt_id_1}], |
| 154 | + [{"mpt_issuance_id": mpt_id_2}], |
| 155 | + ] |
| 156 | + |
| 157 | + pathset = PathSet.from_value(path) |
| 158 | + # "40" + mpt_id_1 → first path: one MPT hop |
| 159 | + # "FF" → path boundary separating alternative paths |
| 160 | + # "40" + mpt_id_2 → second path: one MPT hop |
| 161 | + # "00" → end of PathSet |
| 162 | + expected_hex = "40" + mpt_id_1 + "FF" + "40" + mpt_id_2 + "00" |
| 163 | + self.assertEqual(str(pathset).upper(), expected_hex) |
| 164 | + |
| 165 | + self.assertEqual(pathset.to_json(), path) |
| 166 | + |
| 167 | + parser = BinaryParser(expected_hex) |
| 168 | + self.assertEqual(str(PathSet.from_parser(parser)), str(pathset)) |
| 169 | + |
| 170 | + def test_path_with_mpt_and_currency_path_elements(self): |
| 171 | + """One path with two distinct steps: an MPT hop followed by a Currency hop.""" |
| 172 | + mpt_issuance_id = "00000001B5F762798A53D543A014CAF8B297CFF8F2F937E8" |
| 173 | + currency_code = "ABC" |
| 174 | + path = [ |
| 175 | + [ |
| 176 | + {"mpt_issuance_id": mpt_issuance_id}, |
| 177 | + {"currency": currency_code}, |
| 178 | + ] |
| 179 | + ] |
| 180 | + |
| 181 | + pathset = PathSet.from_value(path) |
| 182 | + currency_hex = str(Currency.from_value(currency_code)).upper() |
| 183 | + # "40" + mpt_id → first step: MPT hop (0x40 type flag) |
| 184 | + # "10" + currency_hex → second step: Currency hop (0x10 type flag) |
| 185 | + # "00" → end of PathSet |
| 186 | + expected_hex = "40" + mpt_issuance_id + "10" + currency_hex + "00" |
| 187 | + self.assertEqual(str(pathset).upper(), expected_hex) |
| 188 | + |
| 189 | + self.assertEqual(pathset.to_json(), path) |
| 190 | + |
| 191 | + parser = BinaryParser(expected_hex) |
| 192 | + self.assertEqual(str(PathSet.from_parser(parser)), str(pathset)) |
| 193 | + |
| 194 | + def test_path_with_mpt_and_issuer_path_elements(self): |
| 195 | + """One path with two distinct steps: an MPT hop followed by an Issuer hop.""" |
| 196 | + mpt_issuance_id = "00000001B5F762798A53D543A014CAF8B297CFF8F2F937E8" |
| 197 | + issuer_account = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" |
| 198 | + path = [ |
| 199 | + [ |
| 200 | + {"mpt_issuance_id": mpt_issuance_id}, |
| 201 | + {"issuer": issuer_account}, |
| 202 | + ] |
| 203 | + ] |
| 204 | + |
| 205 | + pathset = PathSet.from_value(path) |
| 206 | + issuer_hex = str(AccountID.from_value(issuer_account)).upper() |
| 207 | + # "40" + mpt_id → first step: MPT hop (0x40 type flag) |
| 208 | + # "20" + issuer_hex → second step: Issuer hop (0x20 type flag) |
| 209 | + # "00" → end of PathSet |
| 210 | + expected_hex = "40" + mpt_issuance_id + "20" + issuer_hex + "00" |
| 211 | + self.assertEqual(str(pathset).upper(), expected_hex) |
| 212 | + |
| 213 | + self.assertEqual(pathset.to_json(), path) |
| 214 | + |
| 215 | + parser = BinaryParser(expected_hex) |
| 216 | + self.assertEqual(str(PathSet.from_parser(parser)), str(pathset)) |
| 217 | + |
| 218 | + def test_currency_and_mpt_mutually_exclusive_in_serialization(self): |
| 219 | + """Providing both currency and mpt_issuance_id in a single step must raise.""" |
| 220 | + path = [ |
| 221 | + [ |
| 222 | + { |
| 223 | + "currency": "ABC", |
| 224 | + "mpt_issuance_id": "00000001B5F762798A53" |
| 225 | + "D543A014CAF8B297CFF8F2F937E8", |
| 226 | + } |
| 227 | + ] |
| 228 | + ] |
| 229 | + self.assertRaises(XRPLBinaryCodecException, PathSet.from_value, path) |
| 230 | + |
| 231 | + def test_currency_and_mpt_mutually_exclusive_in_deserialization(self): |
| 232 | + """A type byte with both Currency (0x10) and MPT (0x40) flags must raise.""" |
| 233 | + # "50" = 0x10 | 0x40 — an invalid combination |
| 234 | + currency_hex = str(Currency.from_value("ABC")).upper() |
| 235 | + mpt_hex = "00000001B5F762798A53D543A014CAF8B297CFF8F2F937E8" |
| 236 | + invalid_hex = "50" + currency_hex + mpt_hex + "00" |
| 237 | + |
| 238 | + parser = BinaryParser(invalid_hex) |
| 239 | + self.assertRaises(XRPLBinaryCodecException, PathSet.from_parser, parser) |
0 commit comments