Skip to content

feat: estimate transaction size in the FFI crate #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 19, 2025
Merged
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
1 change: 1 addition & 0 deletions crates/algokit_transact/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
2 changes: 1 addition & 1 deletion crates/algokit_transact/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 12 additions & 8 deletions crates/algokit_transact/src/test_utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::fs::File;
use crate::{
transactions::{AssetTransferTransactionBuilder, PaymentTransactionBuilder},
Address, AlgorandMsgpack, Byte32, SignedTransaction, Transaction, TransactionHeaderBuilder,
Expand All @@ -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 {
Expand Down Expand Up @@ -56,8 +56,9 @@ impl TransactionMother {
.header(TransactionHeaderMother::simple_testnet().build().unwrap())
.amount(101000)
.receiver(
"VXH5UP6JLU2CGIYPUFZ4Z5OTLJCLMA5EXD3YHTMVNDE5P7ILZ324FSYSPQ".parse::<Address>()
.unwrap()
"VXH5UP6JLU2CGIYPUFZ4Z5OTLJCLMA5EXD3YHTMVNDE5P7ILZ324FSYSPQ"
.parse::<Address>()
.unwrap(),
)
.to_owned()
}
Expand All @@ -83,8 +84,9 @@ impl TransactionMother {
.header(
TransactionHeaderMother::simple_testnet()
.sender(
"JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA".parse::<Address>()
.unwrap()
"JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA"
.parse::<Address>()
.unwrap(),
)
.first_valid(51183672)
.last_valid(51183872)
Expand All @@ -94,8 +96,9 @@ impl TransactionMother {
.asset_id(107686045)
.amount(0)
.receiver(
"JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA".parse::<Address>()
.unwrap()
"JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA"
.parse::<Address>()
.unwrap(),
)
.to_owned()
}
Expand All @@ -108,7 +111,8 @@ impl AddressMother {
}

pub fn address() -> Address {
"RIMARGKZU46OZ77OLPDHHPUJ7YBSHRTCYMQUC64KZCCMESQAFQMYU6SL2Q".parse::<Address>()
"RIMARGKZU46OZ77OLPDHHPUJ7YBSHRTCYMQUC64KZCCMESQAFQMYU6SL2Q"
.parse::<Address>()
.unwrap()
}
}
Expand Down
24 changes: 23 additions & 1 deletion crates/algokit_transact/src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::constants::ALGORAND_SIGNATURE_ENCODING_INCR;
use crate::{
test_utils::{AddressMother, TransactionMother},
Address, AlgorandMsgpack, SignedTransaction, Transaction, TransactionId,
Address, AlgorandMsgpack, EstimateTransactionSize, SignedTransaction, Transaction,
TransactionId,
};
use pretty_assertions::assert_eq;

Expand Down Expand Up @@ -119,3 +121,23 @@ fn test_pay_transaction_id() {
assert_eq!(payment_tx.id().unwrap(), expected_tx_id);
assert_eq!(signed_tx.id().unwrap(), expected_tx_id);
}

#[test]
fn test_estimate_transaction_size() {
let tx_builder = TransactionMother::simple_payment();
let payment_tx = tx_builder.build().unwrap();
let encoding_length = payment_tx.encode_raw().unwrap().len();
let estimation = payment_tx.estimate_size().unwrap();

let signed_tx = SignedTransaction {
transaction: payment_tx.clone(),
signature: [0; 64],
};
let actual_size = signed_tx.encode().unwrap().len();

assert_eq!(
estimation,
encoding_length + ALGORAND_SIGNATURE_ENCODING_INCR
);
assert_eq!(estimation, actual_size);
}
8 changes: 6 additions & 2 deletions crates/algokit_transact/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, AlgoKitTransactError> {
if bytes.is_empty() {
Expand All @@ -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
Expand Down Expand Up @@ -140,3 +140,7 @@ pub trait TransactionId: AlgorandMsgpack {
))
}
}

pub trait EstimateTransactionSize: AlgorandMsgpack {
fn estimate_size(&self) -> Result<usize, AlgoKitTransactError>;
}
16 changes: 14 additions & 2 deletions crates/algokit_transact/src/transactions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ 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, TransactionId};
use crate::traits::{AlgorandMsgpack, EstimateTransactionSize, TransactionId};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, Bytes};
use std::any::Any;
Expand Down Expand Up @@ -59,6 +59,12 @@ impl AssetTransferTransactionBuilder {
impl AlgorandMsgpack for Transaction {}
impl TransactionId for Transaction {}

impl EstimateTransactionSize for Transaction {
fn estimate_size(&self) -> Result<usize, AlgoKitTransactError> {
return Ok(self.encode_raw()?.len() + ALGORAND_SIGNATURE_ENCODING_INCR);
}
}

/// A signed transaction.
#[serde_as]
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
Expand Down Expand Up @@ -127,3 +133,9 @@ impl TransactionId for SignedTransaction {
self.transaction.raw_id()
}
}

impl EstimateTransactionSize for SignedTransaction {
fn estimate_size(&self) -> Result<usize, AlgoKitTransactError> {
return Ok(self.encode()?.len());
}
}
24 changes: 22 additions & 2 deletions crates/algokit_transact_ffi/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<u64, AlgoKitTransactError> {
let core_tx: algokit_transact::Transaction = transaction.clone().try_into()?;
return core_tx
.estimate_size()
.map_err(|e| {
AlgoKitTransactError::EncodingError(format!(
"Failed to estimate transaction size: {}",
e
))
})?
.try_into()
.map_err(|_| {
AlgoKitTransactError::EncodingError("Failed to convert size to u64".to_string())
});
}

#[ffi_func]
pub fn address_from_pub_key(pub_key: &[u8]) -> Result<Address, AlgoKitTransactError> {
Ok(
Expand All @@ -418,7 +437,8 @@ pub fn address_from_pub_key(pub_key: &[u8]) -> Result<Address, AlgoKitTransactEr

#[ffi_func]
pub fn address_from_string(address: &str) -> Result<Address, AlgoKitTransactError> {
address.parse::<algokit_transact::Address>()
address
.parse::<algokit_transact::Address>()
.map(Into::into)
.map_err(|e| AlgoKitTransactError::EncodingError(e.to_string()))
}
Expand Down