hopr_chain_types/payload/
bindings_based.rs

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