Repository: ergoplatform/sigma-rust (language: rust)

Issue:
Add tx fee estimation to TxBuilder
Tx fee estimations are implemented in https://github.com/ergoplatform/ergo/pull/1175

> Currently good value is about 0.001 erg/ kilobyte (ErgoMixer is paying something like that). With node's default value of 0.001 erg regardless the size, a transaction can stay for a while in the mempool if it is e.g. collecting many inputs. Some exchanges paying 0.01 to get into a block ASAP.


Discussion / hints:

Fix patch:
diff --git a/bindings/ergo-lib-wasm/src/ergo_box.rs b/bindings/ergo-lib-wasm/src/ergo_box.rs
index e0525cd15..9b4197421 100644
--- a/bindings/ergo-lib-wasm/src/ergo_box.rs
+++ b/bindings/ergo-lib-wasm/src/ergo_box.rs
@@ -149,10 +149,10 @@ impl From<chain::ergo_box::ErgoBox> for ErgoBox {
     }
 }
 
-/// Box value with with bound checks
+/// Box value in nanoERGs with bound checks
 #[wasm_bindgen]
 #[derive(PartialEq, Eq, Debug, Clone)]
-pub struct BoxValue(chain::ergo_box::box_value::BoxValue);
+pub struct BoxValue(pub(crate) chain::ergo_box::box_value::BoxValue);
 
 #[wasm_bindgen]
 impl BoxValue {
diff --git a/bindings/ergo-lib-wasm/src/tx_builder.rs b/bindings/ergo-lib-wasm/src/tx_builder.rs
index 158640b4e..0c2ef4ac5 100644
--- a/bindings/ergo-lib-wasm/src/tx_builder.rs
+++ b/bindings/ergo-lib-wasm/src/tx_builder.rs
@@ -15,6 +15,12 @@ pub struct TxBuilder(wallet::tx_builder::TxBuilder<chain::ergo_box::ErgoBox>);
 
 #[wasm_bindgen]
 impl TxBuilder {
+    /// Suggested transaction fee (semi-default value used across wallets and dApps as of Oct 2020)
+    #[allow(non_snake_case)]
+    pub fn SUGGESTED_TX_FEE() -> BoxValue {
+        BoxValue(wallet::tx_builder::SUGGESTED_TX_FEE)
+    }
+
     /// Creates new TxBuilder
     /// `box_selection` - selected input boxes (via [`BoxSelector`])
     /// `output_candidates` - output boxes to be "created" in this transaction,
diff --git a/ergo-lib/src/chain/ergo_box/box_value.rs b/ergo-lib/src/chain/ergo_box/box_value.rs
index af6c17ad5..96afcfdbf 100644
--- a/ergo-lib/src/chain/ergo_box/box_value.rs
+++ b/ergo-lib/src/chain/ergo_box/box_value.rs
@@ -10,10 +10,10 @@ use sigma_ser::vlq_encode;
 use std::{convert::TryFrom, io};
 use thiserror::Error;
 
-/// Box value with bound checks
+/// Box value in nanoERGs with bound checks
 #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
 #[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
-pub struct BoxValue(u64);
+pub struct BoxValue(pub(crate) u64);
 
 impl BoxValue {
     /// Minimal box value per byte of the serialized box that was set on on launch
@@ -40,7 +40,7 @@ impl BoxValue {
     }
 
     /// Check if a value is in bounds
-    pub fn within_bounds(v: u64) -> bool {
+    pub const fn within_bounds(v: u64) -> bool {
         v >= BoxValue::MIN_RAW as u64 && v <= BoxValue::MAX_RAW as u64
     }
 
diff --git a/ergo-lib/src/wallet/tx_builder.rs b/ergo-lib/src/wallet/tx_builder.rs
index 0dca50e19..1ed8bf6c3 100644
--- a/ergo-lib/src/wallet/tx_builder.rs
+++ b/ergo-lib/src/wallet/tx_builder.rs
@@ -11,6 +11,10 @@ use crate::chain::data_input::DataInput;
 use crate::chain::ergo_box::box_builder::ErgoBoxCandidateBuilder;
 use crate::chain::ergo_box::box_builder::ErgoBoxCandidateBuilderError;
 use crate::chain::ergo_box::box_value;
+use crate::chain::input::Input;
+use crate::chain::prover_result::ProofBytes;
+use crate::chain::prover_result::ProverResult;
+use crate::chain::transaction::Transaction;
 use crate::chain::{
     ergo_box::ErgoBoxAssets,
     ergo_box::ErgoBoxId,
@@ -20,9 +24,14 @@ use crate::chain::{
 };
 use crate::constants::MINERS_FEE_MAINNET_ADDRESS;
 use crate::serialization::SerializationError;
+use crate::serialization::SigmaSerializable;
+use crate::sigma_protocol;
 
 use super::box_selector::{BoxSelection, BoxSelectorError};
 
+/// Suggested transaction fee (1100000 nanoERGs, semi-default value used across wallets and dApps as of Oct 2020)
+pub const SUGGESTED_TX_FEE: BoxValue = BoxValue(1100000u64);
+
 /// Unsigned transaction builder
 pub struct TxBuilder<S: ErgoBoxAssets> {
     box_selection: BoxSelection<S>,
@@ -39,7 +48,7 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
     /// `box_selection` - selected input boxes  (via [`BoxSelector`])
     /// `output_candidates` - output boxes to be "created" in this transaction,
     /// `current_height` - chain height that will be used in additionally created boxes (change, miner's fee, etc.),
-    /// `fee_amount` - miner's fee,
+    /// `fee_amount` - miner's fee (higher values will speed up inclusion in blocks),
     /// `change_address` - change (inputs - outputs) will be sent to this address,
     /// `min_change_value` - minimal value of the change to be sent to `change_address`, value less than that
     /// will be given to miners,
@@ -67,8 +76,30 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
         self.data_inputs = data_inputs;
     }
 
-    /// Build the unsigned transaction
-    pub fn build(self) -> Result<UnsignedTransaction, TxBuilderError> {
+    /// Estimated serialized transaction size in bytes after signing (assuming P2PK box spending)
+    pub fn estimate_tx_size_bytes(&self) -> Result<usize, TxBuilderError> {
+        let tx = self.build_tx()?;
+        let inputs = tx
+            .inputs
+            .iter()
+            .map(|ui| {
+                // mock proof of the size of ProveDlog's proof (P2PK box spending)
+                // as it's the most often used proof
+                let proof = ProofBytes::Some(vec![0u8, sigma_protocol::SOUNDNESS_BYTES as u8]);
+                Input {
+                    box_id: ui.box_id.clone(),
+                    spending_proof: ProverResult {
+                        proof,
+                        extension: ui.extension.clone(),
+                    },
+                }
+            })
+            .collect();
+        let signed_tx_mock = Transaction::new(inputs, tx.data_inputs, tx.output_candidates);
+        Ok(signed_tx_mock.sigma_serialize_bytes().len())
+    }
+
+    fn build_tx(&self) -> Result<UnsignedTransaction, TxBuilderError> {
         if self.box_selection.boxes.is_empty() {
             return Err(TxBuilderError::InvalidArgs("inputs is empty".to_string()));
         }
@@ -101,13 +132,19 @@ impl<S: ErgoBoxAssets + ErgoBoxId + Clone> TxBuilder<S> {
         Ok(UnsignedTransaction::new(
             self.box_selection
                 .boxes
+                .clone()
                 .into_iter()
                 .map(UnsignedInput::from)
                 .collect(),
-            self.data_inputs,
+            self.data_inputs.clone(),
             output_candidates,
         ))
     }
+
+    /// Build the unsigned transaction
+    pub fn build(self) -> Result<UnsignedTransaction, TxBuilderError> {
+        self.build_tx()
+    }
 }
 
 /// Create a box with miner's contract and a given value
@@ -156,6 +193,7 @@ mod tests {
     use crate::chain::token::TokenId;
     use crate::chain::transaction::TxId;
     use crate::test_util::force_any_val;
+    use crate::test_util::force_any_val_with;
     use crate::wallet::box_selector::BoxSelector;
     use crate::wallet::box_selector::SimpleBoxSelector;
     use crate::ErgoTree;
@@ -238,6 +276,31 @@ mod tests {
         assert!(tx.output_candidates.get(0).unwrap().tokens().is_empty());
     }
 
+    #[test]
+    fn test_est_tx_size() {
+        let input = force_any_val_with::<ErgoBox>(
+            (BoxValue::MIN_RAW * 5000..BoxValue::MIN_RAW * 10000).into(),
+        );
+        let tx_fee = super::SUGGESTED_TX_FEE;
+        let out_box_value = input.value.checked_sub(&tx_fee).unwrap();
+        let box_builder =
+            ErgoBoxCandidateBuilder::new(out_box_value, force_any_val::<ErgoTree>(), 0);
+        let out_box = box_builder.build().unwrap();
+        let outputs = vec![out_box];
+        let tx_builder = TxBuilder::new(
+            BoxSelection {
+                boxes: vec![input],
+                change_boxes: vec![],
+            },
+            outputs,
+            0,
+            tx_fee,
+            force_any_val::<Address>(),
+            BoxValue::SAFE_USER_MIN,
+        );
+        assert!(tx_builder.estimate_tx_size_bytes().unwrap() > 0);
+    }
+
     proptest! {
 
         #![proptest_config(ProptestConfig::with_cases(16))]


Test patch:
diff --git a/bindings/ergo-lib-wasm/tests/test_transaction.js b/bindings/ergo-lib-wasm/tests/test_transaction.js
index afb342af3..dd6ba9467 100644
--- a/bindings/ergo-lib-wasm/tests/test_transaction.js
+++ b/bindings/ergo-lib-wasm/tests/test_transaction.js
@@ -23,7 +23,7 @@ it('TxBuilder test', async () => {
   const contract = Contract.pay_to_address(recipient);
   const outbox = new ErgoBoxCandidateBuilder(BoxValue.from_u32(10000000), contract, 0).build();
   const tx_outputs = new ErgoBoxCandidates(outbox);
-  const fee = BoxValue.from_u32(1000000);
+  const fee = TxBuilder.SUGGESTED_TX_FEE();
   const change_address = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
   const min_change_value = BoxValue.SAFE_USER_MIN();
   const data_inputs = new DataInputs();
@@ -46,7 +46,7 @@ it('sign transaction', async () => {
   const contract = Contract.pay_to_address(recipient);
   const outbox = new ErgoBoxCandidateBuilder(BoxValue.from_u32(10000000), contract, 0).build();
   const tx_outputs = new ErgoBoxCandidates(outbox);
-  const fee = BoxValue.from_u32(1000000);
+  const fee = TxBuilder.SUGGESTED_TX_FEE();
   const change_address = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
   const min_change_value = BoxValue.SAFE_USER_MIN();
   const box_selector = new SimpleBoxSelector();
@@ -83,7 +83,7 @@ it('TxBuilder mint token test', async () => {
   box_builder.mint_token(token, "TKN", "token desc", 2)
   const outbox = box_builder.build();
   const tx_outputs = new ErgoBoxCandidates(outbox);
-  const fee = BoxValue.from_u32(1000000);
+  const fee = TxBuilder.SUGGESTED_TX_FEE();
   const change_address = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
   const min_change_value = BoxValue.SAFE_USER_MIN();
   const data_inputs = new DataInputs();
@@ -128,7 +128,7 @@ it('TxBuilder burn token test', async () => {
   const box_builder = new ErgoBoxCandidateBuilder(BoxValue.from_u32(10000000), contract, 0);
   const outbox = box_builder.build();
   const tx_outputs = new ErgoBoxCandidates(outbox);
-  const fee = BoxValue.from_u32(1000000);
+  const fee = TxBuilder.SUGGESTED_TX_FEE();
   const change_address = Address.from_testnet_str('3WvsT2Gm4EpsM9Pg18PdY6XyhNNMqXDsvJTbbf6ihLvAmSb7u5RN');
   const min_change_value = BoxValue.SAFE_USER_MIN();
   const data_inputs = new DataInputs();
diff --git a/ergo-lib/src/test_util.rs b/ergo-lib/src/test_util.rs
index 38d8d781a..a6ecaa597 100644
--- a/ergo-lib/src/test_util.rs
+++ b/ergo-lib/src/test_util.rs
@@ -6,3 +6,8 @@ pub fn force_any_val<T: Arbitrary>() -> T {
     let mut runner = TestRunner::default();
     any::<T>().new_tree(&mut runner).unwrap().current()
 }
+
+pub fn force_any_val_with<T: Arbitrary>(args: T::Parameters) -> T {
+    let mut runner = TestRunner::default();
+    any_with::<T>(args).new_tree(&mut runner).unwrap().current()
+}