Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [[Unreleased]]

### Added

- Add `encode_mptoken_metadata` and `decode_mptoken_metadata` helper functions to encode and decode MPTokenMetadata as per XLS-89 standard.

### Fixed

- Removed snippets files from the xrpl-py code repository. Updated the README file to point to the correct location on XRPL.org.
- [Breaking change] Fix `MPTokenMetadata` type to adhere to the XLS-89 standard. Since XLS-89 is still in a forming state and undergoing changes, this breaking change is being released as a bug fix via patch version bump. If you are using `MPTokenMetadata` in your code, please verify that it adheres to the updated type definition.
- [Breaking change] Fix `validate_mptoken_metadata` to correctly validate MPTokenMetadata as per XLS-89 standard. Since XLS-89 is still in a forming state and undergoing changes, this breaking change is being released as a bug fix via patch version bump. If you are using `validateMPTokenMetadata` in your code, expect it to change as per the XLS-89 standard.

## [[4.3.0]] - 2025-07-29

Expand Down
41 changes: 38 additions & 3 deletions tests/integration/transactions/test_mptoken_issuance_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,38 @@
test_async_and_sync,
)
from tests.integration.reusable_values import WALLET
from xrpl.models.mptoken_metadata import MPTokenMetadata, MPTokenMetadataUri
from xrpl.models.requests.account_objects import AccountObjects, AccountObjectType
from xrpl.models.requests.tx import Tx
from xrpl.models.transactions import MPTokenIssuanceCreate
from xrpl.utils.mptoken_metadata import decode_mptoken_metadata, encode_mptoken_metadata


class TestMPTokenIssuanceCreate(IntegrationTestCase):
@test_async_and_sync(globals())
async def test_basic_functionality(self, client):
metadata = MPTokenMetadata(
ticker="TBILL",
name="T-Bill Yield Token",
icon="https://example.org/tbill-icon.png",
asset_class="rwa",
asset_subclass="treasury",
issuer_name="Example Yield Co.",
uris=[
MPTokenMetadataUri(
uri="https://exampleyield.co/tbill",
category="website",
title="Product Page",
),
],
)

encoded_metadata = encode_mptoken_metadata(metadata)
tx = MPTokenIssuanceCreate(
account=WALLET.classic_address,
maximum_amount="9223372036854775807", # "7fffffffffffffff"
asset_scale=2,
mptoken_metadata=encoded_metadata,
)

response = await sign_and_reliable_submission_async(
Expand All @@ -26,11 +47,25 @@ async def test_basic_functionality(self, client):
self.assertTrue(response.is_successful())
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

tx_hash = response.result["tx_json"]["hash"]
tx_res = await client.request(Tx(transaction=tx_hash))
mpt_issuance_id = tx_res.result["meta"]["mpt_issuance_id"]

# confirm MPTokenIssuance ledger object was created
account_objects_response = await client.request(
AccountObjects(account=WALLET.address, type=AccountObjectType.MPT_ISSUANCE)
)
mptoken_issuance_obj = next(
(
obj
for obj in account_objects_response.result["account_objects"]
if obj["mpt_issuance_id"] == mpt_issuance_id
),
None,
)

# subsequent integration tests (sync/async + json/websocket) add one
# MPTokenIssuance object to the account
self.assertTrue(len(account_objects_response.result["account_objects"]) > 0)
self.assertIsNotNone(mptoken_issuance_obj)
self.assertEqual(
decode_mptoken_metadata(mptoken_issuance_obj["MPTokenMetadata"]),
metadata,
)
Comment on lines +67 to +71
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 31, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add explicit assertion for MPTokenMetadata field before accessing it.

The test directly accesses mptoken_issuance_obj["MPTokenMetadata"] on line 69, which will raise a KeyError if the field is missing. While the field should be present since metadata was included in the transaction, an explicit assertion provides a clearer error message when debugging unexpected ledger behavior.

Apply this diff to add an explicit assertion:

 self.assertIsNotNone(mptoken_issuance_obj)
+self.assertIn(
+    "MPTokenMetadata",
+    mptoken_issuance_obj,
+    "MPTokenMetadata field missing from issuance object"
+)
 self.assertEqual(
     decode_mptoken_metadata(mptoken_issuance_obj["MPTokenMetadata"]),
     metadata,
 )

Alternatively, use safe dictionary access:

 self.assertIsNotNone(mptoken_issuance_obj)
+metadata_hex = mptoken_issuance_obj.get("MPTokenMetadata")
+self.assertIsNotNone(metadata_hex, "MPTokenMetadata field missing from issuance object")
 self.assertEqual(
-    decode_mptoken_metadata(mptoken_issuance_obj["MPTokenMetadata"]),
+    decode_mptoken_metadata(metadata_hex),
     metadata,
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.assertIsNotNone(mptoken_issuance_obj)
self.assertEqual(
decode_mptoken_metadata(mptoken_issuance_obj["MPTokenMetadata"]),
metadata,
)
self.assertIsNotNone(mptoken_issuance_obj)
self.assertIn(
"MPTokenMetadata",
mptoken_issuance_obj,
"MPTokenMetadata field missing from issuance object"
)
self.assertEqual(
decode_mptoken_metadata(mptoken_issuance_obj["MPTokenMetadata"]),
metadata,
)
🤖 Prompt for AI Agents
In tests/integration/transactions/test_mptoken_issuance_create.py around lines
67 to 71, the test accesses mptoken_issuance_obj["MPTokenMetadata"] directly
which will raise KeyError if the key is missing; add an explicit assertion that
the "MPTokenMetadata" key exists (e.g., assertIn("MPTokenMetadata",
mptoken_issuance_obj) or
assertIsNotNone(mptoken_issuance_obj.get("MPTokenMetadata"))) before decoding,
then use the existing decode_mptoken_metadata call so the failure message is
clear and not a raw KeyError.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fetched object using mpt_issuance_id from validated ledger. There is no way MPTokenMetadata won't exists.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Patel-Raj11, you're absolutely right. Since the object is retrieved using the mpt_issuance_id from a validated ledger transaction that successfully included metadata, the MPTokenMetadata field should indeed be present. If it's missing, that would indicate a fundamental issue with the ledger behavior itself rather than a test concern. Your current test flow is appropriate for this integration test scenario.


🧠 Learnings used
Learnt from: mvadari
Repo: XRPLF/xrpl-py PR: 759
File: tests/integration/transactions/test_credential.py:31-37
Timestamp: 2024-11-04T19:41:04.808Z
Learning: The `is_cred_object_present` function in `tests/integration/transactions/test_credential.py` assumes that the fields `'Issuer'`, `'Subject'`, and `'CredentialType'` are always present in the account object dictionaries within `account_objects`.

Learnt from: ckeshava
Repo: XRPLF/xrpl-py PR: 759
File: tests/unit/models/transactions/test_credential_create.py:107-114
Timestamp: 2024-10-30T20:26:32.925Z
Learning: In `tests/unit/models/transactions/test_credential_create.py`, when testing invalid inputs, avoid adding assertions on specific error messages if it increases the complexity and maintenance overhead of the tests.

Learnt from: khancode
Repo: XRPLF/xrpl-py PR: 732
File: xrpl/models/transactions/mptoken_issuance_create.py:70-76
Timestamp: 2024-12-10T20:35:09.762Z
Learning: In the `xrpl/models/transactions/mptoken_issuance_create.py` file, the `maximum_amount` field in the `MPTokenIssuanceCreate` class is a non-negative integer string and should be typed as `Optional[str]`.

Learnt from: ckeshava
Repo: XRPLF/xrpl-py PR: 814
File: xrpl/models/amounts/mpt_amount.py:54-64
Timestamp: 2025-06-04T22:17:47.164Z
Learning: For MPTIssue class in xrpl-py, the user ckeshava confirmed that a simple class implementation without BaseModel inheritance works correctly for their current requirements, even though other currency types in the same Union (IssuedCurrency, MPTCurrency, XRP) extend BaseModel.

298 changes: 298 additions & 0 deletions tests/unit/models/mptoken-metadata-encode-decode-fixtures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
[
{
"testName": "valid long MPTokenMetadata",
"mptMetadata": {
"ticker": "TBILL",
"name": "T-Bill Yield Token",
"desc": "A yield-bearing stablecoin backed by short-term U.S. Treasuries and money market instruments.",
"icon": "https://example.org/tbill-icon.png",
"asset_class": "rwa",
"asset_subclass": "treasury",
"issuer_name": "Example Yield Co.",
"uris": [
{
"uri": "https://exampleyield.co/tbill",
"category": "website",
"title": "Product Page"
},
{
"uri": "https://exampleyield.co/docs",
"category": "docs",
"title": "Yield Token Docs"
}
],
"additional_info": {
"interest_rate": "5.00%",
"interest_type": "variable",
"yield_source": "U.S. Treasury Bills",
"maturity_date": "2045-06-30",
"cusip": "912796RX0"
}
},
"expectedLongForm": {
"ticker": "TBILL",
"name": "T-Bill Yield Token",
"desc": "A yield-bearing stablecoin backed by short-term U.S. Treasuries and money market instruments.",
"icon": "https://example.org/tbill-icon.png",
"asset_class": "rwa",
"asset_subclass": "treasury",
"issuer_name": "Example Yield Co.",
"uris": [
{
"uri": "https://exampleyield.co/tbill",
"category": "website",
"title": "Product Page"
},
{
"uri": "https://exampleyield.co/docs",
"category": "docs",
"title": "Yield Token Docs"
}
],
"additional_info": {
"interest_rate": "5.00%",
"interest_type": "variable",
"yield_source": "U.S. Treasury Bills",
"maturity_date": "2045-06-30",
"cusip": "912796RX0"
}
},
"hex": "7B226163223A22727761222C226169223A7B226375736970223A22393132373936525830222C22696E7465726573745F72617465223A22352E303025222C22696E7465726573745F74797065223A227661726961626C65222C226D617475726974795F64617465223A22323034352D30362D3330222C227969656C645F736F75726365223A22552E532E2054726561737572792042696C6C73227D2C226173223A227472656173757279222C2264223A2241207969656C642D62656172696E6720737461626C65636F696E206261636B65642062792073686F72742D7465726D20552E532E205472656173757269657320616E64206D6F6E6579206D61726B657420696E737472756D656E74732E222C2269223A2268747470733A2F2F6578616D706C652E6F72672F7462696C6C2D69636F6E2E706E67222C22696E223A224578616D706C65205969656C6420436F2E222C226E223A22542D42696C6C205969656C6420546F6B656E222C2274223A225442494C4C222C227573223A5B7B2263223A2277656273697465222C2274223A2250726F647563742050616765222C2275223A2268747470733A2F2F6578616D706C657969656C642E636F2F7462696C6C227D2C7B2263223A22646F6373222C2274223A225969656C6420546F6B656E20446F6373222C2275223A2268747470733A2F2F6578616D706C657969656C642E636F2F646F6373227D5D7D"
},
{
"testName": "valid MPTokenMetadata with all short field names",
"mptMetadata": {
"t": "TBILL",
"n": "T-Bill Yield Token",
"d": "A yield-bearing stablecoin backed by short-term U.S. Treasuries and money market instruments.",
"i": "https://example.org/tbill-icon.png",
"ac": "rwa",
"as": "treasury",
"in": "Example Yield Co.",
"us": [
{
"u": "https://exampleyield.co/tbill",
"c": "website",
"t": "Product Page"
},
{
"u": "https://exampleyield.co/docs",
"c": "docs",
"t": "Yield Token Docs"
}
],
"ai": {
"interest_rate": "5.00%",
"interest_type": "variable",
"yield_source": "U.S. Treasury Bills",
"maturity_date": "2045-06-30",
"cusip": "912796RX0"
}
},
"expectedLongForm": {
"ticker": "TBILL",
"name": "T-Bill Yield Token",
"desc": "A yield-bearing stablecoin backed by short-term U.S. Treasuries and money market instruments.",
"icon": "https://example.org/tbill-icon.png",
"asset_class": "rwa",
"asset_subclass": "treasury",
"issuer_name": "Example Yield Co.",
"uris": [
{
"uri": "https://exampleyield.co/tbill",
"category": "website",
"title": "Product Page"
},
{
"uri": "https://exampleyield.co/docs",
"category": "docs",
"title": "Yield Token Docs"
}
],
"additional_info": {
"interest_rate": "5.00%",
"interest_type": "variable",
"yield_source": "U.S. Treasury Bills",
"maturity_date": "2045-06-30",
"cusip": "912796RX0"
}
},
"hex": "7B226163223A22727761222C226169223A7B226375736970223A22393132373936525830222C22696E7465726573745F72617465223A22352E303025222C22696E7465726573745F74797065223A227661726961626C65222C226D617475726974795F64617465223A22323034352D30362D3330222C227969656C645F736F75726365223A22552E532E2054726561737572792042696C6C73227D2C226173223A227472656173757279222C2264223A2241207969656C642D62656172696E6720737461626C65636F696E206261636B65642062792073686F72742D7465726D20552E532E205472656173757269657320616E64206D6F6E6579206D61726B657420696E737472756D656E74732E222C2269223A2268747470733A2F2F6578616D706C652E6F72672F7462696C6C2D69636F6E2E706E67222C22696E223A224578616D706C65205969656C6420436F2E222C226E223A22542D42696C6C205969656C6420546F6B656E222C2274223A225442494C4C222C227573223A5B7B2263223A2277656273697465222C2274223A2250726F647563742050616765222C2275223A2268747470733A2F2F6578616D706C657969656C642E636F2F7462696C6C227D2C7B2263223A22646F6373222C2274223A225969656C6420546F6B656E20446F6373222C2275223A2268747470733A2F2F6578616D706C657969656C642E636F2F646F6373227D5D7D"
},
{
"testName": "valid MPTokenMetadata with mixed short and long field names",
"mptMetadata": {
"ticker": "CRYPTO",
"n": "Crypto Token",
"icon": "https://example.org/crypto-icon.png",
"asset_class": "gaming",
"d": "A gaming token for virtual worlds.",
"issuer_name": "Gaming Studios Inc.",
"as": "equity",
"uris": [
{
"uri": "https://gamingstudios.com",
"c": "website",
"title": "Main Website"
},
{
"uri": "https://gamingstudios.com",
"category": "website",
"t": "Main Website"
}
],
"ai": "Gaming ecosystem token"
},
"expectedLongForm": {
"ticker": "CRYPTO",
"name": "Crypto Token",
"icon": "https://example.org/crypto-icon.png",
"asset_class": "gaming",
"desc": "A gaming token for virtual worlds.",
"issuer_name": "Gaming Studios Inc.",
"asset_subclass": "equity",
"uris": [
{
"uri": "https://gamingstudios.com",
"category": "website",
"title": "Main Website"
},
{
"uri": "https://gamingstudios.com",
"category": "website",
"title": "Main Website"
}
],
"additional_info": "Gaming ecosystem token"
},
"hex": "7B226163223A2267616D696E67222C226169223A2247616D696E672065636F73797374656D20746F6B656E222C226173223A22657175697479222C2264223A22412067616D696E6720746F6B656E20666F72207669727475616C20776F726C64732E222C2269223A2268747470733A2F2F6578616D706C652E6F72672F63727970746F2D69636F6E2E706E67222C22696E223A2247616D696E672053747564696F7320496E632E222C226E223A2243727970746F20546F6B656E222C2274223A2243525950544F222C227573223A5B7B2263223A2277656273697465222C2274223A224D61696E2057656273697465222C2275223A2268747470733A2F2F67616D696E6773747564696F732E636F6D227D2C7B2263223A2277656273697465222C2274223A224D61696E2057656273697465222C2275223A2268747470733A2F2F67616D696E6773747564696F732E636F6D227D5D7D"
},
{
"testName": "with extra fields",
"mptMetadata": {
"ticker": "CRYPTO",
"n": "Crypto Token",
"icon": "https://example.org/crypto-icon.png",
"asset_class": "gaming",
"d": "A gaming token for virtual worlds.",
"issuer_name": "Gaming Studios Inc.",
"as": "equity",
"uris": [
{
"uri": "https://gamingstudios.com",
"c": "website",
"title": "Main Website"
},
{
"uri": "https://gamingstudios.com",
"category": "website",
"t": "Main Website"
}
],
"ai": "Gaming ecosystem token",
"extra": {
"extra": "extra"
}
},
"expectedLongForm": {
"ticker": "CRYPTO",
"name": "Crypto Token",
"icon": "https://example.org/crypto-icon.png",
"asset_class": "gaming",
"desc": "A gaming token for virtual worlds.",
"issuer_name": "Gaming Studios Inc.",
"asset_subclass": "equity",
"uris": [
{
"uri": "https://gamingstudios.com",
"category": "website",
"title": "Main Website"
},
{
"uri": "https://gamingstudios.com",
"category": "website",
"title": "Main Website"
}
],
"additional_info": "Gaming ecosystem token",
"extra": {
"extra": "extra"
}
},
"hex": "7B226163223A2267616D696E67222C226169223A2247616D696E672065636F73797374656D20746F6B656E222C226173223A22657175697479222C2264223A22412067616D696E6720746F6B656E20666F72207669727475616C20776F726C64732E222C226578747261223A7B226578747261223A226578747261227D2C2269223A2268747470733A2F2F6578616D706C652E6F72672F63727970746F2D69636F6E2E706E67222C22696E223A2247616D696E672053747564696F7320496E632E222C226E223A2243727970746F20546F6B656E222C2274223A2243525950544F222C227573223A5B7B2263223A2277656273697465222C2274223A224D61696E2057656273697465222C2275223A2268747470733A2F2F67616D696E6773747564696F732E636F6D227D2C7B2263223A2277656273697465222C2274223A224D61696E2057656273697465222C2275223A2268747470733A2F2F67616D696E6773747564696F732E636F6D227D5D7D"
},
{
"testName": "with unkown null fields",
"mptMetadata": {
"t": "CRYPTO",
"extra": null
},
"expectedLongForm": {
"ticker": "CRYPTO",
"extra": null
},
"hex": "7B226578747261223A6E756C6C2C2274223A2243525950544F227D"
},
{
"testName": "multiple uris and us",
"mptMetadata": {
"t": "CRYPTO",
"uris": [
{
"u": "https://gamingstudios.com",
"c": "website",
"t": "Main Website"
}
],
"us": [
{
"uri": "https://gamingstudios.com",
"category": "website",
"title": "Main Website"
}
]
},
"expectedLongForm": {
"ticker": "CRYPTO",
"uris": [
{
"uri": "https://gamingstudios.com",
"category": "website",
"title": "Main Website"
}
],
"us": [
{
"uri": "https://gamingstudios.com",
"category": "website",
"title": "Main Website"
}
]
},
"hex": "7B2274223A2243525950544F222C2275726973223A5B7B2263223A2277656273697465222C2274223A224D61696E2057656273697465222C2275223A2268747470733A2F2F67616D696E6773747564696F732E636F6D227D5D2C227573223A5B7B2263223A2277656273697465222C2274223A224D61696E2057656273697465222C2275223A2268747470733A2F2F67616D696E6773747564696F732E636F6D227D5D7D"
},
{
"testName": "multiple keys in uri",
"mptMetadata": {
"us": [
{
"uri": "https://gamingstudios.com",
"u": "website",
"category": "Main Website",
"c": "Main Website"
}
]
},
"expectedLongForm": {
"uris": [
{
"uri": "https://gamingstudios.com",
"u": "website",
"category": "Main Website",
"c": "Main Website"
}
]
},
"hex": "7B227573223A5B7B2263223A224D61696E2057656273697465222C2263617465676F7279223A224D61696E2057656273697465222C2275223A2277656273697465222C22757269223A2268747470733A2F2F67616D696E6773747564696F732E636F6D227D5D7D"
}
]
Loading