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