diff --git a/stackslib/src/net/api/getstxtransfercost.rs b/stackslib/src/net/api/getstxtransfercost.rs index 5f732c650..22e4b4826 100644 --- a/stackslib/src/net/api/getstxtransfercost.rs +++ b/stackslib/src/net/api/getstxtransfercost.rs @@ -16,6 +16,7 @@ use std::io::{Read, Write}; +use clarity::vm::costs::ExecutionCost; use regex::{Captures, Regex}; use stacks_common::types::chainstate::{ BlockHeaderHash, ConsensusHash, StacksBlockId, StacksPublicKey, @@ -23,6 +24,7 @@ use stacks_common::types::chainstate::{ use stacks_common::types::net::PeerHost; use stacks_common::types::StacksPublicKeyBuffer; use stacks_common::util::hash::{Hash160, Sha256Sum}; +use url::form_urlencoded; use crate::burnchains::affirmation::AffirmationMap; use crate::burnchains::Txid; @@ -30,19 +32,23 @@ use crate::chainstate::burn::db::sortdb::SortitionDB; use crate::chainstate::stacks::db::blocks::MINIMUM_TX_FEE_RATE_PER_BYTE; use crate::chainstate::stacks::db::StacksChainState; use crate::core::mempool::MemPoolDB; +use crate::net::api::postfeerate::RPCPostFeeRateRequestHandler; use crate::net::http::{ - parse_json, Error, HttpRequest, HttpRequestContents, HttpRequestPreamble, HttpResponse, - HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, + parse_json, Error, HttpBadRequest, HttpRequest, HttpRequestContents, HttpRequestPreamble, + HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, }; use crate::net::httpcore::{ HttpPreambleExtensions, RPCRequestHandler, StacksHttpRequest, StacksHttpResponse, }; use crate::net::p2p::PeerNetwork; -use crate::net::{Error as NetError, StacksNodeState}; +use crate::net::{Error as NetError, HttpServerError, StacksNodeState}; use crate::version_string; +pub(crate) const SINGLESIG_TX_TRANSFER_LEN: u64 = 180; + #[derive(Clone)] pub struct RPCGetStxTransferCostRequestHandler {} + impl RPCGetStxTransferCostRequestHandler { pub fn new() -> Self { Self {} @@ -74,7 +80,7 @@ impl HttpRequest for RPCGetStxTransferCostRequestHandler { ) -> Result { if preamble.get_content_length() != 0 { return Err(Error::DecodeError( - "Invalid Http request: expected 0-length body for GetInfo".to_string(), + "Invalid Http request: expected 0-length body".to_string(), )); } Ok(HttpRequestContents::new().query_string(query)) @@ -92,9 +98,57 @@ impl RPCRequestHandler for RPCGetStxTransferCostRequestHandler { _contents: HttpRequestContents, node: &mut StacksNodeState, ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> { - // todo -- need to actually estimate the cost / length for token transfers - // right now, it just uses the minimum. - let fee = MINIMUM_TX_FEE_RATE_PER_BYTE; + // NOTE: The estimated length isn't needed per se because we're returning a fee rate, but + // we do need an absolute length to use the estimator (so supply a common one). + let estimated_len = SINGLESIG_TX_TRANSFER_LEN; + + let fee_resp = node.with_node_state(|_network, sortdb, _chainstate, _mempool, rpc_args| { + let tip = self.get_canonical_burn_chain_tip(&preamble, sortdb)?; + let stacks_epoch = self.get_stacks_epoch(&preamble, sortdb, tip.block_height)?; + + if let Some((_, fee_estimator, metric)) = rpc_args.get_estimators_ref() { + // STX transfer transactions have zero runtime cost + let estimated_cost = ExecutionCost::zero(); + let estimations = + RPCPostFeeRateRequestHandler::estimate_tx_fee_from_cost_and_length( + &preamble, + fee_estimator, + metric, + estimated_cost, + estimated_len, + stacks_epoch, + )? + .estimations; + if estimations.len() != 3 { + // logic bug, but treat as runtime error + return Err(StacksHttpResponse::new_error( + &preamble, + &HttpServerError::new( + "Logic error in fee estimation: did not get three estimates".into(), + ), + )); + } + + // safety -- checked estimations.len() == 3 above + let median_estimation = &estimations[1]; + + // NOTE: this returns the fee _rate_ + Ok(median_estimation.fee / estimated_len) + } else { + // unlike `POST /v2/fees/transaction`, this method can't fail due to the + // unavailability of cost estimation, so just assume the minimum fee. + debug!("Fee and cost estimation not configured on this stacks node"); + Ok(MINIMUM_TX_FEE_RATE_PER_BYTE) + } + }); + + let fee = match fee_resp { + Ok(fee) => fee, + Err(response) => { + return response.try_into_contents().map_err(NetError::from); + } + }; + let mut preamble = HttpResponsePreamble::ok_json(&preamble); preamble.set_canonical_stacks_tip_height(Some(node.canonical_stacks_tip_height())); let body = HttpResponseContents::try_from_json(&fee)?; @@ -116,13 +170,9 @@ impl HttpResponse for RPCGetStxTransferCostRequestHandler { impl StacksHttpRequest { pub fn new_get_stx_transfer_cost(host: PeerHost) -> StacksHttpRequest { - StacksHttpRequest::new_for_peer( - host, - "GET".into(), - "/v2/fees/transfer".into(), - HttpRequestContents::new(), - ) - .expect("FATAL: failed to construct request from infallible data") + let mut contents = HttpRequestContents::new(); + StacksHttpRequest::new_for_peer(host, "GET".into(), "/v2/fees/transfer".into(), contents) + .expect("FATAL: failed to construct request from infallible data") } } diff --git a/stackslib/src/net/api/postfeerate.rs b/stackslib/src/net/api/postfeerate.rs index ab9691fde..376d8bf3d 100644 --- a/stackslib/src/net/api/postfeerate.rs +++ b/stackslib/src/net/api/postfeerate.rs @@ -34,7 +34,9 @@ use crate::chainstate::stacks::db::blocks::MINIMUM_TX_FEE_RATE_PER_BYTE; use crate::chainstate::stacks::db::StacksChainState; use crate::chainstate::stacks::TransactionPayload; use crate::core::mempool::MemPoolDB; -use crate::cost_estimates::FeeRateEstimate; +use crate::core::StacksEpoch; +use crate::cost_estimates::metrics::CostMetric; +use crate::cost_estimates::{CostEstimator, FeeEstimator, FeeRateEstimate}; use crate::net::http::{ parse_json, Error, HttpBadRequest, HttpContentType, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble, HttpResponse, HttpResponseContents, @@ -92,6 +94,7 @@ pub struct RPCPostFeeRateRequestHandler { pub estimated_len: Option, pub transaction_payload: Option, } + impl RPCPostFeeRateRequestHandler { pub fn new() -> Self { Self { @@ -99,6 +102,48 @@ impl RPCPostFeeRateRequestHandler { transaction_payload: None, } } + + /// Estimate a transaction fee, given its execution cost estimation and length estimation + /// and cost estimators. + /// Returns Ok(fee structure) on success + /// Returns Err(HTTP response) on error + pub fn estimate_tx_fee_from_cost_and_length( + preamble: &HttpRequestPreamble, + fee_estimator: &dyn FeeEstimator, + metric: &dyn CostMetric, + estimated_cost: ExecutionCost, + estimated_len: u64, + stacks_epoch: StacksEpoch, + ) -> Result { + let scalar_cost = + metric.from_cost_and_len(&estimated_cost, &stacks_epoch.block_limit, estimated_len); + let fee_rates = fee_estimator.get_rate_estimates().map_err(|e| { + StacksHttpResponse::new_error( + &preamble, + &HttpBadRequest::new(format!( + "Estimator RPC endpoint failed to estimate fees for tx: {:?}", + &e + )), + ) + })?; + + let mut estimations = RPCFeeEstimate::estimate_fees(scalar_cost, fee_rates).to_vec(); + + let minimum_fee = estimated_len * MINIMUM_TX_FEE_RATE_PER_BYTE; + + for estimate in estimations.iter_mut() { + if estimate.fee < minimum_fee { + estimate.fee = minimum_fee; + } + } + + Ok(RPCFeeEstimateResponse { + estimated_cost, + estimations, + estimated_cost_scalar: scalar_cost, + cost_scalar_change_by_byte: metric.change_per_byte(), + }) + } } /// Decode the HTTP request @@ -206,39 +251,14 @@ impl RPCRequestHandler for RPCPostFeeRateRequestHandler { ) })?; - let scalar_cost = metric.from_cost_and_len( - &estimated_cost, - &stacks_epoch.block_limit, - estimated_len, - ); - let fee_rates = fee_estimator.get_rate_estimates().map_err(|e| { - StacksHttpResponse::new_error( - &preamble, - &HttpBadRequest::new(format!( - "Estimator RPC endpoint failed to estimate fees for tx {}: {:?}", - &tx.name(), - &e - )), - ) - })?; - - let mut estimations = - RPCFeeEstimate::estimate_fees(scalar_cost, fee_rates).to_vec(); - - let minimum_fee = estimated_len * MINIMUM_TX_FEE_RATE_PER_BYTE; - - for estimate in estimations.iter_mut() { - if estimate.fee < minimum_fee { - estimate.fee = minimum_fee; - } - } - - Ok(RPCFeeEstimateResponse { + Self::estimate_tx_fee_from_cost_and_length( + &preamble, + fee_estimator, + metric, estimated_cost, - estimations, - estimated_cost_scalar: scalar_cost, - cost_scalar_change_by_byte: metric.change_per_byte(), - }) + estimated_len, + stacks_epoch, + ) } else { debug!("Fee and cost estimation not configured on this stacks node"); Err(StacksHttpResponse::new_error( diff --git a/stackslib/src/net/api/tests/getstxtransfercost.rs b/stackslib/src/net/api/tests/getstxtransfercost.rs index 6c4cccc36..66e557f41 100644 --- a/stackslib/src/net/api/tests/getstxtransfercost.rs +++ b/stackslib/src/net/api/tests/getstxtransfercost.rs @@ -25,6 +25,7 @@ use stacks_common::types::Address; use super::test_rpc; use crate::chainstate::stacks::db::blocks::MINIMUM_TX_FEE_RATE_PER_BYTE; use crate::core::BLOCK_LIMIT_MAINNET_21; +use crate::net::api::getstxtransfercost::SINGLESIG_TX_TRANSFER_LEN; use crate::net::api::*; use crate::net::connection::ConnectionOptions; use crate::net::httpcore::{ @@ -67,6 +68,7 @@ fn test_try_make_response() { let mut responses = test_rpc(function_name!(), vec![request]); assert_eq!(responses.len(), 1); + responses.reverse(); let response = responses.pop().unwrap(); debug!( @@ -80,5 +82,6 @@ fn test_try_make_response() { ); let fee_rate = response.decode_stx_transfer_fee().unwrap(); + debug!("fee_rate = {:?}", &fee_rate); assert_eq!(fee_rate, MINIMUM_TX_FEE_RATE_PER_BYTE); }