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/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/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/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..4c8caee0 100644 --- a/crates/algokit_transact/src/transactions/mod.rs +++ b/crates/algokit_transact/src/transactions/mod.rs @@ -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; @@ -59,6 +59,12 @@ impl AssetTransferTransactionBuilder { impl AlgorandMsgpack for Transaction {} impl TransactionId for Transaction {} +impl EstimateTransactionSize for Transaction { + fn estimate_size(&self) -> Result { + return Ok(self.encode()?.len() + ALGORAND_SIGNATURE_ENCODING_INCR); + } +} + /// A signed transaction. #[serde_as] #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] @@ -127,3 +133,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 6eb3cbbe..f882d5f2 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; @@ -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()?; + 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 { Ok( @@ -418,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())) }