hopr_chain_types/payload/
bindings_based.rs

1pub use hopr_bindings::exports::alloy::rpc::types::TransactionRequest;
2use hopr_bindings::{
3    exports::alloy::{
4        self,
5        consensus::TxEnvelope,
6        eips::Encodable2718,
7        network::{EthereumWallet, TransactionBuilder},
8        primitives::{
9            B256, U256,
10            aliases::{U24, U48, U56, U96},
11        },
12        signers::local::PrivateKeySigner,
13        sol,
14        sol_types::{SolCall, SolValue},
15    },
16    hopr_channels::{
17        HoprChannels::{
18            RedeemableTicket as OnChainRedeemableTicket, TicketData, closeIncomingChannelCall,
19            closeIncomingChannelSafeCall, finalizeOutgoingChannelClosureCall, finalizeOutgoingChannelClosureSafeCall,
20            fundChannelCall, fundChannelSafeCall, initiateOutgoingChannelClosureCall,
21            initiateOutgoingChannelClosureSafeCall, redeemTicketCall, redeemTicketSafeCall,
22        },
23        HoprCrypto::{CompactSignature, VRFParameters},
24    },
25    hopr_node_management_module::HoprNodeManagementModule::execTransactionFromModuleCall,
26    hopr_node_safe_registry::HoprNodeSafeRegistry::{deregisterNodeBySafeCall, registerSafeByNodeCall},
27    hopr_token::HoprToken::{approveCall, sendCall, transferCall},
28};
29use hopr_crypto_types::prelude::*;
30use hopr_internal_types::prelude::*;
31use hopr_primitive_types::prelude::*;
32
33use crate::{
34    ContractAddresses,
35    errors::ChainTypesError::{InvalidArguments, InvalidState, SigningError},
36    payload,
37    payload::{GasEstimation, PayloadGenerator, SignableTransaction},
38};
39
40const DEFAULT_TX_GAS: u64 = 400_000;
41
42// Used instead of From implementation to avoid alloy being a dependency of the primitive crates
43fn a2h(a: hopr_primitive_types::prelude::Address) -> alloy::primitives::Address {
44    alloy::primitives::Address::from_slice(a.as_ref())
45}
46
47sol! {
48    /// Mirrors the Solidity layout: (address, bytes32, bytes32, bytes32, string)
49    struct KeyBindAndAnnouncePayload {
50        address callerNode;
51        bytes32 ed25519_sig_0;
52        bytes32 ed25519_sig_1;
53        bytes32 ed25519_pub_key;
54        string  multiaddress;
55    }
56}
57
58#[repr(u8)]
59#[derive(Copy, Clone, Debug, PartialEq, Eq)]
60enum Operation {
61    Call = 0,
62    // Future use: DelegateCall = 1,
63}
64
65#[async_trait::async_trait]
66impl SignableTransaction for TransactionRequest {
67    async fn sign_and_encode_to_eip2718(
68        self,
69        nonce: u64,
70        max_gas: Option<GasEstimation>,
71        chain_keypair: &ChainKeypair,
72    ) -> payload::Result<Box<[u8]>> {
73        let max_gas = max_gas.unwrap_or_default();
74        let signer: EthereumWallet = PrivateKeySigner::from_slice(chain_keypair.secret().as_ref())
75            .map_err(|e| SigningError(e.into()))?
76            .into();
77        let signed: TxEnvelope = self
78            .nonce(nonce)
79            .gas_limit(max_gas.gas_limit)
80            .max_fee_per_gas(max_gas.max_fee_per_gas)
81            .max_priority_fee_per_gas(max_gas.max_priority_fee_per_gas)
82            .build(&signer)
83            .await
84            .map_err(|e| SigningError(e.into()))?;
85
86        Ok(signed.encoded_2718().into_boxed_slice())
87    }
88}
89
90fn channels_payload(hopr_channels: Address, call_data: Vec<u8>) -> Vec<u8> {
91    execTransactionFromModuleCall {
92        to: a2h(hopr_channels),
93        value: U256::ZERO,
94        data: call_data.into(),
95        operation: Operation::Call as u8,
96    }
97    .abi_encode()
98}
99
100fn approve_tx(spender: Address, amount: HoprBalance) -> TransactionRequest {
101    TransactionRequest::default().with_input(
102        approveCall {
103            spender: a2h(spender),
104            value: U256::from_be_bytes(amount.amount().to_be_bytes()),
105        }
106        .abi_encode(),
107    )
108}
109
110fn transfer_tx<C: Currency>(destination: Address, amount: Balance<C>) -> TransactionRequest {
111    let amount_u256 = U256::from_be_bytes(amount.amount().to_be_bytes());
112    let tx = TransactionRequest::default();
113    if WxHOPR::is::<C>() {
114        tx.with_input(
115            transferCall {
116                recipient: a2h(destination),
117                amount: amount_u256,
118            }
119            .abi_encode(),
120        )
121    } else if XDai::is::<C>() {
122        tx.with_value(amount_u256)
123    } else {
124        unimplemented!("other currencies are currently not supported")
125    }
126}
127
128fn register_safe_tx(safe_addr: Address) -> TransactionRequest {
129    TransactionRequest::default().with_input(
130        registerSafeByNodeCall {
131            safeAddr: a2h(safe_addr),
132        }
133        .abi_encode(),
134    )
135}
136
137/// Generates transaction payloads that do not use Safe-compliant ABI
138#[derive(Debug, Clone, Copy)]
139pub struct BasicPayloadGenerator {
140    me: Address,
141    contract_addrs: ContractAddresses,
142}
143
144impl BasicPayloadGenerator {
145    pub fn new(me: Address, contract_addrs: ContractAddresses) -> Self {
146        Self { me, contract_addrs }
147    }
148}
149
150impl PayloadGenerator for BasicPayloadGenerator {
151    type TxRequest = TransactionRequest;
152
153    fn approve(&self, spender: Address, amount: HoprBalance) -> payload::Result<Self::TxRequest> {
154        let tx = approve_tx(spender, amount).with_to(a2h(self.contract_addrs.token));
155        Ok(tx)
156    }
157
158    fn transfer<C: Currency>(&self, destination: Address, amount: Balance<C>) -> payload::Result<Self::TxRequest> {
159        let to = if XDai::is::<C>() {
160            destination
161        } else if WxHOPR::is::<C>() {
162            self.contract_addrs.token
163        } else {
164            return Err(InvalidArguments("invalid currency"));
165        };
166        let tx = transfer_tx(destination, amount).with_to(a2h(to));
167        Ok(tx)
168    }
169
170    fn announce(
171        &self,
172        announcement: AnnouncementData,
173        key_binding_fee: HoprBalance,
174    ) -> payload::Result<Self::TxRequest> {
175        // when the keys have already bounded, now only try to announce without key binding
176        // when keys are not bounded yet, bind keys and announce together
177        let serialized_signature = announcement.key_binding().signature.as_ref();
178
179        let inner_payload = KeyBindAndAnnouncePayload {
180            callerNode: a2h(self.me),
181            ed25519_sig_0: B256::from_slice(&serialized_signature[0..32]),
182            ed25519_sig_1: B256::from_slice(&serialized_signature[32..64]),
183            ed25519_pub_key: B256::from_slice(announcement.key_binding().packet_key.as_ref()),
184            multiaddress: announcement
185                .multiaddress()
186                .as_ref()
187                .map(ToString::to_string)
188                .unwrap_or_default(), // "" if None
189        }
190        .abi_encode();
191
192        let call_data = sendCall {
193            recipient: a2h(self.contract_addrs.announcements),
194            amount: alloy::primitives::U256::from_be_slice(&key_binding_fee.amount().to_be_bytes()),
195            data: inner_payload[32..].to_vec().into(),
196        }
197        .abi_encode();
198
199        Ok(TransactionRequest::default()
200            .with_input(call_data)
201            .with_to(a2h(self.contract_addrs.token)))
202    }
203
204    fn fund_channel(&self, dest: Address, amount: HoprBalance) -> payload::Result<Self::TxRequest> {
205        if dest.eq(&self.me) {
206            return Err(InvalidArguments("Cannot fund channel to self"));
207        }
208
209        let tx = TransactionRequest::default()
210            .with_input(
211                fundChannelCall {
212                    account: a2h(dest),
213                    amount: U96::from_be_slice(&amount.amount().to_be_bytes()[32 - 12..]),
214                }
215                .abi_encode(),
216            )
217            .with_to(a2h(self.contract_addrs.channels));
218        Ok(tx)
219    }
220
221    fn close_incoming_channel(&self, source: Address) -> payload::Result<Self::TxRequest> {
222        if source.eq(&self.me) {
223            return Err(InvalidArguments("Cannot close incoming channel from self"));
224        }
225
226        let tx = TransactionRequest::default()
227            .with_input(closeIncomingChannelCall { source: a2h(source) }.abi_encode())
228            .with_to(a2h(self.contract_addrs.channels));
229        Ok(tx)
230    }
231
232    fn initiate_outgoing_channel_closure(&self, destination: Address) -> payload::Result<Self::TxRequest> {
233        if destination.eq(&self.me) {
234            return Err(InvalidArguments("Cannot initiate closure of incoming channel to self"));
235        }
236
237        let tx = TransactionRequest::default()
238            .with_input(
239                initiateOutgoingChannelClosureCall {
240                    destination: a2h(destination),
241                }
242                .abi_encode(),
243            )
244            .with_to(a2h(self.contract_addrs.channels));
245        Ok(tx)
246    }
247
248    fn finalize_outgoing_channel_closure(&self, destination: Address) -> payload::Result<Self::TxRequest> {
249        if destination.eq(&self.me) {
250            return Err(InvalidArguments("Cannot initiate closure of incoming channel to self"));
251        }
252
253        let tx = TransactionRequest::default()
254            .with_input(
255                finalizeOutgoingChannelClosureCall {
256                    destination: a2h(destination),
257                }
258                .abi_encode(),
259            )
260            .with_to(a2h(self.contract_addrs.channels));
261        Ok(tx)
262    }
263
264    fn redeem_ticket(&self, acked_ticket: RedeemableTicket) -> payload::Result<Self::TxRequest> {
265        let redeemable = convert_acknowledged_ticket(&acked_ticket)?;
266
267        let params = convert_vrf_parameters(
268            &acked_ticket.vrf_params,
269            &self.me,
270            acked_ticket.ticket.verified_hash(),
271            &acked_ticket.channel_dst,
272        );
273
274        let tx = TransactionRequest::default()
275            .with_input(redeemTicketCall { redeemable, params }.abi_encode())
276            .with_to(a2h(self.contract_addrs.channels));
277        Ok(tx)
278    }
279
280    fn register_safe_by_node(&self, safe_addr: Address) -> payload::Result<Self::TxRequest> {
281        let tx = register_safe_tx(safe_addr).with_to(a2h(self.contract_addrs.node_safe_registry));
282        Ok(tx)
283    }
284
285    fn deregister_node_by_safe(&self) -> payload::Result<Self::TxRequest> {
286        Err(InvalidState("Can only deregister an address if Safe is activated"))
287    }
288}
289
290/// Payload generator that generates Safe-compliant ABI
291#[derive(Debug, Clone, Copy)]
292pub struct SafePayloadGenerator {
293    me: Address,
294    contract_addrs: ContractAddresses,
295    module: Address,
296}
297
298impl SafePayloadGenerator {
299    pub fn new(chain_keypair: &ChainKeypair, contract_addrs: ContractAddresses, module: Address) -> Self {
300        Self {
301            me: chain_keypair.into(),
302            contract_addrs,
303            module,
304        }
305    }
306}
307
308impl PayloadGenerator for SafePayloadGenerator {
309    type TxRequest = TransactionRequest;
310
311    fn approve(&self, spender: Address, amount: HoprBalance) -> payload::Result<Self::TxRequest> {
312        let tx = approve_tx(spender, amount)
313            .with_to(a2h(self.contract_addrs.token))
314            .with_gas_limit(DEFAULT_TX_GAS);
315
316        Ok(tx)
317    }
318
319    fn transfer<C: Currency>(&self, destination: Address, amount: Balance<C>) -> payload::Result<Self::TxRequest> {
320        let to = if XDai::is::<C>() {
321            destination
322        } else if WxHOPR::is::<C>() {
323            self.contract_addrs.token
324        } else {
325            return Err(InvalidArguments("invalid currency"));
326        };
327        let tx = transfer_tx(destination, amount)
328            .with_to(a2h(to))
329            .with_gas_limit(DEFAULT_TX_GAS);
330
331        Ok(tx)
332    }
333
334    fn announce(
335        &self,
336        announcement: AnnouncementData,
337        key_binding_fee: HoprBalance,
338    ) -> payload::Result<Self::TxRequest> {
339        // when the keys have already bounded, now only try to announce without key binding
340        // when keys are not bounded yet, bind keys and announce together
341        let serialized_signature = announcement.key_binding().signature.as_ref();
342
343        let inner_payload = KeyBindAndAnnouncePayload {
344            callerNode: a2h(self.me),
345            ed25519_sig_0: B256::from_slice(&serialized_signature[0..32]),
346            ed25519_sig_1: B256::from_slice(&serialized_signature[32..64]),
347            ed25519_pub_key: B256::from_slice(announcement.key_binding().packet_key.as_ref()),
348            multiaddress: announcement
349                .multiaddress()
350                .as_ref()
351                .map(ToString::to_string)
352                .unwrap_or_default(),
353        }
354        .abi_encode();
355
356        let call_data = sendCall {
357            recipient: a2h(self.contract_addrs.announcements),
358            amount: alloy::primitives::U256::from_be_slice(&key_binding_fee.amount().to_be_bytes()),
359            data: inner_payload[32..].to_vec().into(),
360        }
361        .abi_encode();
362
363        Ok(TransactionRequest::default()
364            .with_input(
365                execTransactionFromModuleCall {
366                    to: a2h(self.contract_addrs.token),
367                    value: U256::ZERO,
368                    data: call_data.into(),
369                    operation: Operation::Call as u8,
370                }
371                .abi_encode(),
372            )
373            .with_to(a2h(self.module))
374            .with_gas_limit(DEFAULT_TX_GAS))
375    }
376
377    fn fund_channel(&self, dest: Address, amount: HoprBalance) -> payload::Result<Self::TxRequest> {
378        if dest.eq(&self.me) {
379            return Err(InvalidArguments("Cannot fund channel to self"));
380        }
381
382        if amount.amount() > hopr_primitive_types::prelude::U256::from(ChannelEntry::MAX_CHANNEL_BALANCE) {
383            return Err(InvalidArguments("Cannot fund channel with amount larger than 96 bits"));
384        }
385
386        let call_data = fundChannelSafeCall {
387            selfAddress: a2h(self.me),
388            account: a2h(dest),
389            amount: U96::from_be_slice(&amount.amount().to_be_bytes()[32 - 12..]),
390        }
391        .abi_encode();
392
393        let tx = TransactionRequest::default()
394            .with_input(channels_payload(self.contract_addrs.channels, call_data))
395            .with_to(a2h(self.module))
396            .with_gas_limit(DEFAULT_TX_GAS);
397
398        Ok(tx)
399    }
400
401    fn close_incoming_channel(&self, source: Address) -> payload::Result<Self::TxRequest> {
402        if source.eq(&self.me) {
403            return Err(InvalidArguments("Cannot close incoming channel from self"));
404        }
405
406        let call_data = closeIncomingChannelSafeCall {
407            selfAddress: a2h(self.me),
408            source: a2h(source),
409        }
410        .abi_encode();
411
412        let tx = TransactionRequest::default()
413            .with_input(channels_payload(self.contract_addrs.channels, call_data))
414            .with_to(a2h(self.module))
415            .with_gas_limit(DEFAULT_TX_GAS);
416
417        Ok(tx)
418    }
419
420    fn initiate_outgoing_channel_closure(&self, destination: Address) -> payload::Result<Self::TxRequest> {
421        if destination.eq(&self.me) {
422            return Err(InvalidArguments("Cannot initiate closure of incoming channel to self"));
423        }
424
425        let call_data = initiateOutgoingChannelClosureSafeCall {
426            selfAddress: a2h(self.me),
427            destination: a2h(destination),
428        }
429        .abi_encode();
430
431        let tx = TransactionRequest::default()
432            .with_input(channels_payload(self.contract_addrs.channels, call_data))
433            .with_to(a2h(self.module))
434            .with_gas_limit(DEFAULT_TX_GAS);
435
436        Ok(tx)
437    }
438
439    fn finalize_outgoing_channel_closure(&self, destination: Address) -> payload::Result<Self::TxRequest> {
440        if destination.eq(&self.me) {
441            return Err(InvalidArguments("Cannot initiate closure of incoming channel to self"));
442        }
443
444        let call_data = finalizeOutgoingChannelClosureSafeCall {
445            selfAddress: a2h(self.me),
446            destination: a2h(destination),
447        }
448        .abi_encode();
449
450        let tx = TransactionRequest::default()
451            .with_input(channels_payload(self.contract_addrs.channels, call_data))
452            .with_to(a2h(self.module))
453            .with_gas_limit(DEFAULT_TX_GAS);
454
455        Ok(tx)
456    }
457
458    fn redeem_ticket(&self, acked_ticket: RedeemableTicket) -> payload::Result<Self::TxRequest> {
459        let redeemable = convert_acknowledged_ticket(&acked_ticket)?;
460
461        let params = convert_vrf_parameters(
462            &acked_ticket.vrf_params,
463            &self.me,
464            acked_ticket.ticket.verified_hash(),
465            &acked_ticket.channel_dst,
466        );
467
468        let call_data = redeemTicketSafeCall {
469            selfAddress: a2h(self.me),
470            redeemable,
471            params,
472        }
473        .abi_encode();
474
475        let tx = TransactionRequest::default()
476            .with_input(channels_payload(self.contract_addrs.channels, call_data))
477            .with_to(a2h(self.module))
478            .with_gas_limit(DEFAULT_TX_GAS);
479
480        Ok(tx)
481    }
482
483    fn register_safe_by_node(&self, safe_addr: Address) -> payload::Result<Self::TxRequest> {
484        let tx = register_safe_tx(safe_addr)
485            .with_to(a2h(self.contract_addrs.node_safe_registry))
486            .with_gas_limit(DEFAULT_TX_GAS);
487
488        Ok(tx)
489    }
490
491    fn deregister_node_by_safe(&self) -> payload::Result<Self::TxRequest> {
492        let tx = TransactionRequest::default()
493            .with_input(deregisterNodeBySafeCall { nodeAddr: a2h(self.me) }.abi_encode())
494            .with_to(a2h(self.module))
495            .with_gas_limit(DEFAULT_TX_GAS);
496
497        Ok(tx)
498    }
499}
500
501/// Converts off-chain representation of VRF parameters into a representation
502/// that the smart contract understands
503///
504/// Not implemented using From trait because logic fits better here
505fn convert_vrf_parameters(
506    off_chain: &VrfParameters,
507    signer: &Address,
508    ticket_hash: &Hash,
509    domain_separator: &Hash,
510) -> VRFParameters {
511    // skip the secp256k1 curvepoint prefix
512    let v = off_chain.get_v_encoded_point();
513    let s_b = off_chain
514        .get_s_b_witness(signer, &ticket_hash.into(), domain_separator.as_ref())
515        // Safe: hash value is always in the allowed length boundaries,
516        //       only fails for longer values
517        // Safe: always encoding to secp256k1 whose field elements are in
518        //       allowed length boundaries
519        .expect("ticket hash exceeded hash2field boundaries or encoding to unsupported curve");
520
521    let h_v = off_chain.get_h_v_witness();
522
523    VRFParameters {
524        vx: U256::from_be_slice(&v.as_bytes()[1..33]),
525        vy: U256::from_be_slice(&v.as_bytes()[33..65]),
526        s: U256::from_be_slice(&off_chain.s.to_bytes()),
527        h: U256::from_be_slice(&off_chain.h.to_bytes()),
528        sBx: U256::from_be_slice(&s_b.as_bytes()[1..33]),
529        sBy: U256::from_be_slice(&s_b.as_bytes()[33..65]),
530        hVx: U256::from_be_slice(&h_v.as_bytes()[1..33]),
531        hVy: U256::from_be_slice(&h_v.as_bytes()[33..65]),
532    }
533}
534
535/// Convert off-chain representation of an acknowledged ticket to representation
536/// that the smart contract understands
537///
538/// Not implemented using From trait because logic fits better here
539fn convert_acknowledged_ticket(off_chain: &RedeemableTicket) -> payload::Result<OnChainRedeemableTicket> {
540    if let Some(ref signature) = off_chain.verified_ticket().signature {
541        let serialized_signature = signature.as_ref();
542
543        Ok(OnChainRedeemableTicket {
544            data: TicketData {
545                channelId: B256::from_slice(off_chain.verified_ticket().channel_id.as_ref()),
546                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) */
547                ticketIndex: U48::from_be_slice(&off_chain.verified_ticket().index.to_be_bytes()[8 - 6..]),
548                epoch: U24::from_be_slice(&off_chain.verified_ticket().channel_epoch.to_be_bytes()[4 - 3..]),
549                winProb: U56::from_be_slice(&off_chain.verified_ticket().encoded_win_prob),
550            },
551            signature: CompactSignature {
552                r: B256::from_slice(&serialized_signature[0..32]),
553                vs: B256::from_slice(&serialized_signature[32..64]),
554            },
555            porSecret: U256::from_be_slice(off_chain.response.as_ref()),
556        })
557    } else {
558        Err(InvalidArguments("Acknowledged ticket must be signed"))
559    }
560}
561
562#[cfg(test)]
563pub(crate) mod tests {
564    use std::str::FromStr;
565
566    use hex_literal::hex;
567    use hopr_crypto_types::prelude::*;
568    use hopr_internal_types::prelude::*;
569    use hopr_primitive_types::prelude::*;
570    use multiaddr::Multiaddr;
571
572    use crate::payload::{
573        BasicPayloadGenerator, PayloadGenerator, SafePayloadGenerator, SignableTransaction, tests::CONTRACT_ADDRS,
574    };
575
576    const PRIVATE_KEY_1: [u8; 32] = hex!("c14b8faa0a9b8a5fa4453664996f23a7e7de606d42297d723fc4a794f375e260");
577    const PRIVATE_KEY_2: [u8; 32] = hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775");
578
579    lazy_static::lazy_static! {
580        static ref REDEEMABLE_TICKET: RedeemableTicket = bincode::serde::decode_from_slice(&hex!(
581            "20d7f3fa28f0caa757e81226e1ee86a9efdbe7991442286183797296ebaa4d292a0330783101ffffffffffffff01766fb51964b4b8797d0ad0d580481ba0024dcf3c01409f24b8d12487dd48340af4ebdc44eff0f4c85ee25b27aca72f50af361022b32232fb24b408a8ed17967c1d700acc3fccbce3408d885d5b8fc41f01c4cf103deb2010312e537a39dbd374fb8dbb7031f255a75ceab085d4789154830f90d9c3e297668430def9eacd2b5064acf85d73fb0b351a1c8c20b58f99c83ae0e7dd6a69f755305b38c7610c7687d2931ff3f70103f8f92b90bb61026ffdd80eb690f54e85a2d085cb4f0cabdcf96f545af546b0fa8d67402d8c14ef04274e009938aaa5eea217f4103349d4b6bc80cabdd3b8177d473b8b00872d3c20bbac4b6ad5c187d6a7e7118df6d2e63a04f194b54204c18f61372f5b78d154200000000000000000000000000000000000000000000000000000000000000000"
582        ), bincode::config::standard()).unwrap().0;
583    }
584
585    #[tokio::test]
586    async fn test_announce() -> anyhow::Result<()> {
587        let test_multiaddr = Multiaddr::from_str("/ip4/1.2.3.4/tcp/56")?;
588
589        let chain_key_0 = ChainKeypair::from_secret(&PRIVATE_KEY_1)?;
590
591        let generator = BasicPayloadGenerator::new((&chain_key_0).into(), *CONTRACT_ADDRS);
592
593        let kb = KeyBinding::new((&chain_key_0).into(), &OffchainKeypair::from_secret(&PRIVATE_KEY_1)?);
594
595        let ad = AnnouncementData::new(kb, Some(test_multiaddr))?;
596
597        let signed_tx = generator
598            .announce(ad, 100_u32.into())?
599            .sign_and_encode_to_eip2718(2, None, &chain_key_0)
600            .await?;
601        insta::assert_snapshot!("announce_basic", hex::encode(signed_tx));
602
603        let test_multiaddr_reannounce = Multiaddr::from_str("/ip4/5.6.7.8/tcp/99")?;
604        let ad_reannounce = AnnouncementData::new(kb, Some(test_multiaddr_reannounce))?;
605
606        let signed_tx = generator
607            .announce(ad_reannounce, 0_u32.into())?
608            .sign_and_encode_to_eip2718(1, None, &chain_key_0)
609            .await?;
610        insta::assert_snapshot!("announce_safe", hex::encode(signed_tx.clone()));
611
612        Ok(())
613    }
614
615    #[tokio::test]
616    async fn redeem_ticket() -> anyhow::Result<()> {
617        let chain_key_bob = ChainKeypair::from_secret(&PRIVATE_KEY_2)?;
618
619        let acked_ticket = REDEEMABLE_TICKET.clone();
620
621        // Bob redeems the ticket
622        let generator = BasicPayloadGenerator::new((&chain_key_bob).into(), *CONTRACT_ADDRS);
623        let redeem_ticket_tx = generator.redeem_ticket(acked_ticket.clone())?;
624        let signed_tx = redeem_ticket_tx
625            .sign_and_encode_to_eip2718(1, None, &chain_key_bob)
626            .await?;
627
628        insta::assert_snapshot!("redeem_ticket_basic", hex::encode(signed_tx));
629
630        // Bob redeems the ticket
631        let generator =
632            SafePayloadGenerator::new((&chain_key_bob).into(), *CONTRACT_ADDRS, [1u8; Address::SIZE].into());
633        let redeem_ticket_tx = generator.redeem_ticket(acked_ticket)?;
634        let signed_tx = redeem_ticket_tx
635            .sign_and_encode_to_eip2718(2, None, &chain_key_bob)
636            .await?;
637
638        insta::assert_snapshot!("redeem_ticket_safe", hex::encode(signed_tx));
639
640        Ok(())
641    }
642
643    #[tokio::test]
644    async fn withdraw_token() -> anyhow::Result<()> {
645        let chain_key_alice = ChainKeypair::from_secret(&PRIVATE_KEY_1)?;
646        let chain_key_bob = ChainKeypair::from_secret(&PRIVATE_KEY_2)?;
647
648        let generator = BasicPayloadGenerator::new((&chain_key_alice).into(), *CONTRACT_ADDRS);
649        let tx = generator.transfer((&chain_key_bob).into(), HoprBalance::from(100))?;
650
651        let signed_tx = tx.sign_and_encode_to_eip2718(1, None, &chain_key_bob).await?;
652
653        insta::assert_snapshot!("withdraw_basic", hex::encode(signed_tx));
654
655        let generator =
656            SafePayloadGenerator::new((&chain_key_alice).into(), *CONTRACT_ADDRS, [1u8; Address::SIZE].into());
657        let tx = generator.transfer((&chain_key_bob).into(), HoprBalance::from(100))?;
658
659        let signed_tx = tx.sign_and_encode_to_eip2718(2, None, &chain_key_bob).await?;
660
661        insta::assert_snapshot!("withdraw_safe", hex::encode(signed_tx));
662
663        Ok(())
664    }
665}