From 528f18b98e7377149b26f52e88ad4b01f24637ba Mon Sep 17 00:00:00 2001 From: CiottiGiorgio Date: Wed, 14 May 2025 19:49:05 +0200 Subject: [PATCH 1/3] feat: estimate transaction size in the FFI crate --- crates/algokit_transact_ffi/src/lib.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/algokit_transact_ffi/src/lib.rs b/crates/algokit_transact_ffi/src/lib.rs index 6eb3cbbe..71a29695 100644 --- a/crates/algokit_transact_ffi/src/lib.rs +++ b/crates/algokit_transact_ffi/src/lib.rs @@ -406,6 +406,25 @@ pub fn attach_signature( Ok(signed_tx.encode()?) } +#[ffi_func] +/// Return the size of the transaction in bytes as if it was already signed and encoded. +/// This is useful for estimating the fee for the transaction. +pub fn estimate_transaction_size(transaction: &Transaction) -> Result { + let core_tx: algokit_transact::Transaction = transaction.clone().try_into()?; + // We are simulating a signature on this transaction in order to encode it, and get the size + // with which we will be able to estimate the fee. + let signed_tx = algokit_transact::SignedTransaction { + transaction: core_tx, + // Avoiding a zero signature just in case it's compressed or removed in the encoding. + signature: [1; 64], + }; + return Ok(signed_tx + .encode()? + .len() + .try_into() + .expect("size should fit in u64")); +} + #[ffi_func] pub fn address_from_pub_key(pub_key: &[u8]) -> Result { Ok( From e8e3ac51307a8c5f22726f4ce3684c16e4844bbc Mon Sep 17 00:00:00 2001 From: CiottiGiorgio Date: Fri, 16 May 2025 11:28:23 +0200 Subject: [PATCH 2/3] feat: refactored the logic code into the rust crate and re-exposed it in the FFI crate. --- crates/algokit_transact/src/lib.rs | 2 +- crates/algokit_transact/src/traits.rs | 8 +++++-- .../algokit_transact/src/transactions/mod.rs | 21 +++++++++++++++- crates/algokit_transact_ffi/src/lib.rs | 24 +++++++++---------- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/crates/algokit_transact/src/lib.rs b/crates/algokit_transact/src/lib.rs index ca137909..51007df4 100644 --- a/crates/algokit_transact/src/lib.rs +++ b/crates/algokit_transact/src/lib.rs @@ -12,7 +12,7 @@ pub use constants::{ ALGORAND_PUBLIC_KEY_BYTE_LENGTH, HASH_BYTES_LENGTH, }; pub use error::AlgoKitTransactError; -pub use traits::{AlgorandMsgpack, TransactionId}; +pub use traits::{AlgorandMsgpack, EstimateTransactionSize, TransactionId}; pub use transactions::{ AssetTransferTransactionBuilder, AssetTransferTransactionFields, PaymentTransactionBuilder, PaymentTransactionFields, SignedTransaction, Transaction, TransactionHeader, diff --git a/crates/algokit_transact/src/traits.rs b/crates/algokit_transact/src/traits.rs index 03effb5e..b1147787 100644 --- a/crates/algokit_transact/src/traits.rs +++ b/crates/algokit_transact/src/traits.rs @@ -58,7 +58,7 @@ pub trait AlgorandMsgpack: Serialize + for<'de> Deserialize<'de> { /// * `bytes` - The MessagePack encoded bytes /// /// # Returns - /// The decoded instance or an AlgoKitTransactError if the input is empty or + /// The decoded instance or an AlgoKitTransactError if the input is empty or /// deserialization fails. fn decode(bytes: &[u8]) -> Result { if bytes.is_empty() { @@ -84,7 +84,7 @@ pub trait AlgorandMsgpack: Serialize + for<'de> Deserialize<'de> { /// /// This method performs canonical encoding and prepends the domain separation /// prefix defined by the PREFIX constant. - /// + /// /// Use `encode_raw()` if you want to encode without the prefix. /// /// # Returns @@ -140,3 +140,7 @@ pub trait TransactionId: AlgorandMsgpack { )) } } + +pub trait EstimateTransactionSize: AlgorandMsgpack { + fn estimate_size(&self) -> Result; +} diff --git a/crates/algokit_transact/src/transactions/mod.rs b/crates/algokit_transact/src/transactions/mod.rs index 14012ac4..37b07fe6 100644 --- a/crates/algokit_transact/src/transactions/mod.rs +++ b/crates/algokit_transact/src/transactions/mod.rs @@ -16,7 +16,7 @@ pub use payment::{PaymentTransactionBuilder, PaymentTransactionFields}; use crate::constants::HASH_BYTES_LENGTH; use crate::error::AlgoKitTransactError; -use crate::traits::{AlgorandMsgpack, TransactionId}; +use crate::traits::{AlgorandMsgpack, EstimateTransactionSize, TransactionId}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, Bytes}; use std::any::Any; @@ -59,6 +59,19 @@ impl AssetTransferTransactionBuilder { impl AlgorandMsgpack for Transaction {} impl TransactionId for Transaction {} +impl EstimateTransactionSize for Transaction { + fn estimate_size(&self) -> Result { + // We are simulating a signature on this transaction in order to encode it, and get the size + // with which we will be able to estimate the fee. + let signed_tx = SignedTransaction { + transaction: self.clone(), + // Avoiding a zero signature just in case it's compressed or removed in the encoding. + signature: [1; 64], + }; + return Ok(signed_tx.encode()?.len()); + } +} + /// A signed transaction. #[serde_as] #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] @@ -127,3 +140,9 @@ impl TransactionId for SignedTransaction { self.transaction.raw_id() } } + +impl EstimateTransactionSize for SignedTransaction { + fn estimate_size(&self) -> Result { + return Ok(self.encode()?.len()); + } +} diff --git a/crates/algokit_transact_ffi/src/lib.rs b/crates/algokit_transact_ffi/src/lib.rs index 71a29695..c15029b3 100644 --- a/crates/algokit_transact_ffi/src/lib.rs +++ b/crates/algokit_transact_ffi/src/lib.rs @@ -1,4 +1,4 @@ -use algokit_transact::{AlgorandMsgpack, Byte32, TransactionId}; +use algokit_transact::{AlgorandMsgpack, Byte32, EstimateTransactionSize, TransactionId}; use ffi_macros::{ffi_func, ffi_record}; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; @@ -411,18 +411,18 @@ pub fn attach_signature( /// This is useful for estimating the fee for the transaction. pub fn estimate_transaction_size(transaction: &Transaction) -> Result { let core_tx: algokit_transact::Transaction = transaction.clone().try_into()?; - // We are simulating a signature on this transaction in order to encode it, and get the size - // with which we will be able to estimate the fee. - let signed_tx = algokit_transact::SignedTransaction { - transaction: core_tx, - // Avoiding a zero signature just in case it's compressed or removed in the encoding. - signature: [1; 64], - }; - return Ok(signed_tx - .encode()? - .len() + return core_tx + .estimate_size() + .map_err(|e| { + AlgoKitTransactError::EncodingError(format!( + "Failed to estimate transaction size: {}", + e + )) + })? .try_into() - .expect("size should fit in u64")); + .map_err(|_| { + AlgoKitTransactError::EncodingError("Failed to convert size to u64".to_string()) + }); } #[ffi_func] From 8bb8860d9f2294a690e53d10a2a9020f6fd772c8 Mon Sep 17 00:00:00 2001 From: CiottiGiorgio Date: Fri, 16 May 2025 16:47:00 +0200 Subject: [PATCH 3/3] fix: use a pre-computed constant to determine the impact of a signature in the encoded transaction --- crates/algokit_transact/src/constants.rs | 1 + crates/algokit_transact/src/test_utils/mod.rs | 20 +++++++++++-------- .../algokit_transact/src/transactions/mod.rs | 11 ++-------- crates/algokit_transact_ffi/src/lib.rs | 3 ++- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/crates/algokit_transact/src/constants.rs b/crates/algokit_transact/src/constants.rs index 8ee12feb..f3fa7413 100644 --- a/crates/algokit_transact/src/constants.rs +++ b/crates/algokit_transact/src/constants.rs @@ -2,4 +2,5 @@ pub const HASH_BYTES_LENGTH: usize = 32; pub const ALGORAND_CHECKSUM_BYTE_LENGTH: usize = 4; pub const ALGORAND_ADDRESS_LENGTH: usize = 58; pub const ALGORAND_PUBLIC_KEY_BYTE_LENGTH: usize = 32; +pub const ALGORAND_SIGNATURE_ENCODING_INCR: usize = 75; pub type Byte32 = [u8; 32]; diff --git a/crates/algokit_transact/src/test_utils/mod.rs b/crates/algokit_transact/src/test_utils/mod.rs index cb7f7101..d0ee164e 100644 --- a/crates/algokit_transact/src/test_utils/mod.rs +++ b/crates/algokit_transact/src/test_utils/mod.rs @@ -1,4 +1,3 @@ -use std::fs::File; use crate::{ transactions::{AssetTransferTransactionBuilder, PaymentTransactionBuilder}, Address, AlgorandMsgpack, Byte32, SignedTransaction, Transaction, TransactionHeaderBuilder, @@ -9,6 +8,7 @@ use convert_case::{Case, Casing}; use ed25519_dalek::{Signer, SigningKey}; use serde::Serialize; use serde_json::to_writer_pretty; +use std::fs::File; pub struct TransactionHeaderMother {} impl TransactionHeaderMother { @@ -56,8 +56,9 @@ impl TransactionMother { .header(TransactionHeaderMother::simple_testnet().build().unwrap()) .amount(101000) .receiver( - "VXH5UP6JLU2CGIYPUFZ4Z5OTLJCLMA5EXD3YHTMVNDE5P7ILZ324FSYSPQ".parse::
() - .unwrap() + "VXH5UP6JLU2CGIYPUFZ4Z5OTLJCLMA5EXD3YHTMVNDE5P7ILZ324FSYSPQ" + .parse::
() + .unwrap(), ) .to_owned() } @@ -83,8 +84,9 @@ impl TransactionMother { .header( TransactionHeaderMother::simple_testnet() .sender( - "JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA".parse::
() - .unwrap() + "JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA" + .parse::
() + .unwrap(), ) .first_valid(51183672) .last_valid(51183872) @@ -94,8 +96,9 @@ impl TransactionMother { .asset_id(107686045) .amount(0) .receiver( - "JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA".parse::
() - .unwrap() + "JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA" + .parse::
() + .unwrap(), ) .to_owned() } @@ -108,7 +111,8 @@ impl AddressMother { } pub fn address() -> Address { - "RIMARGKZU46OZ77OLPDHHPUJ7YBSHRTCYMQUC64KZCCMESQAFQMYU6SL2Q".parse::
() + "RIMARGKZU46OZ77OLPDHHPUJ7YBSHRTCYMQUC64KZCCMESQAFQMYU6SL2Q" + .parse::
() .unwrap() } } diff --git a/crates/algokit_transact/src/transactions/mod.rs b/crates/algokit_transact/src/transactions/mod.rs index 37b07fe6..4c8caee0 100644 --- a/crates/algokit_transact/src/transactions/mod.rs +++ b/crates/algokit_transact/src/transactions/mod.rs @@ -14,7 +14,7 @@ pub use common::{TransactionHeader, TransactionHeaderBuilder}; use payment::PaymentTransactionBuilderError; pub use payment::{PaymentTransactionBuilder, PaymentTransactionFields}; -use crate::constants::HASH_BYTES_LENGTH; +use crate::constants::{ALGORAND_SIGNATURE_ENCODING_INCR, HASH_BYTES_LENGTH}; use crate::error::AlgoKitTransactError; use crate::traits::{AlgorandMsgpack, EstimateTransactionSize, TransactionId}; use serde::{Deserialize, Serialize}; @@ -61,14 +61,7 @@ impl TransactionId for Transaction {} impl EstimateTransactionSize for Transaction { fn estimate_size(&self) -> Result { - // We are simulating a signature on this transaction in order to encode it, and get the size - // with which we will be able to estimate the fee. - let signed_tx = SignedTransaction { - transaction: self.clone(), - // Avoiding a zero signature just in case it's compressed or removed in the encoding. - signature: [1; 64], - }; - return Ok(signed_tx.encode()?.len()); + return Ok(self.encode()?.len() + ALGORAND_SIGNATURE_ENCODING_INCR); } } diff --git a/crates/algokit_transact_ffi/src/lib.rs b/crates/algokit_transact_ffi/src/lib.rs index c15029b3..f882d5f2 100644 --- a/crates/algokit_transact_ffi/src/lib.rs +++ b/crates/algokit_transact_ffi/src/lib.rs @@ -437,7 +437,8 @@ pub fn address_from_pub_key(pub_key: &[u8]) -> Result Result { - address.parse::() + address + .parse::() .map(Into::into) .map_err(|e| AlgoKitTransactError::EncodingError(e.to_string())) }