diff --git a/src/account/account_create_transaction.rs b/src/account/account_create_transaction.rs index 3a28ca82e..ccc625c40 100644 --- a/src/account/account_create_transaction.rs +++ b/src/account/account_create_transaction.rs @@ -76,6 +76,9 @@ pub struct AccountCreateTransactionData { /// If true, the account declines receiving a staking reward. The default value is false. decline_staking_reward: bool, + + /// Hooks to add immediately after creating this account. + hooks: Vec, } impl Default for AccountCreateTransactionData { @@ -91,6 +94,7 @@ impl Default for AccountCreateTransactionData { alias: None, staked_id: None, decline_staking_reward: false, + hooks: Vec::new(), } } } @@ -293,6 +297,20 @@ impl AccountCreateTransaction { self.data_mut().decline_staking_reward = decline; self } + + pub fn add_hook(&mut self, hook: LambdaEvmHook) -> &mut Self { + self.data_mut().hooks.push(hook); + self + } + + pub fn set_hooks(&mut self, hooks: Vec) -> &mut Self { + self.data_mut().hooks = hooks; + self + } + + pub fn get_hooks(&self) -> &[LambdaEvmHook] { + &self.data().hooks + } } impl TransactionData for AccountCreateTransactionData {} @@ -353,6 +371,11 @@ impl FromProtobuf for AccountCreateTransa alias, staked_id: Option::from_protobuf(pb.staked_id)?, decline_staking_reward: pb.decline_reward, + hooks: pb + .hook_creation_details + .into_iter() + .map(LambdaEvmHook::from_protobuf) + .collect::, _>>()?, }) } } @@ -391,6 +414,7 @@ impl ToProtobuf for AccountCreateTransactionData { alias: self.alias.map_or(vec![], |it| it.to_bytes().to_vec()), decline_reward: self.decline_staking_reward, staked_id, + hooks: self.hooks.iter().map(|hook| hook.to_protobuf()).collect(), } } } diff --git a/src/account/account_update_transaction.rs b/src/account/account_update_transaction.rs index 84f555bbe..93929533a 100644 --- a/src/account/account_update_transaction.rs +++ b/src/account/account_update_transaction.rs @@ -8,6 +8,7 @@ use time::{ }; use tonic::transport::Channel; +use crate::hooks::LambdaEvmHook; use crate::ledger_id::RefLedgerId; use crate::protobuf::{ FromProtobuf, @@ -89,6 +90,10 @@ pub struct AccountUpdateTransactionData { /// If true, the account declines receiving a staking reward. The default value is false. decline_staking_reward: Option, + + /// Hooks to add immediately after updating this account. + hooks: Vec, + hook_ids_to_delete: Vec, } impl AccountUpdateTransaction { @@ -271,6 +276,42 @@ impl AccountUpdateTransaction { self.data_mut().decline_staking_reward = Some(decline); self } + + /// Returns the hooks to be created. + #[must_use] + pub fn get_hooks_to_create(&self) -> &[LambdaEvmHook] { + &self.data().hooks + } + + /// Adds a hook to be created. + pub fn add_hook(&mut self, hook: LambdaEvmHook) -> &mut Self { + self.data_mut().hooks.push(hook); + self + } + + /// Sets the hooks to be created. + pub fn set_hooks(&mut self, hooks: Vec) -> &mut Self { + self.data_mut().hooks = hooks; + self + } + + /// Returns the hook IDs to be deleted. + #[must_use] + pub fn get_hooks_to_delete(&self) -> &[i64] { + &self.data().hook_ids_to_delete + } + + /// Adds a hook ID to be deleted. + pub fn delete_hook(&mut self, hook_id: i64) -> &mut Self { + self.data_mut().hook_ids_to_delete.push(hook_id); + self + } + + /// Sets the hook IDs to be deleted. + pub fn delete_hooks(&mut self, hook_ids: Vec) -> &mut Self { + self.data_mut().hook_ids_to_delete = hook_ids; + self + } } impl TransactionData for AccountUpdateTransactionData {} @@ -339,6 +380,12 @@ impl FromProtobuf for AccountUpdateTransa max_automatic_token_associations: pb.max_automatic_token_associations, staked_id: Option::from_protobuf(pb.staked_id)?, decline_staking_reward: pb.decline_reward, + hooks: pb + .hook_creation_details + .into_iter() + .map(LambdaEvmHook::from_protobuf) + .collect::, _>>()?, + hook_ids_to_delete: pb.hook_ids_to_delete, }) } } @@ -382,6 +429,8 @@ impl ToProtobuf for AccountUpdateTransactionData { receive_record_threshold_field: None, receiver_sig_required_field: receiver_signature_required, staked_id, + hooks: self.hooks.iter().map(|hook| hook.to_protobuf()).collect(), + hook_ids_to_delete: self.hook_ids_to_delete, } } } diff --git a/src/contract/contract_create_transaction.rs b/src/contract/contract_create_transaction.rs index dd170ca55..4e999cd40 100644 --- a/src/contract/contract_create_transaction.rs +++ b/src/contract/contract_create_transaction.rs @@ -57,6 +57,9 @@ pub struct ContractCreateTransactionData { staked_id: Option, decline_staking_reward: bool, + + /// Hooks to add immediately after creating this contract. + hooks: Vec, } impl Default for ContractCreateTransactionData { @@ -74,6 +77,7 @@ impl Default for ContractCreateTransactionData { auto_renew_account_id: None, staked_id: None, decline_staking_reward: false, + hooks: Vec::new(), } } } @@ -313,6 +317,11 @@ impl FromProtobuf for ContractCreateTra auto_renew_account_id: Option::from_protobuf(pb.auto_renew_account_id)?, staked_id: Option::from_protobuf(pb.staked_id)?, decline_staking_reward: pb.decline_reward, + hooks: pb + .hook_creation_details + .into_iter() + .map(LambdaEvmHook::from_protobuf) + .collect::, _>>()?, }) } } @@ -372,6 +381,7 @@ impl ToProtobuf for ContractCreateTransactionData { decline_reward: self.decline_staking_reward, initcode_source, staked_id, + hooks: self.hooks.iter().map(|hook| hook.to_protobuf()).collect(), } } } diff --git a/src/contract/contract_update_transaction.rs b/src/contract/contract_update_transaction.rs index 6d4225fc1..bc4540a68 100644 --- a/src/contract/contract_update_transaction.rs +++ b/src/contract/contract_update_transaction.rs @@ -8,6 +8,7 @@ use time::{ }; use tonic::transport::Channel; +use crate::hooks::LambdaEvmHook; use crate::ledger_id::RefLedgerId; use crate::protobuf::FromProtobuf; use crate::staked_id::StakedId; @@ -55,6 +56,10 @@ pub struct ContractUpdateTransactionData { staked_id: Option, decline_staking_reward: Option, + + /// Hooks to add immediately after updating this contract. + hooks: Vec, + hook_ids_to_delete: Vec, } impl ContractUpdateTransaction { @@ -195,6 +200,42 @@ impl ContractUpdateTransaction { self.data_mut().decline_staking_reward = Some(decline); self } + + /// Returns the hooks to be created. + #[must_use] + pub fn get_hooks_to_create(&self) -> &[LambdaEvmHook] { + &self.data().hooks + } + + /// Adds a hook to be created. + pub fn add_hook(&mut self, hook: LambdaEvmHook) -> &mut Self { + self.data_mut().hooks.push(hook); + self + } + + /// Sets the hooks to be created. + pub fn set_hooks(&mut self, hooks: Vec) -> &mut Self { + self.data_mut().hooks = hooks; + self + } + + /// Returns the hook IDs to be deleted. + #[must_use] + pub fn get_hooks_to_delete(&self) -> &[i64] { + &self.data().hook_ids_to_delete + } + + /// Adds a hook ID to be deleted. + pub fn delete_hook(&mut self, hook_id: i64) -> &mut Self { + self.data_mut().hook_ids_to_delete.push(hook_id); + self + } + + /// Sets the hook IDs to be deleted. + pub fn delete_hooks(&mut self, hook_ids: Vec) -> &mut Self { + self.data_mut().hook_ids_to_delete = hook_ids; + self + } } impl TransactionData for ContractUpdateTransactionData {} @@ -255,6 +296,12 @@ impl FromProtobuf for ContractUpdateTra proxy_account_id: Option::from_protobuf(pb.proxy_account_id)?, staked_id: Option::from_protobuf(pb.staked_id)?, decline_staking_reward: pb.decline_reward, + hooks: pb + .hook_creation_details + .into_iter() + .map(LambdaEvmHook::from_protobuf) + .collect::, _>>()?, + hook_ids_to_delete: pb.hook_ids_to_delete, }) } } @@ -301,6 +348,8 @@ impl ToProtobuf for ContractUpdateTransactionData { staked_id, file_id: None, memo_field, + hook_creation_details: self.hooks.iter().map(|hook| hook.to_protobuf()).collect(), + hook_ids_to_delete: self.hook_ids_to_delete.clone(), } } } diff --git a/src/hooks/evm_hook_call.rs b/src/hooks/evm_hook_call.rs new file mode 100644 index 000000000..37d2f28d5 --- /dev/null +++ b/src/hooks/evm_hook_call.rs @@ -0,0 +1,71 @@ +use crate::protobuf::services; +use crate::{ + FromProtobuf, + ToProtobuf, +}; + +/// An EVM hook call. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EvmHookCall { + /// The call data for the EVM hook. + pub call_data: Option>, +} + +impl EvmHookCall { + /// Create a new `EvmHookCall`. + pub fn new(call_data: Option>) -> Self { + Self { call_data } + } + + pub fn set_call_data(&mut self, call_data: Vec) -> &mut Self { + self.call_data = Some(call_data); + self + } +} + +impl ToProtobuf for EvmHookCall { + type Protobuf = services::EvmHookCall; + + fn to_protobuf(&self) -> Self::Protobuf { + services::EvmHookCall { call_data: self.call_data.clone().unwrap_or_default() } + } +} + +impl FromProtobuf for EvmHookCall { + fn from_protobuf(pb: services::EvmHookCall) -> crate::Result { + Ok(Self { call_data: if pb.call_data.is_empty() { None } else { Some(pb.call_data) } }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_evm_hook_call_creation() { + let call_data = vec![1, 2, 3, 4, 5]; + let hook_call = EvmHookCall::with_call_data(call_data.clone()); + + assert_eq!(hook_call.call_data, Some(call_data)); + } + + #[test] + fn test_evm_hook_call_setters() { + let mut hook_call = EvmHookCall::new(None); + let call_data = vec![6, 7, 8, 9, 10]; + + hook_call.set_call_data(call_data.clone()); + assert_eq!(hook_call.call_data, Some(call_data)); + } + + #[test] + fn test_evm_hook_call_protobuf_roundtrip() { + let call_data = vec![11, 12, 13, 14, 15]; + let original = EvmHookCall::with_call_data(call_data); + + let protobuf = original.to_protobuf(); + let reconstructed = EvmHookCall::from_protobuf(protobuf).unwrap(); + + assert_eq!(original, reconstructed); + } +} diff --git a/src/hooks/evm_hook_spec.rs b/src/hooks/evm_hook_spec.rs new file mode 100644 index 000000000..36e0114ea --- /dev/null +++ b/src/hooks/evm_hook_spec.rs @@ -0,0 +1,75 @@ +use crate::contract::ContractId; +use crate::protobuf::services; +use crate::{ + FromProtobuf, + ToProtobuf, +}; + +/// Shared specifications for an EVM hook. May be used for any extension point. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EvmHookSpec { + /// The id of a contract that implements the extension point API with EVM bytecode. + pub contract_id: Option, +} + +impl EvmHookSpec { + /// Create a new `EvmHookSpec`. + pub fn new(contract_id: Option) -> Self { + Self { contract_id } + } + + /// Create a new `EvmHookSpec` with a contract ID. + pub fn with_contract_id(contract_id: ContractId) -> Self { + Self { contract_id: Some(contract_id) } + } +} + +impl ToProtobuf for EvmHookSpec { + type Protobuf = services::EvmHookSpec; + + fn to_protobuf(&self) -> Self::Protobuf { + services::EvmHookSpec { + bytecode_source: self + .contract_id + .as_ref() + .map(|id| services::evm_hook_spec::BytecodeSource::ContractId(id.to_protobuf())), + } + } +} + +impl FromProtobuf for EvmHookSpec { + fn from_protobuf(pb: services::EvmHookSpec) -> crate::Result { + let contract_id = match pb.bytecode_source { + Some(services::evm_hook_spec::BytecodeSource::ContractId(id)) => { + Some(ContractId::from_protobuf(id)?) + } + None => None, + }; + + Ok(Self { contract_id }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::contract::ContractId; + + #[test] + fn test_evm_hook_spec_creation() { + let contract_id = ContractId::new(0, 0, 123); + let spec = EvmHookSpec::with_contract_id(contract_id.clone()); + + assert_eq!(spec.contract_id, Some(contract_id)); + } + + #[test] + fn test_evm_hook_spec_protobuf_roundtrip() { + let contract_id = ContractId::new(0, 0, 456); + let original = EvmHookSpec::with_contract_id(contract_id); + let protobuf = original.to_protobuf(); + let reconstructed = EvmHookSpec::from_protobuf(protobuf).unwrap(); + + assert_eq!(original, reconstructed); + } +} diff --git a/src/hooks/hook_call.rs b/src/hooks/hook_call.rs new file mode 100644 index 000000000..a5d94117f --- /dev/null +++ b/src/hooks/hook_call.rs @@ -0,0 +1,125 @@ +use crate::hooks::EvmHookCall; +use crate::protobuf::services; +use crate::{ + FromProtobuf, + ToProtobuf, +}; + +/// A hook call containing a hook ID and call data. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HookCall { + /// The ID of the hook to call. + pub hook_id: Option, + /// The call data for the hook. + pub call: Option, +} + +impl HookCall { + /// Create a new `HookCall`. + pub fn new(hook_id: Option, call: Option) -> Self { + Self { hook_id, call } + } + + /// Create a new `HookCall` with a hook ID. + pub fn with_hook_id(hook_id: i64) -> Self { + Self { hook_id: Some(hook_id), call: None } + } + + /// Create a new `HookCall` with call data. + pub fn with_call(call: EvmHookCall) -> Self { + Self { hook_id: None, call: Some(call) } + } + + /// Set the hook ID. + pub fn set_hook_id(&mut self, hook_id: i64) -> &mut Self { + self.hook_id = Some(hook_id); + self + } + + /// Set the call data. + pub fn set_call(&mut self, call: EvmHookCall) -> &mut Self { + self.call = Some(call); + self + } +} + +impl ToProtobuf for HookCall { + type Protobuf = services::HookCall; + + fn to_protobuf(&self) -> Self::Protobuf { + services::HookCall { + hook_id: self.hook_id, + evm_hook_call: self.call.as_ref().map(|call| call.to_protobuf()), + } + } +} + +impl FromProtobuf for HookCall { + fn from_protobuf(pb: services::HookCall) -> crate::Result { + let call = pb.evm_hook_call.map(EvmHookCall::from_protobuf).transpose()?; + + Ok(Self { hook_id: pb.hook_id, call }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hook_call_creation() { + let hook_id = 123; + let call_data = vec![1, 2, 3, 4, 5]; + let evm_call = EvmHookCall::with_call_data(call_data); + + let hook_call = HookCall::new(Some(hook_id), Some(evm_call.clone())); + + assert_eq!(hook_call.hook_id, Some(hook_id)); + assert_eq!(hook_call.call, Some(evm_call)); + } + + #[test] + fn test_hook_call_with_hook_id() { + let hook_id = 456; + let hook_call = HookCall::with_hook_id(hook_id); + + assert_eq!(hook_call.hook_id, Some(hook_id)); + assert_eq!(hook_call.call, None); + } + + #[test] + fn test_hook_call_with_call() { + let call_data = vec![6, 7, 8, 9, 10]; + let evm_call = EvmHookCall::with_call_data(call_data.clone()); + let hook_call = HookCall::with_call(evm_call.clone()); + + assert_eq!(hook_call.hook_id, None); + assert_eq!(hook_call.call, Some(evm_call)); + } + + #[test] + fn test_hook_call_setters() { + let mut hook_call = HookCall::new(None, None); + let hook_id = 789; + let call_data = vec![11, 12, 13, 14, 15]; + let evm_call = EvmHookCall::with_call_data(call_data); + + hook_call.set_hook_id(hook_id).set_call(evm_call.clone()); + + assert_eq!(hook_call.hook_id, Some(hook_id)); + assert_eq!(hook_call.call, Some(evm_call)); + } + + #[test] + fn test_hook_call_protobuf_roundtrip() { + let hook_id = 999; + let call_data = vec![16, 17, 18, 19, 20]; + let evm_call = EvmHookCall::with_call_data(call_data); + let original = HookCall::new(Some(hook_id), Some(evm_call)); + + let protobuf = original.to_protobuf(); + let reconstructed = HookCall::from_protobuf(protobuf).unwrap(); + + assert_eq!(original, reconstructed); + } +} diff --git a/src/hooks/hook_entity_id.rs b/src/hooks/hook_entity_id.rs new file mode 100644 index 000000000..98615e12f --- /dev/null +++ b/src/hooks/hook_entity_id.rs @@ -0,0 +1,65 @@ +use crate::account::AccountId; +use crate::protobuf::services; +use crate::{ + FromProtobuf, + ToProtobuf, +}; + +/// A hook entity identifier that can contain an account ID. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct HookEntityId { + pub account_id: Option, +} + +impl HookEntityId { + pub fn new(account_id: Option) -> Self { + Self { account_id } + } +} + +impl ToProtobuf for HookEntityId { + type Protobuf = services::HookEntityId; + + fn to_protobuf(&self) -> Self::Protobuf { + services::HookEntityId { account_id: self.account_id.map(|id| id.to_protobuf()) } + } +} + +impl FromProtobuf for HookEntityId { + fn from_protobuf(pb: services::HookEntityId) -> crate::Result { + let account_id = pb.account_id.map(AccountId::from_protobuf).transpose()?; + + Ok(Self { account_id }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hook_entity_id_with_account_id() { + let account_id = AccountId::new(0, 0, 123); + let hook_entity_id = HookEntityId::with_account_id(account_id); + + assert_eq!(hook_entity_id.account_id, Some(account_id)); + } + + #[test] + fn test_hook_entity_id_empty() { + let hook_entity_id = HookEntityId::empty(); + + assert_eq!(hook_entity_id.account_id, None); + } + + #[test] + fn test_hook_entity_id_protobuf_roundtrip() { + let account_id = AccountId::new(0, 0, 456); + let original = HookEntityId::with_account_id(account_id); + + let protobuf = original.to_protobuf(); + let reconstructed = HookEntityId::from_protobuf(protobuf).unwrap(); + + assert_eq!(original, reconstructed); + } +} diff --git a/src/hooks/hook_extension_point.rs b/src/hooks/hook_extension_point.rs new file mode 100644 index 000000000..06d1a025f --- /dev/null +++ b/src/hooks/hook_extension_point.rs @@ -0,0 +1,70 @@ +/// Hook extension points that can be used to register hooks. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum HookExtensionPoint { + /// Account allowance hook extension point. + AccountAllowanceHook = 0, +} + +impl HookExtensionPoint { + /// Get the numeric value of the extension point. + pub const fn value(self) -> u32 { + self as u32 + } + + /// Create a `HookExtensionPoint` from a numeric value. + /// + /// # Errors + /// Returns `None` if the value is not a valid extension point. + pub const fn from_value(value: u32) -> Option { + match value { + 0 => Some(Self::AccountAllowanceHook), + _ => None, + } + } +} + +impl From for u32 { + fn from(point: HookExtensionPoint) -> Self { + point.value() + } +} + +impl TryFrom for HookExtensionPoint { + type Error = (); + + fn try_from(value: u32) -> Result { + Self::from_value(value).ok_or(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hook_extension_point_values() { + assert_eq!(HookExtensionPoint::AccountAllowanceHook.value(), 0); + } + + #[test] + fn test_hook_extension_point_from_value() { + assert_eq!( + HookExtensionPoint::from_value(0), + Some(HookExtensionPoint::AccountAllowanceHook) + ); + assert_eq!(HookExtensionPoint::from_value(1), None); + } + + #[test] + fn test_hook_extension_point_conversions() { + let point = HookExtensionPoint::AccountAllowanceHook; + let value: u32 = point.into(); + assert_eq!(value, 0); + + let point_from_value = HookExtensionPoint::try_from(0).unwrap(); + assert_eq!(point_from_value, HookExtensionPoint::AccountAllowanceHook); + + assert!(HookExtensionPoint::try_from(999).is_err()); + } +} diff --git a/src/hooks/hook_id.rs b/src/hooks/hook_id.rs new file mode 100644 index 000000000..6ab1733f8 --- /dev/null +++ b/src/hooks/hook_id.rs @@ -0,0 +1,98 @@ +use crate::hooks::HookEntityId; +use crate::protobuf::services; +use crate::{ + FromProtobuf, + ToProtobuf, +}; + +/// A hook identifier containing an entity ID and hook ID. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct HookId { + /// The entity ID associated with this hook. + pub entity_id: Option, + /// The hook ID number. + pub hook_id: Option, +} + +impl HookId { + /// Create a new `HookId`. + pub fn new(entity_id: Option, hook_id: Option) -> Self { + Self { entity_id, hook_id } + } + + /// Create a new `HookId` with an entity ID. + pub fn with_entity_id(entity_id: HookEntityId) -> Self { + Self { entity_id: Some(entity_id), hook_id: None } + } + + /// Create a new `HookId` with a hook ID. + pub fn with_hook_id(hook_id: i64) -> Self { + Self { entity_id: None, hook_id: Some(hook_id) } + } + + /// Create a new `HookId` with both entity ID and hook ID. + pub fn with_both(entity_id: HookEntityId, hook_id: i64) -> Self { + Self { entity_id: Some(entity_id), hook_id: Some(hook_id) } + } +} + +impl ToProtobuf for HookId { + type Protobuf = services::HookId; + + fn to_protobuf(&self) -> Self::Protobuf { + services::HookId { + entity_id: self.entity_id.as_ref().map(|id| id.to_protobuf()), + hook_id: self.hook_id, + } + } +} + +impl FromProtobuf for HookId { + fn from_protobuf(pb: services::HookId) -> crate::Result { + let entity_id = pb.entity_id.map(HookEntityId::from_protobuf).transpose()?; + + Ok(Self { entity_id, hook_id: pb.hook_id }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::account::AccountId; + + #[test] + fn test_hook_id_creation() { + let entity_id = HookEntityId::with_account_id(AccountId::new(0, 0, 123)); + let hook_id = HookId::with_both(entity_id.clone(), 456); + + assert_eq!(hook_id.entity_id, Some(entity_id)); + assert_eq!(hook_id.hook_id, Some(456)); + } + + #[test] + fn test_hook_id_with_entity_id_only() { + let entity_id = HookEntityId::with_account_id(AccountId::new(0, 0, 789)); + let hook_id = HookId::with_entity_id(entity_id.clone()); + + assert_eq!(hook_id.entity_id, Some(entity_id)); + assert_eq!(hook_id.hook_id, None); + } + + #[test] + fn test_hook_id_with_hook_id_only() { + let hook_id = HookId::with_hook_id(999); + + assert_eq!(hook_id.entity_id, None); + assert_eq!(hook_id.hook_id, Some(999)); + } + + #[test] + fn test_hook_id_protobuf_roundtrip() { + let entity_id = HookEntityId::with_account_id(AccountId::new(0, 0, 111)); + let original = HookId::with_both(entity_id, 222); + let protobuf = original.to_protobuf(); + let reconstructed = HookId::from_protobuf(protobuf).unwrap(); + + assert_eq!(original, reconstructed); + } +} diff --git a/src/hooks/hook_type.rs b/src/hooks/hook_type.rs new file mode 100644 index 000000000..6b7ee3be5 --- /dev/null +++ b/src/hooks/hook_type.rs @@ -0,0 +1,80 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum HookType { + PreHook = 0, + PrePostHook = 1, + PreHookSender = 2, + PrePostHookSender = 3, + PreHookReceiver = 4, + PrePostHookReceiver = 5, +} + +impl HookType { + pub const fn value(self) -> u32 { + self as u32 + } + + pub const fn from_value(value: u32) -> Option { + match value { + 0 => Some(Self::PreHook), + 1 => Some(Self::PrePostHook), + 2 => Some(Self::PreHookSender), + 3 => Some(Self::PrePostHookSender), + 4 => Some(Self::PreHookReceiver), + 5 => Some(Self::PrePostHookReceiver), + _ => None, + } + } +} + +impl From for u32 { + fn from(hook_type: HookType) -> Self { + hook_type.value() + } +} + +impl TryFrom for HookType { + type Error = (); + + fn try_from(value: u32) -> Result { + Self::from_value(value).ok_or(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hook_type_values() { + assert_eq!(HookType::PreHook.value(), 0); + assert_eq!(HookType::PrePostHook.value(), 1); + assert_eq!(HookType::PreHookSender.value(), 2); + assert_eq!(HookType::PrePostHookSender.value(), 3); + assert_eq!(HookType::PreHookReceiver.value(), 4); + assert_eq!(HookType::PrePostHookReceiver.value(), 5); + } + + #[test] + fn test_hook_type_from_value() { + assert_eq!(HookType::from_value(0), Some(HookType::PreHook)); + assert_eq!(HookType::from_value(1), Some(HookType::PrePostHook)); + assert_eq!(HookType::from_value(2), Some(HookType::PreHookSender)); + assert_eq!(HookType::from_value(3), Some(HookType::PrePostHookSender)); + assert_eq!(HookType::from_value(4), Some(HookType::PreHookReceiver)); + assert_eq!(HookType::from_value(5), Some(HookType::PrePostHookReceiver)); + assert_eq!(HookType::from_value(6), None); + } + + #[test] + fn test_hook_type_conversions() { + let hook_type = HookType::PrePostHook; + let value: u32 = hook_type.into(); + assert_eq!(value, 1); + + let hook_type_from_value = HookType::try_from(1).unwrap(); + assert_eq!(hook_type_from_value, HookType::PrePostHook); + + assert!(HookType::try_from(999).is_err()); + } +} diff --git a/src/hooks/lamba_storage_slot.rs b/src/hooks/lamba_storage_slot.rs new file mode 100644 index 000000000..0feb7c303 --- /dev/null +++ b/src/hooks/lamba_storage_slot.rs @@ -0,0 +1,48 @@ +use crate::protobuf::services; +use crate::{ + FromProtobuf, + ToProtobuf, +}; + +/// A slot in the storage of a lambda EVM hook. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LambdaStorageSlot { + pub key: Vec, + pub value: Vec, +} + +impl LambdaStorageSlot { + pub fn new(key: Vec, value: Vec) -> Self { + Self { key, value } + } + pub fn get_key(&self) -> &[u8] { + &self.key + } + + pub fn get_value(&self) -> &[u8] { + &self.value + } + + /// Set the value. + pub fn set_value(&mut self, value: Vec) { + self.value = value; + } + + pub fn set_key(&mut self, key: Vec) { + self.key = key; + } +} + +impl ToProtobuf for LambdaStorageSlot { + type Protobuf = services::LambdaStorageSlot; + + fn to_protobuf(&self) -> Self::Protobuf { + services::LambdaStorageSlot { key: self.key.clone(), value: self.value.clone() } + } +} + +impl FromProtobuf for LambdaStorageSlot { + fn from_protobuf(pb: services::LambdaStorageSlot) -> crate::Result { + Ok(Self { key: pb.key, value: pb.value }) + } +} diff --git a/src/hooks/lambda_evm_hook.rs b/src/hooks/lambda_evm_hook.rs new file mode 100644 index 000000000..74d8f4118 --- /dev/null +++ b/src/hooks/lambda_evm_hook.rs @@ -0,0 +1,126 @@ +use crate::hooks::{ + EvmHookSpec, + LambdaStorageUpdate, +}; +use crate::protobuf::services; +use crate::{ + FromProtobuf, + ToProtobuf, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LambdaEvmHook { + pub spec: EvmHookSpec, + pub storage_updates: Vec, +} + +impl LambdaEvmHook { + pub fn new(spec: EvmHookSpec, storage_updates: Vec) -> Self { + Self { spec, storage_updates } + } + + pub fn set_storage_updates(&mut self, storage_updates: Vec) -> &mut Self { + self.storage_updates = storage_updates; + self + } + + pub fn add_storage_update(&mut self, storage_update: LambdaStorageUpdate) -> &mut Self { + self.storage_updates.push(storage_update); + self + } +} + +impl ToProtobuf for LambdaEvmHook { + type Protobuf = services::LambdaEvmHook; + + fn to_protobuf(&self) -> Self::Protobuf { + services::LambdaEvmHook { + spec: Some(self.spec.to_protobuf()), + storage_updates: self + .storage_updates + .iter() + .map(|update| update.to_protobuf()) + .collect(), + } + } +} + +impl FromProtobuf for LambdaEvmHook { + fn from_protobuf(pb: services::LambdaEvmHook) -> crate::Result { + let spec = + pb.spec.map(EvmHookSpec::from_protobuf).transpose()?.unwrap_or_else(EvmHookSpec::new); + + let storage_updates = pb + .storage_updates + .into_iter() + .map(LambdaStorageUpdate::from_protobuf) + .collect::, _>>()?; + + Ok(Self { spec, storage_updates }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::contract::ContractId; + use crate::hooks::LambdaStorageSlot; + + #[test] + fn test_lambda_evm_hook_creation() { + let contract_id = ContractId::new(0, 0, 123); + let spec = EvmHookSpec::with_contract_id(contract_id); + let storage_slot = LambdaStorageSlot::new(vec![1, 2, 3], vec![4, 5, 6]); + let storage_update = LambdaStorageUpdate::with_storage_slot(storage_slot); + let storage_updates = vec![storage_update]; + + let hook = LambdaEvmHook::new(spec.clone(), storage_updates.clone()); + + assert_eq!(hook.spec, spec); + assert_eq!(hook.storage_updates, storage_updates); + } + + #[test] + fn test_lambda_evm_hook_with_spec_only() { + let contract_id = ContractId::new(0, 0, 456); + let spec = EvmHookSpec::with_contract_id(contract_id); + let hook = LambdaEvmHook::with_spec(spec.clone()); + + assert_eq!(hook.spec, spec); + assert_eq!(hook.storage_updates.len(), 0); + } + + #[test] + fn test_lambda_evm_hook_setters() { + let contract_id = ContractId::new(0, 0, 789); + let spec = EvmHookSpec::with_contract_id(contract_id); + let mut hook = LambdaEvmHook::with_spec(spec); + + let storage_slot = LambdaStorageSlot::new(vec![1, 2, 3], vec![4, 5, 6]); + let storage_update = LambdaStorageUpdate::with_storage_slot(storage_slot); + let storage_updates = vec![storage_update.clone()]; + + hook.set_storage_updates(storage_updates.clone()); + assert_eq!(hook.storage_updates, storage_updates); + + let another_slot = LambdaStorageSlot::new(vec![7, 8, 9], vec![10, 11, 12]); + let another_update = LambdaStorageUpdate::with_storage_slot(another_slot); + hook.add_storage_update(another_update); + + assert_eq!(hook.storage_updates.len(), 2); + } + + #[test] + fn test_lambda_evm_hook_protobuf_roundtrip() { + let contract_id = ContractId::new(0, 0, 111); + let spec = EvmHookSpec::with_contract_id(contract_id); + let storage_slot = LambdaStorageSlot::new(vec![1, 2, 3], vec![4, 5, 6]); + let storage_update = LambdaStorageUpdate::with_storage_slot(storage_slot); + let original = LambdaEvmHook::new(spec, vec![storage_update]); + + let protobuf = original.to_protobuf(); + let reconstructed = LambdaEvmHook::from_protobuf(protobuf).unwrap(); + + assert_eq!(original, reconstructed); + } +} diff --git a/src/hooks/lambda_s_store_transaction.rs b/src/hooks/lambda_s_store_transaction.rs new file mode 100644 index 000000000..5613ed208 --- /dev/null +++ b/src/hooks/lambda_s_store_transaction.rs @@ -0,0 +1,166 @@ +use hedera_proto::services; +use hedera_proto::services::smart_contract_service_client::SmartContractServiceClient; +use tonic::transport::Channel; + +use crate::hooks::{ + HookId, + LambdaStorageUpdate, +}; +use crate::transaction::{ + ChunkInfo, + ToSchedulableTransactionDataProtobuf, + 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 LambdaSStoreTransactionData { + /// Create a new `LambdaSStoreTransactionData`. + pub fn new() -> Self { + Self::default() + } + + /// Set the hook ID. + pub fn hook_id(mut self, hook_id: HookId) -> Self { + self.hook_id = Some(hook_id); + self + } + + /// Set the storage updates. + pub fn storage_updates(mut self, storage_updates: Vec) -> Self { + self.storage_updates = storage_updates; + self + } + + /// Add a storage update. + pub fn add_storage_update(mut self, storage_update: LambdaStorageUpdate) -> Self { + self.storage_updates.push(storage_update); + self + } + + /// Get the hook ID. + pub fn get_hook_id(&self) -> Option<&HookId> { + self.hook_id.as_ref() + } + + /// Get the storage updates. + pub fn get_storage_updates(&self) -> &[LambdaStorageUpdate] { + &self.storage_updates + } +} + +impl TransactionData for LambdaSStoreTransactionData { + fn default_max_transaction_fee(&self) -> crate::Hbar { + crate::Hbar::new(2) + } +} + +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::with_hook_id(123); + let storage_slot = LambdaStorageSlot::new(vec![1, 2, 3], vec![4, 5, 6]); + let storage_update = LambdaStorageUpdate::with_storage_slot(storage_slot); + + let transaction_data = LambdaSStoreTransactionData::new() + .hook_id(hook_id.clone()) + .add_storage_update(storage_update); + + assert_eq!(transaction_data.get_hook_id(), Some(&hook_id)); + assert_eq!(transaction_data.get_storage_updates().len(), 1); + } + + #[test] + fn test_lambda_s_store_transaction_default() { + let transaction_data = LambdaSStoreTransactionData::new(); + assert_eq!(transaction_data.get_hook_id(), None); + assert_eq!(transaction_data.get_storage_updates().len(), 0); + } +} diff --git a/src/hooks/lambda_storage_update.rs b/src/hooks/lambda_storage_update.rs new file mode 100644 index 000000000..29c931a16 --- /dev/null +++ b/src/hooks/lambda_storage_update.rs @@ -0,0 +1,174 @@ +use crate::hooks::LambdaStorageSlot; +use crate::protobuf::services; +use crate::{ + FromProtobuf, + ToProtobuf, +}; + +/// A lambda storage update containing either a storage slot or mapping entries. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LambdaStorageUpdate { + StorageSlot(LambdaStorageSlot), + MappingEntries(LambdaMappingEntries), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LambdaMappingEntries { + pub mapping_slot: Vec, + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LambdaMappingEntry { + pub key: Option>, + pub value: Option>, +} + +impl LambdaStorageUpdate { + pub fn with_storage_slot(storage_slot: LambdaStorageSlot) -> Self { + Self::StorageSlot(storage_slot) + } + + pub fn with_mapping_entries(mapping_entries: LambdaMappingEntries) -> Self { + Self::MappingEntries(mapping_entries) + } +} + +impl LambdaMappingEntries { + pub fn new(mapping_slot: Vec, entries: Vec) -> Self { + Self { mapping_slot, entries } + } +} + +impl LambdaMappingEntry { + pub fn new(key: Option>, value: Option>) -> Self { + Self { key, value } + } + + pub fn set_key(&mut self, key: Vec) -> &mut Self { + self.key = Some(key); + self + } + + pub fn set_value(&mut self, value: Vec) -> &mut Self { + self.value = Some(value); + self + } +} + +impl ToProtobuf for LambdaStorageUpdate { + type Protobuf = services::LambdaStorageUpdate; + + fn to_protobuf(&self) -> Self::Protobuf { + match self { + Self::StorageSlot(slot) => services::LambdaStorageUpdate { + update: Some(services::lambda_storage_update::Update::StorageSlot( + slot.to_protobuf(), + )), + }, + Self::MappingEntries(entries) => services::LambdaStorageUpdate { + update: Some(services::lambda_storage_update::Update::MappingEntries( + entries.to_protobuf(), + )), + }, + } + } +} + +impl FromProtobuf for LambdaStorageUpdate { + fn from_protobuf(pb: services::LambdaStorageUpdate) -> crate::Result { + match pb.update { + Some(services::lambda_storage_update::Update::StorageSlot(slot)) => { + Ok(Self::StorageSlot(LambdaStorageSlot::from_protobuf(slot)?)) + } + Some(services::lambda_storage_update::Update::MappingEntries(entries)) => { + Ok(Self::MappingEntries(LambdaMappingEntries::from_protobuf(entries)?)) + } + None => Err(crate::Error::basic_parse( + "LambdaStorageUpdate must have either storage_slot or mapping_entries", + )), + } + } +} + +impl ToProtobuf for LambdaMappingEntries { + type Protobuf = services::LambdaMappingEntries; + + fn to_protobuf(&self) -> Self::Protobuf { + services::LambdaMappingEntries { + mapping_slot: self.mapping_slot.clone(), + entries: self.entries.iter().map(|entry| entry.to_protobuf()).collect(), + } + } +} + +impl FromProtobuf for LambdaMappingEntries { + fn from_protobuf(pb: services::LambdaMappingEntries) -> crate::Result { + let entries = pb + .entries + .into_iter() + .map(LambdaMappingEntry::from_protobuf) + .collect::, _>>()?; + + Ok(Self { mapping_slot: pb.mapping_slot, entries }) + } +} + +impl ToProtobuf for LambdaMappingEntry { + type Protobuf = services::LambdaMappingEntry; + + fn to_protobuf(&self) -> Self::Protobuf { + services::LambdaMappingEntry { + entry_key: self + .key + .as_ref() + .map(|key| services::lambda_mapping_entry::EntryKey::Key(key.clone())), + value: self.value.clone().unwrap_or_default(), + } + } +} + +impl FromProtobuf for LambdaMappingEntry { + fn from_protobuf(pb: services::LambdaMappingEntry) -> crate::Result { + let key = match pb.entry_key { + Some(services::lambda_mapping_entry::EntryKey::Key(k)) => Some(k), + Some(services::lambda_mapping_entry::EntryKey::Preimage(_)) => { + // Note: Preimage is not supported in the simplified version + None + } + None => None, + }; + + Ok(Self { key, value: if pb.value.is_empty() { None } else { Some(pb.value) } }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_lambda_mapping_entry_creation() { + let entry = LambdaMappingEntry::with_key_value(vec![1, 2, 3], vec![4, 5, 6]); + assert_eq!(entry.key, Some(vec![1, 2, 3])); + assert_eq!(entry.value, Some(vec![4, 5, 6])); + } + + #[test] + fn test_lambda_mapping_entry_setters() { + let mut entry = LambdaMappingEntry::new(None, None); + entry.set_key(vec![7, 8, 9]).set_value(vec![10, 11, 12]); + + assert_eq!(entry.key, Some(vec![7, 8, 9])); + assert_eq!(entry.value, Some(vec![10, 11, 12])); + } + + #[test] + fn test_lambda_mapping_entry_protobuf_roundtrip() { + let original = LambdaMappingEntry::with_key_value(vec![1, 2, 3], vec![4, 5, 6]); + let protobuf = original.to_protobuf(); + let reconstructed = LambdaMappingEntry::from_protobuf(protobuf).unwrap(); + + assert_eq!(original, reconstructed); + } +} diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs new file mode 100644 index 000000000..22535d021 --- /dev/null +++ b/src/hooks/mod.rs @@ -0,0 +1,30 @@ +pub mod evm_hook_call; +pub mod evm_hook_spec; +pub mod hook_call; +pub mod hook_entity_id; +pub mod hook_extension_point; +pub mod hook_id; +pub mod hook_type; +pub mod lamba_storage_slot; +pub mod lambda_evm_hook; +pub mod lambda_s_store_transaction; +pub mod lambda_storage_update; + +pub use evm_hook_call::EvmHookCall; +pub use evm_hook_spec::EvmHookSpec; +pub use hook_call::HookCall; +pub use hook_entity_id::HookEntityId; +pub use hook_extension_point::HookExtensionPoint; +pub use hook_id::HookId; +pub use hook_type::HookType; +pub use lamba_storage_slot::LambdaStorageSlot; +pub use lambda_evm_hook::LambdaEvmHook; +pub use lambda_s_store_transaction::{ + LambdaSStoreTransaction, + LambdaSStoreTransactionData, +}; +pub use lambda_storage_update::{ + LambdaMappingEntries, + LambdaMappingEntry, + LambdaStorageUpdate, +}; diff --git a/src/token/token_airdrop_transaction.rs b/src/token/token_airdrop_transaction.rs index 71824760a..797ed7ce6 100644 --- a/src/token/token_airdrop_transaction.rs +++ b/src/token/token_airdrop_transaction.rs @@ -197,7 +197,13 @@ impl TokenAirdropTransaction { amount: i64, is_approved: bool, ) -> &mut Self { - let transfer = Transfer { account_id, amount, is_approval: is_approved }; + let transfer = Transfer { + account_id, + amount, + is_approval: is_approved, + pre_tx_allowance_hook: None, + pre_post_tx_allowance_hook: None, + }; let data = self.data_mut(); if let Some(tt) = data.token_transfers.iter_mut().find(|tt| tt.token_id == token_id) { @@ -216,6 +222,8 @@ impl TokenAirdropTransaction { expected_decimals: None, nft_transfers: Vec::new(), transfers: vec![transfer], + pre_tx_allowance_hook: None, + pre_post_tx_allowance_hook: None, }); } self @@ -229,7 +237,13 @@ 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, + pre_tx_allowance_hook: None, + pre_post_tx_allowance_hook: None, + }; let data = self.data_mut(); if let Some(tt) = data.token_transfers.iter_mut().find(|tt| tt.token_id == token_id) { @@ -254,6 +268,8 @@ impl TokenAirdropTransaction { expected_decimals, nft_transfers: Vec::new(), transfers: vec![transfer], + pre_tx_allowance_hook: None, + pre_post_tx_allowance_hook: None, }); } self @@ -279,6 +295,8 @@ impl TokenAirdropTransaction { expected_decimals: None, transfers: Vec::new(), nft_transfers: vec![transfer], + pre_tx_allowance_hook: None, + pre_post_tx_allowance_hook: None, }); } diff --git a/src/transaction/any.rs b/src/transaction/any.rs index be10410f6..be0afdf47 100644 --- a/src/transaction/any.rs +++ b/src/transaction/any.rs @@ -54,6 +54,8 @@ mod data { FileDeleteTransactionData as FileDelete, FileUpdateTransactionData as FileUpdate, }; + // TODO: Uncomment when hooks module is working + // 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 +154,8 @@ pub enum AnyTransactionData { TokenClaimAirdrop(data::TokenClaimAirdrop), TokenCancelAirdrop(data::TokenCancelAirdrop), Batch(data::Batch), + // TODO: Uncomment when hooks module is working + // LambdaSStore(data::LambdaSStore), } impl ToTransactionDataProtobuf for AnyTransactionData { @@ -305,6 +309,8 @@ impl ToTransactionDataProtobuf for AnyTransactionData { transaction.to_transaction_data_protobuf(chunk_info) } Self::Batch(transaction) => transaction.to_transaction_data_protobuf(chunk_info), + // TODO: Uncomment when hooks module is working + // Self::LambdaSStore(transaction) => transaction.to_transaction_data_protobuf(chunk_info), } } } @@ -362,6 +368,8 @@ 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(), + // TODO: Uncomment when hooks module is working + // Self::LambdaSStore(transaction) => transaction.default_max_transaction_fee(), } } @@ -417,6 +425,8 @@ 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(), + // TODO: Uncomment when hooks module is working + // Self::LambdaSStore(it) => it.maybe_chunk_data(), } } @@ -472,6 +482,8 @@ 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(), + // TODO: Uncomment when hooks module is working + // Self::LambdaSStore(it) => it.wait_for_receipt(), } } } @@ -533,6 +545,8 @@ 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), + // TODO: Uncomment when hooks module is working + // Self::LambdaSStore(transaction) => transaction.execute(channel, request), } } } @@ -592,6 +606,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 +693,8 @@ impl FromProtobuf for AnyTransactionData { "unsupported transaction `NodeStakeUpdateTransaction`", )) } + // TODO: Uncomment when LambdaSStore is added to protobuf + // Data::LambdaSStore(pb) => data::LambdaSStore::from_protobuf(pb)?.into(), Data::AtomicBatch(_) => { return Err(Error::from_protobuf( "unsupported transaction `AtomicBatchTransaction`", @@ -1249,5 +1266,6 @@ impl_cast_any! { TokenAirdrop, TokenClaimAirdrop, TokenCancelAirdrop, - Batch + Batch, + LambdaSStore } diff --git a/src/transfer_transaction.rs b/src/transfer_transaction.rs index 54bea4bbd..9441d9869 100644 --- a/src/transfer_transaction.rs +++ b/src/transfer_transaction.rs @@ -7,6 +7,10 @@ use hedera_proto::services; use hedera_proto::services::crypto_service_client::CryptoServiceClient; use tonic::transport::Channel; +use crate::hooks::{ + HookCall, + HookType, +}; use crate::ledger_id::RefLedgerId; use crate::protobuf::FromProtobuf; use crate::transaction::{ @@ -60,6 +64,16 @@ pub(crate) struct Transfer { /// If this is an approved transfer. pub is_approval: bool, + + /// If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` on scoped + /// account; the hook's invoked methods must not revert and must return + /// true for the containing CryptoTransfer to succeed. + pub pre_tx_allowance_hook: Option, + + /// If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` on scoped + /// account; the hook's invoked methods must not revert and must return + /// true for the containing CryptoTransfer to succeed. + pub pre_post_tx_allowance_hook: Option, } #[derive(Debug, Clone)] @@ -72,14 +86,27 @@ pub(crate) struct TokenTransfer { pub nft_transfers: Vec, pub expected_decimals: Option, + + pub pre_tx_allowance_hook: Option, + + pub pre_post_tx_allowance_hook: Option, } 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, + pre_tx_allowance_hook: Option, + pre_post_tx_allowance_hook: Option, + ) -> &mut Self { self.data_mut().transfers.push(Transfer { account_id, amount: amount.to_tinybars(), is_approval: approved, + pre_tx_allowance_hook, + pre_post_tx_allowance_hook, }); self @@ -87,12 +114,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, 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, None) } /// Returns all transfers associated with this transaction. @@ -111,8 +138,16 @@ impl TransferTransaction { amount: i64, approved: bool, expected_decimals: Option, + pre_tx_allowance_hook: Option, + pre_post_tx_allowance_hook: Option, ) -> &mut Self { - let transfer = Transfer { account_id, amount, is_approval: approved }; + let transfer = Transfer { + account_id, + amount, + is_approval: approved, + pre_tx_allowance_hook, + pre_post_tx_allowance_hook, + }; let data = self.data_mut(); if let Some(tt) = data.token_transfers.iter_mut().find(|tt| tt.token_id == token_id) { @@ -124,6 +159,8 @@ impl TransferTransaction { expected_decimals, nft_transfers: Vec::new(), transfers: vec![transfer], + pre_tx_allowance_hook, + pre_post_tx_allowance_hook, }); } @@ -139,7 +176,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, None) } /// Add an approved token transfer to the transaction. @@ -151,7 +188,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, None) } // todo: make the examples into code, just not sure how to do that. @@ -166,7 +203,15 @@ 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, + None, + ) } /// Add an approved token transfer, ensuring that the token has `expected_decimals` decimals. @@ -180,7 +225,15 @@ 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, + None, + ) } /// Returns all the token transfers associated associated with this transaction. @@ -219,6 +272,8 @@ impl TransferTransaction { sender_account_id: AccountId, receiver_account_id: AccountId, approved: bool, + pre_tx_allowance_hook: Option, + pre_post_tx_allowance_hook: Option, ) -> &mut Self { let NftId { token_id, serial } = nft_id; let transfer = TokenNftTransfer { @@ -239,6 +294,8 @@ impl TransferTransaction { expected_decimals: None, transfers: Vec::new(), nft_transfers: vec![transfer], + pre_tx_allowance_hook, + pre_post_tx_allowance_hook, }); } @@ -252,7 +309,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 +319,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 +330,79 @@ impl TransferTransaction { .map(|it| (it.token_id, it.nft_transfers.clone())) .collect() } + + /// Add a hbar transfer with a hook. + pub fn add_hbar_transfer_with_hook( + &mut self, + account_id: AccountId, + amount: Hbar, + hook: HookCall, + hook_type: HookType, + ) -> &mut Self { + if hook_type == HookType::PrePostHookReceiver { + self._hbar_transfer(account_id, amount, false, None, Some(hook)) + } else { + self._hbar_transfer(account_id, amount, false, Some(hook), None) + } + } + + /// Add a hbar transfer with a pre-post transaction hook. + pub fn add_hbar_transfer_with_pre_post_tx_hook( + &mut self, + account_id: AccountId, + amount: Hbar, + pre_post_tx_allowance_hook: HookCall, + ) -> &mut Self { + self._hbar_transfer(account_id, amount, false, None, Some(pre_post_tx_allowance_hook)) + } + + /// Add an NFT transfer with a sender hook. + pub fn add_nft_transfer_with_sender_hook( + &mut self, + nft_id: impl Into, + sender: AccountId, + receiver: AccountId, + hook: HookCall, + hook_type: HookType, + ) -> &mut Self { + if hook_type == HookType::PrePostHookSender { + self._nft_transfer(nft_id.into(), sender, receiver, false, Some(hook), None) + } else { + self._nft_transfer(nft_id.into(), sender, receiver, false, None, Some(hook)) + } + } + + /// Add an NFT transfer with a receiver hook. + pub fn add_nft_transfer_with_receiver_hook( + &mut self, + nft_id: impl Into, + sender: AccountId, + receiver: AccountId, + hook: HookCall, + hook_type: HookType, + ) -> &mut Self { + if hook_type == HookType::PrePostHookReceiver { + self._nft_transfer(nft_id.into(), sender, receiver, false, None, Some(hook)) + } else { + self._nft_transfer(nft_id.into(), sender, receiver, false, None, Some(hook)) + } + } + + /// Add a token transfer with a hook. + pub fn add_token_transfer_with_hook( + &mut self, + token_id: TokenId, + account_id: AccountId, + amount: i64, + hook: HookCall, + hook_type: HookType, + ) -> &mut Self { + if hook_type == HookType::PrePostHookReceiver { + self._token_transfer(token_id, account_id, amount, false, None, None, Some(hook)) + } else { + self._token_transfer(token_id, account_id, amount, false, None, Some(hook), None) + } + } } impl TransactionExecute for TransferTransactionData { @@ -313,6 +443,14 @@ impl FromProtobuf for Transfer { amount: pb.amount, account_id: AccountId::from_protobuf(pb_getf!(pb, account_id)?)?, is_approval: pb.is_approval, + pre_tx_allowance_hook: pb + .pre_tx_allowance_hook + .map(HookCall::from_protobuf) + .transpose()?, + pre_post_tx_allowance_hook: pb + .pre_post_tx_allowance_hook + .map(HookCall::from_protobuf) + .transpose()?, }) } } @@ -325,6 +463,14 @@ impl ToProtobuf for Transfer { amount: self.amount, account_id: Some(self.account_id.to_protobuf()), is_approval: self.is_approval, + pre_tx_allowance_hook: self + .pre_tx_allowance_hook + .as_ref() + .map(|hook| hook.to_protobuf()), + pre_post_tx_allowance_hook: self + .pre_post_tx_allowance_hook + .as_ref() + .map(|hook| hook.to_protobuf()), } } } @@ -342,6 +488,14 @@ impl FromProtobuf for TokenTransfer { .map(|pb| TokenNftTransfer::from_protobuf(pb, token_id)) .collect::, _>>()?, expected_decimals: pb.expected_decimals, + pre_tx_allowance_hook: pb + .pre_tx_allowance_hook + .map(HookCall::from_protobuf) + .transpose()?, + pre_post_tx_allowance_hook: pb + .pre_post_tx_allowance_hook + .map(HookCall::from_protobuf) + .transpose()?, }) } } @@ -358,6 +512,14 @@ impl ToProtobuf for TokenTransfer { transfers, nft_transfers, expected_decimals: self.expected_decimals, + pre_tx_allowance_hook: self + .pre_tx_allowance_hook + .as_ref() + .map(|hook| hook.to_protobuf()), + pre_post_tx_allowance_hook: self + .pre_post_tx_allowance_hook + .as_ref() + .map(|hook| hook.to_protobuf()), } } }