Repository navigation
feat: Support LendingProtocolV1_1
#373
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -14,12 +14,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||||
|
|
||||||
| ### Added | ||||||
|
|
||||||
| - Support for the [`LendingProtocol` amendment (XLS-66)](https://github.com/XRPLF/XRPL-Standards/pull/192): the `LoanBrokerSet`, `LoanBrokerDelete`, `LoanBrokerCoverDeposit`, `LoanBrokerCoverWithdraw`, `LoanBrokerCoverClawback`, `LoanSet`, `LoanPay`, `LoanManage` and `LoanDelete` transactions, the `Loan` and `LoanBroker` ledger objects, and `signing::sign_loan_set_by_counterparty` ([#162](https://github.com/XRPLF/xrpl-rust/pull/162)). | ||||||
| - Support for the `LendingProtocolV1_1` amendment ([XLS-0058](https://github.com/XRPLF/XRPL-Standards/pull/582), [XLS-0066 V1_1](https://github.com/XRPLF/XRPL-Standards/pull/587)): close-ended vaults (`VaultKind`, `SubscriptionDate` and `RedemptionDate` on `VaultCreate` and on the `Vault` ledger object), `VaultDelete.MemoData`, and `CredentialIDs` on `VaultWithdraw` and `LoanBrokerCoverWithdraw`. | ||||||
| - Support for the `fixCleanup3_4_0` counterparty signer prefixes: `encode_for_signing_counterparty` (`"CPT\0"`) and `encode_for_multisigning_counterparty` (`"CPM\0"`), used by `signing::sign_loan_set_by_counterparty`. A counterparty signature produced by an earlier version does not validate on a `fixCleanup3_4_0` server. | ||||||
| - `signing::combine_loan_set_counterparty_signers`, which merges counterparty signatures produced independently — on separate machines, say — into a single `LoanSet`. | ||||||
| - `signing::CounterpartySigningMode`, the third argument to `sign_loan_set_by_counterparty`: `Single`, `Multisign`, or `MultisignAs(account)` for a wallet signing with that account's RegularKey (parity with xrpl.js's `opts.multisign?: boolean | string`). | ||||||
| - `autofill` now covers a `LoanSet`'s counterparty signatures in the `Fee`, reading the counterparty's signer list (and resolving the counterparty from `LoanBrokerID` when the broker initiates). `Fee` is a signing field and so cannot be raised once the first party has signed, which previously made this the caller's problem. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same mix-up as
Suggested change
|
||||||
| - `ledger_entry` selectors for `Loan` and `LoanBroker` (`LoanIdentifier` and `LoanBrokerIdentifier`). | ||||||
| - Support for [XLS-0094D DynamicMPT](https://github.com/XRPLF/XRPL-Standards/pull/583). | ||||||
| - **XLS-0096 Confidential MPT:** support for the [XLS-0096 ConfidentialTransfer amendment](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0096-confidential-mpt). Adds the vendored `mpt-crypto` native crypto library via the internal `mpt-crypto` (safe Rust wrappers) and `mpt-crypto-sys` (FFI bindings, statically linked) crates. | ||||||
| - **`GenericRequest`:** an untyped, catch-all request type for XRPL RPC commands that don't have a dedicated typed model yet (e.g. `ledger_accept` on standalone rippled, `server_state`). Accepts a `command` string plus a free-form `params: serde_json::Map<String, Value>` bag; `Serialize` flattens `params` alongside `command`/`id` and strips any collision with those reserved keys. Slots into `XRPLRequest::Generic` and the existing `Request` trait so it flows through `client.request(...)` unchanged. | ||||||
|
|
||||||
| ### Changed | ||||||
|
|
||||||
| - `definitions.json` regenerated from `server_definitions` on rippled 3.4.0-rc1: adds the `LendingProtocolV1_1` fields (`VaultKind`, `SubscriptionDate`, `RedemptionDate`, `LEVersion`), the smart-contract and confidential-MPT key-epoch fields, six new transaction results, and the `SponsorshipSet` / `EscrowCreate` format updates; drops the Hooks fields, which rippled no longer supports. No existing field's code, type or signing flag changed. | ||||||
| - **Breaking:** `XRPLSubmitAndWaitException::SubmissionFailed` changed from the tuple variant `SubmissionFailed(String)` to the struct variant `SubmissionFailed { result_code: String, message: Option<String> }`. Callers that pattern-matched on `SubmissionFailed(msg)` must now match `SubmissionFailed { result_code, message }` — the code (`temBAD_SIGNATURE`, `tecUNFUNDED_PAYMENT`, `txnNotFound`, ...) is available without substring-parsing the `Display` string. See [#371](https://github.com/XRPLF/xrpl-rust/issues/371) for the follow-up on typing the code itself. | ||||||
| - Both polling-timeout paths in `wait_for_final_transaction_result` now surface `XRPLSubmitAndWaitException::SubmissionTimeout` (retaining the ledger-sequence context). Previously the retry-cap branch (`c > 20`) returned `SubmissionTimeout` while the after-loop fall-through returned `SubmissionFailed { "submission_timeout" }`; `SubmissionFailed` is now reserved for definite rippled result codes. | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Seeds for failure cases proptest has generated in the past. It is | ||
| # automatically read and these particular cases re-run before any | ||
| # novel cases are generated. | ||
| # | ||
| # It is recommended to check this file in to source control so that | ||
| # everyone who runs the test benefits from these saved cases. | ||
| cc 98d6525f48ecdceefc3011f86b79524f2cff7fdacb0f866cde56124f1724e5c4 # shrinks to count = 1 |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -17,8 +17,14 @@ use crate::{ | |||||
| }, | ||||||
| core::binarycodec::encode, | ||||||
| models::{ | ||||||
| requests::{server_state::ServerState, submit::Submit}, | ||||||
| results::{server_state::ServerState as ServerStateResult, submit::Submit as SubmitResult}, | ||||||
| requests::{ | ||||||
| account_info::AccountInfo, ledger_entry::LedgerEntry, server_state::ServerState, | ||||||
| submit::Submit, | ||||||
| }, | ||||||
| results::{ | ||||||
| ledger_entry::LedgerEntry as LedgerEntryResult, | ||||||
| server_state::ServerState as ServerStateResult, submit::Submit as SubmitResult, | ||||||
| }, | ||||||
| transactions::{Transaction, TransactionType}, | ||||||
| Model, XRPAmount, XRPLModelException, | ||||||
| }, | ||||||
|
|
@@ -194,6 +200,18 @@ where | |||||
| }; | ||||||
| } | ||||||
| let mut base_fee_decimal: BigDecimal = base_fee.try_into()?; | ||||||
| // A `LoanSet` also pays one reference base fee per counterparty signer | ||||||
| // (rippled `LoanSet::calculateBaseFee`: `normalCost + signerCount * base`). | ||||||
| // `Fee` is a signing field, so it cannot be raised after the first party | ||||||
| // signs — it has to be right here, before the transaction is distributed | ||||||
| // for counterparty signing. | ||||||
| if transaction.get_transaction_type() == &TransactionType::LoanSet { | ||||||
| if let Some(client) = client { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With no client, nothing is added for the counterparty, so the public offline |
||||||
| let counterparty_signers = get_counterparty_signers_count(transaction, client).await?; | ||||||
| let net_fee_decimal: BigDecimal = net_fee.clone().try_into()?; | ||||||
| base_fee_decimal += &(net_fee_decimal * BigDecimal::from(counterparty_signers)); | ||||||
| } | ||||||
| } | ||||||
| if let Some(signers_count) = signers_count { | ||||||
| // rippled charges one extra reference base fee per signer on top of the | ||||||
| // transaction's own base cost (Transactor::calculateBaseFee: | ||||||
|
|
@@ -210,6 +228,109 @@ where | |||||
| .into()) | ||||||
| } | ||||||
|
|
||||||
| /// The number of counterparty signatures a `LoanSet` will carry, for fee | ||||||
| /// purposes. | ||||||
| /// | ||||||
| /// The counterparty is the `Counterparty` account when the transaction names | ||||||
| /// one; otherwise the loan broker initiated it and the counterparty is the | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The roles are reversed here. When
Suggested change
|
||||||
| /// broker's owner, which is read from the `LoanBroker` ledger entry. An account | ||||||
| /// with a signer list contributes one signature per signer entry; an account | ||||||
| /// without one signs once. | ||||||
| /// | ||||||
| /// This is an upper bound, not the exact count. rippled charges on the | ||||||
| /// signatures the transaction actually carries | ||||||
| /// (`max(1, |CounterpartySignature.Signers|)`), but those do not exist yet at | ||||||
| /// autofill time — the counterparty signs later, possibly on another machine — | ||||||
| /// so the signer list is the only thing available to size the fee from. A | ||||||
| /// counterparty that owns a signer list but single-signs therefore overpays by | ||||||
| /// one base fee per unused signer entry; it never underpays, which would cost a | ||||||
| /// `telINSUF_FEE_P` and a re-signing round trip, since `Fee` is a signing field. | ||||||
| /// Callers who know the exact count can set `Fee` themselves before calling | ||||||
| /// `autofill`, which skips this calculation entirely. | ||||||
| /// | ||||||
| /// Mirrors `fetchCounterPartySignersCount` in xrpl.js's `autofill`, which sizes | ||||||
| /// the fee the same way. | ||||||
| async fn get_counterparty_signers_count<'a, T, F, C>( | ||||||
| transaction: &T, | ||||||
| client: &C, | ||||||
| ) -> XRPLHelperResult<u32> | ||||||
| where | ||||||
| T: Transaction<'a, F>, | ||||||
| F: IntoEnumIterator + Serialize + Debug + PartialEq, | ||||||
| C: XRPLAsyncClient, | ||||||
| { | ||||||
| fn unquote(value: String) -> String { | ||||||
| value.trim_matches('"').to_string() | ||||||
| } | ||||||
|
|
||||||
| let counterparty = match transaction.get_field_value("Counterparty")? { | ||||||
| Some(counterparty) => unquote(counterparty), | ||||||
| None => { | ||||||
| let loan_broker_id = transaction | ||||||
| .get_field_value("LoanBrokerID")? | ||||||
| .map(unquote) | ||||||
| .ok_or_else(|| { | ||||||
| XRPLModelException::MissingField( | ||||||
| "LoanBrokerID (required to resolve the LoanSet counterparty)".to_string(), | ||||||
| ) | ||||||
| })?; | ||||||
| let response = client | ||||||
| .request( | ||||||
| LedgerEntry { | ||||||
| index: Some(loan_broker_id.into()), | ||||||
| ..Default::default() | ||||||
| } | ||||||
| .into(), | ||||||
| ) | ||||||
| .await?; | ||||||
| let result: LedgerEntryResult = response.try_into()?; | ||||||
| let node = result.node.ok_or_else(|| { | ||||||
| XRPLModelException::MissingField("LoanBroker ledger entry".to_string()) | ||||||
| })?; | ||||||
| node["Owner"] | ||||||
| .as_str() | ||||||
| .ok_or_else(|| XRPLModelException::MissingField("LoanBroker.Owner".to_string()))? | ||||||
| .to_string() | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| let response = client | ||||||
| .request( | ||||||
| AccountInfo::new( | ||||||
| None, | ||||||
| counterparty.into(), | ||||||
| None, | ||||||
| None, | ||||||
| None, | ||||||
| None, | ||||||
| Some(true), | ||||||
| ) | ||||||
| .into(), | ||||||
| ) | ||||||
| .await?; | ||||||
| let result = response | ||||||
| .raw_result | ||||||
| .ok_or_else(|| XRPLModelException::MissingField("account_info result".to_string()))?; | ||||||
|
|
||||||
| // `signer_lists` sits at the root of the response under api_version 2 but | ||||||
| // nested inside `account_data` under api_version 1, which is what rippled | ||||||
| // serves when the request does not ask for a version — so accept either. | ||||||
| let signer_lists = result.get("signer_lists").or_else(|| { | ||||||
| result | ||||||
| .get("account_data") | ||||||
| .and_then(|d| d.get("signer_lists")) | ||||||
| }); | ||||||
|
|
||||||
| // An account without a signer list signs once; otherwise it contributes one | ||||||
| // signature per signer entry. | ||||||
| Ok(signer_lists | ||||||
| .and_then(|lists| lists.get(0)) | ||||||
| .and_then(|list| list.get("SignerEntries")) | ||||||
| .and_then(|entries| entries.as_array()) | ||||||
| .map(|entries| entries.len() as u32) | ||||||
| .unwrap_or(1)) | ||||||
| } | ||||||
|
|
||||||
| async fn get_owner_reserve_from_response( | ||||||
| client: &impl XRPLAsyncClient, | ||||||
| ) -> XRPLHelperResult<XRPAmount<'_>> { | ||||||
|
|
||||||
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.
The
fixCleanup3_4_0limitation is stated backwards.sign_loan_set_by_counterpartyalways signs withCPT/CPM(src/signing/mod.rslines 413 and 433), but rippled keeps expectingSTX/SMTfor counterparty signatures untilfixCleanup3_4_0is enabled (signingPrefixinsrc/libxrpl/protocol/Sign.cpp). So on a network that enables LendingProtocol first, every LoanSet signed by this SDK is rejected. Today Mainnet and Testnet have neither amendment enabled. This matches xrpl.js and xrpl-py, so I'm not asking for a legacy-prefix option, but users need to be told.The current sentence only applies to builds of unreleased
main(Lending isn't in 1.3.0), and it leaves out the direction that matters: signatures from this version are only valid wherefixCleanup3_4_0is enabled. The same note belongs in thesign_loan_set_by_counterpartydocs (lines 145-159 ofsrc/signing/mod.rs).Line 18 has a related mix-up:
CredentialIDsonVaultWithdrawandLoanBrokerCoverWithdrawisn't gated onLendingProtocolV1_1; rippled requiresCredentialsplusfixCleanup3_4_0(VaultWithdraw::checkExtraFeatures,LoanBrokerCoverWithdraw::checkExtraFeatures). Suggested wording for both: