Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/rs-dapi-client/src/transport/grpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
26 changes: 26 additions & 0 deletions packages/rs-drive-proof-verifier/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
32 changes: 31 additions & 1 deletion packages/rs-drive-proof-verifier/src/unproved.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -158,6 +158,36 @@ pub trait FromUnproved<Req> {
}
}

impl FromUnproved<platform::GetAddressFundingFeeQuoteRequest> for AddressFundingFeeQuote {
type Request = platform::GetAddressFundingFeeQuoteRequest;
type Response = platform::GetAddressFundingFeeQuoteResponse;

fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
_request: I,
response: O,
_network: Network,
_platform_version: &PlatformVersion,
) -> Result<(Option<Self>, 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<platform::GetCurrentQuorumsInfoRequest> for CurrentQuorumsInfo {
type Request = platform::GetCurrentQuorumsInfoRequest;
type Response = platform::GetCurrentQuorumsInfoResponse;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::PlatformWalletError;

mod fund_from_asset_lock;
pub(crate) mod provider;
mod quote;
mod sync;
mod transfer;
mod wallet;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AddressFundingFeeQuote, PlatformWalletError> {
Ok(quote_address_funding_fee(
&self.sdk,
AddressFundingFeeQuoteQuery {
recipient,
asset_lock_outpoint: prepared_outpoint,
user_fee_increase,
signable_bytes_len_hint: None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Wallet quotes always discard a known signable-length hint

The server uses a calibrated 390-byte default but explicitly supports the real signable length for larger funding transactions. This wrapper always sends None, even though its documented prepared_outpoint case represents an already-built and signed lock whose caller may know the future transition size. Expose an optional signable_bytes_len_hint parameter, or provide a wallet method accepting a fully specified AddressFundingFeeQuoteQuery, so callers can request the more accurate hashing charge.

source: ['codex']

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in this update — Wallet quotes always discard a known signable-length hint no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

},
)
.await?)
}
}
4 changes: 3 additions & 1 deletion packages/rs-sdk/src/mock/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -502,6 +503,7 @@ impl_mock_response!(TotalCreditsInPlatform);
impl_mock_response!(ElementFetchRequestItem);
impl_mock_response!(EvoNodeStatus);
impl_mock_response!(CurrentQuorumsInfo);
impl_mock_response!(AddressFundingFeeQuote);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: MockResponse support is not connected to FetchUnproved

The new serialization implementation satisfies the MockResponse bound, but the unproved-fetch path never consults the typed expectation cache. MockDashPlatformSdk::expect_fetch only accepts Fetch, and load_expectations_sync has no GetAddressFundingFeeQuoteRequest arm, so callers cannot configure Sdk::new_mock() to return this quote. Add a typed unproved-response expectation path or endpoint-specific raw protobuf expectation support, then exercise quote_address_funding_fee and its unknown-protocol-version guard through the mock SDK.

source: ['codex']

impl_mock_response!(Group);
impl_mock_response!(TokenPricingSchedule);
impl_mock_response!(RewardDistributionMoment);
Expand Down
1 change: 1 addition & 0 deletions packages/rs-sdk/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
138 changes: 138 additions & 0 deletions packages/rs-sdk/src/platform/address_funding_fee_quote.rs
Original file line number Diff line number Diff line change
@@ -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]>,
Comment on lines +29 to +34

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Keep asset-lock outpoints typed until wire encoding

The public query exposes an asset-lock outpoint as [u8; 36], making callers responsible for txid byte order and little-endian vout encoding. dpp::dashcore::OutPoint is the existing domain type and has the canonical conversion to the 36-byte representation used by Platform; the wallet's asset-lock flows already use it. Accept Option<OutPoint> in the SDK query and wallet wrapper, and convert it to bytes only while constructing the protobuf request.

source: ['codex']

/// 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<u32>,
}

impl Query<GetAddressFundingFeeQuoteRequest> for AddressFundingFeeQuoteQuery {
fn query(
&self,
_settings: &QuerySettings<'_>,
) -> Result<GetAddressFundingFeeQuoteRequest, Error> {
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<AddressFundingFeeQuote, Error> {
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);
}
}
4 changes: 4 additions & 0 deletions packages/rs-sdk/src/platform/fetch_unproved.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading