From 5b3ed30d53cc006f24e30dd0339fd0aba9732e65 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Fri, 7 Nov 2025 15:07:47 +0200 Subject: [PATCH 1/9] feat: lambda tx Signed-off-by: Ivaylo Nikolov --- src/hooks/lambda_s_store_transaction.rs | 177 ++++++++++++++++++++++++ src/transaction/any.rs | 23 ++- 2 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 src/hooks/lambda_s_store_transaction.rs diff --git a/src/hooks/lambda_s_store_transaction.rs b/src/hooks/lambda_s_store_transaction.rs new file mode 100644 index 000000000..2abfad5c5 --- /dev/null +++ b/src/hooks/lambda_s_store_transaction.rs @@ -0,0 +1,177 @@ +use hedera_proto::services; +use hedera_proto::services::smart_contract_service_client::SmartContractServiceClient; +use tonic::transport::Channel; + +use crate::hooks::{ + HookId, + LambdaStorageUpdate, +}; +use crate::ledger_id::RefLedgerId; +use crate::protobuf::ToProtobuf; +use crate::transaction::{ + ChunkInfo, + ToTransactionDataProtobuf, + Transaction, + TransactionData, + TransactionExecute, +}; +use crate::{ + BoxGrpcFuture, + ValidateChecksums, +}; + +/// A transaction to store lambda data in hook storage. +pub type LambdaSStoreTransaction = Transaction; + +#[derive(Debug, Clone)] +pub struct LambdaSStoreTransactionData { + /// The hook ID to store data for. + hook_id: Option, + /// The storage updates to apply. + storage_updates: Vec, +} + +impl Default for LambdaSStoreTransactionData { + fn default() -> Self { + Self { hook_id: None, storage_updates: Vec::new() } + } +} + +impl LambdaSStoreTransaction { + /// Set the hook ID. + pub fn hook_id(&mut self, hook_id: HookId) -> &mut Self { + self.data_mut().hook_id = Some(hook_id); + self + } + + /// Set the storage updates. + pub fn storage_updates(&mut self, storage_updates: Vec) -> &mut Self { + self.data_mut().storage_updates = storage_updates; + self + } + + /// Add a storage update. + pub fn add_storage_update(&mut self, storage_update: LambdaStorageUpdate) -> &mut Self { + self.data_mut().storage_updates.push(storage_update); + self + } + + /// Get the hook ID. + pub fn get_hook_id(&self) -> Option<&HookId> { + self.data().hook_id.as_ref() + } + + /// Get the storage updates. + pub fn get_storage_updates(&self) -> &[LambdaStorageUpdate] { + &self.data().storage_updates + } +} + +impl LambdaSStoreTransactionData { + /// Create a new `LambdaSStoreTransactionData`. + pub fn new() -> Self { + Self::default() + } +} + +impl TransactionData for LambdaSStoreTransactionData { + fn default_max_transaction_fee(&self) -> crate::Hbar { + crate::Hbar::new(2) + } +} + +impl ValidateChecksums for LambdaSStoreTransactionData { + fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), crate::Error> { + if let Some(hook_id) = &self.hook_id { + hook_id.entity_id.validate_checksums(ledger_id)?; + } + Ok(()) + } +} + +impl TransactionExecute for LambdaSStoreTransactionData { + fn execute( + &self, + channel: Channel, + request: services::Transaction, + ) -> BoxGrpcFuture<'_, services::TransactionResponse> { + Box::pin(async { SmartContractServiceClient::new(channel).lambda_s_store(request).await }) + } +} + +impl ToTransactionDataProtobuf for LambdaSStoreTransactionData { + fn to_transaction_data_protobuf( + &self, + chunk_info: &ChunkInfo, + ) -> services::transaction_body::Data { + let _ = chunk_info.assert_single_transaction(); + services::transaction_body::Data::LambdaSstore(self.to_protobuf()) + } +} + +impl crate::protobuf::ToProtobuf for LambdaSStoreTransactionData { + type Protobuf = services::LambdaSStoreTransactionBody; + + fn to_protobuf(&self) -> Self::Protobuf { + services::LambdaSStoreTransactionBody { + hook_id: self.hook_id.as_ref().map(|id| id.to_protobuf()), + storage_updates: self + .storage_updates + .iter() + .map(|update| update.to_protobuf()) + .collect(), + } + } +} + +impl crate::protobuf::FromProtobuf + for LambdaSStoreTransactionData +{ + fn from_protobuf(pb: services::LambdaSStoreTransactionBody) -> crate::Result { + let hook_id = pb.hook_id.map(HookId::from_protobuf).transpose()?; + + let storage_updates = pb + .storage_updates + .into_iter() + .map(LambdaStorageUpdate::from_protobuf) + .collect::, _>>()?; + + Ok(Self { hook_id, storage_updates }) + } +} + +impl From for crate::transaction::AnyTransactionData { + fn from(transaction: LambdaSStoreTransactionData) -> Self { + Self::LambdaSStore(transaction) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hooks::{ + HookId, + LambdaStorageSlot, + LambdaStorageUpdate, + }; + + #[test] + fn test_lambda_s_store_transaction_creation() { + let hook_id = HookId::new(None, 123); + let storage_slot = LambdaStorageSlot::new(vec![1, 2, 3], vec![4, 5, 6]); + let storage_update = LambdaStorageUpdate::StorageSlot(storage_slot); + + let mut transaction = LambdaSStoreTransaction::new(); + transaction.hook_id(hook_id.clone()).add_storage_update(storage_update); + + assert_eq!(transaction.get_hook_id(), Some(&hook_id)); + assert_eq!(transaction.get_storage_updates().len(), 1); + } + + #[test] + fn test_lambda_s_store_transaction_default() { + let transaction = LambdaSStoreTransaction::new(); + assert_eq!(transaction.get_hook_id(), None); + assert_eq!(transaction.get_storage_updates().len(), 0); + } +} diff --git a/src/transaction/any.rs b/src/transaction/any.rs index be10410f6..53316eb2e 100644 --- a/src/transaction/any.rs +++ b/src/transaction/any.rs @@ -54,6 +54,7 @@ mod data { FileDeleteTransactionData as FileDelete, FileUpdateTransactionData as FileUpdate, }; + pub(super) use crate::hooks::LambdaSStoreTransactionData as LambdaSStore; pub(super) use crate::prng_transaction::PrngTransactionData as Prng; pub(super) use crate::schedule::{ ScheduleCreateTransactionData as ScheduleCreate, @@ -152,6 +153,7 @@ pub enum AnyTransactionData { TokenClaimAirdrop(data::TokenClaimAirdrop), TokenCancelAirdrop(data::TokenCancelAirdrop), Batch(data::Batch), + LambdaSStore(data::LambdaSStore), } impl ToTransactionDataProtobuf for AnyTransactionData { @@ -305,6 +307,7 @@ impl ToTransactionDataProtobuf for AnyTransactionData { transaction.to_transaction_data_protobuf(chunk_info) } Self::Batch(transaction) => transaction.to_transaction_data_protobuf(chunk_info), + Self::LambdaSStore(transaction) => transaction.to_transaction_data_protobuf(chunk_info), } } } @@ -362,6 +365,7 @@ impl TransactionData for AnyTransactionData { Self::TokenClaimAirdrop(transaction) => transaction.default_max_transaction_fee(), Self::TokenCancelAirdrop(transaction) => transaction.default_max_transaction_fee(), Self::Batch(transaction) => transaction.default_max_transaction_fee(), + Self::LambdaSStore(transaction) => transaction.default_max_transaction_fee(), } } @@ -417,6 +421,7 @@ impl TransactionData for AnyTransactionData { Self::TokenClaimAirdrop(it) => it.maybe_chunk_data(), Self::TokenCancelAirdrop(it) => it.maybe_chunk_data(), Self::Batch(it) => it.maybe_chunk_data(), + Self::LambdaSStore(it) => it.maybe_chunk_data(), } } @@ -472,6 +477,7 @@ impl TransactionData for AnyTransactionData { Self::TokenClaimAirdrop(it) => it.wait_for_receipt(), Self::TokenCancelAirdrop(it) => it.wait_for_receipt(), Self::Batch(it) => it.wait_for_receipt(), + Self::LambdaSStore(it) => it.wait_for_receipt(), } } } @@ -533,6 +539,7 @@ impl TransactionExecute for AnyTransactionData { Self::TokenClaimAirdrop(transaction) => transaction.execute(channel, request), Self::TokenCancelAirdrop(transaction) => transaction.execute(channel, request), Self::Batch(transaction) => transaction.execute(channel, request), + Self::LambdaSStore(transaction) => transaction.execute(channel, request), } } } @@ -592,6 +599,7 @@ impl ValidateChecksums for AnyTransactionData { Self::TokenClaimAirdrop(transaction) => transaction.validate_checksums(ledger_id), Self::TokenCancelAirdrop(transaction) => transaction.validate_checksums(ledger_id), Self::Batch(transaction) => transaction.validate_checksums(ledger_id), + Self::LambdaSStore(transaction) => transaction.validate_checksums(ledger_id), } } } @@ -678,6 +686,12 @@ impl FromProtobuf for AnyTransactionData { "unsupported transaction `NodeStakeUpdateTransaction`", )) } + Data::LambdaSstore(pb) => data::LambdaSStore::from_protobuf(pb)?.into(), + Data::HookDispatch(_) => { + return Err(Error::from_protobuf( + "unsupported transaction `HookDispatchTransaction`", + )) + } Data::AtomicBatch(_) => { return Err(Error::from_protobuf( "unsupported transaction `AtomicBatchTransaction`", @@ -1064,6 +1078,12 @@ impl FromProtobuf> for ServicesTransaction "unsupported transaction `NodeStakeUpdateTransaction`", )) } + Data::LambdaSstore(_) => { + return Err(Error::from_protobuf("LambdaSstore transactions cannot be chunked")) + } + Data::HookDispatch(_) => { + return Err(Error::from_protobuf("HookDispatch transactions are not supported")) + } Data::AtomicBatch(_) => { return Err(Error::from_protobuf("AtomicBatch transactions are not supported")) } @@ -1249,5 +1269,6 @@ impl_cast_any! { TokenAirdrop, TokenClaimAirdrop, TokenCancelAirdrop, - Batch + Batch, + LambdaSStore } From 6d6f4fbfc92751b2b0decfd1a16ae75e12b2db5f Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Fri, 7 Nov 2025 15:08:28 +0200 Subject: [PATCH 2/9] feat: transfer tx with hooks Signed-off-by: Ivaylo Nikolov --- examples/transfer_with_hooks.rs | 275 ++++++++ src/fee_schedules.rs | 1 + src/hooks/nft_hook_call.rs | 74 +++ src/hooks/nft_hook_type.rs | 35 ++ src/query/payment_transaction.rs | 2 + src/schedule/schedulable_transaction_body.rs | 3 + src/schedule/schedule_create_transaction.rs | 4 + .../transaction_record/serialize.txt | 2 + .../transaction_record/serialize2.txt | 2 + .../token_airdrop_transaction/serialize.txt | 18 + src/token/token_airdrop_transaction.rs | 26 +- src/token/token_nft_transfer.rs | 38 ++ src/transaction_record.rs | 5 + src/transfer.rs | 41 +- src/transfer_transaction.rs | 219 ++++++- tests/e2e/token/mod.rs | 1 + tests/e2e/token/transfer_with_hooks.rs | 594 ++++++++++++++++++ 17 files changed, 1322 insertions(+), 18 deletions(-) create mode 100644 examples/transfer_with_hooks.rs create mode 100644 src/hooks/nft_hook_call.rs create mode 100644 src/hooks/nft_hook_type.rs create mode 100644 tests/e2e/token/transfer_with_hooks.rs diff --git a/examples/transfer_with_hooks.rs b/examples/transfer_with_hooks.rs new file mode 100644 index 000000000..a602ee7eb --- /dev/null +++ b/examples/transfer_with_hooks.rs @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use hedera::{ + AccountCreateTransaction, AccountId, Client, ContractCreateTransaction, ContractId, EvmHookCall, EvmHookSpec, FungibleHookCall, FungibleHookType, Hbar, HookCall, HookCreationDetails, HookExtensionPoint, LambdaEvmHook, NftHookCall, NftHookType, PrivateKey, TokenCreateTransaction, TokenMintTransaction, TokenSupplyType, TokenType, TransferTransaction +}; + +#[derive(Parser, Debug)] +struct Args { + #[clap(long, env)] + operator_account_id: AccountId, + + #[clap(long, env)] + operator_key: PrivateKey, + + #[clap(long, env, default_value = "testnet")] + hedera_network: String, +} + +const HOOK_BYTECODE: &str = "6080604052348015600e575f5ffd5b506107d18061001c5f395ff3fe608060405260043610610033575f3560e01c8063124d8b301461003757806394112e2f14610067578063bd0dd0b614610097575b5f5ffd5b610051600480360381019061004c91906106f2565b6100c7565b60405161005e9190610782565b60405180910390f35b610081600480360381019061007c91906106f2565b6100d2565b60405161008e9190610782565b60405180910390f35b6100b160048036038101906100ac91906106f2565b6100dd565b6040516100be9190610782565b60405180910390f35b5f6001905092915050565b5f6001905092915050565b5f6001905092915050565b5f604051905090565b5f5ffd5b5f5ffd5b5f5ffd5b5f60a08284031215610112576101116100f9565b5b81905092915050565b5f5ffd5b5f601f19601f8301169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6101658261011f565b810181811067ffffffffffffffff821117156101845761018361012f565b5b80604052505050565b5f6101966100e8565b90506101a2828261015c565b919050565b5f5ffd5b5f5ffd5b5f67ffffffffffffffff8211156101c9576101c861012f565b5b602082029050602081019050919050565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610207826101de565b9050919050565b610217816101fd565b8114610221575f5ffd5b50565b5f813590506102328161020e565b92915050565b5f8160070b9050919050565b61024d81610238565b8114610257575f5ffd5b50565b5f8135905061026881610244565b92915050565b5f604082840312156102835761028261011b565b5b61028d604061018d565b90505f61029c84828501610224565b5f8301525060206102af8482850161025a565b60208301525092915050565b5f6102cd6102c8846101af565b61018d565b905080838252602082019050604084028301858111156102f0576102ef6101da565b5b835b818110156103195780610305888261026e565b8452602084019350506040810190506102f2565b5050509392505050565b5f82601f830112610337576103366101ab565b5b81356103478482602086016102bb565b91505092915050565b5f67ffffffffffffffff82111561036a5761036961012f565b5b602082029050602081019050919050565b5f67ffffffffffffffff8211156103955761039461012f565b5b602082029050602081019050919050565b5f606082840312156103bb576103ba61011b565b5b6103c5606061018d565b90505f6103d484828501610224565b5f8301525060206103e784828501610224565b60208301525060406103fb8482850161025a565b60408301525092915050565b5f6104196104148461037b565b61018d565b9050808382526020820190506060840283018581111561043c5761043b6101da565b5b835b81811015610465578061045188826103a6565b84526020840193505060608101905061043e565b5050509392505050565b5f82601f830112610483576104826101ab565b5b8135610493848260208601610407565b91505092915050565b5f606082840312156104b1576104b061011b565b5b6104bb606061018d565b90505f6104ca84828501610224565b5f83015250602082013567ffffffffffffffff8111156104ed576104ec6101a7565b5b6104f984828501610323565b602083015250604082013567ffffffffffffffff81111561051d5761051c6101a7565b5b6105298482850161046f565b60408301525092915050565b5f61054761054284610350565b61018d565b9050808382526020820190506020840283018581111561056a576105696101da565b5b835b818110156105b157803567ffffffffffffffff81111561058f5761058e6101ab565b5b80860161059c898261049c565b8552602085019450505060208101905061056c565b5050509392505050565b5f82601f8301126105cf576105ce6101ab565b5b81356105df848260208601610535565b91505092915050565b5f604082840312156105fd576105fc61011b565b5b610607604061018d565b90505f82013567ffffffffffffffff811115610626576106256101a7565b5b61063284828501610323565b5f83015250602082013567ffffffffffffffff811115610655576106546101a7565b5b610661848285016105bb565b60208301525092915050565b5f604082840312156106825761068161011b565b5b61068c604061018d565b90505f82013567ffffffffffffffff8111156106ab576106aa6101a7565b5b6106b7848285016105e8565b5f83015250602082013567ffffffffffffffff8111156106da576106d96101a7565b5b6106e6848285016105e8565b60208301525092915050565b5f5f60408385031215610708576107076100f1565b5b5f83013567ffffffffffffffff811115610725576107246100f5565b5b610731858286016100fd565b925050602083013567ffffffffffffffff811115610752576107516100f5565b5b61075e8582860161066d565b9150509250929050565b5f8115159050919050565b61077c81610768565b82525050565b5f6020820190506107955f830184610773565b9291505056fea26469706673582212207dfe7723f6d6869419b1cb0619758b439da0cf4ffd9520997c40a3946299d4dc64736f6c634300081e0033"; + +async fn create_hook_contract(client: &Client) -> anyhow::Result { + let bytecode = hex::decode(HOOK_BYTECODE)?; + + let receipt = ContractCreateTransaction::new() + .bytecode(bytecode) + .gas(1_700_000) + .execute(client) + .await? + .get_receipt(client) + .await?; + + Ok(receipt.contract_id.unwrap()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + let Args { + operator_account_id, + operator_key, + hedera_network, + } = Args::parse(); + + let client = Client::for_name(&hedera_network)?; + client.set_operator(operator_account_id, operator_key); + + println!("Transfer Transaction Hooks Example Start!"); + + // Step 1: Set up prerequisites - create hook contract + println!("Setting up prerequisites..."); + + let hook_contract_id = create_hook_contract(&client).await?; + println!("Created hook contract: {hook_contract_id}"); + + // Create hook details + let hook_id = 1; + let spec = EvmHookSpec::new(Some(hook_contract_id)); + let lambda_hook = LambdaEvmHook::new(spec, vec![]); + let hook_details = HookCreationDetails::new( + HookExtensionPoint::AccountAllowanceHook, + hook_id, + Some(lambda_hook), + ); + + // Create sender account with hook + let sender_key = PrivateKey::generate_ed25519(); + let sender_receipt = AccountCreateTransaction::new() + .set_key_without_alias(sender_key.public_key()) + .initial_balance(Hbar::new(10)) + .add_hook(hook_details.clone()) + .freeze_with(&client)? + .sign(sender_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let sender_account_id = sender_receipt.account_id.unwrap(); + println!("Created sender account: {sender_account_id}"); + + // Create receiver account with hook and unlimited token associations + let receiver_key = PrivateKey::generate_ed25519(); + let receiver_receipt = AccountCreateTransaction::new() + .set_key_without_alias(receiver_key.public_key()) + .initial_balance(Hbar::new(10)) + .max_automatic_token_associations(-1) + .add_hook(hook_details) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let receiver_account_id = receiver_receipt.account_id.unwrap(); + println!("Created receiver account: {receiver_account_id}"); + + // Create fungible token + println!("Creating fungible token..."); + let fungible_token_id = TokenCreateTransaction::new() + .name("Example Fungible Token") + .symbol("EFT") + .decimals(2) + .initial_supply(10_000) + .treasury_account_id(sender_account_id) + .admin_key(sender_key.public_key()) + .supply_key(sender_key.public_key()) + .token_type(TokenType::FungibleCommon) + .token_supply_type(TokenSupplyType::Infinite) + .freeze_with(&client)? + .sign(sender_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .token_id + .unwrap(); + + println!("Created fungible token: {fungible_token_id}"); + + // Create NFT token + println!("Creating NFT token..."); + let nft_token_id = TokenCreateTransaction::new() + .name("Example NFT Token") + .symbol("ENT") + .treasury_account_id(sender_account_id) + .admin_key(sender_key.public_key()) + .supply_key(sender_key.public_key()) + .token_type(TokenType::NonFungibleUnique) + .token_supply_type(TokenSupplyType::Infinite) + .freeze_with(&client)? + .sign(sender_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .token_id + .unwrap(); + + println!("Created NFT token: {nft_token_id}"); + + // Mint NFT + println!("Minting NFT..."); + let nft_serial = TokenMintTransaction::new() + .token_id(nft_token_id) + .metadata(vec![b"Example NFT Metadata".to_vec()]) + .freeze_with(&client)? + .sign(sender_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .serials[0] as u64; + + let nft_id = nft_token_id.nft(nft_serial); + println!("Minted NFT: {nft_id}"); + + // Step 2: Demonstrate TransferTransaction API with hooks + println!("\n=== TransferTransaction with Hooks API Demonstration ==="); + + // Create hook call objects + println!("Creating hook call objects..."); + + // HBAR transfer with pre-tx allowance hook + let hbar_hook = FungibleHookCall { + hook_call: HookCall::new( + Some(hook_id), + { + let mut evm_call = EvmHookCall::new(Some(vec![0x01, 0x02])); + evm_call.set_gas_limit(20_000); + Some(evm_call) + }, + ), + hook_type: FungibleHookType::PreTxAllowanceHook, + }; + + // NFT sender hook (pre-hook) + let nft_sender_hook = NftHookCall { + hook_call: HookCall::new( + Some(hook_id), + { + let mut evm_call = EvmHookCall::new(Some(vec![0x03, 0x04])); + evm_call.set_gas_limit(20_000); + Some(evm_call) + }, + ), + hook_type: NftHookType::PreHookSender, + }; + + // NFT receiver hook (pre-hook) + let nft_receiver_hook = NftHookCall { + hook_call: HookCall::new( + Some(hook_id), + { + let mut evm_call = EvmHookCall::new(Some(vec![0x05, 0x06])); + evm_call.set_gas_limit(20_000); + Some(evm_call) + }, + ), + hook_type: NftHookType::PreHookReceiver, + }; + + // Fungible token transfer with pre-post allowance hook + let fungible_token_hook = FungibleHookCall { + hook_call: HookCall::new( + Some(hook_id), + { + let mut evm_call = EvmHookCall::new(Some(vec![0x07, 0x08])); + evm_call.set_gas_limit(20_000); + Some(evm_call) + }, + ), + hook_type: FungibleHookType::PrePostTxAllowanceHook, + }; + + // Build separate TransferTransactions with hooks + println!("Building separate TransferTransactions with hooks..."); + + // Transaction 1: HBAR transfers with hook + println!("\n1. Executing HBAR TransferTransaction with hook..."); + TransferTransaction::new() + .hbar_transfer_with_hook(sender_account_id, Hbar::from_tinybars(-1), hbar_hook) + .hbar_transfer(receiver_account_id, Hbar::from_tinybars(1)) + .freeze_with(&client)? + .sign(sender_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + println!(" ✓ HBAR transfer with pre-tx allowance hook completed"); + + // Transaction 2: NFT transfer with sender and receiver hooks + println!("\n2. Executing NFT TransferTransaction with hooks..."); + TransferTransaction::new() + .nft_transfer_with_both_hooks( + nft_id, + sender_account_id, + receiver_account_id, + nft_sender_hook, + nft_receiver_hook, + ) + .freeze_with(&client)? + .sign(sender_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + println!(" ✓ NFT transfer with sender and receiver hooks completed"); + + // Transaction 3: Fungible token transfers with hook + println!("\n3. Executing Fungible Token TransferTransaction with hook..."); + TransferTransaction::new() + .token_transfer_with_hook( + fungible_token_id, + sender_account_id, + -1_000, + fungible_token_hook, + ) + .token_transfer(fungible_token_id, receiver_account_id, 1_000) + .freeze_with(&client)? + .sign(sender_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + println!(" ✓ Fungible token transfer with pre-post allowance hook completed"); + + println!("\nAll TransferTransactions executed successfully with the following hook calls:"); + println!(" - Transaction 1: HBAR transfer with pre-tx allowance hook"); + println!(" - Transaction 2: NFT transfer with sender and receiver hooks"); + println!(" - Transaction 3: Fungible token transfer with pre-post allowance hook"); + + println!("Transfer Transaction Hooks Example Complete!"); + + Ok(()) +} diff --git a/src/fee_schedules.rs b/src/fee_schedules.rs index 4213ed1c4..b2fd91bad 100644 --- a/src/fee_schedules.rs +++ b/src/fee_schedules.rs @@ -814,6 +814,7 @@ impl FromProtobuf for FeeDataType { SubType::ScheduleCreateContractCall => Self::ScheduleCreateContractCall, SubType::TopicCreateWithCustomFees => Self::TopicCreateWithCustomFees, SubType::SubmitMessageWithCustomFees => Self::SubmitMessageWithCustomFees, + SubType::CryptoTransferWithHooks => Self::Default, // Treat as default for now }; Ok(value) diff --git a/src/hooks/nft_hook_call.rs b/src/hooks/nft_hook_call.rs new file mode 100644 index 000000000..3f11f4a49 --- /dev/null +++ b/src/hooks/nft_hook_call.rs @@ -0,0 +1,74 @@ +use hedera_proto::services; + +use crate::hooks::{ + EvmHookCall, + HookCall, + NftHookType, +}; +use crate::{ + FromProtobuf, + ToProtobuf, +}; + +/// A typed hook call for NFT transfers. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NftHookCall { + /// The underlying hook call data. + pub hook_call: HookCall, + /// The type of NFT hook. + pub hook_type: NftHookType, +} + +impl NftHookCall { + /// Create a new `NftHookCall`. + pub fn new(hook_call: HookCall, hook_type: NftHookType) -> Self { + Self { hook_call, hook_type } + } + + /// Internal method to create from protobuf with a known type. + pub(crate) fn from_protobuf_with_type( + pb: services::HookCall, + hook_type: NftHookType, + ) -> crate::Result { + Ok(Self { hook_call: HookCall::from_protobuf(pb)?, hook_type }) + } +} + +impl ToProtobuf for NftHookCall { + type Protobuf = services::HookCall; + + fn to_protobuf(&self) -> Self::Protobuf { + self.hook_call.to_protobuf() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_nft_hook_call_creation() { + let hook_id = 123; + let hook_type = NftHookType::PreHookSender; + let mut hook_call_obj = HookCall::new(None, None); + hook_call_obj.set_hook_id(hook_id); + let hook_call = NftHookCall::new(hook_call_obj, hook_type); + + assert_eq!(hook_call.hook_call.hook_id, Some(hook_id)); + assert_eq!(hook_call.hook_type, hook_type); + } + + #[test] + fn test_nft_hook_call_with_call() { + let call_data = vec![1, 2, 3, 4, 5]; + let mut evm_call = EvmHookCall::new(Some(call_data)); + evm_call.set_gas_limit(0); + let hook_type = NftHookType::PrePostHookReceiver; + let mut hook_call_obj = HookCall::new(None, None); + hook_call_obj.set_call(evm_call.clone()); + let hook_call = NftHookCall::new(hook_call_obj, hook_type); + + assert_eq!(hook_call.hook_call.call, Some(evm_call)); + assert_eq!(hook_call.hook_type, hook_type); + } +} diff --git a/src/hooks/nft_hook_type.rs b/src/hooks/nft_hook_type.rs new file mode 100644 index 000000000..ba54f7576 --- /dev/null +++ b/src/hooks/nft_hook_type.rs @@ -0,0 +1,35 @@ +/// Types of NFT hooks. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum NftHookType { + /// A single call made before attempting the NFT transfer, to a hook on the sender account. + PreHookSender = 0, + /// Two calls - first before attempting the NFT transfer (allowPre), and second after + /// attempting the NFT transfer (allowPost) on the sender account. + PrePostHookSender = 1, + /// A single call made before attempting the NFT transfer, to a hook on the receiver account. + PreHookReceiver = 2, + /// Two calls - first before attempting the NFT transfer (allowPre), and second after + /// attempting the NFT transfer (allowPost) on the receiver account. + PrePostHookReceiver = 3, +} + +impl NftHookType { + /// Returns the numeric value of the hook type. + pub fn value(&self) -> u8 { + *self as u8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_nft_hook_type_values() { + assert_eq!(NftHookType::PreHookSender.value(), 0); + assert_eq!(NftHookType::PrePostHookSender.value(), 1); + assert_eq!(NftHookType::PreHookReceiver.value(), 2); + assert_eq!(NftHookType::PrePostHookReceiver.value(), 3); + } +} diff --git a/src/query/payment_transaction.rs b/src/query/payment_transaction.rs index 68f80600b..784250f70 100644 --- a/src/query/payment_transaction.rs +++ b/src/query/payment_transaction.rs @@ -85,11 +85,13 @@ impl ToTransactionDataProtobuf for PaymentTransactionData { account_id: node_account_id.to_protobuf(), amount: amount.to_tinybars(), is_approval: false, + hook_call: None, }, services::AccountAmount { account_id: Some(transaction_id.account_id.to_protobuf()), amount: -(amount.to_tinybars()), is_approval: false, + hook_call: None, }, ], }), diff --git a/src/schedule/schedulable_transaction_body.rs b/src/schedule/schedulable_transaction_body.rs index 27ccdd5ee..d6e430c4f 100644 --- a/src/schedule/schedulable_transaction_body.rs +++ b/src/schedule/schedulable_transaction_body.rs @@ -547,6 +547,9 @@ impl TryFrom for AnySchedulableTransactionData { AnyTransactionData::Batch(_) => { Err(crate::Error::basic_parse("Cannot schedule `BatchTransaction`")) } + AnyTransactionData::LambdaSStore(_) => { + Err(crate::Error::basic_parse("Cannot schedule `LambdaSStoreTransaction`")) + } } } } diff --git a/src/schedule/schedule_create_transaction.rs b/src/schedule/schedule_create_transaction.rs index 03cfc6a79..839d04b5e 100644 --- a/src/schedule/schedule_create_transaction.rs +++ b/src/schedule/schedule_create_transaction.rs @@ -315,6 +315,7 @@ mod tests { ), amount: -1000000000, is_approval: false, + hook_call: None, }, AccountAmount { account_id: Some( @@ -330,6 +331,7 @@ mod tests { ), amount: 1000000000, is_approval: false, + hook_call: None, }, ], }, @@ -449,11 +451,13 @@ mod tests { account_id: "0.0.555", amount: -1000000000, is_approval: false, + hook_call: None, }, Transfer { account_id: "0.0.666", amount: 1000000000, is_approval: false, + hook_call: None, }, ], token_transfers: [], diff --git a/src/snapshots/transaction_record/serialize.txt b/src/snapshots/transaction_record/serialize.txt index 1dbf2a579..1a9a94c7b 100644 --- a/src/snapshots/transaction_record/serialize.txt +++ b/src/snapshots/transaction_record/serialize.txt @@ -186,6 +186,7 @@ TransactionRecord { ), amount: 500000000, is_approval: false, + hook_call: None, }, ], }, @@ -214,6 +215,7 @@ TransactionRecord { ), amount: 4, is_approval: false, + hook_call: None, }, ], nft_transfers: [], diff --git a/src/snapshots/transaction_record/serialize2.txt b/src/snapshots/transaction_record/serialize2.txt index 6c47c797c..e84a0ce0b 100644 --- a/src/snapshots/transaction_record/serialize2.txt +++ b/src/snapshots/transaction_record/serialize2.txt @@ -186,6 +186,7 @@ TransactionRecord { ), amount: 500000000, is_approval: false, + hook_call: None, }, ], }, @@ -214,6 +215,7 @@ TransactionRecord { ), amount: 4, is_approval: false, + hook_call: None, }, ], nft_transfers: [], diff --git a/src/token/snapshots/token_airdrop_transaction/serialize.txt b/src/token/snapshots/token_airdrop_transaction/serialize.txt index d5c4d67a2..433dd8b7f 100644 --- a/src/token/snapshots/token_airdrop_transaction/serialize.txt +++ b/src/token/snapshots/token_airdrop_transaction/serialize.txt @@ -36,6 +36,8 @@ TokenAirdrop( ), serial_number: 4, is_approval: false, + sender_allowance_hook_call: None, + receiver_allowance_hook_call: None, }, ], expected_decimals: None, @@ -75,6 +77,8 @@ TokenAirdrop( ), serial_number: 1, is_approval: false, + sender_allowance_hook_call: None, + receiver_allowance_hook_call: None, }, NftTransfer { sender_account_id: Some( @@ -101,6 +105,8 @@ TokenAirdrop( ), serial_number: 2, is_approval: false, + sender_allowance_hook_call: None, + receiver_allowance_hook_call: None, }, NftTransfer { sender_account_id: Some( @@ -127,6 +133,8 @@ TokenAirdrop( ), serial_number: 3, is_approval: false, + sender_allowance_hook_call: None, + receiver_allowance_hook_call: None, }, NftTransfer { sender_account_id: Some( @@ -153,6 +161,8 @@ TokenAirdrop( ), serial_number: 4, is_approval: false, + sender_allowance_hook_call: None, + receiver_allowance_hook_call: None, }, ], expected_decimals: None, @@ -180,6 +190,7 @@ TokenAirdrop( ), amount: -1, is_approval: false, + hook_call: None, }, AccountAmount { account_id: Some( @@ -195,6 +206,7 @@ TokenAirdrop( ), amount: 123, is_approval: true, + hook_call: None, }, AccountAmount { account_id: Some( @@ -210,6 +222,7 @@ TokenAirdrop( ), amount: 1, is_approval: false, + hook_call: None, }, ], nft_transfers: [ @@ -238,6 +251,8 @@ TokenAirdrop( ), serial_number: 4, is_approval: true, + sender_allowance_hook_call: None, + receiver_allowance_hook_call: None, }, ], expected_decimals: None, @@ -265,6 +280,7 @@ TokenAirdrop( ), amount: -800, is_approval: false, + hook_call: None, }, AccountAmount { account_id: Some( @@ -280,6 +296,7 @@ TokenAirdrop( ), amount: -400, is_approval: false, + hook_call: None, }, ], nft_transfers: [], @@ -310,6 +327,7 @@ TokenAirdrop( ), amount: 400, is_approval: false, + hook_call: None, }, ], nft_transfers: [], diff --git a/src/token/token_airdrop_transaction.rs b/src/token/token_airdrop_transaction.rs index 71824760a..fc128672c 100644 --- a/src/token/token_airdrop_transaction.rs +++ b/src/token/token_airdrop_transaction.rs @@ -11,6 +11,7 @@ use super::{ TokenId, TokenNftTransfer, }; +use crate::hooks::FungibleHookCall; use crate::ledger_id::RefLedgerId; use crate::protobuf::{ FromProtobuf, @@ -99,7 +100,7 @@ impl TokenAirdropTransaction { account_id: AccountId, value: i64, ) -> &mut Self { - self._token_transfer(token_id, account_id, value, false) + self._token_transfer(token_id, account_id, value, false, None) } /// Return a non-approved token transfer. @@ -163,7 +164,7 @@ impl TokenAirdropTransaction { account_id: AccountId, amount: i64, ) -> &mut Self { - self._token_transfer(token_id, account_id, amount, true); + self._token_transfer(token_id, account_id, amount, true, None); self } @@ -196,8 +197,9 @@ impl TokenAirdropTransaction { account_id: AccountId, amount: i64, is_approved: bool, + hook_call: Option, ) -> &mut Self { - let transfer = Transfer { account_id, amount, is_approval: is_approved }; + let transfer = Transfer { account_id, amount, is_approval: is_approved, hook_call }; let data = self.data_mut(); if let Some(tt) = data.token_transfers.iter_mut().find(|tt| tt.token_id == token_id) { @@ -229,7 +231,7 @@ impl TokenAirdropTransaction { approved: bool, expected_decimals: Option, ) -> &mut Self { - let transfer = Transfer { account_id, amount, is_approval: approved }; + let transfer = Transfer { account_id, amount, is_approval: approved, hook_call: None }; let data = self.data_mut(); if let Some(tt) = data.token_transfers.iter_mut().find(|tt| tt.token_id == token_id) { @@ -267,7 +269,15 @@ impl TokenAirdropTransaction { is_approved: bool, ) -> &mut Self { let NftId { token_id, serial } = nft_id; - let transfer = TokenNftTransfer { token_id, serial, sender, receiver, is_approved }; + let transfer = TokenNftTransfer { + token_id, + serial, + sender, + receiver, + is_approved, + sender_hook_call: None, + receiver_hook_call: None, + }; let data = self.data_mut(); @@ -523,21 +533,25 @@ mod tests { account_id: Some(AccountId::from_str("0.0.5008").unwrap().to_protobuf()), amount: 200, is_approval: false, + hook_call: None, }, AccountAmount { account_id: Some(AccountId::from_str("0.0.5009").unwrap().to_protobuf()), amount: -100, is_approval: false, + hook_call: None, }, AccountAmount { account_id: Some(AccountId::from_str("0.0.5010").unwrap().to_protobuf()), amount: 40, is_approval: false, + hook_call: None, }, AccountAmount { account_id: Some(AccountId::from_str("0.0.5011").unwrap().to_protobuf()), amount: 20, is_approval: false, + hook_call: None, }, ], nft_transfers: vec![NftTransfer { @@ -547,6 +561,8 @@ mod tests { ), serial_number: 1, is_approval: true, + sender_allowance_hook_call: None, + receiver_allowance_hook_call: None, }], expected_decimals: Some(3), }], diff --git a/src/token/token_nft_transfer.rs b/src/token/token_nft_transfer.rs index 31f57f941..9986422d0 100644 --- a/src/token/token_nft_transfer.rs +++ b/src/token/token_nft_transfer.rs @@ -1,5 +1,9 @@ use hedera_proto::services; +use crate::hooks::{ + NftHookCall, + NftHookType, +}; use crate::protobuf::FromProtobuf; use crate::{ AccountId, @@ -25,6 +29,12 @@ pub struct TokenNftTransfer { /// If true then the transfer is expected to be an approved allowance and the /// `sender` is expected to be the owner. The default is false. pub is_approved: bool, + + /// Optional hook call for the sender side of this NFT transfer. + pub sender_hook_call: Option, + + /// Optional hook call for the receiver side of this NFT transfer. + pub receiver_hook_call: Option, } impl TokenNftTransfer { @@ -32,12 +42,40 @@ impl TokenNftTransfer { pb: services::NftTransfer, token_id: TokenId, ) -> crate::Result { + // Extract sender hook call from the oneof union + let sender_hook_call = match pb.sender_allowance_hook_call { + Some(services::nft_transfer::SenderAllowanceHookCall::PreTxSenderAllowanceHook( + hook, + )) => Some(NftHookCall::from_protobuf_with_type(hook, NftHookType::PreHookSender)?), + Some( + services::nft_transfer::SenderAllowanceHookCall::PrePostTxSenderAllowanceHook(hook), + ) => Some(NftHookCall::from_protobuf_with_type(hook, NftHookType::PrePostHookSender)?), + None => None, + }; + + // Extract receiver hook call from the oneof union + let receiver_hook_call = match pb.receiver_allowance_hook_call { + Some( + services::nft_transfer::ReceiverAllowanceHookCall::PreTxReceiverAllowanceHook(hook), + ) => Some(NftHookCall::from_protobuf_with_type(hook, NftHookType::PreHookReceiver)?), + Some( + services::nft_transfer::ReceiverAllowanceHookCall::PrePostTxReceiverAllowanceHook( + hook, + ), + ) => { + Some(NftHookCall::from_protobuf_with_type(hook, NftHookType::PrePostHookReceiver)?) + } + None => None, + }; + Ok(Self { token_id, sender: AccountId::from_protobuf(pb_getf!(pb, sender_account_id)?)?, receiver: AccountId::from_protobuf(pb_getf!(pb, receiver_account_id)?)?, serial: pb.serial_number as u64, is_approved: pb.is_approval, + sender_hook_call, + receiver_hook_call, }) } } diff --git a/src/transaction_record.rs b/src/transaction_record.rs index 20d20c51e..b170f9aac 100644 --- a/src/transaction_record.rs +++ b/src/transaction_record.rs @@ -274,6 +274,7 @@ impl ToProtobuf for TransactionRecord { account_id: Some(it.0.to_protobuf()), amount: *it.1, is_approval: false, + hook_call: None, }) .collect(), nft_transfers: Vec::new(), @@ -365,6 +366,8 @@ mod tests { transfers: Vec::from([Transfer { account_id: AccountId::new(4, 4, 4), amount: Hbar::new(5), + is_approved: false, + hook_call: None, }]), token_transfers: HashMap::from([( TokenId::new(6, 6, 6), @@ -378,6 +381,8 @@ mod tests { receiver: AccountId::new(3, 2, 1), serial: 4, is_approved: true, + sender_hook_call: None, + receiver_hook_call: None, }]), )]), transaction_id: TEST_TX_ID, diff --git a/src/transfer.rs b/src/transfer.rs index ea18c0e40..21769068e 100644 --- a/src/transfer.rs +++ b/src/transfer.rs @@ -1,5 +1,9 @@ use hedera_proto::services; +use crate::hooks::{ + FungibleHookCall, + FungibleHookType, +}; use crate::protobuf::{ FromProtobuf, ToProtobuf, @@ -21,6 +25,12 @@ pub struct Transfer { /// /// Negative if the account sends/withdraws hbar, positive if it receives hbar. pub amount: Hbar, + + /// If true then the transfer is expected to be an approved allowance. + pub is_approved: bool, + + /// Optional hook call for this transfer. + pub hook_call: Option, } impl FromProtobuf for Transfer { @@ -28,9 +38,28 @@ impl FromProtobuf for Transfer { where Self: Sized, { + // Determine which hook type is present, if any + let hook_call = match pb.hook_call { + Some(services::account_amount::HookCall::PreTxAllowanceHook(hook)) => { + Some(FungibleHookCall::from_protobuf_with_type( + hook, + FungibleHookType::PreTxAllowanceHook, + )?) + } + Some(services::account_amount::HookCall::PrePostTxAllowanceHook(hook)) => { + Some(FungibleHookCall::from_protobuf_with_type( + hook, + FungibleHookType::PrePostTxAllowanceHook, + )?) + } + None => None, + }; + Ok(Self { account_id: AccountId::from_protobuf(pb_getf!(pb, account_id)?)?, amount: Hbar::from_tinybars(pb.amount), + is_approved: pb.is_approval, + hook_call, }) } } @@ -39,10 +68,20 @@ impl ToProtobuf for Transfer { type Protobuf = services::AccountAmount; fn to_protobuf(&self) -> Self::Protobuf { + let hook_call = self.hook_call.as_ref().map(|hook| match hook.hook_type { + FungibleHookType::PreTxAllowanceHook => { + services::account_amount::HookCall::PreTxAllowanceHook(hook.to_protobuf()) + } + FungibleHookType::PrePostTxAllowanceHook => { + services::account_amount::HookCall::PrePostTxAllowanceHook(hook.to_protobuf()) + } + }); + services::AccountAmount { account_id: Some(self.account_id.to_protobuf()), amount: self.amount.to_tinybars(), - is_approval: false, + is_approval: self.is_approved, + hook_call, } } } diff --git a/src/transfer_transaction.rs b/src/transfer_transaction.rs index 54bea4bbd..58716c051 100644 --- a/src/transfer_transaction.rs +++ b/src/transfer_transaction.rs @@ -7,6 +7,12 @@ use hedera_proto::services; use hedera_proto::services::crypto_service_client::CryptoServiceClient; use tonic::transport::Channel; +use crate::hooks::{ + FungibleHookCall, + FungibleHookType, + NftHookCall, + NftHookType, +}; use crate::ledger_id::RefLedgerId; use crate::protobuf::FromProtobuf; use crate::transaction::{ @@ -60,6 +66,9 @@ pub(crate) struct Transfer { /// If this is an approved transfer. pub is_approval: bool, + + /// Optional fungible hook call for this transfer. + pub hook_call: Option, } #[derive(Debug, Clone)] @@ -75,11 +84,18 @@ pub(crate) struct TokenTransfer { } impl TransferTransaction { - fn _hbar_transfer(&mut self, account_id: AccountId, amount: Hbar, approved: bool) -> &mut Self { + fn _hbar_transfer( + &mut self, + account_id: AccountId, + amount: Hbar, + approved: bool, + hook_call: Option, + ) -> &mut Self { self.data_mut().transfers.push(Transfer { account_id, amount: amount.to_tinybars(), is_approval: approved, + hook_call, }); self @@ -87,12 +103,12 @@ impl TransferTransaction { /// Add a non-approved hbar transfer to the transaction. pub fn hbar_transfer(&mut self, account_id: AccountId, amount: Hbar) -> &mut Self { - self._hbar_transfer(account_id, amount, false) + self._hbar_transfer(account_id, amount, false, None) } /// Add an approved hbar transfer to the transaction. pub fn approved_hbar_transfer(&mut self, account_id: AccountId, amount: Hbar) -> &mut Self { - self._hbar_transfer(account_id, amount, true) + self._hbar_transfer(account_id, amount, true, None) } /// Returns all transfers associated with this transaction. @@ -111,8 +127,9 @@ impl TransferTransaction { amount: i64, approved: bool, expected_decimals: Option, + hook_call: Option, ) -> &mut Self { - let transfer = Transfer { account_id, amount, is_approval: approved }; + let transfer = Transfer { account_id, amount, is_approval: approved, hook_call }; let data = self.data_mut(); if let Some(tt) = data.token_transfers.iter_mut().find(|tt| tt.token_id == token_id) { @@ -139,7 +156,7 @@ impl TransferTransaction { account_id: AccountId, amount: i64, ) -> &mut Self { - self._token_transfer(token_id, account_id, amount, false, None) + self._token_transfer(token_id, account_id, amount, false, None, None) } /// Add an approved token transfer to the transaction. @@ -151,7 +168,7 @@ impl TransferTransaction { account_id: AccountId, amount: i64, ) -> &mut Self { - self._token_transfer(token_id, account_id, amount, true, None) + self._token_transfer(token_id, account_id, amount, true, None, None) } // todo: make the examples into code, just not sure how to do that. @@ -166,7 +183,7 @@ impl TransferTransaction { amount: i64, expected_decimals: u32, ) -> &mut Self { - self._token_transfer(token_id, account_id, amount, false, Some(expected_decimals)) + self._token_transfer(token_id, account_id, amount, false, Some(expected_decimals), None) } /// Add an approved token transfer, ensuring that the token has `expected_decimals` decimals. @@ -180,7 +197,7 @@ impl TransferTransaction { amount: i64, expected_decimals: u32, ) -> &mut Self { - self._token_transfer(token_id, account_id, amount, true, Some(expected_decimals)) + self._token_transfer(token_id, account_id, amount, true, Some(expected_decimals), None) } /// Returns all the token transfers associated associated with this transaction. @@ -219,6 +236,8 @@ impl TransferTransaction { sender_account_id: AccountId, receiver_account_id: AccountId, approved: bool, + sender_hook_call: Option, + receiver_hook_call: Option, ) -> &mut Self { let NftId { token_id, serial } = nft_id; let transfer = TokenNftTransfer { @@ -227,6 +246,8 @@ impl TransferTransaction { sender: sender_account_id, receiver: receiver_account_id, is_approved: approved, + sender_hook_call, + receiver_hook_call, }; let data = self.data_mut(); @@ -252,7 +273,7 @@ impl TransferTransaction { sender_account_id: AccountId, receiver_account_id: AccountId, ) -> &mut Self { - self._nft_transfer(nft_id.into(), sender_account_id, receiver_account_id, true) + self._nft_transfer(nft_id.into(), sender_account_id, receiver_account_id, true, None, None) } /// Add a non-approved nft transfer to the transaction. @@ -262,7 +283,7 @@ impl TransferTransaction { sender_account_id: AccountId, receiver_account_id: AccountId, ) -> &mut Self { - self._nft_transfer(nft_id.into(), sender_account_id, receiver_account_id, false) + self._nft_transfer(nft_id.into(), sender_account_id, receiver_account_id, false, None, None) } /// Returns all the NFT transfers associated with this transaction. @@ -273,6 +294,89 @@ impl TransferTransaction { .map(|it| (it.token_id, it.nft_transfers.clone())) .collect() } + + /// Add a hbar transfer with a fungible hook call. + pub fn add_hbar_transfer_with_hook( + &mut self, + account_id: AccountId, + amount: Hbar, + hook_call: FungibleHookCall, + ) -> &mut Self { + self._hbar_transfer(account_id, amount, false, Some(hook_call)) + } + + /// Add an NFT transfer with a sender hook call. + pub fn nft_transfer_with_sender_hook( + &mut self, + nft_id: impl Into, + sender: AccountId, + receiver: AccountId, + sender_hook_call: NftHookCall, + ) -> &mut Self { + self._nft_transfer(nft_id.into(), sender, receiver, false, Some(sender_hook_call), None) + } + + /// Add an NFT transfer with a receiver hook call. + pub fn nft_transfer_with_receiver_hook( + &mut self, + nft_id: impl Into, + sender: AccountId, + receiver: AccountId, + receiver_hook_call: NftHookCall, + ) -> &mut Self { + self._nft_transfer(nft_id.into(), sender, receiver, false, None, Some(receiver_hook_call)) + } + + /// Add a token transfer with a fungible hook call. + pub fn add_token_transfer_with_hook( + &mut self, + token_id: TokenId, + account_id: AccountId, + amount: i64, + hook_call: FungibleHookCall, + ) -> &mut Self { + self._token_transfer(token_id, account_id, amount, false, None, Some(hook_call)) + } + + /// Add a hbar transfer with a fungible hook call. + pub fn hbar_transfer_with_hook( + &mut self, + account_id: AccountId, + amount: Hbar, + hook_call: FungibleHookCall, + ) -> &mut Self { + self._hbar_transfer(account_id, amount, false, Some(hook_call)) + } + + /// Add a token transfer with a fungible hook call. + pub fn token_transfer_with_hook( + &mut self, + token_id: TokenId, + account_id: AccountId, + amount: i64, + hook_call: FungibleHookCall, + ) -> &mut Self { + self._token_transfer(token_id, account_id, amount, false, None, Some(hook_call)) + } + + /// Add an NFT transfer with both sender and receiver hook calls. + pub fn nft_transfer_with_both_hooks( + &mut self, + nft_id: impl Into, + sender: AccountId, + receiver: AccountId, + sender_hook_call: NftHookCall, + receiver_hook_call: NftHookCall, + ) -> &mut Self { + self._nft_transfer( + nft_id.into(), + sender, + receiver, + false, + Some(sender_hook_call), + Some(receiver_hook_call), + ) + } } impl TransactionExecute for TransferTransactionData { @@ -309,10 +413,27 @@ impl ValidateChecksums for TransferTransactionData { impl FromProtobuf for Transfer { fn from_protobuf(pb: services::AccountAmount) -> crate::Result { + let hook_call = match pb.hook_call { + Some(services::account_amount::HookCall::PreTxAllowanceHook(hook)) => { + Some(FungibleHookCall::from_protobuf_with_type( + hook, + FungibleHookType::PreTxAllowanceHook, + )?) + } + Some(services::account_amount::HookCall::PrePostTxAllowanceHook(hook)) => { + Some(FungibleHookCall::from_protobuf_with_type( + hook, + FungibleHookType::PrePostTxAllowanceHook, + )?) + } + None => None, + }; + Ok(Self { - amount: pb.amount, account_id: AccountId::from_protobuf(pb_getf!(pb, account_id)?)?, + amount: pb.amount, is_approval: pb.is_approval, + hook_call, }) } } @@ -321,10 +442,20 @@ impl ToProtobuf for Transfer { type Protobuf = services::AccountAmount; fn to_protobuf(&self) -> Self::Protobuf { + let hook_call = self.hook_call.as_ref().map(|hook| match hook.hook_type { + FungibleHookType::PreTxAllowanceHook => { + services::account_amount::HookCall::PreTxAllowanceHook(hook.to_protobuf()) + } + FungibleHookType::PrePostTxAllowanceHook => { + services::account_amount::HookCall::PrePostTxAllowanceHook(hook.to_protobuf()) + } + }); + services::AccountAmount { - amount: self.amount, account_id: Some(self.account_id.to_protobuf()), + amount: self.amount, is_approval: self.is_approval, + hook_call, } } } @@ -366,11 +497,57 @@ impl ToProtobuf for TokenNftTransfer { type Protobuf = services::NftTransfer; fn to_protobuf(&self) -> Self::Protobuf { + // Serialize sender hook call to oneof union + let sender_allowance_hook_call = self.sender_hook_call.as_ref().map(|hook| { + match hook.hook_type { + NftHookType::PreHookSender => { + services::nft_transfer::SenderAllowanceHookCall::PreTxSenderAllowanceHook( + hook.to_protobuf(), + ) + } + NftHookType::PrePostHookSender => { + services::nft_transfer::SenderAllowanceHookCall::PrePostTxSenderAllowanceHook( + hook.to_protobuf(), + ) + } + _ => { + // This shouldn't happen as sender_hook_call should only have sender types + services::nft_transfer::SenderAllowanceHookCall::PreTxSenderAllowanceHook( + hook.to_protobuf(), + ) + } + } + }); + + // Serialize receiver hook call to oneof union + let receiver_allowance_hook_call = self.receiver_hook_call.as_ref().map(|hook| { + match hook.hook_type { + NftHookType::PreHookReceiver => { + services::nft_transfer::ReceiverAllowanceHookCall::PreTxReceiverAllowanceHook( + hook.to_protobuf(), + ) + } + NftHookType::PrePostHookReceiver => { + services::nft_transfer::ReceiverAllowanceHookCall::PrePostTxReceiverAllowanceHook( + hook.to_protobuf(), + ) + } + _ => { + // This shouldn't happen as receiver_hook_call should only have receiver types + services::nft_transfer::ReceiverAllowanceHookCall::PreTxReceiverAllowanceHook( + hook.to_protobuf(), + ) + } + } + }); + services::NftTransfer { sender_account_id: Some(self.sender.to_protobuf()), receiver_account_id: Some(self.receiver.to_protobuf()), serial_number: self.serial as i64, is_approval: self.is_approved, + sender_allowance_hook_call, + receiver_allowance_hook_call, } } } @@ -516,6 +693,7 @@ mod tests { ), amount: 400, is_approval: false, + hook_call: None, }, AccountAmount { account_id: Some( @@ -531,6 +709,7 @@ mod tests { ), amount: -800, is_approval: false, + hook_call: None, }, AccountAmount { account_id: Some( @@ -546,6 +725,7 @@ mod tests { ), amount: 400, is_approval: true, + hook_call: None, }, ], }, @@ -574,6 +754,7 @@ mod tests { ), amount: 400, is_approval: false, + hook_call: None, }, AccountAmount { account_id: Some( @@ -589,6 +770,7 @@ mod tests { ), amount: -800, is_approval: false, + hook_call: None, }, AccountAmount { account_id: Some( @@ -604,6 +786,7 @@ mod tests { ), amount: 400, is_approval: false, + hook_call: None, }, ], nft_transfers: [], @@ -634,6 +817,7 @@ mod tests { ), amount: 1, is_approval: false, + hook_call: None, }, AccountAmount { account_id: Some( @@ -649,6 +833,7 @@ mod tests { ), amount: -1, is_approval: true, + hook_call: None, }, ], nft_transfers: [], @@ -689,6 +874,8 @@ mod tests { ), serial_number: 2, is_approval: false, + sender_allowance_hook_call: None, + receiver_allowance_hook_call: None, }, NftTransfer { sender_account_id: Some( @@ -715,6 +902,8 @@ mod tests { ), serial_number: 1, is_approval: true, + sender_allowance_hook_call: None, + receiver_allowance_hook_call: None, }, NftTransfer { sender_account_id: Some( @@ -741,6 +930,8 @@ mod tests { ), serial_number: 3, is_approval: false, + sender_allowance_hook_call: None, + receiver_allowance_hook_call: None, }, NftTransfer { sender_account_id: Some( @@ -767,6 +958,8 @@ mod tests { ), serial_number: 4, is_approval: false, + sender_allowance_hook_call: None, + receiver_allowance_hook_call: None, }, ], expected_decimals: None, @@ -806,6 +999,8 @@ mod tests { ), serial_number: 4, is_approval: false, + sender_allowance_hook_call: None, + receiver_allowance_hook_call: None, }, ], expected_decimals: None, diff --git a/tests/e2e/token/mod.rs b/tests/e2e/token/mod.rs index 6ed180bf8..b54e16e09 100644 --- a/tests/e2e/token/mod.rs +++ b/tests/e2e/token/mod.rs @@ -20,6 +20,7 @@ mod cancel_airdrop; mod claim_airdrop; mod revoke_kyc; mod transfer; +mod transfer_with_hooks; mod unfreeze; mod unpause; mod update; diff --git a/tests/e2e/token/transfer_with_hooks.rs b/tests/e2e/token/transfer_with_hooks.rs new file mode 100644 index 000000000..877659eb2 --- /dev/null +++ b/tests/e2e/token/transfer_with_hooks.rs @@ -0,0 +1,594 @@ +use assert_matches::assert_matches; +use hedera::{ + AccountCreateTransaction, + ContractCreateTransaction, + ContractId, + EvmHookCall, + EvmHookSpec, + FungibleHookCall, + FungibleHookType, + Hbar, + HookCall, + HookCreationDetails, + HookExtensionPoint, + LambdaEvmHook, + NftHookCall, + NftHookType, + PrivateKey, + Status, + TokenCreateTransaction, + TokenMintTransaction, + TokenSupplyType, + TokenType, + TransferTransaction, +}; + +use crate::common::{ + setup_nonfree, + TestEnvironment, +}; + +const HOOK_BYTECODE: &str = "6080604052348015600e575f5ffd5b506107d18061001c5f395ff3fe608060405260043610610033575f3560e01c8063124d8b301461003757806394112e2f14610067578063bd0dd0b614610097575b5f5ffd5b610051600480360381019061004c91906106f2565b6100c7565b60405161005e9190610782565b60405180910390f35b610081600480360381019061007c91906106f2565b6100d2565b60405161008e9190610782565b60405180910390f35b6100b160048036038101906100ac91906106f2565b6100dd565b6040516100be9190610782565b60405180910390f35b5f6001905092915050565b5f6001905092915050565b5f6001905092915050565b5f604051905090565b5f5ffd5b5f5ffd5b5f5ffd5b5f60a08284031215610112576101116100f9565b5b81905092915050565b5f5ffd5b5f601f19601f8301169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6101658261011f565b810181811067ffffffffffffffff821117156101845761018361012f565b5b80604052505050565b5f6101966100e8565b90506101a2828261015c565b919050565b5f5ffd5b5f5ffd5b5f67ffffffffffffffff8211156101c9576101c861012f565b5b602082029050602081019050919050565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610207826101de565b9050919050565b610217816101fd565b8114610221575f5ffd5b50565b5f813590506102328161020e565b92915050565b5f8160070b9050919050565b61024d81610238565b8114610257575f5ffd5b50565b5f8135905061026881610244565b92915050565b5f604082840312156102835761028261011b565b5b61028d604061018d565b90505f61029c84828501610224565b5f8301525060206102af8482850161025a565b60208301525092915050565b5f6102cd6102c8846101af565b61018d565b905080838252602082019050604084028301858111156102f0576102ef6101da565b5b835b818110156103195780610305888261026e565b8452602084019350506040810190506102f2565b5050509392505050565b5f82601f830112610337576103366101ab565b5b81356103478482602086016102bb565b91505092915050565b5f67ffffffffffffffff82111561036a5761036961012f565b5b602082029050602081019050919050565b5f67ffffffffffffffff8211156103955761039461012f565b5b602082029050602081019050919050565b5f606082840312156103bb576103ba61011b565b5b6103c5606061018d565b90505f6103d484828501610224565b5f8301525060206103e784828501610224565b60208301525060406103fb8482850161025a565b60408301525092915050565b5f6104196104148461037b565b61018d565b9050808382526020820190506060840283018581111561043c5761043b6101da565b5b835b81811015610465578061045188826103a6565b84526020840193505060608101905061043e565b5050509392505050565b5f82601f830112610483576104826101ab565b5b8135610493848260208601610407565b91505092915050565b5f606082840312156104b1576104b061011b565b5b6104bb606061018d565b90505f6104ca84828501610224565b5f83015250602082013567ffffffffffffffff8111156104ed576104ec6101a7565b5b6104f984828501610323565b602083015250604082013567ffffffffffffffff81111561051d5761051c6101a7565b5b6105298482850161046f565b60408301525092915050565b5f61054761054284610350565b61018d565b9050808382526020820190506020840283018581111561056a576105696101da565b5b835b818110156105b157803567ffffffffffffffff81111561058f5761058e6101ab565b5b80860161059c898261049c565b8552602085019450505060208101905061056c565b5050509392505050565b5f82601f8301126105cf576105ce6101ab565b5b81356105df848260208601610535565b91505092915050565b5f604082840312156105fd576105fc61011b565b5b610607604061018d565b90505f82013567ffffffffffffffff811115610626576106256101a7565b5b61063284828501610323565b5f83015250602082013567ffffffffffffffff811115610655576106546101a7565b5b610661848285016105bb565b60208301525092915050565b5f604082840312156106825761068161011b565b5b61068c604061018d565b90505f82013567ffffffffffffffff8111156106ab576106aa6101a7565b5b6106b7848285016105e8565b5f83015250602082013567ffffffffffffffff8111156106da576106d96101a7565b5b6106e6848285016105e8565b60208301525092915050565b5f5f60408385031215610708576107076100f1565b5b5f83013567ffffffffffffffff811115610725576107246100f5565b5b610731858286016100fd565b925050602083013567ffffffffffffffff811115610752576107516100f5565b5b61075e8582860161066d565b9150509250929050565b5f8115159050919050565b61077c81610768565b82525050565b5f6020820190506107955f830184610773565b9291505056fea26469706673582212207dfe7723f6d6869419b1cb0619758b439da0cf4ffd9520997c40a3946299d4dc64736f6c634300081e0033"; + +async fn create_hook_contract(client: &hedera::Client) -> anyhow::Result { + let bytecode = hex::decode(HOOK_BYTECODE)?; + + let receipt = ContractCreateTransaction::new() + .bytecode(bytecode) + .gas(1_700_000) + .execute(client) + .await? + .get_receipt(client) + .await?; + + Ok(receipt.contract_id.unwrap()) +} + +#[tokio::test] +async fn can_transfer_hbar_with_pre_tx_allowance_hook() -> anyhow::Result<()> { + let Some(TestEnvironment { config: _, client }) = setup_nonfree() else { + return Ok(()); + }; + + let contract_id = create_hook_contract(&client).await?; + + let sender_key = PrivateKey::generate_ed25519(); + let receiver_key = PrivateKey::generate_ed25519(); + + // Create sender account with hook + let spec = EvmHookSpec::new(Some(contract_id)); + let lambda_hook = LambdaEvmHook::new(spec, vec![]); + let hook_details = HookCreationDetails::new( + HookExtensionPoint::AccountAllowanceHook, + 1, + Some(lambda_hook), + ); + + let sender_receipt = AccountCreateTransaction::new() + .key(sender_key.public_key()) + .initial_balance(Hbar::new(10)) + .add_hook(hook_details) + .freeze_with(&client)? + .sign(sender_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let sender_id = sender_receipt.account_id.unwrap(); + + // Create receiver account + let receiver_receipt = AccountCreateTransaction::new() + .key(receiver_key.public_key()) + .initial_balance(Hbar::new(1)) + .max_automatic_token_associations(-1) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let receiver_id = receiver_receipt.account_id.unwrap(); + + // Create hook call for the transfer + let mut evm_hook_call = EvmHookCall::new(Some(vec![])); + evm_hook_call.set_gas_limit(25_000); + let hook_call = HookCall::new(Some(1), Some(evm_hook_call)); + let fungible_hook_call = + FungibleHookCall { hook_call, hook_type: FungibleHookType::PreTxAllowanceHook }; + + // Perform transfer with hook + let transfer_receipt = TransferTransaction::new() + .hbar_transfer_with_hook(sender_id, Hbar::from_tinybars(-100), fungible_hook_call) + .hbar_transfer(receiver_id, Hbar::from_tinybars(100)) + .freeze_with(&client)? + .sign(sender_key) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + assert_matches!(transfer_receipt.status, Status::Success); + + Ok(()) +} + +#[tokio::test] +async fn can_transfer_hbar_with_pre_post_tx_allowance_hook() -> anyhow::Result<()> { + let Some(TestEnvironment { config: _, client }) = setup_nonfree() else { + return Ok(()); + }; + + let contract_id = create_hook_contract(&client).await?; + + let sender_key = PrivateKey::generate_ed25519(); + let receiver_key = PrivateKey::generate_ed25519(); + + // Create sender account with hook + let spec = EvmHookSpec::new(Some(contract_id)); + let lambda_hook = LambdaEvmHook::new(spec, vec![]); + let hook_details = HookCreationDetails::new( + HookExtensionPoint::AccountAllowanceHook, + 1, + Some(lambda_hook), + ); + + let sender_receipt = AccountCreateTransaction::new() + .key(sender_key.public_key()) + .initial_balance(Hbar::new(10)) + .add_hook(hook_details) + .freeze_with(&client)? + .sign(sender_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let sender_id = sender_receipt.account_id.unwrap(); + + // Create receiver account + let receiver_receipt = AccountCreateTransaction::new() + .key(receiver_key.public_key()) + .initial_balance(Hbar::new(1)) + .max_automatic_token_associations(-1) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let receiver_id = receiver_receipt.account_id.unwrap(); + + // Create hook call for the transfer + let mut evm_hook_call = EvmHookCall::new(Some(vec![])); + evm_hook_call.set_gas_limit(25_000); + let hook_call = HookCall::new(Some(1), Some(evm_hook_call)); + let fungible_hook_call = + FungibleHookCall { hook_call, hook_type: FungibleHookType::PrePostTxAllowanceHook }; + + // Perform transfer with hook + let transfer_receipt = TransferTransaction::new() + .hbar_transfer_with_hook(sender_id, Hbar::from_tinybars(-100), fungible_hook_call) + .hbar_transfer(receiver_id, Hbar::from_tinybars(100)) + .freeze_with(&client)? + .sign(sender_key) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + assert_matches!(transfer_receipt.status, Status::Success); + + Ok(()) +} + +#[tokio::test] +async fn can_transfer_fungible_token_with_hook() -> anyhow::Result<()> { + let Some(TestEnvironment { config: _, client }) = setup_nonfree() else { + return Ok(()); + }; + + let contract_id = create_hook_contract(&client).await?; + + let treasury_key = PrivateKey::generate_ed25519(); + let sender_key = PrivateKey::generate_ed25519(); + let receiver_key = PrivateKey::generate_ed25519(); + + // Create sender account with hook + let spec = EvmHookSpec::new(Some(contract_id)); + let lambda_hook = LambdaEvmHook::new(spec, vec![]); + let hook_details = HookCreationDetails::new( + HookExtensionPoint::AccountAllowanceHook, + 1, + Some(lambda_hook), + ); + + let sender_receipt = AccountCreateTransaction::new() + .key(sender_key.public_key()) + .initial_balance(Hbar::new(10)) + .add_hook(hook_details) + .freeze_with(&client)? + .sign(sender_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let sender_id = sender_receipt.account_id.unwrap(); + + // Create receiver account + let receiver_receipt = AccountCreateTransaction::new() + .key(receiver_key.public_key()) + .initial_balance(Hbar::new(1)) + .max_automatic_token_associations(-1) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let receiver_id = receiver_receipt.account_id.unwrap(); + + // Create fungible token + let token_receipt = TokenCreateTransaction::new() + .name("Test Token") + .symbol("TEST") + .decimals(2) + .initial_supply(1000) + .treasury_account_id(sender_id) + .admin_key(treasury_key.public_key()) + .supply_key(treasury_key.public_key()) + .token_type(TokenType::FungibleCommon) + .token_supply_type(TokenSupplyType::Infinite) + .freeze_with(&client)? + .sign(sender_key.clone()) + .sign(treasury_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let token_id = token_receipt.token_id.unwrap(); + + // Create hook call for the transfer + let mut evm_hook_call = EvmHookCall::new(Some(vec![])); + evm_hook_call.set_gas_limit(25_000); + let hook_call = HookCall::new(Some(1), Some(evm_hook_call)); + let fungible_hook_call = + FungibleHookCall { hook_call, hook_type: FungibleHookType::PreTxAllowanceHook }; + + // Perform token transfer with hook + let transfer_receipt = TransferTransaction::new() + .token_transfer_with_hook(token_id, sender_id, -10, fungible_hook_call) + .token_transfer(token_id, receiver_id, 10) + .freeze_with(&client)? + .sign(sender_key) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + assert_matches!(transfer_receipt.status, Status::Success); + + Ok(()) +} + +#[tokio::test] +async fn can_transfer_nft_with_sender_hook() -> anyhow::Result<()> { + let Some(TestEnvironment { config: _, client }) = setup_nonfree() else { + return Ok(()); + }; + + let contract_id = create_hook_contract(&client).await?; + + let treasury_key = PrivateKey::generate_ed25519(); + let sender_key = PrivateKey::generate_ed25519(); + let receiver_key = PrivateKey::generate_ed25519(); + + // Create sender account with hook + let spec = EvmHookSpec::new(Some(contract_id)); + let lambda_hook = LambdaEvmHook::new(spec, vec![]); + let hook_details = HookCreationDetails::new( + HookExtensionPoint::AccountAllowanceHook, + 1, + Some(lambda_hook), + ); + + let sender_receipt = AccountCreateTransaction::new() + .key(sender_key.public_key()) + .initial_balance(Hbar::new(10)) + .add_hook(hook_details) + .freeze_with(&client)? + .sign(sender_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let sender_id = sender_receipt.account_id.unwrap(); + + // Create receiver account + let receiver_receipt = AccountCreateTransaction::new() + .key(receiver_key.public_key()) + .initial_balance(Hbar::new(1)) + .max_automatic_token_associations(-1) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let receiver_id = receiver_receipt.account_id.unwrap(); + + // Create NFT token + let token_receipt = TokenCreateTransaction::new() + .name("Test NFT") + .symbol("TNFT") + .treasury_account_id(sender_id) + .admin_key(treasury_key.public_key()) + .supply_key(treasury_key.public_key()) + .token_type(TokenType::NonFungibleUnique) + .token_supply_type(TokenSupplyType::Infinite) + .freeze_with(&client)? + .sign(sender_key.clone()) + .sign(treasury_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let token_id = token_receipt.token_id.unwrap(); + + // Mint NFT + let mint_receipt = TokenMintTransaction::new() + .token_id(token_id) + .metadata(vec![vec![1, 2, 3]]) + .freeze_with(&client)? + .sign(treasury_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let serial_number = mint_receipt.serials[0] as u64; + + // Create hook call for sender + let mut evm_hook_call = EvmHookCall::new(Some(vec![])); + evm_hook_call.set_gas_limit(25_000); + let hook_call = HookCall::new(Some(1), Some(evm_hook_call)); + let sender_nft_hook_call = NftHookCall { hook_call, hook_type: NftHookType::PreHookSender }; + + // Perform NFT transfer with sender hook + let transfer_receipt = TransferTransaction::new() + .nft_transfer_with_sender_hook( + token_id.nft(serial_number), + sender_id, + receiver_id, + sender_nft_hook_call, + ) + .freeze_with(&client)? + .sign(sender_key) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + assert_matches!(transfer_receipt.status, Status::Success); + + Ok(()) +} + +#[tokio::test] +async fn can_transfer_nft_with_receiver_hook() -> anyhow::Result<()> { + let Some(TestEnvironment { config: _, client }) = setup_nonfree() else { + return Ok(()); + }; + + let contract_id = create_hook_contract(&client).await?; + + let treasury_key = PrivateKey::generate_ed25519(); + let sender_key = PrivateKey::generate_ed25519(); + let receiver_key = PrivateKey::generate_ed25519(); + + // Create sender account + let sender_receipt = AccountCreateTransaction::new() + .key(sender_key.public_key()) + .initial_balance(Hbar::new(10)) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let sender_id = sender_receipt.account_id.unwrap(); + + // Create receiver account with hook + let spec = EvmHookSpec::new(Some(contract_id)); + let lambda_hook = LambdaEvmHook::new(spec, vec![]); + let hook_details = HookCreationDetails::new( + HookExtensionPoint::AccountAllowanceHook, + 1, + Some(lambda_hook), + ); + + let receiver_receipt = AccountCreateTransaction::new() + .key(receiver_key.public_key()) + .initial_balance(Hbar::new(1)) + .max_automatic_token_associations(-1) + .add_hook(hook_details) + .freeze_with(&client)? + .sign(receiver_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let receiver_id = receiver_receipt.account_id.unwrap(); + + // Create NFT token + let token_receipt = TokenCreateTransaction::new() + .name("Test NFT") + .symbol("TNFT") + .treasury_account_id(sender_id) + .admin_key(treasury_key.public_key()) + .supply_key(treasury_key.public_key()) + .token_type(TokenType::NonFungibleUnique) + .token_supply_type(TokenSupplyType::Infinite) + .freeze_with(&client)? + .sign(sender_key.clone()) + .sign(treasury_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let token_id = token_receipt.token_id.unwrap(); + + // Mint NFT + let mint_receipt = TokenMintTransaction::new() + .token_id(token_id) + .metadata(vec![vec![1, 2, 3]]) + .freeze_with(&client)? + .sign(treasury_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let serial_number = mint_receipt.serials[0] as u64; + + // Create hook call for receiver + let mut evm_hook_call = EvmHookCall::new(Some(vec![])); + evm_hook_call.set_gas_limit(25_000); + let hook_call = HookCall::new(Some(1), Some(evm_hook_call)); + let receiver_nft_hook_call = NftHookCall { hook_call, hook_type: NftHookType::PreHookReceiver }; + + // Perform NFT transfer with receiver hook + let transfer_receipt = TransferTransaction::new() + .nft_transfer_with_receiver_hook( + token_id.nft(serial_number), + sender_id, + receiver_id, + receiver_nft_hook_call, + ) + .freeze_with(&client)? + .sign(sender_key) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + assert_matches!(transfer_receipt.status, Status::Success); + + Ok(()) +} + +#[tokio::test] +async fn can_transfer_nft_with_both_sender_and_receiver_hooks() -> anyhow::Result<()> { + let Some(TestEnvironment { config: _, client }) = setup_nonfree() else { + return Ok(()); + }; + + let contract_id = create_hook_contract(&client).await?; + + let treasury_key = PrivateKey::generate_ed25519(); + let sender_key = PrivateKey::generate_ed25519(); + let receiver_key = PrivateKey::generate_ed25519(); + + let spec = EvmHookSpec::new(Some(contract_id)); + let lambda_hook = LambdaEvmHook::new(spec, vec![]); + let hook_details = HookCreationDetails::new( + HookExtensionPoint::AccountAllowanceHook, + 1, + Some(lambda_hook), + ); + + // Create sender account with hook + let sender_receipt = AccountCreateTransaction::new() + .key(sender_key.public_key()) + .initial_balance(Hbar::new(10)) + .add_hook(hook_details.clone()) + .freeze_with(&client)? + .sign(sender_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let sender_id = sender_receipt.account_id.unwrap(); + + // Create receiver account with hook + let receiver_receipt = AccountCreateTransaction::new() + .key(receiver_key.public_key()) + .initial_balance(Hbar::new(1)) + .max_automatic_token_associations(-1) + .add_hook(hook_details) + .freeze_with(&client)? + .sign(receiver_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let receiver_id = receiver_receipt.account_id.unwrap(); + + // Create NFT token + let token_receipt = TokenCreateTransaction::new() + .name("Test NFT") + .symbol("TNFT") + .treasury_account_id(sender_id) + .admin_key(treasury_key.public_key()) + .supply_key(treasury_key.public_key()) + .token_type(TokenType::NonFungibleUnique) + .token_supply_type(TokenSupplyType::Infinite) + .freeze_with(&client)? + .sign(sender_key.clone()) + .sign(treasury_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let token_id = token_receipt.token_id.unwrap(); + + // Mint NFT + let mint_receipt = TokenMintTransaction::new() + .token_id(token_id) + .metadata(vec![vec![1, 2, 3]]) + .freeze_with(&client)? + .sign(treasury_key.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let serial_number = mint_receipt.serials[0] as u64; + + // Create hook calls + let mut sender_evm_hook_call = EvmHookCall::new(Some(vec![])); + sender_evm_hook_call.set_gas_limit(25_000); + let sender_hook_call = HookCall::new(Some(1), Some(sender_evm_hook_call)); + let sender_nft_hook_call = + NftHookCall { hook_call: sender_hook_call, hook_type: NftHookType::PrePostHookSender }; + + let mut receiver_evm_hook_call = EvmHookCall::new(Some(vec![])); + receiver_evm_hook_call.set_gas_limit(25_000); + let receiver_hook_call = HookCall::new(Some(1), Some(receiver_evm_hook_call)); + let receiver_nft_hook_call = + NftHookCall { hook_call: receiver_hook_call, hook_type: NftHookType::PrePostHookReceiver }; + + // Perform NFT transfer with both hooks + let transfer_receipt = TransferTransaction::new() + .nft_transfer_with_both_hooks( + token_id.nft(serial_number), + sender_id, + receiver_id, + sender_nft_hook_call, + receiver_nft_hook_call, + ) + .freeze_with(&client)? + .sign(sender_key) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + assert_matches!(transfer_receipt.status, Status::Success); + + Ok(()) +} From bcd5035035ce6957a13cabb970b70eeab323ba60 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Sun, 9 Nov 2025 23:19:05 +0200 Subject: [PATCH 3/9] fix: uncomment hook modules required for compilation --- src/hooks/mod.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index 090959287..e66bb31ef 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -1,31 +1,35 @@ pub mod evm_hook_call; pub mod evm_hook_spec; -// pub mod fungible_hook_call; -// pub mod fungible_hook_type; -// pub mod hook_call; +pub mod fungible_hook_call; +pub mod fungible_hook_type; +pub mod hook_call; pub mod hook_creation_details; pub mod hook_entity_id; pub mod hook_extension_point; pub mod hook_id; pub mod hook_type; pub mod lambda_evm_hook; -// pub mod lambda_s_store_transaction; +pub mod lambda_s_store_transaction; pub mod lambda_storage_slot; pub mod lambda_storage_update; -// pub mod nft_hook_call; -// pub mod nft_hook_type; +pub mod nft_hook_call; +pub mod nft_hook_type; pub use evm_hook_call::EvmHookCall; pub use evm_hook_spec::EvmHookSpec; -//pub use fungible_hook_call::FungibleHookCall; -// pub use fungible_hook_type::FungibleHookType; -//pub use hook_call::HookCall; +pub use fungible_hook_call::FungibleHookCall; +pub use fungible_hook_type::FungibleHookType; +pub use hook_call::HookCall; pub use hook_creation_details::HookCreationDetails; pub use hook_entity_id::HookEntityId; pub use hook_extension_point::HookExtensionPoint; pub use hook_id::HookId; pub use lambda_evm_hook::LambdaEvmHook; +pub use lambda_s_store_transaction::{ + LambdaSStoreTransaction, + LambdaSStoreTransactionData, +}; pub use lambda_storage_slot::LambdaStorageSlot; pub use lambda_storage_update::LambdaStorageUpdate; -//pub use nft_hook_call::NftHookCall; -// pub use nft_hook_type::NftHookType; +pub use nft_hook_call::NftHookCall; +pub use nft_hook_type::NftHookType; From 7ecc355dd1ec00db9f09fd17f805808e1fee0c43 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Sun, 9 Nov 2025 23:50:50 +0200 Subject: [PATCH 4/9] fix: comment out types for public api Signed-off-by: Ivaylo Nikolov --- src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bdf94b0dc..986744529 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -248,9 +248,9 @@ pub use hedera_proto::services::ResponseCodeEnum as Status; pub use hooks::{ EvmHookCall, EvmHookSpec, - // FungibleHookCall, - // FungibleHookType, - // HookCall, + FungibleHookCall, + FungibleHookType, + HookCall, HookCreationDetails, HookEntityId, HookExtensionPoint, @@ -258,8 +258,8 @@ pub use hooks::{ LambdaEvmHook, LambdaStorageSlot, LambdaStorageUpdate, - // NftHookCall, - // NftHookType, + NftHookCall, + NftHookType, }; pub use key::{ Key, From e34a671ae0398b9d68b8a74bc183edb057b68d43 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Sun, 9 Nov 2025 23:55:01 +0200 Subject: [PATCH 5/9] refactor: foramtting Signed-off-by: Ivaylo Nikolov --- examples/transfer_with_hooks.rs | 52 ++++++++++---------------- tests/e2e/token/transfer_with_hooks.rs | 42 ++++++--------------- 2 files changed, 32 insertions(+), 62 deletions(-) diff --git a/examples/transfer_with_hooks.rs b/examples/transfer_with_hooks.rs index a602ee7eb..573f718a1 100644 --- a/examples/transfer_with_hooks.rs +++ b/examples/transfer_with_hooks.rs @@ -162,53 +162,41 @@ async fn main() -> anyhow::Result<()> { // HBAR transfer with pre-tx allowance hook let hbar_hook = FungibleHookCall { - hook_call: HookCall::new( - Some(hook_id), - { - let mut evm_call = EvmHookCall::new(Some(vec![0x01, 0x02])); - evm_call.set_gas_limit(20_000); - Some(evm_call) - }, - ), + hook_call: HookCall::new(Some(hook_id), { + let mut evm_call = EvmHookCall::new(Some(vec![0x01, 0x02])); + evm_call.set_gas_limit(20_000); + Some(evm_call) + }), hook_type: FungibleHookType::PreTxAllowanceHook, }; // NFT sender hook (pre-hook) let nft_sender_hook = NftHookCall { - hook_call: HookCall::new( - Some(hook_id), - { - let mut evm_call = EvmHookCall::new(Some(vec![0x03, 0x04])); - evm_call.set_gas_limit(20_000); - Some(evm_call) - }, - ), + hook_call: HookCall::new(Some(hook_id), { + let mut evm_call = EvmHookCall::new(Some(vec![0x03, 0x04])); + evm_call.set_gas_limit(20_000); + Some(evm_call) + }), hook_type: NftHookType::PreHookSender, }; // NFT receiver hook (pre-hook) let nft_receiver_hook = NftHookCall { - hook_call: HookCall::new( - Some(hook_id), - { - let mut evm_call = EvmHookCall::new(Some(vec![0x05, 0x06])); - evm_call.set_gas_limit(20_000); - Some(evm_call) - }, - ), + hook_call: HookCall::new(Some(hook_id), { + let mut evm_call = EvmHookCall::new(Some(vec![0x05, 0x06])); + evm_call.set_gas_limit(20_000); + Some(evm_call) + }), hook_type: NftHookType::PreHookReceiver, }; // Fungible token transfer with pre-post allowance hook let fungible_token_hook = FungibleHookCall { - hook_call: HookCall::new( - Some(hook_id), - { - let mut evm_call = EvmHookCall::new(Some(vec![0x07, 0x08])); - evm_call.set_gas_limit(20_000); - Some(evm_call) - }, - ), + hook_call: HookCall::new(Some(hook_id), { + let mut evm_call = EvmHookCall::new(Some(vec![0x07, 0x08])); + evm_call.set_gas_limit(20_000); + Some(evm_call) + }), hook_type: FungibleHookType::PrePostTxAllowanceHook, }; diff --git a/tests/e2e/token/transfer_with_hooks.rs b/tests/e2e/token/transfer_with_hooks.rs index 877659eb2..3b3802636 100644 --- a/tests/e2e/token/transfer_with_hooks.rs +++ b/tests/e2e/token/transfer_with_hooks.rs @@ -58,11 +58,8 @@ async fn can_transfer_hbar_with_pre_tx_allowance_hook() -> anyhow::Result<()> { // Create sender account with hook let spec = EvmHookSpec::new(Some(contract_id)); let lambda_hook = LambdaEvmHook::new(spec, vec![]); - let hook_details = HookCreationDetails::new( - HookExtensionPoint::AccountAllowanceHook, - 1, - Some(lambda_hook), - ); + let hook_details = + HookCreationDetails::new(HookExtensionPoint::AccountAllowanceHook, 1, Some(lambda_hook)); let sender_receipt = AccountCreateTransaction::new() .key(sender_key.public_key()) @@ -126,11 +123,8 @@ async fn can_transfer_hbar_with_pre_post_tx_allowance_hook() -> anyhow::Result<( // Create sender account with hook let spec = EvmHookSpec::new(Some(contract_id)); let lambda_hook = LambdaEvmHook::new(spec, vec![]); - let hook_details = HookCreationDetails::new( - HookExtensionPoint::AccountAllowanceHook, - 1, - Some(lambda_hook), - ); + let hook_details = + HookCreationDetails::new(HookExtensionPoint::AccountAllowanceHook, 1, Some(lambda_hook)); let sender_receipt = AccountCreateTransaction::new() .key(sender_key.public_key()) @@ -195,11 +189,8 @@ async fn can_transfer_fungible_token_with_hook() -> anyhow::Result<()> { // Create sender account with hook let spec = EvmHookSpec::new(Some(contract_id)); let lambda_hook = LambdaEvmHook::new(spec, vec![]); - let hook_details = HookCreationDetails::new( - HookExtensionPoint::AccountAllowanceHook, - 1, - Some(lambda_hook), - ); + let hook_details = + HookCreationDetails::new(HookExtensionPoint::AccountAllowanceHook, 1, Some(lambda_hook)); let sender_receipt = AccountCreateTransaction::new() .key(sender_key.public_key()) @@ -285,11 +276,8 @@ async fn can_transfer_nft_with_sender_hook() -> anyhow::Result<()> { // Create sender account with hook let spec = EvmHookSpec::new(Some(contract_id)); let lambda_hook = LambdaEvmHook::new(spec, vec![]); - let hook_details = HookCreationDetails::new( - HookExtensionPoint::AccountAllowanceHook, - 1, - Some(lambda_hook), - ); + let hook_details = + HookCreationDetails::new(HookExtensionPoint::AccountAllowanceHook, 1, Some(lambda_hook)); let sender_receipt = AccountCreateTransaction::new() .key(sender_key.public_key()) @@ -400,11 +388,8 @@ async fn can_transfer_nft_with_receiver_hook() -> anyhow::Result<()> { // Create receiver account with hook let spec = EvmHookSpec::new(Some(contract_id)); let lambda_hook = LambdaEvmHook::new(spec, vec![]); - let hook_details = HookCreationDetails::new( - HookExtensionPoint::AccountAllowanceHook, - 1, - Some(lambda_hook), - ); + let hook_details = + HookCreationDetails::new(HookExtensionPoint::AccountAllowanceHook, 1, Some(lambda_hook)); let receiver_receipt = AccountCreateTransaction::new() .key(receiver_key.public_key()) @@ -492,11 +477,8 @@ async fn can_transfer_nft_with_both_sender_and_receiver_hooks() -> anyhow::Resul let spec = EvmHookSpec::new(Some(contract_id)); let lambda_hook = LambdaEvmHook::new(spec, vec![]); - let hook_details = HookCreationDetails::new( - HookExtensionPoint::AccountAllowanceHook, - 1, - Some(lambda_hook), - ); + let hook_details = + HookCreationDetails::new(HookExtensionPoint::AccountAllowanceHook, 1, Some(lambda_hook)); // Create sender account with hook let sender_receipt = AccountCreateTransaction::new() From 85df9a6349b15fcd9e2da4d52af00d287dc0e137 Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Mon, 10 Nov 2025 00:11:24 +0200 Subject: [PATCH 6/9] fix: follow the js api Signed-off-by: Ivaylo Nikolov --- examples/transfer_with_hooks.rs | 6 ++-- src/hooks/fungible_hook_call.rs | 2 +- src/hooks/nft_hook_call.rs | 2 +- src/transfer_transaction.rs | 45 +------------------------- tests/e2e/token/transfer_with_hooks.rs | 24 +++++++++----- 5 files changed, 22 insertions(+), 57 deletions(-) diff --git a/examples/transfer_with_hooks.rs b/examples/transfer_with_hooks.rs index 573f718a1..567bf423a 100644 --- a/examples/transfer_with_hooks.rs +++ b/examples/transfer_with_hooks.rs @@ -206,7 +206,7 @@ async fn main() -> anyhow::Result<()> { // Transaction 1: HBAR transfers with hook println!("\n1. Executing HBAR TransferTransaction with hook..."); TransferTransaction::new() - .hbar_transfer_with_hook(sender_account_id, Hbar::from_tinybars(-1), hbar_hook) + .add_hbar_transfer_with_hook(sender_account_id, Hbar::from_tinybars(-1), hbar_hook) .hbar_transfer(receiver_account_id, Hbar::from_tinybars(1)) .freeze_with(&client)? .sign(sender_key.clone()) @@ -219,7 +219,7 @@ async fn main() -> anyhow::Result<()> { // Transaction 2: NFT transfer with sender and receiver hooks println!("\n2. Executing NFT TransferTransaction with hooks..."); TransferTransaction::new() - .nft_transfer_with_both_hooks( + .add_nft_transfer_with_hook( nft_id, sender_account_id, receiver_account_id, @@ -237,7 +237,7 @@ async fn main() -> anyhow::Result<()> { // Transaction 3: Fungible token transfers with hook println!("\n3. Executing Fungible Token TransferTransaction with hook..."); TransferTransaction::new() - .token_transfer_with_hook( + .add_token_transfer_with_hook( fungible_token_id, sender_account_id, -1_000, diff --git a/src/hooks/fungible_hook_call.rs b/src/hooks/fungible_hook_call.rs index 395de3da4..1b723c08d 100644 --- a/src/hooks/fungible_hook_call.rs +++ b/src/hooks/fungible_hook_call.rs @@ -1,7 +1,6 @@ use hedera_proto::services; use crate::hooks::{ - EvmHookCall, FungibleHookType, HookCall, }; @@ -45,6 +44,7 @@ impl ToProtobuf for FungibleHookCall { #[cfg(test)] mod tests { use super::*; + use crate::hooks::EvmHookCall; #[test] fn test_fungible_hook_call_creation() { diff --git a/src/hooks/nft_hook_call.rs b/src/hooks/nft_hook_call.rs index 3f11f4a49..3f679041b 100644 --- a/src/hooks/nft_hook_call.rs +++ b/src/hooks/nft_hook_call.rs @@ -1,7 +1,6 @@ use hedera_proto::services; use crate::hooks::{ - EvmHookCall, HookCall, NftHookType, }; @@ -45,6 +44,7 @@ impl ToProtobuf for NftHookCall { #[cfg(test)] mod tests { use super::*; + use crate::hooks::EvmHookCall; #[test] fn test_nft_hook_call_creation() { diff --git a/src/transfer_transaction.rs b/src/transfer_transaction.rs index 58716c051..d1a8cab0a 100644 --- a/src/transfer_transaction.rs +++ b/src/transfer_transaction.rs @@ -305,28 +305,6 @@ impl TransferTransaction { self._hbar_transfer(account_id, amount, false, Some(hook_call)) } - /// Add an NFT transfer with a sender hook call. - pub fn nft_transfer_with_sender_hook( - &mut self, - nft_id: impl Into, - sender: AccountId, - receiver: AccountId, - sender_hook_call: NftHookCall, - ) -> &mut Self { - self._nft_transfer(nft_id.into(), sender, receiver, false, Some(sender_hook_call), None) - } - - /// Add an NFT transfer with a receiver hook call. - pub fn nft_transfer_with_receiver_hook( - &mut self, - nft_id: impl Into, - sender: AccountId, - receiver: AccountId, - receiver_hook_call: NftHookCall, - ) -> &mut Self { - self._nft_transfer(nft_id.into(), sender, receiver, false, None, Some(receiver_hook_call)) - } - /// Add a token transfer with a fungible hook call. pub fn add_token_transfer_with_hook( &mut self, @@ -338,29 +316,8 @@ impl TransferTransaction { self._token_transfer(token_id, account_id, amount, false, None, Some(hook_call)) } - /// Add a hbar transfer with a fungible hook call. - pub fn hbar_transfer_with_hook( - &mut self, - account_id: AccountId, - amount: Hbar, - hook_call: FungibleHookCall, - ) -> &mut Self { - self._hbar_transfer(account_id, amount, false, Some(hook_call)) - } - - /// Add a token transfer with a fungible hook call. - pub fn token_transfer_with_hook( - &mut self, - token_id: TokenId, - account_id: AccountId, - amount: i64, - hook_call: FungibleHookCall, - ) -> &mut Self { - self._token_transfer(token_id, account_id, amount, false, None, Some(hook_call)) - } - /// Add an NFT transfer with both sender and receiver hook calls. - pub fn nft_transfer_with_both_hooks( + pub fn add_nft_transfer_with_hook( &mut self, nft_id: impl Into, sender: AccountId, diff --git a/tests/e2e/token/transfer_with_hooks.rs b/tests/e2e/token/transfer_with_hooks.rs index 3b3802636..53107dfb6 100644 --- a/tests/e2e/token/transfer_with_hooks.rs +++ b/tests/e2e/token/transfer_with_hooks.rs @@ -95,7 +95,7 @@ async fn can_transfer_hbar_with_pre_tx_allowance_hook() -> anyhow::Result<()> { // Perform transfer with hook let transfer_receipt = TransferTransaction::new() - .hbar_transfer_with_hook(sender_id, Hbar::from_tinybars(-100), fungible_hook_call) + .add_hbar_transfer_with_hook(sender_id, Hbar::from_tinybars(-100), fungible_hook_call) .hbar_transfer(receiver_id, Hbar::from_tinybars(100)) .freeze_with(&client)? .sign(sender_key) @@ -160,7 +160,7 @@ async fn can_transfer_hbar_with_pre_post_tx_allowance_hook() -> anyhow::Result<( // Perform transfer with hook let transfer_receipt = TransferTransaction::new() - .hbar_transfer_with_hook(sender_id, Hbar::from_tinybars(-100), fungible_hook_call) + .add_hbar_transfer_with_hook(sender_id, Hbar::from_tinybars(-100), fungible_hook_call) .hbar_transfer(receiver_id, Hbar::from_tinybars(100)) .freeze_with(&client)? .sign(sender_key) @@ -247,7 +247,7 @@ async fn can_transfer_fungible_token_with_hook() -> anyhow::Result<()> { // Perform token transfer with hook let transfer_receipt = TransferTransaction::new() - .token_transfer_with_hook(token_id, sender_id, -10, fungible_hook_call) + .add_token_transfer_with_hook(token_id, sender_id, -10, fungible_hook_call) .token_transfer(token_id, receiver_id, 10) .freeze_with(&client)? .sign(sender_key) @@ -342,13 +342,18 @@ async fn can_transfer_nft_with_sender_hook() -> anyhow::Result<()> { let hook_call = HookCall::new(Some(1), Some(evm_hook_call)); let sender_nft_hook_call = NftHookCall { hook_call, hook_type: NftHookType::PreHookSender }; - // Perform NFT transfer with sender hook + // Perform NFT transfer with sender hook (receiver hook is dummy/empty) + let dummy_receiver_hook = NftHookCall { + hook_call: HookCall::new(None, None), + hook_type: NftHookType::PreHookReceiver, + }; let transfer_receipt = TransferTransaction::new() - .nft_transfer_with_sender_hook( + .add_nft_transfer_with_hook( token_id.nft(serial_number), sender_id, receiver_id, sender_nft_hook_call, + dummy_receiver_hook, ) .freeze_with(&client)? .sign(sender_key) @@ -443,12 +448,15 @@ async fn can_transfer_nft_with_receiver_hook() -> anyhow::Result<()> { let hook_call = HookCall::new(Some(1), Some(evm_hook_call)); let receiver_nft_hook_call = NftHookCall { hook_call, hook_type: NftHookType::PreHookReceiver }; - // Perform NFT transfer with receiver hook + // Perform NFT transfer with receiver hook (sender hook is dummy/empty) + let dummy_sender_hook = + NftHookCall { hook_call: HookCall::new(None, None), hook_type: NftHookType::PreHookSender }; let transfer_receipt = TransferTransaction::new() - .nft_transfer_with_receiver_hook( + .add_nft_transfer_with_hook( token_id.nft(serial_number), sender_id, receiver_id, + dummy_sender_hook, receiver_nft_hook_call, ) .freeze_with(&client)? @@ -556,7 +564,7 @@ async fn can_transfer_nft_with_both_sender_and_receiver_hooks() -> anyhow::Resul // Perform NFT transfer with both hooks let transfer_receipt = TransferTransaction::new() - .nft_transfer_with_both_hooks( + .add_nft_transfer_with_hook( token_id.nft(serial_number), sender_id, receiver_id, From 50bb069a25019f1508bd70ef8f643b9b0d4f39bf Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Mon, 10 Nov 2025 00:17:49 +0200 Subject: [PATCH 7/9] fix: follow the js api in lambda sstore Signed-off-by: Ivaylo Nikolov --- src/hooks/lambda_s_store_transaction.rs | 6 ++-- tests/e2e/hooks/lambda_sstore.rs | 45 +++++++++++++++---------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/hooks/lambda_s_store_transaction.rs b/src/hooks/lambda_s_store_transaction.rs index 2abfad5c5..209bf9d64 100644 --- a/src/hooks/lambda_s_store_transaction.rs +++ b/src/hooks/lambda_s_store_transaction.rs @@ -39,13 +39,13 @@ impl Default for LambdaSStoreTransactionData { impl LambdaSStoreTransaction { /// Set the hook ID. - pub fn hook_id(&mut self, hook_id: HookId) -> &mut Self { + pub fn set_hook_id(&mut self, hook_id: HookId) -> &mut Self { self.data_mut().hook_id = Some(hook_id); self } /// Set the storage updates. - pub fn storage_updates(&mut self, storage_updates: Vec) -> &mut Self { + pub fn set_storage_updates(&mut self, storage_updates: Vec) -> &mut Self { self.data_mut().storage_updates = storage_updates; self } @@ -162,7 +162,7 @@ mod tests { let storage_update = LambdaStorageUpdate::StorageSlot(storage_slot); let mut transaction = LambdaSStoreTransaction::new(); - transaction.hook_id(hook_id.clone()).add_storage_update(storage_update); + transaction.set_hook_id(hook_id.clone()).add_storage_update(storage_update); assert_eq!(transaction.get_hook_id(), Some(&hook_id)); assert_eq!(transaction.get_storage_updates().len(), 1); diff --git a/tests/e2e/hooks/lambda_sstore.rs b/tests/e2e/hooks/lambda_sstore.rs index 2149f51bf..519d4842e 100644 --- a/tests/e2e/hooks/lambda_sstore.rs +++ b/tests/e2e/hooks/lambda_sstore.rs @@ -45,11 +45,13 @@ async fn create_account_with_hook( let account_key = PrivateKey::generate_ed25519(); // Create initial storage slot (use minimal representation - no leading zeros) - let storage_slot = LambdaStorageSlot::new(vec![0x01, 0x02, 0x03, 0x04], vec![0x05, 0x06, 0x07, 0x08]); + let storage_slot = + LambdaStorageSlot::new(vec![0x01, 0x02, 0x03, 0x04], vec![0x05, 0x06, 0x07, 0x08]); // Create lambda hook with storage let spec = EvmHookSpec::new(Some(contract_id)); - let lambda_hook = LambdaEvmHook::new(spec, vec![LambdaStorageUpdate::StorageSlot(storage_slot)]); + let lambda_hook = + LambdaEvmHook::new(spec, vec![LambdaStorageUpdate::StorageSlot(storage_slot)]); let hook_details = HookCreationDetails::new(HookExtensionPoint::AccountAllowanceHook, 1, Some(lambda_hook)); @@ -82,15 +84,17 @@ async fn can_update_storage_slots_with_valid_signatures() -> anyhow::Result<()> }; let contract_id = create_hook_contract(&client).await?; - let (_account_id, account_key, hook_id) = create_account_with_hook(&client, contract_id).await?; + let (_account_id, account_key, hook_id) = + create_account_with_hook(&client, contract_id).await?; // Create new storage update (use minimal representation - no leading zeros) - let new_storage_slot = LambdaStorageSlot::new(vec![0x09, 0x0a, 0x0b, 0x0c], vec![0x0d, 0x0e, 0x0f, 0x10]); + let new_storage_slot = + LambdaStorageSlot::new(vec![0x09, 0x0a, 0x0b, 0x0c], vec![0x0d, 0x0e, 0x0f, 0x10]); let storage_update = LambdaStorageUpdate::StorageSlot(new_storage_slot); // Update storage slots let receipt = LambdaSStoreTransaction::new() - .hook_id(hook_id) + .set_hook_id(hook_id) .add_storage_update(storage_update) .freeze_with(&client)? .sign(account_key) @@ -111,16 +115,18 @@ async fn cannot_update_more_than_256_storage_slots() -> anyhow::Result<()> { }; let contract_id = create_hook_contract(&client).await?; - let (_account_id, account_key, hook_id) = create_account_with_hook(&client, contract_id).await?; + let (_account_id, account_key, hook_id) = + create_account_with_hook(&client, contract_id).await?; // Create 257 storage slots (exceeds limit) - let storage_slot = LambdaStorageSlot::new(vec![0x01, 0x02, 0x03, 0x04], vec![0x05, 0x06, 0x07, 0x08]); + let storage_slot = + LambdaStorageSlot::new(vec![0x01, 0x02, 0x03, 0x04], vec![0x05, 0x06, 0x07, 0x08]); let storage_updates: Vec = (0..257).map(|_| LambdaStorageUpdate::StorageSlot(storage_slot.clone())).collect(); let result = LambdaSStoreTransaction::new() - .hook_id(hook_id) - .storage_updates(storage_updates) + .set_hook_id(hook_id) + .set_storage_updates(storage_updates) .freeze_with(&client)? .sign(account_key) .execute(&client) @@ -149,16 +155,18 @@ async fn cannot_update_storage_with_invalid_signature() -> anyhow::Result<()> { }; let contract_id = create_hook_contract(&client).await?; - let (_account_id, _account_key, hook_id) = create_account_with_hook(&client, contract_id).await?; + let (_account_id, _account_key, hook_id) = + create_account_with_hook(&client, contract_id).await?; // Use wrong key let invalid_key = PrivateKey::generate_ed25519(); - let storage_slot = LambdaStorageSlot::new(vec![0x31, 0x32, 0x33, 0x34], vec![0x35, 0x36, 0x37, 0x38]); + let storage_slot = + LambdaStorageSlot::new(vec![0x31, 0x32, 0x33, 0x34], vec![0x35, 0x36, 0x37, 0x38]); let storage_update = LambdaStorageUpdate::StorageSlot(storage_slot); let result = LambdaSStoreTransaction::new() - .hook_id(hook_id) + .set_hook_id(hook_id) .add_storage_update(storage_update) .freeze_with(&client)? .sign(invalid_key) @@ -185,13 +193,15 @@ async fn cannot_update_storage_for_nonexistent_hook() -> anyhow::Result<()> { }; let contract_id = create_hook_contract(&client).await?; - let (account_id, account_key, _hook_id) = create_account_with_hook(&client, contract_id).await?; + let (account_id, account_key, _hook_id) = + create_account_with_hook(&client, contract_id).await?; // Use non-existent hook ID let entity_id = HookEntityId::new(Some(account_id)); let nonexistent_hook_id = HookId::new(Some(entity_id), 999); - let storage_slot = LambdaStorageSlot::new(vec![0x41, 0x42, 0x43, 0x44], vec![0x45, 0x46, 0x47, 0x48]); + let storage_slot = + LambdaStorageSlot::new(vec![0x41, 0x42, 0x43, 0x44], vec![0x45, 0x46, 0x47, 0x48]); let storage_update = LambdaStorageUpdate::StorageSlot(storage_slot); let result = LambdaSStoreTransaction::new() @@ -224,7 +234,8 @@ async fn can_update_multiple_storage_slots() -> anyhow::Result<()> { }; let contract_id = create_hook_contract(&client).await?; - let (_account_id, account_key, hook_id) = create_account_with_hook(&client, contract_id).await?; + let (_account_id, account_key, hook_id) = + create_account_with_hook(&client, contract_id).await?; // Create multiple storage updates let storage_slot1 = @@ -242,8 +253,8 @@ async fn can_update_multiple_storage_slots() -> anyhow::Result<()> { // Update multiple storage slots at once let receipt = LambdaSStoreTransaction::new() - .hook_id(hook_id) - .storage_updates(storage_updates) + .set_hook_id(hook_id) + .set_storage_updates(storage_updates) .freeze_with(&client)? .sign(account_key) .execute(&client) From 9dac84cdcc1474a5306505286c0e5c15d897a40e Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Mon, 10 Nov 2025 16:01:35 +0200 Subject: [PATCH 8/9] feat: implement partial eq and eq Signed-off-by: Ivaylo Nikolov --- src/hooks/lambda_s_store_transaction.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/lambda_s_store_transaction.rs b/src/hooks/lambda_s_store_transaction.rs index 209bf9d64..590a8e380 100644 --- a/src/hooks/lambda_s_store_transaction.rs +++ b/src/hooks/lambda_s_store_transaction.rs @@ -23,7 +23,7 @@ use crate::{ /// A transaction to store lambda data in hook storage. pub type LambdaSStoreTransaction = Transaction; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct LambdaSStoreTransactionData { /// The hook ID to store data for. hook_id: Option, From 3aee687eb861d7c7ba222bf97f571438bd4bec4d Mon Sep 17 00:00:00 2001 From: Ivaylo Nikolov Date: Mon, 10 Nov 2025 16:04:04 +0200 Subject: [PATCH 9/9] docs: add coment why 2 habrs Signed-off-by: Ivaylo Nikolov --- src/hooks/lambda_s_store_transaction.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hooks/lambda_s_store_transaction.rs b/src/hooks/lambda_s_store_transaction.rs index 590a8e380..228fb54a9 100644 --- a/src/hooks/lambda_s_store_transaction.rs +++ b/src/hooks/lambda_s_store_transaction.rs @@ -75,6 +75,7 @@ impl LambdaSStoreTransactionData { } impl TransactionData for LambdaSStoreTransactionData { + // 2 habrs are the default max transaction fee for most transaction acrooss the SDK fn default_max_transaction_fee(&self) -> crate::Hbar { crate::Hbar::new(2) }