hopr_chain_actions/
payload.rs

1//! Module defining various Ethereum transaction payload generators for the actions.
2//!
3//! This module defines the basic [PayloadGenerator] trait that describes how an action
4//! is translated into a [TransactionRequest] that can be submitted on-chain (via an RPC provider)
5//! using a [TransactionExecutor](crate::action_queue::TransactionExecutor).
6//!
7//! There are two main implementations:
8//! - [BasicPayloadGenerator] which implements generation of a direct EIP1559 transaction payload. This is currently not
9//!   used by a HOPR node.
10//! - [SafePayloadGenerator] which implements generation of a payload that embeds the transaction data into the SAFE
11//!   transaction. This is currently the main mode of HOPR node operation.
12
13use alloy::{
14    network::TransactionBuilder,
15    primitives::{
16        B256, U256,
17        aliases::{U24, U48, U56, U96},
18    },
19    rpc::types::TransactionRequest,
20    sol_types::SolCall,
21};
22use hopr_bindings::{
23    hoprannouncements::HoprAnnouncements::{
24        announceCall, announceSafeCall, bindKeysAnnounceCall, bindKeysAnnounceSafeCall,
25    },
26    hoprchannels::{
27        HoprChannels::{
28            RedeemableTicket as OnChainRedeemableTicket, TicketData, closeIncomingChannelCall,
29            closeIncomingChannelSafeCall, finalizeOutgoingChannelClosureCall, finalizeOutgoingChannelClosureSafeCall,
30            fundChannelCall, fundChannelSafeCall, initiateOutgoingChannelClosureCall,
31            initiateOutgoingChannelClosureSafeCall, redeemTicketCall, redeemTicketSafeCall,
32        },
33        HoprCrypto::{CompactSignature, VRFParameters},
34    },
35    hoprnodemanagementmodule::HoprNodeManagementModule::execTransactionFromModuleCall,
36    hoprnodesaferegistry::HoprNodeSafeRegistry::{deregisterNodeBySafeCall, registerSafeByNodeCall},
37    hoprtoken::HoprToken::{approveCall, transferCall},
38};
39use hopr_chain_types::ContractAddresses;
40use hopr_crypto_types::prelude::*;
41use hopr_internal_types::prelude::*;
42use hopr_primitive_types::prelude::*;
43
44use crate::errors::{
45    ChainActionsError::{InvalidArguments, InvalidState},
46    Result,
47};
48
49#[repr(u8)]
50#[derive(Copy, Clone, Debug, PartialEq, Eq)]
51enum Operation {
52    Call = 0,
53    // Future use: DelegateCall = 1,
54}
55
56/// Trait for various implementations of generators of common on-chain transaction payloads.
57pub trait PayloadGenerator<T: Into<TransactionRequest>> {
58    /// Create an ERC20 approve transaction payload. Pre-requisite to open payment channels.
59    /// The `spender` address is typically the HOPR Channels contract address.
60    fn approve(&self, spender: Address, amount: HoprBalance) -> Result<T>;
61
62    /// Create a ERC20 transfer transaction payload
63    fn transfer<C: Currency>(&self, destination: Address, amount: Balance<C>) -> Result<T>;
64
65    /// Creates the transaction payload to announce a node on-chain.
66    fn announce(&self, announcement: AnnouncementData) -> Result<T>;
67
68    /// Creates the transaction payload to open a payment channel
69    fn fund_channel(&self, dest: Address, amount: HoprBalance) -> Result<T>;
70
71    /// Creates the transaction payload to immediately close an incoming payment channel
72    fn close_incoming_channel(&self, source: Address) -> Result<T>;
73
74    /// Creates the transaction payload that initiates the closure of a payment channel.
75    /// Once the notice period is due, the funds can be withdrawn using a
76    /// finalizeChannelClosure transaction.
77    fn initiate_outgoing_channel_closure(&self, destination: Address) -> Result<T>;
78
79    /// Creates a transaction payload that withdraws funds from
80    /// an outgoing payment channel. This will succeed once the closure
81    /// notice period is due.
82    fn finalize_outgoing_channel_closure(&self, destination: Address) -> Result<T>;
83
84    /// Used to create the payload to claim incentives for relaying a mixnet packet.
85    fn redeem_ticket(&self, acked_ticket: RedeemableTicket) -> Result<T>;
86
87    /// Creates a transaction payload to register a Safe instance which is used
88    /// to manage the node's funds
89    fn register_safe_by_node(&self, safe_addr: Address) -> Result<T>;
90
91    /// Creates a transaction payload to remove the Safe instance. Once succeeded,
92    /// the funds are no longer managed by the node.
93    fn deregister_node_by_safe(&self) -> Result<T>;
94}
95
96fn channels_payload(hopr_channels: Address, call_data: Vec<u8>) -> Vec<u8> {
97    execTransactionFromModuleCall {
98        to: hopr_channels.into(),
99        value: U256::ZERO,
100        data: call_data.into(),
101        operation: Operation::Call as u8,
102    }
103    .abi_encode()
104}
105
106fn approve_tx(spender: Address, amount: HoprBalance) -> TransactionRequest {
107    TransactionRequest::default().with_input(
108        approveCall {
109            spender: spender.into(),
110            value: U256::from_be_bytes(amount.amount().to_be_bytes()),
111        }
112        .abi_encode(),
113    )
114}
115
116fn transfer_tx<C: Currency>(destination: Address, amount: Balance<C>) -> TransactionRequest {
117    let amount_u256 = U256::from_be_bytes(amount.amount().to_be_bytes());
118    let tx = TransactionRequest::default();
119    if WxHOPR::is::<C>() {
120        tx.with_input(
121            transferCall {
122                recipient: destination.into(),
123                amount: amount_u256,
124            }
125            .abi_encode(),
126        )
127    } else if XDai::is::<C>() {
128        tx.with_value(amount_u256)
129    } else {
130        unimplemented!("other currencies are currently not supported")
131    }
132}
133
134fn register_safe_tx(safe_addr: Address) -> TransactionRequest {
135    TransactionRequest::default().with_input(
136        registerSafeByNodeCall {
137            safeAddr: safe_addr.into(),
138        }
139        .abi_encode(),
140    )
141}
142
143/// Generates transaction payloads that do not use Safe-compliant ABI
144#[derive(Debug, Clone)]
145pub struct BasicPayloadGenerator {
146    me: Address,
147    contract_addrs: ContractAddresses,
148}
149
150impl BasicPayloadGenerator {
151    pub fn new(me: Address, contract_addrs: ContractAddresses) -> Self {
152        Self { me, contract_addrs }
153    }
154}
155
156impl PayloadGenerator<TransactionRequest> for BasicPayloadGenerator {
157    fn approve(&self, spender: Address, amount: HoprBalance) -> Result<TransactionRequest> {
158        let tx = approve_tx(spender, amount).with_to(self.contract_addrs.token.into());
159        Ok(tx)
160    }
161
162    fn transfer<C: Currency>(&self, destination: Address, amount: Balance<C>) -> Result<TransactionRequest> {
163        let to = if XDai::is::<C>() {
164            destination
165        } else if WxHOPR::is::<C>() {
166            self.contract_addrs.token
167        } else {
168            return Err(InvalidArguments("invalid currency".into()));
169        };
170        let tx = transfer_tx(destination, amount).with_to(to.into());
171        Ok(tx)
172    }
173
174    fn announce(&self, announcement: AnnouncementData) -> Result<TransactionRequest> {
175        let payload = match &announcement.key_binding {
176            Some(binding) => {
177                let serialized_signature = binding.signature.as_ref();
178
179                bindKeysAnnounceCall {
180                    ed25519_sig_0: B256::from_slice(&serialized_signature[0..32]),
181                    ed25519_sig_1: B256::from_slice(&serialized_signature[32..64]),
182                    ed25519_pub_key: B256::from_slice(binding.packet_key.as_ref()),
183                    baseMultiaddr: announcement.multiaddress().to_string(),
184                }
185                .abi_encode()
186            }
187            None => announceCall {
188                baseMultiaddr: announcement.multiaddress().to_string(),
189            }
190            .abi_encode(),
191        };
192
193        let tx = TransactionRequest::default()
194            .with_input(payload)
195            .with_to(self.contract_addrs.announcements.into());
196        Ok(tx)
197    }
198
199    fn fund_channel(&self, dest: Address, amount: HoprBalance) -> Result<TransactionRequest> {
200        if dest.eq(&self.me) {
201            return Err(InvalidArguments("Cannot fund channel to self".into()));
202        }
203
204        let tx = TransactionRequest::default()
205            .with_input(
206                fundChannelCall {
207                    account: dest.into(),
208                    amount: U96::from_be_slice(&amount.amount().to_be_bytes()[32 - 12..]),
209                }
210                .abi_encode(),
211            )
212            .with_to(self.contract_addrs.channels.into());
213        Ok(tx)
214    }
215
216    fn close_incoming_channel(&self, source: Address) -> Result<TransactionRequest> {
217        if source.eq(&self.me) {
218            return Err(InvalidArguments("Cannot close incoming channel from self".into()));
219        }
220
221        let tx = TransactionRequest::default()
222            .with_input(closeIncomingChannelCall { source: source.into() }.abi_encode())
223            .with_to(self.contract_addrs.channels.into());
224        Ok(tx)
225    }
226
227    fn initiate_outgoing_channel_closure(&self, destination: Address) -> Result<TransactionRequest> {
228        if destination.eq(&self.me) {
229            return Err(InvalidArguments(
230                "Cannot initiate closure of incoming channel to self".into(),
231            ));
232        }
233
234        let tx = TransactionRequest::default()
235            .with_input(
236                initiateOutgoingChannelClosureCall {
237                    destination: destination.into(),
238                }
239                .abi_encode(),
240            )
241            .with_to(self.contract_addrs.channels.into());
242        Ok(tx)
243    }
244
245    fn finalize_outgoing_channel_closure(&self, destination: Address) -> Result<TransactionRequest> {
246        if destination.eq(&self.me) {
247            return Err(InvalidArguments(
248                "Cannot initiate closure of incoming channel to self".into(),
249            ));
250        }
251
252        let tx = TransactionRequest::default()
253            .with_input(
254                finalizeOutgoingChannelClosureCall {
255                    destination: destination.into(),
256                }
257                .abi_encode(),
258            )
259            .with_to(self.contract_addrs.channels.into());
260        Ok(tx)
261    }
262
263    fn redeem_ticket(&self, acked_ticket: RedeemableTicket) -> Result<TransactionRequest> {
264        let redeemable = convert_acknowledged_ticket(&acked_ticket)?;
265
266        let params = convert_vrf_parameters(
267            &acked_ticket.vrf_params,
268            &self.me,
269            acked_ticket.ticket.verified_hash(),
270            &acked_ticket.channel_dst,
271        );
272
273        let tx = TransactionRequest::default()
274            .with_input(redeemTicketCall { redeemable, params }.abi_encode())
275            .with_to(self.contract_addrs.channels.into());
276        Ok(tx)
277    }
278
279    fn register_safe_by_node(&self, safe_addr: Address) -> Result<TransactionRequest> {
280        let tx = register_safe_tx(safe_addr).with_to(self.contract_addrs.safe_registry.into());
281        Ok(tx)
282    }
283
284    fn deregister_node_by_safe(&self) -> Result<TransactionRequest> {
285        Err(InvalidState(
286            "Can only deregister an address if Safe is activated".into(),
287        ))
288    }
289}
290
291/// Payload generator that generates Safe-compliant ABI
292#[derive(Debug, Clone)]
293pub struct SafePayloadGenerator {
294    me: Address,
295    contract_addrs: ContractAddresses,
296    module: Address,
297}
298
299pub const DEFAULT_TX_GAS: u64 = 400_000;
300
301impl SafePayloadGenerator {
302    pub fn new(chain_keypair: &ChainKeypair, contract_addrs: ContractAddresses, module: Address) -> Self {
303        Self {
304            me: chain_keypair.into(),
305            contract_addrs,
306            module,
307        }
308    }
309}
310
311impl PayloadGenerator<TransactionRequest> for SafePayloadGenerator {
312    fn approve(&self, spender: Address, amount: HoprBalance) -> Result<TransactionRequest> {
313        let tx = approve_tx(spender, amount)
314            .with_to(self.contract_addrs.token.into())
315            .with_gas_limit(DEFAULT_TX_GAS);
316
317        Ok(tx)
318    }
319
320    fn transfer<C: Currency>(&self, destination: Address, amount: Balance<C>) -> Result<TransactionRequest> {
321        let to = if XDai::is::<C>() {
322            destination
323        } else if WxHOPR::is::<C>() {
324            self.contract_addrs.token
325        } else {
326            return Err(InvalidArguments("invalid currency".into()));
327        };
328        let tx = transfer_tx(destination, amount)
329            .with_to(to.into())
330            .with_gas_limit(DEFAULT_TX_GAS);
331
332        Ok(tx)
333    }
334
335    fn announce(&self, announcement: AnnouncementData) -> Result<TransactionRequest> {
336        let call_data = match &announcement.key_binding {
337            Some(binding) => {
338                let serialized_signature = binding.signature.as_ref();
339
340                bindKeysAnnounceSafeCall {
341                    selfAddress: self.me.into(),
342                    ed25519_sig_0: B256::from_slice(&serialized_signature[0..32]),
343                    ed25519_sig_1: B256::from_slice(&serialized_signature[32..64]),
344                    ed25519_pub_key: B256::from_slice(binding.packet_key.as_ref()),
345                    baseMultiaddr: announcement.multiaddress().to_string(),
346                }
347                .abi_encode()
348            }
349            None => announceSafeCall {
350                selfAddress: self.me.into(),
351                baseMultiaddr: announcement.multiaddress().to_string(),
352            }
353            .abi_encode(),
354        };
355
356        let tx = TransactionRequest::default()
357            .with_input(
358                execTransactionFromModuleCall {
359                    to: self.contract_addrs.announcements.into(),
360                    value: U256::ZERO,
361                    data: call_data.into(),
362                    operation: Operation::Call as u8,
363                }
364                .abi_encode(),
365            )
366            .with_to(self.module.into())
367            .with_gas_limit(DEFAULT_TX_GAS);
368
369        Ok(tx)
370    }
371
372    fn fund_channel(&self, dest: Address, amount: HoprBalance) -> Result<TransactionRequest> {
373        if dest.eq(&self.me) {
374            return Err(InvalidArguments("Cannot fund channel to self".into()));
375        }
376
377        if amount.amount() > hopr_primitive_types::prelude::U256::from(ChannelEntry::MAX_CHANNEL_BALANCE) {
378            return Err(InvalidArguments(
379                "Cannot fund channel with amount larger than 96 bits".into(),
380            ));
381        }
382
383        let call_data = fundChannelSafeCall {
384            selfAddress: self.me.into(),
385            account: dest.into(),
386            amount: U96::from_be_slice(&amount.amount().to_be_bytes()[32 - 12..]),
387        }
388        .abi_encode();
389
390        let tx = TransactionRequest::default()
391            .with_input(channels_payload(self.contract_addrs.channels, call_data))
392            .with_to(self.module.into())
393            .with_gas_limit(DEFAULT_TX_GAS);
394
395        Ok(tx)
396    }
397
398    fn close_incoming_channel(&self, source: Address) -> Result<TransactionRequest> {
399        if source.eq(&self.me) {
400            return Err(InvalidArguments("Cannot close incoming channel from self".into()));
401        }
402
403        let call_data = closeIncomingChannelSafeCall {
404            selfAddress: self.me.into(),
405            source: source.into(),
406        }
407        .abi_encode();
408
409        let tx = TransactionRequest::default()
410            .with_input(channels_payload(self.contract_addrs.channels, call_data))
411            .with_to(self.module.into())
412            .with_gas_limit(DEFAULT_TX_GAS);
413
414        Ok(tx)
415    }
416
417    fn initiate_outgoing_channel_closure(&self, destination: Address) -> Result<TransactionRequest> {
418        if destination.eq(&self.me) {
419            return Err(InvalidArguments(
420                "Cannot initiate closure of incoming channel to self".into(),
421            ));
422        }
423
424        let call_data = initiateOutgoingChannelClosureSafeCall {
425            selfAddress: self.me.into(),
426            destination: destination.into(),
427        }
428        .abi_encode();
429
430        let tx = TransactionRequest::default()
431            .with_input(channels_payload(self.contract_addrs.channels, call_data))
432            .with_to(self.module.into())
433            .with_gas_limit(DEFAULT_TX_GAS);
434
435        Ok(tx)
436    }
437
438    fn finalize_outgoing_channel_closure(&self, destination: Address) -> Result<TransactionRequest> {
439        if destination.eq(&self.me) {
440            return Err(InvalidArguments(
441                "Cannot initiate closure of incoming channel to self".into(),
442            ));
443        }
444
445        let call_data = finalizeOutgoingChannelClosureSafeCall {
446            selfAddress: self.me.into(),
447            destination: destination.into(),
448        }
449        .abi_encode();
450
451        let tx = TransactionRequest::default()
452            .with_input(channels_payload(self.contract_addrs.channels, call_data))
453            .with_to(self.module.into())
454            .with_gas_limit(DEFAULT_TX_GAS);
455
456        Ok(tx)
457    }
458
459    fn redeem_ticket(&self, acked_ticket: RedeemableTicket) -> Result<TransactionRequest> {
460        let redeemable = convert_acknowledged_ticket(&acked_ticket)?;
461
462        let params = convert_vrf_parameters(
463            &acked_ticket.vrf_params,
464            &self.me,
465            acked_ticket.ticket.verified_hash(),
466            &acked_ticket.channel_dst,
467        );
468
469        let call_data = redeemTicketSafeCall {
470            selfAddress: self.me.into(),
471            redeemable,
472            params,
473        }
474        .abi_encode();
475
476        let tx = TransactionRequest::default()
477            .with_input(channels_payload(self.contract_addrs.channels, call_data))
478            .with_to(self.module.into())
479            .with_gas_limit(DEFAULT_TX_GAS);
480
481        Ok(tx)
482    }
483
484    fn register_safe_by_node(&self, safe_addr: Address) -> Result<TransactionRequest> {
485        let tx = register_safe_tx(safe_addr)
486            .with_to(self.contract_addrs.safe_registry.into())
487            .with_gas_limit(DEFAULT_TX_GAS);
488
489        Ok(tx)
490    }
491
492    fn deregister_node_by_safe(&self) -> Result<TransactionRequest> {
493        let tx = TransactionRequest::default()
494            .with_input(
495                deregisterNodeBySafeCall {
496                    nodeAddr: self.me.into(),
497                }
498                .abi_encode(),
499            )
500            .with_to(self.module.into())
501            .with_gas_limit(DEFAULT_TX_GAS);
502
503        Ok(tx)
504    }
505}
506
507/// Converts off-chain representation of VRF parameters into a representation
508/// that the smart contract understands
509///
510/// Not implemented using From trait because logic fits better here
511pub fn convert_vrf_parameters(
512    off_chain: &VrfParameters,
513    signer: &Address,
514    ticket_hash: &Hash,
515    domain_separator: &Hash,
516) -> VRFParameters {
517    // skip the secp256k1 curvepoint prefix
518    let v = off_chain.V.as_uncompressed();
519    let s_b = off_chain
520        .get_s_b_witness(signer, &ticket_hash.into(), domain_separator.as_ref())
521        // Safe: hash value is always in the allowed length boundaries,
522        //       only fails for longer values
523        // Safe: always encoding to secp256k1 whose field elements are in
524        //       allowed length boundaries
525        .expect("ticket hash exceeded hash2field boundaries or encoding to unsupported curve");
526
527    let h_v = off_chain.get_h_v_witness();
528
529    VRFParameters {
530        vx: U256::from_be_slice(&v.as_bytes()[1..33]),
531        vy: U256::from_be_slice(&v.as_bytes()[33..65]),
532        s: U256::from_be_slice(&off_chain.s.to_bytes()),
533        h: U256::from_be_slice(&off_chain.h.to_bytes()),
534        sBx: U256::from_be_slice(&s_b.as_bytes()[1..33]),
535        sBy: U256::from_be_slice(&s_b.as_bytes()[33..65]),
536        hVx: U256::from_be_slice(&h_v.as_bytes()[1..33]),
537        hVy: U256::from_be_slice(&h_v.as_bytes()[33..65]),
538    }
539}
540
541/// Convert off-chain representation of acknowledged ticket to representation
542/// that the smart contract understands
543///
544/// Not implemented using From trait because logic fits better here
545pub fn convert_acknowledged_ticket(off_chain: &RedeemableTicket) -> Result<OnChainRedeemableTicket> {
546    if let Some(ref signature) = off_chain.verified_ticket().signature {
547        let serialized_signature = signature.as_ref();
548
549        println!(
550            "off_chain.verified_ticket().amount.amount() {:?}",
551            off_chain.verified_ticket().amount.amount()
552        );
553
554        Ok(OnChainRedeemableTicket {
555            data: TicketData {
556                channelId: B256::from_slice(off_chain.verified_ticket().channel_id.as_ref()),
557                amount: U96::from_be_slice(&off_chain.verified_ticket().amount.amount().to_be_bytes()[32 - 12..]), /* Extract only the last 12 bytes (lowest 96 bits) */
558                ticketIndex: U48::from_be_slice(&off_chain.verified_ticket().index.to_be_bytes()[8 - 6..]),
559                indexOffset: off_chain.verified_ticket().index_offset,
560                epoch: U24::from_be_slice(&off_chain.verified_ticket().channel_epoch.to_be_bytes()[4 - 3..]),
561                winProb: U56::from_be_slice(&off_chain.verified_ticket().encoded_win_prob),
562            },
563            signature: CompactSignature {
564                r: B256::from_slice(&serialized_signature[0..32]),
565                vs: B256::from_slice(&serialized_signature[32..64]),
566            },
567            porSecret: U256::from_be_slice(off_chain.response.as_ref()),
568        })
569    } else {
570        Err(InvalidArguments("Acknowledged ticket must be signed".into()))
571    }
572}
573
574#[cfg(test)]
575mod tests {
576    use std::str::FromStr;
577
578    use alloy::{primitives::U256, providers::Provider};
579    use anyhow::Context;
580    use hex_literal::hex;
581    use hopr_chain_rpc::client::create_rpc_client_to_anvil;
582    use hopr_chain_types::ContractInstances;
583    use hopr_crypto_types::prelude::*;
584    use hopr_internal_types::prelude::*;
585    use hopr_primitive_types::prelude::HoprBalance;
586    use multiaddr::Multiaddr;
587
588    use super::{BasicPayloadGenerator, PayloadGenerator};
589
590    const PRIVATE_KEY: [u8; 32] = hex!("c14b8faa0a9b8a5fa4453664996f23a7e7de606d42297d723fc4a794f375e260");
591    const RESPONSE_TO_CHALLENGE: [u8; 32] = hex!("b58f99c83ae0e7dd6a69f755305b38c7610c7687d2931ff3f70103f8f92b90bb");
592
593    #[tokio::test]
594    async fn test_announce() -> anyhow::Result<()> {
595        let test_multiaddr = Multiaddr::from_str("/ip4/1.2.3.4/tcp/56")?;
596
597        let anvil = hopr_chain_types::utils::create_anvil(None);
598        let chain_key_0 = ChainKeypair::from_secret(anvil.keys()[0].to_bytes().as_ref())?;
599        let client = create_rpc_client_to_anvil(&anvil, &chain_key_0);
600
601        // Deploy contracts
602        let contract_instances = ContractInstances::deploy_for_testing(client.clone(), &chain_key_0)
603            .await
604            .context("could not deploy contracts")?;
605
606        let generator = BasicPayloadGenerator::new((&chain_key_0).into(), (&contract_instances).into());
607
608        let ad = AnnouncementData::new(
609            test_multiaddr,
610            Some(KeyBinding::new(
611                (&chain_key_0).into(),
612                &OffchainKeypair::from_secret(&PRIVATE_KEY)?,
613            )),
614        )?;
615
616        let tx = generator.announce(ad)?;
617
618        assert!(client.send_transaction(tx).await?.get_receipt().await?.status());
619
620        let test_multiaddr_reannounce = Multiaddr::from_str("/ip4/5.6.7.8/tcp/99")?;
621
622        let ad_reannounce = AnnouncementData::new(test_multiaddr_reannounce, None)?;
623        let reannounce_tx = generator.announce(ad_reannounce)?;
624
625        assert!(
626            client
627                .send_transaction(reannounce_tx)
628                .await?
629                .get_receipt()
630                .await?
631                .status()
632        );
633
634        Ok(())
635    }
636
637    #[tokio::test]
638    async fn redeem_ticket() -> anyhow::Result<()> {
639        let anvil = hopr_chain_types::utils::create_anvil(None);
640        let chain_key_alice = ChainKeypair::from_secret(anvil.keys()[0].to_bytes().as_ref())?;
641        let chain_key_bob = ChainKeypair::from_secret(anvil.keys()[1].to_bytes().as_ref())?;
642        let client = create_rpc_client_to_anvil(&anvil, &chain_key_alice);
643
644        // Deploy contracts
645        let contract_instances = ContractInstances::deploy_for_testing(client.clone(), &chain_key_alice).await?;
646
647        // Mint 1000 HOPR to Alice
648        let _ = hopr_chain_types::utils::mint_tokens(contract_instances.token.clone(), U256::from(1000_u128)).await;
649
650        let domain_separator: Hash = contract_instances.channels.domainSeparator().call().await?.0.into();
651
652        // Open channel Alice -> Bob
653        let _ = hopr_chain_types::utils::fund_channel(
654            (&chain_key_bob).into(),
655            contract_instances.token.clone(),
656            contract_instances.channels.clone(),
657            U256::from(1_u128),
658        )
659        .await;
660
661        // Fund Bob's node
662        let _ = hopr_chain_types::utils::fund_node(
663            (&chain_key_bob).into(),
664            U256::from(1000000000000000000_u128),
665            U256::from(10_u128),
666            contract_instances.token.clone(),
667        )
668        .await;
669
670        let response = Response::try_from(RESPONSE_TO_CHALLENGE.as_ref())?;
671
672        // Alice issues a ticket to Bob
673        let ticket = TicketBuilder::default()
674            .addresses(&chain_key_alice, &chain_key_bob)
675            .amount(1)
676            .index(1)
677            .index_offset(1)
678            .win_prob(1.0.try_into()?)
679            .channel_epoch(1)
680            .challenge(response.to_challenge().into())
681            .build_signed(&chain_key_alice, &domain_separator)?;
682
683        // Bob acknowledges the ticket using the HalfKey from the Response
684        let acked_ticket = ticket
685            .into_acknowledged(response)
686            .into_redeemable(&chain_key_bob, &domain_separator)?;
687
688        // Bob redeems the ticket
689        let generator = BasicPayloadGenerator::new((&chain_key_bob).into(), (&contract_instances).into());
690        let redeem_ticket_tx = generator.redeem_ticket(acked_ticket)?;
691        let client = create_rpc_client_to_anvil(&anvil, &chain_key_bob);
692
693        assert!(
694            client
695                .send_transaction(redeem_ticket_tx)
696                .await?
697                .get_receipt()
698                .await?
699                .status()
700        );
701
702        Ok(())
703    }
704
705    #[tokio::test]
706    async fn withdraw_token() -> anyhow::Result<()> {
707        let anvil = hopr_chain_types::utils::create_anvil(None);
708        let chain_key_alice = ChainKeypair::from_secret(anvil.keys()[0].to_bytes().as_ref())?;
709        let chain_key_bob = ChainKeypair::from_secret(anvil.keys()[1].to_bytes().as_ref())?;
710        let client = create_rpc_client_to_anvil(&anvil, &chain_key_alice);
711
712        // Deploy contracts
713        let contract_instances = ContractInstances::deploy_for_testing(client.clone(), &chain_key_alice).await?;
714        let generator = BasicPayloadGenerator::new((&chain_key_alice).into(), (&contract_instances).into());
715
716        // Mint 1000 HOPR to Alice
717        let _ = hopr_chain_types::utils::mint_tokens(contract_instances.token.clone(), U256::from(1000_u128)).await;
718
719        // Check balance is 1000 HOPR
720        let balance = contract_instances
721            .token
722            .balanceOf(hopr_primitive_types::primitives::Address::from(&chain_key_alice).into())
723            .call()
724            .await?;
725        assert_eq!(balance, U256::from(1000_u128));
726
727        // Alice withdraws 100 HOPR (to Bob's address)
728        let tx = generator.transfer((&chain_key_bob).into(), HoprBalance::from(100))?;
729
730        assert!(client.send_transaction(tx).await?.get_receipt().await?.status());
731
732        // Alice withdraws 100 HOPR, leaving 900 HOPR to the node
733        let balance = contract_instances
734            .token
735            .balanceOf(hopr_primitive_types::primitives::Address::from(&chain_key_alice).into())
736            .call()
737            .await?;
738        assert_eq!(balance, U256::from(900_u128));
739
740        Ok(())
741    }
742}