diff --git a/docs/rpc/api/core-node/post-fee-transaction-response.example.json b/docs/rpc/api/core-node/post-fee-transaction-response.example.json index f78b80eb7..e82274974 100644 --- a/docs/rpc/api/core-node/post-fee-transaction-response.example.json +++ b/docs/rpc/api/core-node/post-fee-transaction-response.example.json @@ -8,14 +8,18 @@ "write_length": 1020 }, "estimated_cost_scalar": 14, - "estimated_fee_rates": [ - 1.2410714285714286, - 8.958333333333332, - 10 - ], - "estimated_fees": [ - 17, - 125, - 140 + "estimations": [ + { + "fee": 17, + "fee_rate": 1.2410714285714286 + }, + { + "fee": 125, + "fee_rate": 8.958333333333332 + }, + { + "fee": 140, + "fee_rate": 10 + } ] } diff --git a/docs/rpc/api/core-node/post-fee-transaction-response.schema.json b/docs/rpc/api/core-node/post-fee-transaction-response.schema.json index 8ddbdd0d1..8a0859134 100644 --- a/docs/rpc/api/core-node/post-fee-transaction-response.schema.json +++ b/docs/rpc/api/core-node/post-fee-transaction-response.schema.json @@ -24,16 +24,18 @@ "write_length": { "type": "integer" } } }, - "estimated_fee_rates": { + "estimations": { "type": "array", "items": { - "type": "number" - } - }, - "estimated_fees": { - "type": "array", - "items": { - "type": "integer" + "type": "object", + "properties": { + "fee_rate": { + "type": "number" + }, + "fee": { + "type": "number" + } + } } } } diff --git a/docs/rpc/openapi.yaml b/docs/rpc/openapi.yaml index 139547841..b3ed8e84e 100644 --- a/docs/rpc/openapi.yaml +++ b/docs/rpc/openapi.yaml @@ -282,7 +282,7 @@ paths: tags: - Fees description: | - Get an estimated fee for that supplied transaction. This + Get an estimated fee for the supplied transaction. This estimates the execution cost of the transaction, the current fee rate of the network, and returns estimates for fee amounts. @@ -290,30 +290,53 @@ paths: * `transaction_payload` is a hex-encoded serialization of the TransactionPayload for the transaction. * `estimated_len` is an optional argument that provides the - endpoint + endpoint with an estimation of the final length (in bytes) + of the transaction, including any post-conditions and + signatures If the node cannot provide an estimate for the transaction (e.g., if the node has never seen a contract-call for the given contract and function) or if estimation is not configured on this node, a 400 response is returned. + The 400 response will be a JSON error containing a `reason` + field which can be one of the following: + + * `DatabaseError` - this Stacks node has had an internal + database error while trying to estimate the costs of the + supplied transaction. + * `NoEstimateAvailable` - this Stacks node has not seen this + kind of contract-call before, and it cannot provide an + estimate yet. + * `CostEstimationDisabled` - this Stacks node does not perform + fee or cost estimation, and it cannot respond on this + endpoint. The 200 response contains the following data: * `estimated_cost` - the estimated multi-dimensional cost of executing the Clarity VM on the provided transaction. - * `estimated_cost_scalar` - an integer that captures the total - proportion of the block limit that a transaction would be - estimated to consume. This value is multiplied by a fee rate - to suggest the total fee amount to be paid by the node. This + * `estimated_cost_scalar` - a unitless integer that captures + the total proportion of the block limit that a transaction + would be estimated to consume. In order to compute an + estimate of total fee amount for the transaction, this value + is multiplied by the estimated fee rate. This value incorporates the estimated transaction length. * `cost_scalar_change_by_byte` - a float value that indicates how much the `estimated_cost_scalar` value would increase for every additional byte in the final transaction. - * `estimated_fee_rates` - three estimated values for the current - fee rates in the network - * `estimated_fees` - three estimated values for the total fee that - the given transaction should pay. These values are the result of - computing: `estimated_fee_rates` x `estimated_cost_scalar` + * `estimations` - an array of estimated fee rates and total fees to + pay in microSTX for the transaction. This array provides a range of + estimates (default: 3) that may be used. Each element of the array + contains the following fields: + * `fee_rate` - the estimated value for the current fee + rates in the network + * `fee` - the estimated value for the total fee in + microSTX that the given transaction should pay. These + values are the result of computing: + `fee_rate` x `estimated_cost_scalar`. + If the estimated fees are less than the minimum relay + fee `(1 ustx x estimated_len)`, then that minimum relay + fee will be returned here instead. Note: If the final transaction's byte size is larger than diff --git a/src/cost_estimates/mod.rs b/src/cost_estimates/mod.rs index 80c39c970..2c342e0be 100644 --- a/src/cost_estimates/mod.rs +++ b/src/cost_estimates/mod.rs @@ -192,7 +192,7 @@ impl EstimatorError { Some(json!({"message": self.to_string()})), ), EstimatorError::SqliteError(_) => { - ("DatavaseError", Some(json!({"message": self.to_string()}))) + ("DatabaseError", Some(json!({"message": self.to_string()}))) } }; let mut result = json!({ diff --git a/src/net/mod.rs b/src/net/mod.rs index 0e6ba04a0..c85c1a58c 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -1029,23 +1029,27 @@ pub struct RPCPoxInfoData { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RPCFeeEstimate { - pub high: u64, - pub middle: u64, - pub low: u64, + pub fee_rate: f64, + pub fee: u64, } impl RPCFeeEstimate { - pub fn estimate_fees(scalar: u64, fee_rates: FeeRateEstimate) -> RPCFeeEstimate { - let estimated_fees_f64 = fee_rates * (scalar as f64); - RPCFeeEstimate { - high: estimated_fees_f64.high as u64, - middle: estimated_fees_f64.middle as u64, - low: estimated_fees_f64.low as u64, - } - } - - pub fn to_vec(self) -> Vec { - vec![self.low, self.middle, self.high] + pub fn estimate_fees(scalar: u64, fee_rates: FeeRateEstimate) -> Vec { + let estimated_fees_f64 = fee_rates.clone() * (scalar as f64); + vec![ + RPCFeeEstimate { + fee: estimated_fees_f64.low as u64, + fee_rate: fee_rates.low, + }, + RPCFeeEstimate { + fee: estimated_fees_f64.middle as u64, + fee_rate: fee_rates.middle, + }, + RPCFeeEstimate { + fee: estimated_fees_f64.high as u64, + fee_rate: fee_rates.high, + }, + ] } } @@ -1053,8 +1057,7 @@ impl RPCFeeEstimate { pub struct RPCFeeEstimateResponse { pub estimated_cost: ExecutionCost, pub estimated_cost_scalar: u64, - pub estimated_fees: Vec, - pub estimated_fee_rates: Vec, + pub estimations: Vec, pub cost_scalar_change_by_byte: f64, } diff --git a/src/net/rpc.rs b/src/net/rpc.rs index 0277ebb68..1e7fde58c 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -1631,16 +1631,22 @@ impl ConversationHttp { } }; - let estimated_fees = - RPCFeeEstimate::estimate_fees(scalar_cost, fee_rates.clone()).to_vec(); + 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; + } + } let response = HttpResponseType::TransactionFeeEstimation( response_metadata, RPCFeeEstimateResponse { estimated_cost, - estimated_fees, + estimations, estimated_cost_scalar: scalar_cost, - estimated_fee_rates: fee_rates.to_vec(), cost_scalar_change_by_byte: metric.change_per_byte(), }, ); diff --git a/testnet/stacks-node/src/tests/integrations.rs b/testnet/stacks-node/src/tests/integrations.rs index a2bbf6f85..3c4d2fb93 100644 --- a/testnet/stacks-node/src/tests/integrations.rs +++ b/testnet/stacks-node/src/tests/integrations.rs @@ -5,6 +5,7 @@ use std::sync::Mutex; use reqwest; use stacks::burnchains::Address; +use stacks::chainstate::stacks::db::blocks::MINIMUM_TX_FEE_RATE_PER_BYTE; use stacks::chainstate::stacks::{ db::blocks::MemPoolRejection, db::StacksChainState, StacksPrivateKey, StacksTransaction, }; @@ -857,13 +858,19 @@ fn integration_test_get_info() { // the estimated scalar should still be non-zero, because the length of the tx goes into this field. assert!(res.get("estimated_cost_scalar").unwrap().as_u64().unwrap() > 0); - let estimated_fee_rates = res.get("estimated_fee_rates").expect("Should have an estimated_fee_rates field") - .as_array() - .expect("Fees should be array"); - let estimated_fees = res.get("estimated_fees").expect("Should have an estimated_fees field") + let estimations = res.get("estimations").expect("Should have an estimations field") .as_array() .expect("Fees should be array"); + let estimated_fee_rates: Vec<_> = estimations + .iter() + .map(|x| x.get("fee_rate").expect("Should have fee_rate field")) + .collect(); + let estimated_fees: Vec<_> = estimations + .iter() + .map(|x| x.get("fee").expect("Should have fee field")) + .collect(); + assert!(estimated_fee_rates.len() == 3, "Fee rates should be length 3 array"); assert!(estimated_fees.len() == 3, "Fees should be length 3 array"); @@ -902,13 +909,19 @@ fn integration_test_get_info() { let estimated_cost_scalar = res.get("estimated_cost_scalar").unwrap().as_u64().unwrap(); assert!(estimated_cost_scalar > 0); - let estimated_fee_rates = res.get("estimated_fee_rates").expect("Should have an estimated_fee_rates field") - .as_array() - .expect("Fees should be array"); - let estimated_fees = res.get("estimated_fees").expect("Should have an estimated_fees field") + let estimations = res.get("estimations").expect("Should have an estimations field") .as_array() .expect("Fees should be array"); + let estimated_fee_rates: Vec<_> = estimations + .iter() + .map(|x| x.get("fee_rate").expect("Should have fee_rate field")) + .collect(); + let estimated_fees: Vec<_> = estimations + .iter() + .map(|x| x.get("fee").expect("Should have fee field")) + .collect(); + assert!(estimated_fee_rates.len() == 3, "Fee rates should be length 3 array"); assert!(estimated_fees.len() == 3, "Fees should be length 3 array"); @@ -922,7 +935,8 @@ fn integration_test_get_info() { let payload_data = tx_payload.serialize_to_vec(); let payload_hex = to_hex(&payload_data); - let body = json!({ "transaction_payload": payload_hex.clone(), "estimated_len": 1550 }); + let estimated_len = 1550; + let body = json!({ "transaction_payload": payload_hex.clone(), "estimated_len": estimated_len }); info!("POST body\n {}", body); let res = client.post(&path) @@ -947,16 +961,27 @@ fn integration_test_get_info() { assert!(estimated_cost_scalar > 0); assert!(new_estimated_cost_scalar > estimated_cost_scalar, "New scalar estimate should be higher because of the tx length increase"); - let new_estimated_fees = res.get("estimated_fees").expect("Should have an estimated_fees field") + let new_estimations = res.get("estimations").expect("Should have an estimations field") .as_array() .expect("Fees should be array"); + let new_estimated_fees: Vec<_> = new_estimations + .iter() + .map(|x| x.get("fee").expect("Should have fee field")) + .collect(); + + let minimum_relay_fee = estimated_len * MINIMUM_TX_FEE_RATE_PER_BYTE; + assert!(new_estimated_fees[2].as_u64().unwrap() >= estimated_fees[2].as_u64().unwrap(), "Supplying an estimated tx length should increase the estimated fees"); assert!(new_estimated_fees[0].as_u64().unwrap() >= estimated_fees[0].as_u64().unwrap(), "Supplying an estimated tx length should increase the estimated fees"); assert!(new_estimated_fees[1].as_u64().unwrap() >= estimated_fees[1].as_u64().unwrap(), "Supplying an estimated tx length should increase the estimated fees"); + for estimate in new_estimated_fees.iter() { + assert!(estimate.as_u64().unwrap() >= minimum_relay_fee, + "The estimated fees must always be greater than minimum_relay_fee"); + } }, _ => {}, }