-
Notifications
You must be signed in to change notification settings - Fork 24
Add XLS-70 Credentials models and credential-based DepositPreauth support (with protocol-compatibility and no_std follow-up fixes) #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
16
commits into
main
Choose a base branch
from
copilot/xls-70-support-credentials
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
357f534
Initial plan
Copilot b0509ce
feat(models): add initial XLS-70 credential transaction and ledger su…
Copilot c059ca6
fix: address review feedback for deposit preauth credential support
Copilot 90fab3d
docs: clarify deposit preauth credential compatibility and changelog
Copilot 1c88010
fix: address credential review blockers and validation gaps
Copilot 13a5c48
style: make deposit preauth credentials length check idiomatic
Copilot dcaee6e
fix: resolve no_std import regressions and credential_delete test mis…
Copilot 044c7cb
fix: correct CredentialDelete authorization logic for implicit roles
e-desouza 28c46c5
fix: use Vec for credential_ids serde and add ledger credential bounds
e-desouza 60dfb49
fix: add credential_ids 1..=8 validation and document breaking changes
e-desouza f9ef06f
fix: close XLS-0070 spec compliance gaps and add comprehensive tests
e-desouza 435d252
test: add property-based tests for XLS-0070 credentials
e-desouza 3e2830b
fix: add missing alloc::vec::Vec import for no_std build
e-desouza af71a30
fix: address XLS-70 credential review feedback
e-desouza 0158394
fix: align XLS-70 credential validation with xrpl.js
e-desouza c4c298d
fix(credentials): support credential preauth lookup
e-desouza File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,3 +22,6 @@ | |
| **/.DS_Store | ||
|
|
||
| rustc-ice* | ||
|
|
||
| # Local working files | ||
| .local/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| use alloc::borrow::Cow; | ||
| use derive_new::new; | ||
| use serde::{Deserialize, Serialize}; | ||
| use serde_with::skip_serializing_none; | ||
|
|
||
| #[skip_serializing_none] | ||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, new)] | ||
| #[serde(rename_all = "PascalCase")] | ||
| pub struct CredentialAuthorizationFields<'a> { | ||
| pub issuer: Cow<'a, str>, | ||
| pub credential_type: Cow<'a, str>, | ||
| } | ||
|
|
||
| #[skip_serializing_none] | ||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, new)] | ||
| #[serde(rename_all = "PascalCase")] | ||
| pub struct CredentialAuthorization<'a> { | ||
| pub credential: CredentialAuthorizationFields<'a>, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,249 @@ | ||
| use crate::models::ledger::objects::LedgerEntryType; | ||
| use crate::models::FlagCollection; | ||
| use crate::models::Model; | ||
| use alloc::borrow::Cow; | ||
|
|
||
| use serde::{Deserialize, Serialize}; | ||
| use serde_repr::{Deserialize_repr, Serialize_repr}; | ||
|
|
||
| use serde_with::skip_serializing_none; | ||
| use strum_macros::{AsRefStr, Display, EnumIter}; | ||
|
|
||
| use super::{CommonFields, LedgerObject}; | ||
|
|
||
| #[derive( | ||
| Debug, Eq, PartialEq, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter, | ||
| )] | ||
| #[repr(u32)] | ||
| pub enum CredentialFlag { | ||
| /// Credential has been accepted by the subject. | ||
| LsfAccepted = 0x00010000, | ||
| } | ||
|
|
||
| /// A `Credential` object is an on-ledger representation of a credential. | ||
| /// | ||
| /// `<https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0070-credentials>` | ||
| #[skip_serializing_none] | ||
| #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] | ||
| #[serde(rename_all = "PascalCase")] | ||
| pub struct Credential<'a> { | ||
| /// The base fields for all ledger object models. | ||
| #[serde(flatten)] | ||
| pub common_fields: CommonFields<'a, CredentialFlag>, | ||
| /// The account the credential is for. | ||
| pub subject: Cow<'a, str>, | ||
| /// The account that issued the credential. | ||
| pub issuer: Cow<'a, str>, | ||
| /// A hex-encoded value identifying the credential type from this issuer. | ||
| pub credential_type: Cow<'a, str>, | ||
| /// Optional expiration for the credential. | ||
| pub expiration: Option<u32>, | ||
| /// Optional additional data, represented as a hex-encoded string. | ||
| #[serde(rename = "URI")] | ||
| pub uri: Option<Cow<'a, str>>, | ||
| /// A hint indicating which page of the subject's owner directory links to this object. | ||
| /// Omitted for self-issued credentials, which only appear in the issuer's owner directory. | ||
| pub subject_node: Option<Cow<'a, str>>, | ||
| /// A hint indicating which page of the issuer's owner directory links to this object. | ||
| pub issuer_node: Cow<'a, str>, | ||
| /// The identifying hash of the transaction that most recently modified this object. | ||
| #[serde(rename = "PreviousTxnID")] | ||
| pub previous_txn_id: Cow<'a, str>, | ||
| /// The index of the ledger containing the transaction that most recently modified this object. | ||
| pub previous_txn_lgr_seq: u32, | ||
| } | ||
|
|
||
| impl<'a> Model for Credential<'a> {} | ||
|
|
||
| impl<'a> LedgerObject<CredentialFlag> for Credential<'a> { | ||
| fn get_ledger_entry_type(&self) -> LedgerEntryType { | ||
| self.common_fields.get_ledger_entry_type() | ||
| } | ||
| } | ||
|
|
||
| impl<'a> Credential<'a> { | ||
| #[allow(clippy::too_many_arguments)] | ||
| pub fn new( | ||
| index: Option<Cow<'a, str>>, | ||
| ledger_index: Option<Cow<'a, str>>, | ||
| subject: Cow<'a, str>, | ||
| issuer: Cow<'a, str>, | ||
| credential_type: Cow<'a, str>, | ||
| expiration: Option<u32>, | ||
| uri: Option<Cow<'a, str>>, | ||
| subject_node: Option<Cow<'a, str>>, | ||
| issuer_node: Cow<'a, str>, | ||
| previous_txn_id: Cow<'a, str>, | ||
| previous_txn_lgr_seq: u32, | ||
| ) -> Self { | ||
| Self { | ||
| common_fields: CommonFields { | ||
| flags: FlagCollection::default(), | ||
| ledger_entry_type: LedgerEntryType::Credential, | ||
| index, | ||
| ledger_index, | ||
| }, | ||
| subject, | ||
| issuer, | ||
| credential_type, | ||
| expiration, | ||
| uri, | ||
| subject_node, | ||
| issuer_node, | ||
| previous_txn_id, | ||
| previous_txn_lgr_seq, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn test_serde() { | ||
| let credential = Credential::new( | ||
| Some(Cow::from( | ||
| "DD40031C6C21164E7673A47C35513D52A6B0F1349A873EE0D188D8994CD4D001", | ||
| )), | ||
| None, | ||
| Cow::from("rALICE1111111111111111111111111111"), | ||
| Cow::from("rISABEL111111111111111111111111111"), | ||
| Cow::from("4B5943"), | ||
| Some(789004799), | ||
| Some(Cow::from( | ||
| "69736162656C2E636F6D2F63726564656E7469616C732F6B79632F616C696365", | ||
| )), | ||
| Some(Cow::from("0000000000000000")), | ||
| Cow::from("0000000000000000"), | ||
| Cow::from("3E8964D5A86B3CD6B9ECB33310D4E073D64C865A5B866200AD2B7E29F8326702"), | ||
| 8, | ||
| ); | ||
| let serialized = serde_json::to_string(&credential).unwrap(); | ||
|
|
||
| let deserialized: Credential = serde_json::from_str(&serialized).unwrap(); | ||
|
|
||
| assert_eq!(credential, deserialized); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_serde_round_trip_all_fields() { | ||
| // Full credential with every field populated | ||
| let credential = Credential { | ||
| common_fields: CommonFields { | ||
| flags: FlagCollection::default(), | ||
| ledger_entry_type: LedgerEntryType::Credential, | ||
| index: Some(Cow::from( | ||
| "DD40031C6C21164E7673A47C35513D52A6B0F1349A873EE0D188D8994CD4D001", | ||
| )), | ||
| ledger_index: Some(Cow::from("42")), | ||
| }, | ||
| subject: Cow::from("rALICE1111111111111111111111111111"), | ||
| issuer: Cow::from("rISABEL111111111111111111111111111"), | ||
| credential_type: Cow::from("4B5943"), | ||
| expiration: Some(789004799), | ||
| uri: Some(Cow::from( | ||
| "69736162656C2E636F6D2F63726564656E7469616C732F6B79632F616C696365", | ||
| )), | ||
| subject_node: Some(Cow::from("0000000000000000")), | ||
| issuer_node: Cow::from("0000000000000001"), | ||
| previous_txn_id: Cow::from( | ||
| "3E8964D5A86B3CD6B9ECB33310D4E073D64C865A5B866200AD2B7E29F8326702", | ||
| ), | ||
| previous_txn_lgr_seq: 8, | ||
| }; | ||
| let json = serde_json::to_string(&credential).unwrap(); | ||
| let deserialized: Credential = serde_json::from_str(&json).unwrap(); | ||
| assert_eq!(credential, deserialized); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_serde_round_trip_optional_fields_omitted() { | ||
| // Credential without optional expiration and URI | ||
| let credential = Credential { | ||
| common_fields: CommonFields { | ||
| flags: FlagCollection::default(), | ||
| ledger_entry_type: LedgerEntryType::Credential, | ||
| index: Some(Cow::from( | ||
| "DD40031C6C21164E7673A47C35513D52A6B0F1349A873EE0D188D8994CD4D001", | ||
| )), | ||
| ledger_index: None, | ||
| }, | ||
| subject: Cow::from("rALICE1111111111111111111111111111"), | ||
| issuer: Cow::from("rISABEL111111111111111111111111111"), | ||
| credential_type: Cow::from("4B5943"), | ||
| expiration: None, | ||
| uri: None, | ||
| subject_node: Some(Cow::from("0000000000000000")), | ||
| issuer_node: Cow::from("0000000000000000"), | ||
| previous_txn_id: Cow::from( | ||
| "3E8964D5A86B3CD6B9ECB33310D4E073D64C865A5B866200AD2B7E29F8326702", | ||
| ), | ||
| previous_txn_lgr_seq: 5, | ||
| }; | ||
| let json = serde_json::to_string(&credential).unwrap(); | ||
| // Verify optional fields are absent from serialized JSON | ||
| assert!(!json.contains("Expiration")); | ||
| assert!(!json.contains("URI")); | ||
|
|
||
| let deserialized: Credential = serde_json::from_str(&json).unwrap(); | ||
| assert_eq!(credential, deserialized); | ||
| assert!(deserialized.expiration.is_none()); | ||
| assert!(deserialized.uri.is_none()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_self_issued_credential_allows_missing_subject_node() { | ||
| let json = r#"{ | ||
| "LedgerEntryType":"Credential", | ||
| "Flags":65536, | ||
| "Subject":"rSELF11111111111111111111111111111", | ||
| "Issuer":"rSELF11111111111111111111111111111", | ||
| "CredentialType":"4B5943", | ||
| "IssuerNode":"0000000000000000", | ||
| "PreviousTxnID":"3E8964D5A86B3CD6B9ECB33310D4E073D64C865A5B866200AD2B7E29F8326702", | ||
| "PreviousTxnLgrSeq":10 | ||
| }"#; | ||
|
|
||
| let credential: Credential = serde_json::from_str(json).unwrap(); | ||
| assert!(credential.subject_node.is_none()); | ||
| assert_eq!(credential.subject, credential.issuer); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_lsf_accepted_flag_value() { | ||
| // Verify the lsfAccepted flag has the correct value per the spec: 0x00010000 | ||
| assert_eq!(CredentialFlag::LsfAccepted as u32, 0x00010000); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_serde_with_accepted_flag() { | ||
| // Credential with the lsfAccepted flag set | ||
| let mut flags = FlagCollection::default(); | ||
| flags.0.push(CredentialFlag::LsfAccepted); | ||
| let credential = Credential { | ||
| common_fields: CommonFields { | ||
| flags, | ||
| ledger_entry_type: LedgerEntryType::Credential, | ||
| index: Some(Cow::from( | ||
| "DD40031C6C21164E7673A47C35513D52A6B0F1349A873EE0D188D8994CD4D001", | ||
| )), | ||
| ledger_index: None, | ||
| }, | ||
| subject: Cow::from("rALICE1111111111111111111111111111"), | ||
| issuer: Cow::from("rISABEL111111111111111111111111111"), | ||
| credential_type: Cow::from("4B5943"), | ||
| expiration: None, | ||
| uri: None, | ||
| subject_node: Some(Cow::from("0000000000000000")), | ||
| issuer_node: Cow::from("0000000000000000"), | ||
| previous_txn_id: Cow::from( | ||
| "3E8964D5A86B3CD6B9ECB33310D4E073D64C865A5B866200AD2B7E29F8326702", | ||
| ), | ||
| previous_txn_lgr_seq: 10, | ||
| }; | ||
| let json = serde_json::to_string(&credential).unwrap(); | ||
| let deserialized: Credential = serde_json::from_str(&json).unwrap(); | ||
| assert_eq!(credential, deserialized); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that the rust library is yet to be equipped with many ledger-objects in the future, this is a good time to refactor common fields such as these to prevent code duplication.
previous_txn_lgr_seqandprevious_txn_idis found in a huge majority of the ledger objects. These fields should be refactored.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left this unresolved because moving PreviousTxnID/PreviousTxnLgrSeq into ledger-object common fields would be a broader public-model refactor across many existing ledger objects, not just the XLS-70 Credential object. I addressed the concrete XLS-70 validation/test feedback in af71a30 and can handle this as a separate refactor if you want it in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tracked separately in #325.
This is broader than PR #154's XLS-70 credential changes because it involves cross-model public API/request-shape refactors:
PreviousTxnID/PreviousTxnLgrSeqLookupByLedgerRequestLeaving this PR focused on the XLS-70 compatibility fixes and handling the CommonFields refactor in the follow-up issue.