Skip to content

Commit 6d6f4fb

Browse files
feat: transfer tx with hooks
Signed-off-by: Ivaylo Nikolov <[email protected]>
1 parent 5b3ed30 commit 6d6f4fb

File tree

17 files changed

+1322
-18
lines changed

17 files changed

+1322
-18
lines changed

examples/transfer_with_hooks.rs

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
use clap::Parser;
4+
use hedera::{
5+
AccountCreateTransaction, AccountId, Client, ContractCreateTransaction, ContractId, EvmHookCall, EvmHookSpec, FungibleHookCall, FungibleHookType, Hbar, HookCall, HookCreationDetails, HookExtensionPoint, LambdaEvmHook, NftHookCall, NftHookType, PrivateKey, TokenCreateTransaction, TokenMintTransaction, TokenSupplyType, TokenType, TransferTransaction
6+
};
7+
8+
#[derive(Parser, Debug)]
9+
struct Args {
10+
#[clap(long, env)]
11+
operator_account_id: AccountId,
12+
13+
#[clap(long, env)]
14+
operator_key: PrivateKey,
15+
16+
#[clap(long, env, default_value = "testnet")]
17+
hedera_network: String,
18+
}
19+
20+
const HOOK_BYTECODE: &str = "6080604052348015600e575f5ffd5b506107d18061001c5f395ff3fe608060405260043610610033575f3560e01c8063124d8b301461003757806394112e2f14610067578063bd0dd0b614610097575b5f5ffd5b610051600480360381019061004c91906106f2565b6100c7565b60405161005e9190610782565b60405180910390f35b610081600480360381019061007c91906106f2565b6100d2565b60405161008e9190610782565b60405180910390f35b6100b160048036038101906100ac91906106f2565b6100dd565b6040516100be9190610782565b60405180910390f35b5f6001905092915050565b5f6001905092915050565b5f6001905092915050565b5f604051905090565b5f5ffd5b5f5ffd5b5f5ffd5b5f60a08284031215610112576101116100f9565b5b81905092915050565b5f5ffd5b5f601f19601f8301169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6101658261011f565b810181811067ffffffffffffffff821117156101845761018361012f565b5b80604052505050565b5f6101966100e8565b90506101a2828261015c565b919050565b5f5ffd5b5f5ffd5b5f67ffffffffffffffff8211156101c9576101c861012f565b5b602082029050602081019050919050565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f610207826101de565b9050919050565b610217816101fd565b8114610221575f5ffd5b50565b5f813590506102328161020e565b92915050565b5f8160070b9050919050565b61024d81610238565b8114610257575f5ffd5b50565b5f8135905061026881610244565b92915050565b5f604082840312156102835761028261011b565b5b61028d604061018d565b90505f61029c84828501610224565b5f8301525060206102af8482850161025a565b60208301525092915050565b5f6102cd6102c8846101af565b61018d565b905080838252602082019050604084028301858111156102f0576102ef6101da565b5b835b818110156103195780610305888261026e565b8452602084019350506040810190506102f2565b5050509392505050565b5f82601f830112610337576103366101ab565b5b81356103478482602086016102bb565b91505092915050565b5f67ffffffffffffffff82111561036a5761036961012f565b5b602082029050602081019050919050565b5f67ffffffffffffffff8211156103955761039461012f565b5b602082029050602081019050919050565b5f606082840312156103bb576103ba61011b565b5b6103c5606061018d565b90505f6103d484828501610224565b5f8301525060206103e784828501610224565b60208301525060406103fb8482850161025a565b60408301525092915050565b5f6104196104148461037b565b61018d565b9050808382526020820190506060840283018581111561043c5761043b6101da565b5b835b81811015610465578061045188826103a6565b84526020840193505060608101905061043e565b5050509392505050565b5f82601f830112610483576104826101ab565b5b8135610493848260208601610407565b91505092915050565b5f606082840312156104b1576104b061011b565b5b6104bb606061018d565b90505f6104ca84828501610224565b5f83015250602082013567ffffffffffffffff8111156104ed576104ec6101a7565b5b6104f984828501610323565b602083015250604082013567ffffffffffffffff81111561051d5761051c6101a7565b5b6105298482850161046f565b60408301525092915050565b5f61054761054284610350565b61018d565b9050808382526020820190506020840283018581111561056a576105696101da565b5b835b818110156105b157803567ffffffffffffffff81111561058f5761058e6101ab565b5b80860161059c898261049c565b8552602085019450505060208101905061056c565b5050509392505050565b5f82601f8301126105cf576105ce6101ab565b5b81356105df848260208601610535565b91505092915050565b5f604082840312156105fd576105fc61011b565b5b610607604061018d565b90505f82013567ffffffffffffffff811115610626576106256101a7565b5b61063284828501610323565b5f83015250602082013567ffffffffffffffff811115610655576106546101a7565b5b610661848285016105bb565b60208301525092915050565b5f604082840312156106825761068161011b565b5b61068c604061018d565b90505f82013567ffffffffffffffff8111156106ab576106aa6101a7565b5b6106b7848285016105e8565b5f83015250602082013567ffffffffffffffff8111156106da576106d96101a7565b5b6106e6848285016105e8565b60208301525092915050565b5f5f60408385031215610708576107076100f1565b5b5f83013567ffffffffffffffff811115610725576107246100f5565b5b610731858286016100fd565b925050602083013567ffffffffffffffff811115610752576107516100f5565b5b61075e8582860161066d565b9150509250929050565b5f8115159050919050565b61077c81610768565b82525050565b5f6020820190506107955f830184610773565b9291505056fea26469706673582212207dfe7723f6d6869419b1cb0619758b439da0cf4ffd9520997c40a3946299d4dc64736f6c634300081e0033";
21+
22+
async fn create_hook_contract(client: &Client) -> anyhow::Result<ContractId> {
23+
let bytecode = hex::decode(HOOK_BYTECODE)?;
24+
25+
let receipt = ContractCreateTransaction::new()
26+
.bytecode(bytecode)
27+
.gas(1_700_000)
28+
.execute(client)
29+
.await?
30+
.get_receipt(client)
31+
.await?;
32+
33+
Ok(receipt.contract_id.unwrap())
34+
}
35+
36+
#[tokio::main]
37+
async fn main() -> anyhow::Result<()> {
38+
let _ = dotenvy::dotenv();
39+
let Args {
40+
operator_account_id,
41+
operator_key,
42+
hedera_network,
43+
} = Args::parse();
44+
45+
let client = Client::for_name(&hedera_network)?;
46+
client.set_operator(operator_account_id, operator_key);
47+
48+
println!("Transfer Transaction Hooks Example Start!");
49+
50+
// Step 1: Set up prerequisites - create hook contract
51+
println!("Setting up prerequisites...");
52+
53+
let hook_contract_id = create_hook_contract(&client).await?;
54+
println!("Created hook contract: {hook_contract_id}");
55+
56+
// Create hook details
57+
let hook_id = 1;
58+
let spec = EvmHookSpec::new(Some(hook_contract_id));
59+
let lambda_hook = LambdaEvmHook::new(spec, vec![]);
60+
let hook_details = HookCreationDetails::new(
61+
HookExtensionPoint::AccountAllowanceHook,
62+
hook_id,
63+
Some(lambda_hook),
64+
);
65+
66+
// Create sender account with hook
67+
let sender_key = PrivateKey::generate_ed25519();
68+
let sender_receipt = AccountCreateTransaction::new()
69+
.set_key_without_alias(sender_key.public_key())
70+
.initial_balance(Hbar::new(10))
71+
.add_hook(hook_details.clone())
72+
.freeze_with(&client)?
73+
.sign(sender_key.clone())
74+
.execute(&client)
75+
.await?
76+
.get_receipt(&client)
77+
.await?;
78+
79+
let sender_account_id = sender_receipt.account_id.unwrap();
80+
println!("Created sender account: {sender_account_id}");
81+
82+
// Create receiver account with hook and unlimited token associations
83+
let receiver_key = PrivateKey::generate_ed25519();
84+
let receiver_receipt = AccountCreateTransaction::new()
85+
.set_key_without_alias(receiver_key.public_key())
86+
.initial_balance(Hbar::new(10))
87+
.max_automatic_token_associations(-1)
88+
.add_hook(hook_details)
89+
.execute(&client)
90+
.await?
91+
.get_receipt(&client)
92+
.await?;
93+
94+
let receiver_account_id = receiver_receipt.account_id.unwrap();
95+
println!("Created receiver account: {receiver_account_id}");
96+
97+
// Create fungible token
98+
println!("Creating fungible token...");
99+
let fungible_token_id = TokenCreateTransaction::new()
100+
.name("Example Fungible Token")
101+
.symbol("EFT")
102+
.decimals(2)
103+
.initial_supply(10_000)
104+
.treasury_account_id(sender_account_id)
105+
.admin_key(sender_key.public_key())
106+
.supply_key(sender_key.public_key())
107+
.token_type(TokenType::FungibleCommon)
108+
.token_supply_type(TokenSupplyType::Infinite)
109+
.freeze_with(&client)?
110+
.sign(sender_key.clone())
111+
.execute(&client)
112+
.await?
113+
.get_receipt(&client)
114+
.await?
115+
.token_id
116+
.unwrap();
117+
118+
println!("Created fungible token: {fungible_token_id}");
119+
120+
// Create NFT token
121+
println!("Creating NFT token...");
122+
let nft_token_id = TokenCreateTransaction::new()
123+
.name("Example NFT Token")
124+
.symbol("ENT")
125+
.treasury_account_id(sender_account_id)
126+
.admin_key(sender_key.public_key())
127+
.supply_key(sender_key.public_key())
128+
.token_type(TokenType::NonFungibleUnique)
129+
.token_supply_type(TokenSupplyType::Infinite)
130+
.freeze_with(&client)?
131+
.sign(sender_key.clone())
132+
.execute(&client)
133+
.await?
134+
.get_receipt(&client)
135+
.await?
136+
.token_id
137+
.unwrap();
138+
139+
println!("Created NFT token: {nft_token_id}");
140+
141+
// Mint NFT
142+
println!("Minting NFT...");
143+
let nft_serial = TokenMintTransaction::new()
144+
.token_id(nft_token_id)
145+
.metadata(vec![b"Example NFT Metadata".to_vec()])
146+
.freeze_with(&client)?
147+
.sign(sender_key.clone())
148+
.execute(&client)
149+
.await?
150+
.get_receipt(&client)
151+
.await?
152+
.serials[0] as u64;
153+
154+
let nft_id = nft_token_id.nft(nft_serial);
155+
println!("Minted NFT: {nft_id}");
156+
157+
// Step 2: Demonstrate TransferTransaction API with hooks
158+
println!("\n=== TransferTransaction with Hooks API Demonstration ===");
159+
160+
// Create hook call objects
161+
println!("Creating hook call objects...");
162+
163+
// HBAR transfer with pre-tx allowance hook
164+
let hbar_hook = FungibleHookCall {
165+
hook_call: HookCall::new(
166+
Some(hook_id),
167+
{
168+
let mut evm_call = EvmHookCall::new(Some(vec![0x01, 0x02]));
169+
evm_call.set_gas_limit(20_000);
170+
Some(evm_call)
171+
},
172+
),
173+
hook_type: FungibleHookType::PreTxAllowanceHook,
174+
};
175+
176+
// NFT sender hook (pre-hook)
177+
let nft_sender_hook = NftHookCall {
178+
hook_call: HookCall::new(
179+
Some(hook_id),
180+
{
181+
let mut evm_call = EvmHookCall::new(Some(vec![0x03, 0x04]));
182+
evm_call.set_gas_limit(20_000);
183+
Some(evm_call)
184+
},
185+
),
186+
hook_type: NftHookType::PreHookSender,
187+
};
188+
189+
// NFT receiver hook (pre-hook)
190+
let nft_receiver_hook = NftHookCall {
191+
hook_call: HookCall::new(
192+
Some(hook_id),
193+
{
194+
let mut evm_call = EvmHookCall::new(Some(vec![0x05, 0x06]));
195+
evm_call.set_gas_limit(20_000);
196+
Some(evm_call)
197+
},
198+
),
199+
hook_type: NftHookType::PreHookReceiver,
200+
};
201+
202+
// Fungible token transfer with pre-post allowance hook
203+
let fungible_token_hook = FungibleHookCall {
204+
hook_call: HookCall::new(
205+
Some(hook_id),
206+
{
207+
let mut evm_call = EvmHookCall::new(Some(vec![0x07, 0x08]));
208+
evm_call.set_gas_limit(20_000);
209+
Some(evm_call)
210+
},
211+
),
212+
hook_type: FungibleHookType::PrePostTxAllowanceHook,
213+
};
214+
215+
// Build separate TransferTransactions with hooks
216+
println!("Building separate TransferTransactions with hooks...");
217+
218+
// Transaction 1: HBAR transfers with hook
219+
println!("\n1. Executing HBAR TransferTransaction with hook...");
220+
TransferTransaction::new()
221+
.hbar_transfer_with_hook(sender_account_id, Hbar::from_tinybars(-1), hbar_hook)
222+
.hbar_transfer(receiver_account_id, Hbar::from_tinybars(1))
223+
.freeze_with(&client)?
224+
.sign(sender_key.clone())
225+
.execute(&client)
226+
.await?
227+
.get_receipt(&client)
228+
.await?;
229+
println!(" ✓ HBAR transfer with pre-tx allowance hook completed");
230+
231+
// Transaction 2: NFT transfer with sender and receiver hooks
232+
println!("\n2. Executing NFT TransferTransaction with hooks...");
233+
TransferTransaction::new()
234+
.nft_transfer_with_both_hooks(
235+
nft_id,
236+
sender_account_id,
237+
receiver_account_id,
238+
nft_sender_hook,
239+
nft_receiver_hook,
240+
)
241+
.freeze_with(&client)?
242+
.sign(sender_key.clone())
243+
.execute(&client)
244+
.await?
245+
.get_receipt(&client)
246+
.await?;
247+
println!(" ✓ NFT transfer with sender and receiver hooks completed");
248+
249+
// Transaction 3: Fungible token transfers with hook
250+
println!("\n3. Executing Fungible Token TransferTransaction with hook...");
251+
TransferTransaction::new()
252+
.token_transfer_with_hook(
253+
fungible_token_id,
254+
sender_account_id,
255+
-1_000,
256+
fungible_token_hook,
257+
)
258+
.token_transfer(fungible_token_id, receiver_account_id, 1_000)
259+
.freeze_with(&client)?
260+
.sign(sender_key.clone())
261+
.execute(&client)
262+
.await?
263+
.get_receipt(&client)
264+
.await?;
265+
println!(" ✓ Fungible token transfer with pre-post allowance hook completed");
266+
267+
println!("\nAll TransferTransactions executed successfully with the following hook calls:");
268+
println!(" - Transaction 1: HBAR transfer with pre-tx allowance hook");
269+
println!(" - Transaction 2: NFT transfer with sender and receiver hooks");
270+
println!(" - Transaction 3: Fungible token transfer with pre-post allowance hook");
271+
272+
println!("Transfer Transaction Hooks Example Complete!");
273+
274+
Ok(())
275+
}

src/fee_schedules.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,7 @@ impl FromProtobuf<services::SubType> for FeeDataType {
814814
SubType::ScheduleCreateContractCall => Self::ScheduleCreateContractCall,
815815
SubType::TopicCreateWithCustomFees => Self::TopicCreateWithCustomFees,
816816
SubType::SubmitMessageWithCustomFees => Self::SubmitMessageWithCustomFees,
817+
SubType::CryptoTransferWithHooks => Self::Default, // Treat as default for now
817818
};
818819

819820
Ok(value)

src/hooks/nft_hook_call.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use hedera_proto::services;
2+
3+
use crate::hooks::{
4+
EvmHookCall,
5+
HookCall,
6+
NftHookType,
7+
};
8+
use crate::{
9+
FromProtobuf,
10+
ToProtobuf,
11+
};
12+
13+
/// A typed hook call for NFT transfers.
14+
#[derive(Debug, Clone, PartialEq, Eq)]
15+
pub struct NftHookCall {
16+
/// The underlying hook call data.
17+
pub hook_call: HookCall,
18+
/// The type of NFT hook.
19+
pub hook_type: NftHookType,
20+
}
21+
22+
impl NftHookCall {
23+
/// Create a new `NftHookCall`.
24+
pub fn new(hook_call: HookCall, hook_type: NftHookType) -> Self {
25+
Self { hook_call, hook_type }
26+
}
27+
28+
/// Internal method to create from protobuf with a known type.
29+
pub(crate) fn from_protobuf_with_type(
30+
pb: services::HookCall,
31+
hook_type: NftHookType,
32+
) -> crate::Result<Self> {
33+
Ok(Self { hook_call: HookCall::from_protobuf(pb)?, hook_type })
34+
}
35+
}
36+
37+
impl ToProtobuf for NftHookCall {
38+
type Protobuf = services::HookCall;
39+
40+
fn to_protobuf(&self) -> Self::Protobuf {
41+
self.hook_call.to_protobuf()
42+
}
43+
}
44+
45+
#[cfg(test)]
46+
mod tests {
47+
use super::*;
48+
49+
#[test]
50+
fn test_nft_hook_call_creation() {
51+
let hook_id = 123;
52+
let hook_type = NftHookType::PreHookSender;
53+
let mut hook_call_obj = HookCall::new(None, None);
54+
hook_call_obj.set_hook_id(hook_id);
55+
let hook_call = NftHookCall::new(hook_call_obj, hook_type);
56+
57+
assert_eq!(hook_call.hook_call.hook_id, Some(hook_id));
58+
assert_eq!(hook_call.hook_type, hook_type);
59+
}
60+
61+
#[test]
62+
fn test_nft_hook_call_with_call() {
63+
let call_data = vec![1, 2, 3, 4, 5];
64+
let mut evm_call = EvmHookCall::new(Some(call_data));
65+
evm_call.set_gas_limit(0);
66+
let hook_type = NftHookType::PrePostHookReceiver;
67+
let mut hook_call_obj = HookCall::new(None, None);
68+
hook_call_obj.set_call(evm_call.clone());
69+
let hook_call = NftHookCall::new(hook_call_obj, hook_type);
70+
71+
assert_eq!(hook_call.hook_call.call, Some(evm_call));
72+
assert_eq!(hook_call.hook_type, hook_type);
73+
}
74+
}

src/hooks/nft_hook_type.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/// Types of NFT hooks.
2+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3+
#[repr(u8)]
4+
pub enum NftHookType {
5+
/// A single call made before attempting the NFT transfer, to a hook on the sender account.
6+
PreHookSender = 0,
7+
/// Two calls - first before attempting the NFT transfer (allowPre), and second after
8+
/// attempting the NFT transfer (allowPost) on the sender account.
9+
PrePostHookSender = 1,
10+
/// A single call made before attempting the NFT transfer, to a hook on the receiver account.
11+
PreHookReceiver = 2,
12+
/// Two calls - first before attempting the NFT transfer (allowPre), and second after
13+
/// attempting the NFT transfer (allowPost) on the receiver account.
14+
PrePostHookReceiver = 3,
15+
}
16+
17+
impl NftHookType {
18+
/// Returns the numeric value of the hook type.
19+
pub fn value(&self) -> u8 {
20+
*self as u8
21+
}
22+
}
23+
24+
#[cfg(test)]
25+
mod tests {
26+
use super::*;
27+
28+
#[test]
29+
fn test_nft_hook_type_values() {
30+
assert_eq!(NftHookType::PreHookSender.value(), 0);
31+
assert_eq!(NftHookType::PrePostHookSender.value(), 1);
32+
assert_eq!(NftHookType::PreHookReceiver.value(), 2);
33+
assert_eq!(NftHookType::PrePostHookReceiver.value(), 3);
34+
}
35+
}

0 commit comments

Comments
 (0)