Add binary codec and transaction models for MPT#131
Conversation
baf2e82 to
841449f
Compare
841449f to
7925f94
Compare
7925f94 to
d136241
Compare
2792500 to
84f099a
Compare
|
/ai-review |
|
@e-desouza can you add integration tests for all transactions? |
|
Added in The authorize/set/destroy tests all need a real issuance to work against, so I added a |
Add a 192-bit (24-byte) hash type required by MPT fields such as MPTokenIssuanceID. Follows the same implementation pattern as Hash128/Hash160/Hash256 with full trait coverage (Hash, XRPLType, TryFromParser, TryFrom<&str>, Display, AsRef<[u8]>) and unit tests.
Add MPTokenAuthorize, MPTokenIssuanceCreate, MPTokenIssuanceDestroy, and MPTokenIssuanceSet to the TransactionType enum for use by MPT transaction models.
Add the MPTokenIssuanceCreate transaction with fields for asset_scale, maximum_amount, transfer_fee, and mptoken_metadata. Includes flags (TfMPTCanLock, TfMPTRequireAuth, TfMPTCanEscrow, TfMPTCanTrade, TfMPTCanTransfer, TfMPTCanClawback), transfer fee validation, builder methods, and serde roundtrip tests. Also adds InvalidFlagCombination variant to XRPLModelException for use by MPTokenIssuanceSet validation.
Add the MPTokenIssuanceDestroy transaction with mptoken_issuance_id field. Uses NoFlags since this transaction has no flag-specific behavior. Includes builder method, serde roundtrip, and validation tests.
Add the MPTokenIssuanceSet transaction with mptoken_issuance_id and optional holder fields. Includes TfMPTLock/TfMPTUnlock flags with validation that prevents setting both simultaneously. Builder methods, serde roundtrip, and conflict detection tests included.
Add the MPTokenAuthorize transaction with mptoken_issuance_id and optional holder fields. Includes TfMPTUnauthorize flag for opt-out flows. Builder methods, serde roundtrip, holder opt-in, and deauthorize flow tests included.
Add MPToken (account balance) and MPTokenIssuance (issuance definition) ledger entry types with full serde support, LedgerEntryType enum variants, and LedgerEntry enum variants. Includes serde roundtrip and ledger entry type tests for both objects.
Tests cover: - Field name encoding/decoding for all 7 new MPT fields (Hash192, AccountID, UInt8, UInt64, Blob types) - Transaction type code resolution (codes 54-57) - Ledger entry type code resolution (codes 126-127) - Field instance metadata validation - Full encode() roundtrip for all 4 MPT transaction types with hex pattern verification - encode_for_signing() with signing prefix - Flag serialization (combined TfMPTCanTransfer | TfMPTCanLock) - Deterministic encoding output
…ture reference
Post-rebase fixup on feat/mpt-binary-codec:
- Fix #[path = "..."] attributes for external test modules: inline mod test{}
resolves paths relative to the test/ subdirectory, not binarycodec/
- Remove test_encode_additional_fixtures which referenced
load_additional_tx_fixtures() that does not exist in test_cases.rs
…uilds - Change mod test gate from #[cfg(test)] to #[cfg(all(test, feature = "std"))] to match main's approach: the external test files and MPT encode tests all require serde_json/to_string which is unavailable in no_std builds - Tighten alloc::boxed::Box import in exceptions.rs to only compile when both no_std and websocket are active (Box only used by XRPLWebSocketError)
The asset_scale field is documented as accepting values 0-9, but rippled rejects values above 9. Without validation, values 10-255 would silently pass model validation and fail at submission time. Add a _get_asset_scale_error helper (mirroring _get_transfer_fee_error) and call it from get_errors(). Include tests for boundary values and the error message format.
…gration tests Update field-level doc comments on MPToken and MPTokenIssuance ledger objects to match the canonical descriptions from xrpl.org. Add integration tests for all four MPToken transaction types: - MPTokenIssuanceCreate (base + with_metadata) - MPTokenAuthorize (holder opt-in) - MPTokenIssuanceSet (lock issuance) - MPTokenIssuanceDestroy (destroy empty issuance) Add create_mptoken_issuance() helper in tests/common that creates an issuance and returns the derived MPTokenIssuanceID for dependent tests.
…e) visibility The FlagCollection tuple constructor is pub(crate), so integration tests (which live outside the crate) cannot use it directly. Use Vec::into() which invokes the public From<Vec<T>> impl instead.
380da30 to
44cc257
Compare
The rippleci/rippled:develop image updated after 2026-04-01 and broke integration tests across all PRs (container exits before becoming healthy, causing Connection refused on localhost:5005). Pin to the last known-good digest and replace the simple until loop with a bounded retry that checks container liveness, prints status per attempt, and dumps container logs on failure.
Removing hardcoded fee: Some("10") lets sign_and_submit autofill compute
the correct fee for the pinned rippled image (was causing telINSUF_FEE_P).
Add TfMPTCanTransfer to the metadata test so transfer_fee is accepted
(was temMALFORMED). Add TfMPTCanLock in the create_mptoken_issuance
helper so the lock test has permission (was tecNO_PERMISSION).
All 5 MPT integration tests now pass against rippled develop.
- Raise MAX_MPT_ASSET_SCALE from 9 to 19 to match rippled preflight (no hard cap; practical ceiling is bounded by maxMPTokenAmount near 2^63). - Replace NoFlags on the MPTokenIssuance and MPToken ledger objects with dedicated flag enums (MPTokenIssuanceFlag, MPTokenFlag) so the ledger flag bits returned by rippled are preserved instead of being dropped. - MPTokenIssuanceSet now requires exactly one of TfMPTLock or TfMPTUnlock (DomainID-only modifications are not yet modelled); the previous flipped test_default assertion is corrected and split into passing and failing cases. - Validate MPTokenIssuanceID across MPTokenIssuanceSet, MPTokenAuthorize, and MPTokenIssuanceDestroy as a 48-char ASCII hex string (24-byte Hash192 per XLS-33), and validate the optional holder field as a classic XRPL address. Unit-test fixtures updated to the spec-correct 48-char form; integration fixtures continue to build IDs dynamically.
… #3270) (#291) ## Summary The `rippled` binary was renamed to `xrpld` upstream, and the `rippleci/rippled` image stopped receiving updates. Our integration tests across every open PR started failing because the published `develop` image exited before becoming healthy (`Connection refused` on `localhost:5005`, **0 passed / 41 failed**). This PR mirrors the upstream fix in xrpl.js: [XRPLF/xrpl.js#3270](XRPLF/xrpl.js#3270). Switching to `rippleci/xrpld:develop` is the **actual root-cause fix** rather than pinning an old digest of the deprecated image. ## Changes `.github/workflows/integration_test.yml`: - `RIPPLED_DOCKER_IMAGE` -> `XRPLD_DOCKER_IMAGE: rippleci/xrpld:develop`. - `docker run` simplified to `${IMAGE} --standalone` (the `xrpld` image handles `mkdir` + launch internally; no more `bash -c "mkdir -p /var/lib/rippled/db/ && rippled -a"` wrapper). - Volume mount changed from `/etc/opt/ripple/` to `/etc/opt/xrpld/`. - Container name: `rippled-service` -> `xrpld-service`. - Removed the docker `--health-cmd` (which shelled out to the renamed `rippled` CLI and always failed) in favour of a direct JSON-RPC poll against `http://localhost:5005/`. - Always dump container logs on the stop step for post-mortem visibility. `.ci-config/rippled.cfg` -> `.ci-config/xrpld.cfg`: - `path=/var/lib/rippled/db/nudb` -> `path=/var/lib/xrpld/db/nudb`. - `[database_path] /var/lib/rippled/db` -> `/var/lib/xrpld/db`. - `[debug_logfile] /var/log/rippled/debug.log` -> `/var/log/xrpld/debug.log`. ## Verification Validated on throwaway PR #292 (now closed): **Integration Test green in 2m53s** on this exact workflow. Unit tests, Build & Lint, Quality Check also pass. ## Related follow-up The 7 in-flight PRs (#130, #131, #151, #153, #156, #157, #158) currently carry a stopgap commit pinning `rippleci/rippled:develop` to a specific digest. After this PR merges to `main`, those branches should: 1. Rebase on `main` to pick up the xrpld switch, or 2. Cherry-pick this commit and drop the stopgap digest pin. ## Test plan - [x] Validated end-to-end on PR #292 - [x] Build & Lint, Unit Test, Integration Test, Quality Check all pass - [ ] Merge and confirm subsequent PRs inherit the fix without manual cherry-pick ## Credit Approach lifted from @ckeshava's [xrpl.js#3270](XRPLF/xrpl.js#3270).
| self | ||
| } | ||
|
|
||
| fn _get_transfer_fee_error(&self) -> XRPLModelResult<()> { |
There was a problem hiding this comment.
We should add another validation for TransferFee: "The field must not be present if the tfMPTCanTransfer flag is not set."
| #[serde(flatten)] | ||
| pub common_fields: CommonFields<'a, MPTokenIssuanceCreateFlag>, | ||
| /// The number of decimal places for the MPT value. Must be in the | ||
| /// range 0-9. Defaults to 0 if not provided. |
There was a problem hiding this comment.
nit: the range in the comment should be 0-19
| pub transfer_fee: Option<u16>, | ||
| /// Arbitrary hex-encoded metadata for the issuance. | ||
| #[serde(rename = "MPTokenMetadata")] | ||
| pub mptoken_metadata: Option<Cow<'a, str>>, |
There was a problem hiding this comment.
Can we implement the same implementation for metadata in light of XLS-89 as this PR:
XRPLF/xrpl.js#3117
| index: Some(Cow::from( | ||
| "BFA9BE27383FA315651E26FDE1FA30815C5A5D0544EE10EC33D3E92532993769", | ||
| )), | ||
| ledger_index: None, |
There was a problem hiding this comment.
Let's update the test to use realistic values of ledger_index. This MPToken object should not be seen on a real-world XRPL network because ledger_index is an essential field.
| LsfMPTCanTransfer = 0x0020, | ||
| /// The issuer can clawback MPTs from holders. | ||
| LsfMPTCanClawback = 0x0040, | ||
| } |
| /// between a standard unit and a corresponding fractional unit. The | ||
| /// asset scale is a non-negative integer (0, 1, 2, ...) and defaults | ||
| /// to 0. | ||
| pub asset_scale: Option<u8>, |
There was a problem hiding this comment.
Fields like asset_scale and transfer_fee are declared as "soeDEFAULT". My understanding is that these fields will always have a certain value, even if they are not explicitly specified by the user. Option is not the best way to represent those values, because they will never be None.
| fn test_minimal_issuance() { | ||
| let issuance = MPTokenIssuance { | ||
| common_fields: CommonFields { | ||
| flags: FlagCollection(vec![]), | ||
| ledger_entry_type: LedgerEntryType::MPTokenIssuance, | ||
| index: None, | ||
| ledger_index: None, | ||
| }, | ||
| issuer: "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B".into(), | ||
| asset_scale: None, | ||
| maximum_amount: None, | ||
| outstanding_amount: "0".into(), | ||
| transfer_fee: None, | ||
| mptoken_metadata: None, | ||
| sequence: 1, | ||
| owner_node: None, | ||
| previous_txn_id: "0000000000000000000000000000000000000000000000000000000000000000" | ||
| .into(), | ||
| previous_txn_lgr_seq: 0, | ||
| }; | ||
|
|
||
| assert!(issuance.validate().is_ok()); | ||
| } |
There was a problem hiding this comment.
Is this test useful? MPTokenIssuance model does not have any validation-methods. In this implementation, we do not throw any error under any circumstance, why do we have error validation in the unit tests?
However, looking at this test case -- we should validate the index and the ledger_index fields with a non-null sanity check.
Furthermore, we should be grouping the values like index, ledger_index, previous_txn_[seq|lgr_index] into the CommonFields type because these are found on all the modern ledger objects.
| if let Some(holder) = self.holder.as_deref() { | ||
| validate_holder_address(holder)?; | ||
| } | ||
| self.validate_currencies() |
There was a problem hiding this comment.
| self.validate_currencies() |
I do not see any amount-related fields in this transaction. We can remove this validation.
|
|
||
| /// Validates that a `holder` string decodes as a classic XRPL address. | ||
| pub(crate) fn validate_holder_address(holder: &str) -> XRPLModelResult<()> { | ||
| if decode_classic_address(holder).is_err() { |
There was a problem hiding this comment.
If users want to use x-addresses instead, this creates a point of friction. Is there a specific reason for only allowing classic addresses?
|
|
||
| // AssetScale = 2: field ID 0x0510 + 0x02 | ||
| assert!( | ||
| hex.contains("051002"), |
There was a problem hiding this comment.
instead of doing sub-string match, it is more rigorous to test for the exact match of the serialized transaction. Please use the test examples from rippled, xrpl.js and other libraries to test the binary-codec of xrpl-rust.
| } | ||
|
|
||
| #[test] | ||
| fn test_encode_mptoken_issuance_create_with_flags() { |
There was a problem hiding this comment.
This file has tests covering serialization of transactions in an end-to-end fashion. However, the round-trip serialization <-> de-serialization of Hash192 values is missing in this file. Adding that will significantly increase the robustness of the Hash192 binary-codec. This does not have to be for every transaction-field, instead it can be tested for an arbitrary Hash192 type.

High Level Overview of Change
Full MPT (Multi-Purpose Token) support for the binary codec and transaction/ledger models:
Hash192type in the binary codec (24-byte fixed-length hash forMPTokenIssuanceID)definitions.jsonwith MPT transaction types, ledger entry types, and field definitionsMPTokenIssuanceCreate,MPTokenIssuanceDestroy,MPTokenIssuanceSet,MPTokenAuthorizeMPToken,MPTokenIssuanceCloses #119
Context of Change
The XRPL MPT amendment introduces four new transaction types (codes 54-57) and two new ledger entry types (codes 126-127). This required:
A new
Hash192type -- MPTokenIssuanceID is a 24-byte (192-bit) hash, which didn't exist in the codec. Followed the same pattern asHash128/Hash160/Hash256.Seven new field definitions in
definitions.json:MPTokenIssuanceID(Hash192),ShareMPTID(Hash192),Holder(AccountID),AssetScale(UInt8),MaximumAmount(UInt64),MPTAmount(UInt64),MPTokenMetadata(Blob).Transaction models following existing patterns in the codebase (
CommonFieldswith#[serde(flatten)],ValidateCurrenciesderive macro, builder methods, flag enums withserde_repr).Validation logic where appropriate:
MPTokenIssuanceCreatevalidatestransfer_fee <= 50000,MPTokenIssuanceSetrejects mutually exclusiveTfMPTLock+TfMPTUnlockflags.The
UInt192type name indefinitions.jsonwas renamed toHash192to match the rippled naming convention (fixed-length hashes use theHashprefix, notUInt).Type of Change
Before / After
Before:
TransactionTypeenumAfter:
Hash192type handles 24-byte hashes correctly (fixed-length, no VL prefix)encode/encode_for_signing/encode_for_multisigning)MPTokenandMPTokenIssuanceledger objects deserialize from JSONTest Plan
All pass with zero warnings and zero failures. The binary codec tests verify:
encode()for all 4 transaction types with assertion on specific hex patternsencode_for_signing()prefix verificationencode_for_multisigning()prefix and suffix verification