Skip to content

Commit f566b46

Browse files
Merge pull request #81 from algorandfoundation/feat/transaction-estimate
feat: estimate transaction size in the FFI crate
1 parent c6de723 commit f566b46

File tree

7 files changed

+79
-16
lines changed

7 files changed

+79
-16
lines changed

crates/algokit_transact/src/constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ pub const HASH_BYTES_LENGTH: usize = 32;
22
pub const ALGORAND_CHECKSUM_BYTE_LENGTH: usize = 4;
33
pub const ALGORAND_ADDRESS_LENGTH: usize = 58;
44
pub const ALGORAND_PUBLIC_KEY_BYTE_LENGTH: usize = 32;
5+
pub const ALGORAND_SIGNATURE_ENCODING_INCR: usize = 75;
56
pub type Byte32 = [u8; 32];

crates/algokit_transact/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pub use constants::{
1212
ALGORAND_PUBLIC_KEY_BYTE_LENGTH, HASH_BYTES_LENGTH,
1313
};
1414
pub use error::AlgoKitTransactError;
15-
pub use traits::{AlgorandMsgpack, TransactionId};
15+
pub use traits::{AlgorandMsgpack, EstimateTransactionSize, TransactionId};
1616
pub use transactions::{
1717
AssetTransferTransactionBuilder, AssetTransferTransactionFields, PaymentTransactionBuilder,
1818
PaymentTransactionFields, SignedTransaction, Transaction, TransactionHeader,

crates/algokit_transact/src/test_utils/mod.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use std::fs::File;
21
use crate::{
32
transactions::{AssetTransferTransactionBuilder, PaymentTransactionBuilder},
43
Address, AlgorandMsgpack, Byte32, SignedTransaction, Transaction, TransactionHeaderBuilder,
@@ -9,6 +8,7 @@ use convert_case::{Case, Casing};
98
use ed25519_dalek::{Signer, SigningKey};
109
use serde::Serialize;
1110
use serde_json::to_writer_pretty;
11+
use std::fs::File;
1212

1313
pub struct TransactionHeaderMother {}
1414
impl TransactionHeaderMother {
@@ -56,8 +56,9 @@ impl TransactionMother {
5656
.header(TransactionHeaderMother::simple_testnet().build().unwrap())
5757
.amount(101000)
5858
.receiver(
59-
"VXH5UP6JLU2CGIYPUFZ4Z5OTLJCLMA5EXD3YHTMVNDE5P7ILZ324FSYSPQ".parse::<Address>()
60-
.unwrap()
59+
"VXH5UP6JLU2CGIYPUFZ4Z5OTLJCLMA5EXD3YHTMVNDE5P7ILZ324FSYSPQ"
60+
.parse::<Address>()
61+
.unwrap(),
6162
)
6263
.to_owned()
6364
}
@@ -83,8 +84,9 @@ impl TransactionMother {
8384
.header(
8485
TransactionHeaderMother::simple_testnet()
8586
.sender(
86-
"JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA".parse::<Address>()
87-
.unwrap()
87+
"JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA"
88+
.parse::<Address>()
89+
.unwrap(),
8890
)
8991
.first_valid(51183672)
9092
.last_valid(51183872)
@@ -94,8 +96,9 @@ impl TransactionMother {
9496
.asset_id(107686045)
9597
.amount(0)
9698
.receiver(
97-
"JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA".parse::<Address>()
98-
.unwrap()
99+
"JB3K6HTAXODO4THESLNYTSG6GQUFNEVIQG7A6ZYVDACR6WA3ZF52TKU5NA"
100+
.parse::<Address>()
101+
.unwrap(),
99102
)
100103
.to_owned()
101104
}
@@ -108,7 +111,8 @@ impl AddressMother {
108111
}
109112

110113
pub fn address() -> Address {
111-
"RIMARGKZU46OZ77OLPDHHPUJ7YBSHRTCYMQUC64KZCCMESQAFQMYU6SL2Q".parse::<Address>()
114+
"RIMARGKZU46OZ77OLPDHHPUJ7YBSHRTCYMQUC64KZCCMESQAFQMYU6SL2Q"
115+
.parse::<Address>()
112116
.unwrap()
113117
}
114118
}

crates/algokit_transact/src/tests.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use crate::constants::ALGORAND_SIGNATURE_ENCODING_INCR;
12
use crate::{
23
test_utils::{AddressMother, TransactionMother},
3-
Address, AlgorandMsgpack, SignedTransaction, Transaction, TransactionId,
4+
Address, AlgorandMsgpack, EstimateTransactionSize, SignedTransaction, Transaction,
5+
TransactionId,
46
};
57
use pretty_assertions::assert_eq;
68

@@ -119,3 +121,23 @@ fn test_pay_transaction_id() {
119121
assert_eq!(payment_tx.id().unwrap(), expected_tx_id);
120122
assert_eq!(signed_tx.id().unwrap(), expected_tx_id);
121123
}
124+
125+
#[test]
126+
fn test_estimate_transaction_size() {
127+
let tx_builder = TransactionMother::simple_payment();
128+
let payment_tx = tx_builder.build().unwrap();
129+
let encoding_length = payment_tx.encode_raw().unwrap().len();
130+
let estimation = payment_tx.estimate_size().unwrap();
131+
132+
let signed_tx = SignedTransaction {
133+
transaction: payment_tx.clone(),
134+
signature: [0; 64],
135+
};
136+
let actual_size = signed_tx.encode().unwrap().len();
137+
138+
assert_eq!(
139+
estimation,
140+
encoding_length + ALGORAND_SIGNATURE_ENCODING_INCR
141+
);
142+
assert_eq!(estimation, actual_size);
143+
}

crates/algokit_transact/src/traits.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub trait AlgorandMsgpack: Serialize + for<'de> Deserialize<'de> {
5858
/// * `bytes` - The MessagePack encoded bytes
5959
///
6060
/// # Returns
61-
/// The decoded instance or an AlgoKitTransactError if the input is empty or
61+
/// The decoded instance or an AlgoKitTransactError if the input is empty or
6262
/// deserialization fails.
6363
fn decode(bytes: &[u8]) -> Result<Self, AlgoKitTransactError> {
6464
if bytes.is_empty() {
@@ -84,7 +84,7 @@ pub trait AlgorandMsgpack: Serialize + for<'de> Deserialize<'de> {
8484
///
8585
/// This method performs canonical encoding and prepends the domain separation
8686
/// prefix defined by the PREFIX constant.
87-
///
87+
///
8888
/// Use `encode_raw()` if you want to encode without the prefix.
8989
///
9090
/// # Returns
@@ -140,3 +140,7 @@ pub trait TransactionId: AlgorandMsgpack {
140140
))
141141
}
142142
}
143+
144+
pub trait EstimateTransactionSize: AlgorandMsgpack {
145+
fn estimate_size(&self) -> Result<usize, AlgoKitTransactError>;
146+
}

crates/algokit_transact/src/transactions/mod.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ pub use common::{TransactionHeader, TransactionHeaderBuilder};
1414
use payment::PaymentTransactionBuilderError;
1515
pub use payment::{PaymentTransactionBuilder, PaymentTransactionFields};
1616

17-
use crate::constants::HASH_BYTES_LENGTH;
17+
use crate::constants::{ALGORAND_SIGNATURE_ENCODING_INCR, HASH_BYTES_LENGTH};
1818
use crate::error::AlgoKitTransactError;
19-
use crate::traits::{AlgorandMsgpack, TransactionId};
19+
use crate::traits::{AlgorandMsgpack, EstimateTransactionSize, TransactionId};
2020
use serde::{Deserialize, Serialize};
2121
use serde_with::{serde_as, Bytes};
2222
use std::any::Any;
@@ -59,6 +59,12 @@ impl AssetTransferTransactionBuilder {
5959
impl AlgorandMsgpack for Transaction {}
6060
impl TransactionId for Transaction {}
6161

62+
impl EstimateTransactionSize for Transaction {
63+
fn estimate_size(&self) -> Result<usize, AlgoKitTransactError> {
64+
return Ok(self.encode_raw()?.len() + ALGORAND_SIGNATURE_ENCODING_INCR);
65+
}
66+
}
67+
6268
/// A signed transaction.
6369
#[serde_as]
6470
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
@@ -127,3 +133,9 @@ impl TransactionId for SignedTransaction {
127133
self.transaction.raw_id()
128134
}
129135
}
136+
137+
impl EstimateTransactionSize for SignedTransaction {
138+
fn estimate_size(&self) -> Result<usize, AlgoKitTransactError> {
139+
return Ok(self.encode()?.len());
140+
}
141+
}

crates/algokit_transact_ffi/src/lib.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use algokit_transact::{AlgorandMsgpack, Byte32, TransactionId};
1+
use algokit_transact::{AlgorandMsgpack, Byte32, EstimateTransactionSize, TransactionId};
22
use ffi_macros::{ffi_func, ffi_record};
33
use serde::{Deserialize, Serialize};
44
use serde_bytes::ByteBuf;
@@ -406,6 +406,25 @@ pub fn attach_signature(
406406
Ok(signed_tx.encode()?)
407407
}
408408

409+
#[ffi_func]
410+
/// Return the size of the transaction in bytes as if it was already signed and encoded.
411+
/// This is useful for estimating the fee for the transaction.
412+
pub fn estimate_transaction_size(transaction: &Transaction) -> Result<u64, AlgoKitTransactError> {
413+
let core_tx: algokit_transact::Transaction = transaction.clone().try_into()?;
414+
return core_tx
415+
.estimate_size()
416+
.map_err(|e| {
417+
AlgoKitTransactError::EncodingError(format!(
418+
"Failed to estimate transaction size: {}",
419+
e
420+
))
421+
})?
422+
.try_into()
423+
.map_err(|_| {
424+
AlgoKitTransactError::EncodingError("Failed to convert size to u64".to_string())
425+
});
426+
}
427+
409428
#[ffi_func]
410429
pub fn address_from_pub_key(pub_key: &[u8]) -> Result<Address, AlgoKitTransactError> {
411430
Ok(
@@ -418,7 +437,8 @@ pub fn address_from_pub_key(pub_key: &[u8]) -> Result<Address, AlgoKitTransactEr
418437

419438
#[ffi_func]
420439
pub fn address_from_string(address: &str) -> Result<Address, AlgoKitTransactError> {
421-
address.parse::<algokit_transact::Address>()
440+
address
441+
.parse::<algokit_transact::Address>()
422442
.map(Into::into)
423443
.map_err(|e| AlgoKitTransactError::EncodingError(e.to_string()))
424444
}

0 commit comments

Comments
 (0)