diff --git a/packages/rs-dapi-client/src/transport/grpc.rs b/packages/rs-dapi-client/src/transport/grpc.rs index 4253ea3d95a..fe62ff0ecb3 100644 --- a/packages/rs-dapi-client/src/transport/grpc.rs +++ b/packages/rs-dapi-client/src/transport/grpc.rs @@ -830,3 +830,12 @@ impl_transport_request_grpc!( }, get_recent_compacted_address_balance_changes ); + +// rpc getAddressFundingFeeQuote(GetAddressFundingFeeQuoteRequest) returns (GetAddressFundingFeeQuoteResponse); +impl_transport_request_grpc!( + platform_proto::GetAddressFundingFeeQuoteRequest, + platform_proto::GetAddressFundingFeeQuoteResponse, + PlatformGrpcClient, + RequestSettings::default(), + get_address_funding_fee_quote +); diff --git a/packages/rs-drive-proof-verifier/src/types.rs b/packages/rs-drive-proof-verifier/src/types.rs index c043fb674b8..8110994059c 100644 --- a/packages/rs-drive-proof-verifier/src/types.rs +++ b/packages/rs-drive-proof-verifier/src/types.rs @@ -253,6 +253,32 @@ pub struct KeysInPath { )] pub struct TotalCreditsInPlatform(pub Credits); +/// A state-aware fee quote for a 0-input / 1-output address funding from a +/// fresh asset lock, computed by a node via `getAddressFundingFeeQuote`. +/// +/// A computed value, not state — the response carries no proof, so the quote +/// reflects what the answering node reports. Treat it as planning data: +/// sizing a funding lock stays governed by `minimum_required_lock_credits` +/// plus the wallet's own margin policy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), + platform_serialize(unversioned) +)] +pub struct AddressFundingFeeQuote { + /// State-aware estimate of the fee the network would charge for the + /// funding executed near the quoted state (includes the requested user + /// fee increase). + pub estimated_fee_credits: Credits, + /// The consensus admission floor for the asset lock. + pub minimum_required_lock_credits: Credits, + /// The protocol version the node quoted with. + pub protocol_version: u32, + /// The committed block height the quote was computed on. + pub state_height: u64, +} + /// A query with no parameters #[derive(Debug, Clone, Copy)] #[cfg_attr( diff --git a/packages/rs-drive-proof-verifier/src/unproved.rs b/packages/rs-drive-proof-verifier/src/unproved.rs index c96956c09cc..a447fb40f0f 100644 --- a/packages/rs-drive-proof-verifier/src/unproved.rs +++ b/packages/rs-drive-proof-verifier/src/unproved.rs @@ -1,5 +1,5 @@ use crate::types::evonode_status::EvoNodeStatus; -use crate::types::CurrentQuorumsInfo; +use crate::types::{AddressFundingFeeQuote, CurrentQuorumsInfo}; use crate::Error; use dapi_grpc::platform::v0::ResponseMetadata; use dapi_grpc::platform::v0::{self as platform}; @@ -158,6 +158,36 @@ pub trait FromUnproved { } } +impl FromUnproved for AddressFundingFeeQuote { + type Request = platform::GetAddressFundingFeeQuoteRequest; + type Response = platform::GetAddressFundingFeeQuoteResponse; + + fn maybe_from_unproved_with_metadata, O: Into>( + _request: I, + response: O, + _network: Network, + _platform_version: &PlatformVersion, + ) -> Result<(Option, ResponseMetadata), Error> + where + Self: Sized, + { + let response: platform::GetAddressFundingFeeQuoteResponse = response.into(); + + let platform::get_address_funding_fee_quote_response::Version::V0(v0) = + response.version.ok_or(Error::EmptyVersion)?; + let metadata = v0.metadata.clone().ok_or(Error::EmptyResponseMetadata)?; + + let quote = AddressFundingFeeQuote { + estimated_fee_credits: v0.estimated_fee_credits, + minimum_required_lock_credits: v0.minimum_required_lock_credits, + protocol_version: v0.protocol_version, + state_height: v0.state_height, + }; + + Ok((Some(quote), metadata)) + } +} + impl FromUnproved for CurrentQuorumsInfo { type Request = platform::GetCurrentQuorumsInfoRequest; type Response = platform::GetCurrentQuorumsInfoResponse; diff --git a/packages/rs-platform-wallet/src/wallet/platform_addresses/mod.rs b/packages/rs-platform-wallet/src/wallet/platform_addresses/mod.rs index 786c5c1ae56..5975a7e1d6e 100644 --- a/packages/rs-platform-wallet/src/wallet/platform_addresses/mod.rs +++ b/packages/rs-platform-wallet/src/wallet/platform_addresses/mod.rs @@ -11,6 +11,7 @@ use crate::PlatformWalletError; mod fund_from_asset_lock; pub(crate) mod provider; +mod quote; mod sync; mod transfer; mod wallet; diff --git a/packages/rs-platform-wallet/src/wallet/platform_addresses/quote.rs b/packages/rs-platform-wallet/src/wallet/platform_addresses/quote.rs new file mode 100644 index 00000000000..afc376e5696 --- /dev/null +++ b/packages/rs-platform-wallet/src/wallet/platform_addresses/quote.rs @@ -0,0 +1,41 @@ +//! State-aware funding fee quote — a thin wrapper over the SDK's +//! `getAddressFundingFeeQuote` call using the wallet's own SDK handle. + +use super::wallet::PlatformAddressWallet; +use crate::error::PlatformWalletError; +use dash_sdk::platform::address_funding_fee_quote::{ + quote_address_funding_fee, AddressFundingFeeQuote, AddressFundingFeeQuoteQuery, +}; +use dpp::address_funds::PlatformAddress; +use dpp::prelude::UserFeeIncrease; + +impl PlatformAddressWallet { + /// Fetches a state-aware fee quote for funding `recipient` with a fresh + /// asset lock (0 address inputs, 1 remainder output). + /// + /// `prepared_outpoint` carries the exact outpoint when the wallet has + /// already built and signed the lock transaction; `None` lets the node + /// use a deterministic placeholder with the same expected search depth. + /// + /// The quote is planning data from a single node (no proof): sizing the + /// lock stays governed by `minimum_required_lock_credits` plus the + /// application's own margin policy. There is no offline fallback — a + /// network failure surfaces as an error. + pub async fn quote_funding_fee( + &self, + recipient: PlatformAddress, + prepared_outpoint: Option<[u8; 36]>, + user_fee_increase: UserFeeIncrease, + ) -> Result { + Ok(quote_address_funding_fee( + &self.sdk, + AddressFundingFeeQuoteQuery { + recipient, + asset_lock_outpoint: prepared_outpoint, + user_fee_increase, + signable_bytes_len_hint: None, + }, + ) + .await?) + } +} diff --git a/packages/rs-sdk/src/mock/requests.rs b/packages/rs-sdk/src/mock/requests.rs index 9015c19a583..a64ef1cb2c0 100644 --- a/packages/rs-sdk/src/mock/requests.rs +++ b/packages/rs-sdk/src/mock/requests.rs @@ -34,7 +34,8 @@ use drive_proof_verifier::types::token_info::{IdentitiesTokenInfos, IdentityToke use drive_proof_verifier::types::token_status::TokenStatuses; use drive::grovedb::GroveTrunkQueryResult; use drive_proof_verifier::types::{ - AddressInfo, Contenders, ContestedResources, CurrentQuorumsInfo, ElementFetchRequestItem, + AddressFundingFeeQuote, AddressInfo, Contenders, ContestedResources, CurrentQuorumsInfo, + ElementFetchRequestItem, IdentityBalanceAndRevision, IndexMap, MasternodeProtocolVote, MostRecentShieldedAnchor, PlatformAddressTrunkState, PrefundedSpecializedBalance, ProposerBlockCounts, RecentAddressBalanceChanges, RecentCompactedAddressBalanceChanges, RetrievedValues, @@ -502,6 +503,7 @@ impl_mock_response!(TotalCreditsInPlatform); impl_mock_response!(ElementFetchRequestItem); impl_mock_response!(EvoNodeStatus); impl_mock_response!(CurrentQuorumsInfo); +impl_mock_response!(AddressFundingFeeQuote); impl_mock_response!(Group); impl_mock_response!(TokenPricingSchedule); impl_mock_response!(RewardDistributionMoment); diff --git a/packages/rs-sdk/src/platform.rs b/packages/rs-sdk/src/platform.rs index d6a3213036e..d709662cba1 100644 --- a/packages/rs-sdk/src/platform.rs +++ b/packages/rs-sdk/src/platform.rs @@ -5,6 +5,7 @@ // generated types. Later these re-exports could be swapped with actual dash-platform-sdk's requests // and while it will change the substance, the API structure will remain the same. +pub mod address_funding_fee_quote; pub mod address_sync; pub mod block_info_from_metadata; pub mod dashpay; diff --git a/packages/rs-sdk/src/platform/address_funding_fee_quote.rs b/packages/rs-sdk/src/platform/address_funding_fee_quote.rs new file mode 100644 index 00000000000..3df9536feaf --- /dev/null +++ b/packages/rs-sdk/src/platform/address_funding_fee_quote.rs @@ -0,0 +1,138 @@ +//! A state-aware fee quote for a 0-input / 1-output address funding from a +//! fresh asset lock, served by the node's `getAddressFundingFeeQuote` query. +//! +//! The node prices the exact production operations with tree depths measured +//! from its committed state, adds the validation-operation fees execution +//! records, and applies the requested user fee increase. The response is a +//! computed value, not state — it carries no proof, so treat the quote as +//! planning data: sizing a funding lock stays governed by +//! `minimum_required_lock_credits` plus the wallet's own margin policy. + +use crate::platform::proto; +use crate::platform::query::Query; +use crate::platform::QuerySettings; +use crate::{error::Error, Sdk}; +use dapi_grpc::platform::v0::GetAddressFundingFeeQuoteRequest; +use dpp::address_funds::PlatformAddress; +use dpp::prelude::UserFeeIncrease; +use dpp::version::PlatformVersion; +pub use drive_proof_verifier::types::AddressFundingFeeQuote; +use rs_dapi_client::RequestSettings; + +use crate::platform::FetchUnproved; + +/// Parameters of an address funding fee quote. +#[derive(Debug, Clone)] +pub struct AddressFundingFeeQuoteQuery { + /// The funding recipient. + pub recipient: PlatformAddress, + /// The exact planned asset lock outpoint (txid bytes followed by the + /// vout as four little-endian bytes), when the wallet has already built + /// and signed the lock transaction. `None` lets the node derive a + /// deterministic placeholder — for a fresh (absent) outpoint both have + /// the same expected search depth. + pub asset_lock_outpoint: Option<[u8; 36]>, + /// The user fee increase the quote should include; the SDK's chain-lock + /// retry loop can raise a funding up to 14 units above the base. + pub user_fee_increase: UserFeeIncrease, + /// Length of the future transition's signable bytes when known; `None` + /// uses the node's default. Clamped server-side, so it cannot understate + /// the fee. + pub signable_bytes_len_hint: Option, +} + +impl Query for AddressFundingFeeQuoteQuery { + fn query( + &self, + _settings: &QuerySettings<'_>, + ) -> Result { + Ok(GetAddressFundingFeeQuoteRequest { + version: Some(proto::get_address_funding_fee_quote_request::Version::V0( + proto::get_address_funding_fee_quote_request::GetAddressFundingFeeQuoteRequestV0 { + address: self.recipient.to_bytes(), + asset_lock_outpoint: self + .asset_lock_outpoint + .map(|outpoint| outpoint.to_vec()) + .unwrap_or_default(), + user_fee_increase: self.user_fee_increase as u32, + signable_bytes_len_hint: self.signable_bytes_len_hint.unwrap_or_default(), + }, + )), + }) + } +} + +/// Fetches a state-aware address funding fee quote from the network. +/// +/// Fails with a protocol error when the node quoted with a protocol version +/// this client does not know — a quote priced under unknown rules must not be +/// displayed as if it were understood. +pub async fn quote_address_funding_fee( + sdk: &Sdk, + query: AddressFundingFeeQuoteQuery, +) -> Result { + let (quote, _metadata) = AddressFundingFeeQuote::fetch_unproved_with_settings( + sdk, + query, + RequestSettings::default(), + ) + .await?; + let quote = quote.ok_or_else(|| { + Error::Generic("address funding fee quote response carried no data".to_string()) + })?; + + // Fail fast on a version this client doesn't know. + PlatformVersion::get(quote.protocol_version).map_err(dpp::ProtocolError::from)?; + + Ok(quote) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_settings(request_settings: &RequestSettings) -> QuerySettings<'_> { + QuerySettings { + request_settings, + protocol_version: PlatformVersion::latest(), + prove: false, + } + } + + #[test] + fn test_query_maps_placeholder_and_exact_outpoint() { + let recipient = PlatformAddress::P2pkh([7; 20]); + let request_settings = RequestSettings::default(); + let settings = test_settings(&request_settings); + + let placeholder = AddressFundingFeeQuoteQuery { + recipient, + asset_lock_outpoint: None, + user_fee_increase: 3, + signable_bytes_len_hint: None, + }; + let request = placeholder.query(&settings).expect("query"); + let Some(proto::get_address_funding_fee_quote_request::Version::V0(v0)) = request.version + else { + panic!("expected V0 request"); + }; + assert_eq!(v0.address, recipient.to_bytes()); + assert!(v0.asset_lock_outpoint.is_empty(), "placeholder sends empty"); + assert_eq!(v0.user_fee_increase, 3); + assert_eq!(v0.signable_bytes_len_hint, 0, "server default"); + + let exact = AddressFundingFeeQuoteQuery { + recipient, + asset_lock_outpoint: Some([0xAB; 36]), + user_fee_increase: 0, + signable_bytes_len_hint: Some(390), + }; + let request = exact.query(&settings).expect("query"); + let Some(proto::get_address_funding_fee_quote_request::Version::V0(v0)) = request.version + else { + panic!("expected V0 request"); + }; + assert_eq!(v0.asset_lock_outpoint, vec![0xAB; 36]); + assert_eq!(v0.signable_bytes_len_hint, 390); + } +} diff --git a/packages/rs-sdk/src/platform/fetch_unproved.rs b/packages/rs-sdk/src/platform/fetch_unproved.rs index f9a8bb30064..59c1f2adb52 100644 --- a/packages/rs-sdk/src/platform/fetch_unproved.rs +++ b/packages/rs-sdk/src/platform/fetch_unproved.rs @@ -117,6 +117,10 @@ impl FetchUnproved for drive_proof_verifier::types::CurrentQuorumsInfo { type Request = platform_proto::GetCurrentQuorumsInfoRequest; } +impl FetchUnproved for drive_proof_verifier::types::AddressFundingFeeQuote { + type Request = platform_proto::GetAddressFundingFeeQuoteRequest; +} + impl FetchUnproved for EvoNodeStatus { type Request = EvoNode; }