feat: add SingleAssetVault support (XLS-65)#158
Conversation
Add six new transaction types for the XLS-65 single-asset vault amendment: VaultCreate, VaultDelete, VaultDeposit, VaultWithdraw, VaultClawback, and VaultSet. Each follows existing transaction patterns with serde support, builder methods, and unit tests.
Add the Vault ledger object with LedgerEntryType::Vault (0x0084) and the corresponding LedgerEntry::Vault variant. The Vault struct models the on-ledger state including asset, share tokens, totals, and metadata fields per the XLS-65 specification.
Add integration test files for all six XLS-65 vault transaction types covering serde roundtrip, builder pattern, and validation. These will be extended with live submission tests once XLS-65 is available on devnet.
- vault_create: add missing Scale (UInt8) field - vault_withdraw: add missing Destination and DestinationTag fields - vault_clawback: change Amount from required Amount<'a> to Option<Cow<str>> (NUMBER type per spec) - vault ledger: add missing Owner, LossUnrealized, ShareMPTID, WithdrawalPolicy, Scale, Sequence fields
e-desouza
left a comment
There was a problem hiding this comment.
Went through the changes
Type mismatch on amount field will cause a compile error — see inline.
Review by ReviewBot 🤖
Review by Claude Opus 4.6 · Prompt: V12
VaultClawback.amount is Option<Cow<str>>, not Amount — fix test to use string values. Add missing `scale` field to VaultCreate tests and missing `destination`/`destination_tag` fields to VaultWithdraw tests.
Introduces a typed Flags enum for VaultCreate plus hex-blob and VaultID validation across every vault transaction, closing the CRITICAL gaps flagged during review. - Add VaultCreateFlag with TfVaultPrivate (0x00010000) and TfVaultShareNonTransferable (0x00020000) per XLS-65. VaultCreate now uses CommonFields<'_, VaultCreateFlag> so flag bits serialize correctly instead of always emitting 0. - Add vault_common module with validate_vault_id and validate_hex_blob helpers. Reused by VaultSet, VaultDelete, VaultDeposit, VaultWithdraw, and VaultClawback so VaultID (64 hex chars, ASCII hexdigits) is rejected at the client boundary instead of by rippled. - VaultCreate.get_errors enforces XLS-65 blob caps: Data <= 256 bytes (512 hex chars), MPTokenMetadata <= 1024 bytes (2048 hex chars), and rejects non-hex characters. - Vault ledger entry tests now assert raw PascalCase keys (Account, Owner, Asset, AssetsTotal, AssetsAvailable, ShareMPTID, WithdrawalPolicy, PreviousTxnID, PreviousTxnLgrSeq, ...) so silent renames that would break wire compatibility with rippled are caught. - New unit tests: combined-flag bit serialization, oversized/non-hex Data and MPTokenMetadata rejection, invalid VaultID rejection (length and character class), and positive/negative paths for the shared helpers. Gates (taskset -c 1-25 -j 25): cargo fmt check, cargo clippy --all-features -D warnings, cargo test --release --lib all pass; 671 library tests green.
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.
… #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).
| /// The account address of the Vault Owner. | ||
| pub owner: Cow<'a, str>, | ||
| /// The address of the Vault's pseudo-account. | ||
| pub account: Cow<'a, str>, |
There was a problem hiding this comment.
Do we really need Cow type for these fields? They will be returned from the rippled server, hence I do not see why a user will need to manually update these values.
From my perspective, a read-only string-slice will sufficiently solve these requirements.
| pub previous_txn_lgr_seq: u32, | ||
| } | ||
|
|
||
| impl<'a> Model for Vault<'a> {} |
There was a problem hiding this comment.
The validation offered by Model are not used for any Ledger Object. These LedgerObjects are constructed by the respective rippled servers. We can remove the implementation of the Model trait
| #[test] | ||
| fn test_xrp_vault() { | ||
| let vault = Vault::new( | ||
| Some(Cow::from("XRPVaultTest")), | ||
| None, | ||
| Cow::from("rwhaYGnJMexktjhxAKzRwoCcQ2g6hvBDWu"), | ||
| Cow::from("rBVxExjRR6oDMWCeQYgJP7q4JBLGeLBPyv"), | ||
| Currency::XRP(XRP::new()), | ||
| Some("0".into()), | ||
| Some("0".into()), | ||
| None, | ||
| Some("0".into()), | ||
| Some("00000001732B0822A31109C996BCDD7E64E05D446E7998EE".into()), | ||
| Some(1), | ||
| Some(0), | ||
| Some(4), | ||
| None, | ||
| Some("0".into()), | ||
| Cow::from("25C3C8BF2C9EE60DFCDA02F3919D0C4D6BF2D0A4AC9354EFDA438F2ECDDA65E4"), | ||
| 5, | ||
| ); | ||
|
|
||
| let serialized = serde_json::to_string(&vault).unwrap(); | ||
| let deserialized: Vault = serde_json::from_str(&serialized).unwrap(); | ||
| assert_eq!(vault, deserialized); | ||
| } | ||
| } |
There was a problem hiding this comment.
there is nothing special about a vault holding XRP currency, especially with respect to serde. This test can be removed.
| #[test] | ||
| fn test_minimal_vault() { | ||
| let vault = Vault::new( | ||
| Some(Cow::from("MinimalTest")), | ||
| None, | ||
| Cow::from("rMinimalOwner789"), | ||
| Cow::from("rMinimalPseudo789"), | ||
| Currency::IssuedCurrency(IssuedCurrency::new("EUR".into(), "rEURIssuer012".into())), | ||
| None, | ||
| None, | ||
| None, | ||
| None, | ||
| None, | ||
| None, | ||
| None, | ||
| None, | ||
| None, | ||
| None, | ||
| Cow::from("1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF"), |
There was a problem hiding this comment.
I can't think of any case where this Vault::new constructor will be used. This test is not useful because all the Vault objects are constructed by the rippled server with the appropriate fields.
| #[test] | ||
| fn test_multiple_memos() { | ||
| let multi_memo_clawback = VaultClawback { | ||
| common_fields: CommonFields { |
There was a problem hiding this comment.
this test already exists in src/models/transactions/mod.rs
|
|
||
| #[test] | ||
| fn test_vault_clawback_serde_roundtrip() { | ||
| let vault_clawback = VaultClawback { |
There was a problem hiding this comment.
The tests in this file do not interact with a rippled server. They are purely unit tests, so we do not know if the binary-codec and the integration with rippled works correctly.
| use xrpl::models::{Currency, IssuedCurrency, Model, XRP}; | ||
|
|
||
| #[test] | ||
| fn test_vault_create_serde_roundtrip() { |
There was a problem hiding this comment.
these are all unit tests, they exist in the transaction model file. please add integration tests here
| pub mod signer_list_set; | ||
| pub mod ticket_create; | ||
| pub mod trust_set; | ||
| pub mod vault_clawback; |
There was a problem hiding this comment.
Why is the visibility set to pub here? Can we declare these as private modules without the pub specifier?
ckeshava
left a comment
There was a problem hiding this comment.
Cross-checked against rippled develop (XLS-65 is merged: include/xrpl/protocol_autogen/transactions/Vault*.h, src/libxrpl/tx/transactors/vault/*.cpp, include/xrpl/protocol/Protocol.h).
Six net-new findings on top of the existing review:
- Type bug:
VaultClawback.amountisOption<Cow<'a, str>>but rippled types it asSF_AMOUNTwith MPT support — cannot represent IOU/MPT clawbacks (the only kinds rippled allows; XRP clawback is explicitly rejected). - Missing validation:
VaultCreateacceptswithdrawal_policyvalues other than1and unboundedscale(rippled rejects both withtemMALFORMED). - Missing validation:
VaultCreatedoes not enforcedomain_id != 0or thedomain_id⇒tfVaultPrivatecross-constraint. - Missing validation:
VaultSetaccepts a body with none of {Data,AssetsMaximum,DomainID} present (rippled rejects), and never validatesDatalength/hex. - Missing validation:
VaultWithdrawdoes not rejectAmount <= 0or all-zeroDestination. - Required-as-Option: Vault ledger entry has
share_mpt_id,withdrawal_policy,sequence,previous_txn_idtyped asOption<…>, but rippled marks themSoeRequired(so a server-emitted vault always has them).
Non-blocking; flagging for the next push. Source for each finding linked inline.
| pub holder: Cow<'a, str>, | ||
| /// The asset amount to clawback as a string-encoded number. | ||
| /// When 0 or omitted, clawback all funds up to the total shares the Holder owns. | ||
| pub amount: Option<Cow<'a, str>>, |
There was a problem hiding this comment.
amount must be Option<Amount<'a>>, not Option<Cow<'a, str>> — rippled types sfAmount as SF_AMOUNT and explicitly notes MPT support for this field. Cow<str> can only encode an XRP drops string, but:
- rippled preflight rejects XRP clawback (
isXRP(amount->asset())→temMALFORMED), so the only thing this type can serialize is the only thing rippled rejects. - IOU clawback needs
{"currency":..., "issuer":..., "value":...}and MPT clawback needs{"mpt_issuance_id":..., "value":...}— neither is expressible as a flat string.
Every other SDK transaction with SF_AMOUNT uses Amount<'a> (Payment, AMMCreate, XChainClaim, VaultDeposit, VaultWithdraw, …) — Cow<str> here is an outlier.
Sources:
VaultClawback.h#L60-L72—SF_AMOUNT::type::value_typegetter with@note This field supports MPT (Multi-Purpose Token) amounts.VaultClawback.cpppreflight —if (isXRP(amount->asset())) … return temMALFORMED;
Fix: change the field type and update tests:
pub amount: Option<Amount<'a>>,After the fix this should compile (currently doesn't):
let _ = VaultClawback {
amount: Some(Amount::IssuedCurrencyAmount(IssuedCurrencyAmount::new(
"USD".into(), "rIssuer".into(), "100".into(),
))),
..Default::default()
};Downstream: with_amount helper, new() signature, and tests that use Some("500".into()) need to switch to Some(Amount::XRPAmount(…)) or Some(Amount::IssuedCurrencyAmount(…)).
| pub domain_id: Option<Cow<'a, str>>, | ||
| /// The withdrawal policy for the vault. | ||
| /// 1 = first-come-first-serve (0x0001). | ||
| pub withdrawal_policy: Option<u8>, |
There was a problem hiding this comment.
withdrawal_policy is not validated. rippled only accepts the value 1 (kVaultStrategyFirstComeFirstServe); anything else → temMALFORMED.
Source: VaultCreate.cpp preflight:
if (auto const withdrawalPolicy = ctx.tx[~sfWithdrawalPolicy]) {
if (*withdrawalPolicy != kVaultStrategyFirstComeFirstServe)
return temMALFORMED;
}Protocol.h#L241 — constexpr std::uint8_t kVaultStrategyFirstComeFirstServe = 1;
The existing test test_issued_currency_vault (line 393) calls .with_withdrawal_policy(0) and asserts validate().is_ok() — that transaction would be rejected on chain. After adding validation, change that test to 1 (or drop the policy).
Test that should fail on current code and pass after the fix:
#[test]
fn test_withdrawal_policy_only_value_one_accepted() {
let bad = VaultCreate { withdrawal_policy: Some(0), asset: Currency::XRP(XRP::new()),
common_fields: CommonFields { account: "rA".into(),
transaction_type: TransactionType::VaultCreate, ..Default::default() },
..Default::default() };
assert!(bad.validate().is_err());
let good = VaultCreate { withdrawal_policy: Some(1), ..bad.clone() };
assert!(good.validate().is_ok());
}| /// when converting it into an integer-based number of shares. | ||
| /// Fixed at 0 for XRP and MPT. Configurable 0-18 for IOU (default 6). | ||
| pub scale: Option<u8>, | ||
| } |
There was a problem hiding this comment.
scale is not validated. rippled enforces two rules in preflight:
scale > kVaultMaximumIouScale(18) →temMALFORMED.scaleset on an XRP or MPT asset →temMALFORMED(only IOU vaults may set scale).
The doc comment on this field even notes "Fixed at 0 for XRP and MPT. Configurable 0-18 for IOU" — but nothing enforces it.
Source: VaultCreate.cpp preflight:
if (auto const scale = ctx.tx[~sfScale]) {
auto const vaultAsset = ctx.tx[sfAsset];
if (vaultAsset.holds<MPTIssue>() || vaultAsset.native())
return temMALFORMED;
if (scale > kVaultMaximumIouScale)
return temMALFORMED;
}Protocol.h#L248 — constexpr std::uint8_t kVaultMaximumIouScale = 18;
Tests demonstrating the gap (boundary + asset-type check):
#[test]
fn test_scale_18_accepted_19_rejected() {
let mk = |s: u8| VaultCreate { scale: Some(s),
asset: Currency::IssuedCurrency(IssuedCurrency::new("USD".into(), "rI".into())),
common_fields: CommonFields { account: "rA".into(),
transaction_type: TransactionType::VaultCreate, ..Default::default() },
..Default::default() };
assert!(mk(18).validate().is_ok());
assert!(mk(19).validate().is_err());
}
#[test]
fn test_scale_on_xrp_rejected() {
let tx = VaultCreate { scale: Some(6), asset: Currency::XRP(XRP::new()),
common_fields: CommonFields { account: "rA".into(),
transaction_type: TransactionType::VaultCreate, ..Default::default() },
..Default::default() };
assert!(tx.validate().is_err());
}|
|
||
| impl Model for VaultSet<'_> { | ||
| fn get_errors(&self) -> XRPLModelResult<()> { | ||
| validate_vault_id(&self.vault_id) |
There was a problem hiding this comment.
VaultSet's local validation is materially weaker than rippled's preflight in two ways:
- Missing "at least one update" check — rippled rejects a VaultSet whose body has none of {
Data,AssetsMaximum,DomainID} present (temMALFORMED). The SDK currently accepts a VaultSet with onlyvault_id. Datais not validated at all — rippled rejects emptyDataandData> 256 bytes; the SDK never callsvalidate_hex_blobhere.
Source: VaultSet.cpp preflight:
if (auto const data = ctx.tx[~sfData]) {
if (data->empty() || data->length() > kMaxDataPayloadLength)
return temMALFORMED;
}
if (auto const assetMax = ctx.tx[~sfAssetsMaximum]) {
if (*assetMax < beast::kZero) return temMALFORMED;
}
if (!ctx.tx.isFieldPresent(sfDomainID) && !ctx.tx.isFieldPresent(sfAssetsMaximum) &&
!ctx.tx.isFieldPresent(sfData))
return temMALFORMED;Protocol.h#L238 — kMaxDataPayloadLength = 256.
Fix: in get_errors, after validate_vault_id, additionally:
- validate
datawith the same helper VaultCreate uses (also rejecting empty); - reject the all-optional-fields-absent case.
Tests:
#[test]
fn test_vault_set_empty_body_rejected() {
let tx = VaultSet { vault_id: VAULT_ID.into(),
common_fields: CommonFields { account: "rA".into(),
transaction_type: TransactionType::VaultSet, ..Default::default() },
..Default::default() };
assert!(tx.validate().is_err());
}
#[test]
fn test_vault_set_empty_data_rejected() {
let tx = VaultSet { vault_id: VAULT_ID.into(), data: Some("".into()),
common_fields: CommonFields { account: "rA".into(),
transaction_type: TransactionType::VaultSet, ..Default::default() },
..Default::default() };
assert!(tx.validate().is_err());
}The same empty-string concern applies to validate_hex_blob in vault_common.rs:37 — rippled also rejects empty sfMPTokenMetadata in VaultCreate, so the helper (or its callers) should treat empty as invalid.
| impl Model for VaultWithdraw<'_> { | ||
| fn get_errors(&self) -> XRPLModelResult<()> { | ||
| self.validate_currencies()?; | ||
| validate_vault_id(&self.vault_id) |
There was a problem hiding this comment.
VaultWithdraw's local validation does not match rippled's preflight, which catches two more cases:
Amount <= 0→temBAD_AMOUNT.Destinationfield equal to the all-zero AccountID →temMALFORMED.
Neither requires a round-trip — both are cheap local checks.
Source: VaultWithdraw.cpp preflight:
if (ctx.tx[sfAmount] <= beast::kZero)
return temBAD_AMOUNT;
if (auto const destination = ctx.tx[~sfDestination]) {
if (*destination == beast::kZero)
return temMALFORMED;
}VaultDeposit has the same amount <= 0 rule and is also missing it locally (src/models/transactions/vault_deposit.rs:46-49).
Test:
#[test]
fn test_vault_withdraw_zero_amount_rejected() {
let tx = VaultWithdraw { vault_id: VAULT_ID.into(),
amount: Amount::XRPAmount(XRPAmount::from("0")), destination: None, destination_tag: None,
common_fields: CommonFields { account: "rA".into(),
transaction_type: TransactionType::VaultWithdraw, ..Default::default() },
..Default::default() };
assert!(tx.validate().is_err());
}| pub loss_unrealized: Option<Cow<'a, str>>, | ||
| /// The identifier of the share MPTokenIssuance object. | ||
| #[serde(rename = "ShareMPTID")] | ||
| pub share_mpt_id: Option<Cow<'a, str>>, |
There was a problem hiding this comment.
share_mpt_id and withdrawal_policy (and below, sequence) are Option<…> here, but rippled marks them all SoeRequired — every server-emitted Vault has them. previous_txn_lgr_seq is correctly typed u32 (required), and the same treatment fits the others.
Leaving them Option<…> lets a deserializer accept a malformed Vault JSON (e.g. one missing ShareMPTID) instead of erroring at parse time, which defeats one of the main reasons to model ledger entries as typed structs.
Source: ledger_entries/Vault.h — the relevant fields:
sfPreviousTxnID // SoeRequired
sfPreviousTxnLgrSeq // SoeRequired
sfSequence // SoeRequired
sfOwnerNode // SoeRequired
sfOwner // SoeRequired
sfAccount // SoeRequired
sfAsset // SoeRequired
sfShareMPTID // SoeRequired ← currently Option in SDK
sfWithdrawalPolicy // SoeRequired ← currently Option in SDK
sfData // SoeOptional
sfAssetsTotal/Available/Maximum/LossUnrealized/Scale // SoeDefaultFix: drop Option on share_mpt_id, withdrawal_policy, sequence, and owner_node; keep Option only on the truly optional/default fields (assets_total, assets_available, assets_maximum, loss_unrealized, scale, data).
After the fix this should fail to compile:
let _ = Vault {
share_mpt_id: None, // required → must not compile
withdrawal_policy: None, // required → must not compile
sequence: None,
..
};|
@e-desouza Oops, I mistakenly rebased this PR because I thought this was my work. My bad. Feel free to revert this merge commit if you feel like it: 12799f9 |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #158 +/- ##
==========================================
+ Coverage 84.24% 85.35% +1.10%
==========================================
Files 200 208 +8
Lines 20754 22652 +1898
==========================================
+ Hits 17485 19335 +1850
- Misses 3269 3317 +48
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
… #3270) (XRPLF#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 XRPLF#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 (XRPLF#130, XRPLF#131, XRPLF#151, XRPLF#153, XRPLF#156, XRPLF#157, XRPLF#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 XRPLF#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).
|
@e-desouza Is this PR ready for my review? Should I take another pass at it? |
|
The trait import as suggested in https://github.com/XRPLF/xrpl-rust/actions/runs/26992360032/job/79654931127?pr=158#step:6:85 should help resolve the unit test error. I can review this PR once the CI tests pass. |
Summary
Implements XLS-65 (SingleAssetVault) support for xrpl-rust.
New transaction types (6)
VaultCreate— Create a new vault with anAsset, optionalAssetsMaximum,MPTokenMetadata,DomainID,WithdrawalPolicy, andDataVaultSet— Update vault parameters (Data,AssetsMaximum,DomainID)VaultDelete— Delete an empty vault byVaultIDVaultDeposit— Deposit assets into a vaultVaultWithdraw— Withdraw assets from a vault (with optionalDestinationandDestinationTag)VaultClawback— Clawback assets from a vault holderNew ledger entry type
Vault(LedgerEntryType = 0x0084) — On-ledger vault object withAccount,Asset,AssetsTotal,AssetsAvailable,AssetsMaximum,LPToken,Share,MPTokenIssuanceID,DomainID, and audit fieldsValidation
VaultCreatederivesValidateCurrenciesforAsset(Currency) field validationVaultDepositandVaultWithdrawderiveValidateCurrenciesforAmountfield validationRegistration
TransactionTypeenumLedgerEntryType::Vault(0x0084) added to enumTest plan
new()constructor testsget_transaction_type()variant teststests/transactions/cargo fmt,cargo clippy --all-features,cargo test --releaseall pass