hopr_chain_types/
parser.rs

1use hopr_bindings::{
2    exports::alloy::{
3        consensus::Transaction,
4        eips::Decodable2718,
5        sol_types::{SolCall, SolType},
6    },
7    hopr_channels::HoprChannels::{
8        closeIncomingChannelCall, closeIncomingChannelSafeCall, finalizeOutgoingChannelClosureCall,
9        finalizeOutgoingChannelClosureSafeCall, fundChannelCall, fundChannelSafeCall,
10        initiateOutgoingChannelClosureCall, initiateOutgoingChannelClosureSafeCall, redeemTicketCall,
11        redeemTicketSafeCall,
12    },
13    hopr_node_management_module::HoprNodeManagementModule::execTransactionFromModuleCall,
14    hopr_node_safe_registry::HoprNodeSafeRegistry::registerSafeByNodeCall,
15    hopr_token::HoprToken::{sendCall, transferCall},
16};
17use hopr_crypto_types::prelude::OffchainPublicKey;
18use hopr_internal_types::prelude::{ChannelId, generate_channel_id};
19use hopr_primitive_types::prelude::*;
20use multiaddr::Multiaddr;
21
22use crate::{ContractAddresses, a2al, errors::ChainTypesError, payload::KeyBindAndAnnouncePayload};
23
24/// Represents the action previously parsed from an EIP-2718 transaction.
25///
26/// This is effectively inverse of a [`PayloadGenerator`](crate::payload::PayloadGenerator).
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum ParsedHoprChainAction {
29    /// Registration of a Safe address.
30    RegisterSafeAddress(Address),
31    /// Announcement of a packet key and optional multiaddress.
32    Announce {
33        /// Announced packet key (key-binding).
34        packet_key: OffchainPublicKey,
35        /// Optional multiaddress to announce.
36        multiaddress: Option<Multiaddr>,
37    },
38    /// Withdrawal of native XDai to an address.
39    WithdrawNative(Address, XDaiBalance),
40    /// Withdrawal of HOPR token to an address.
41    WithdrawToken(Address, HoprBalance),
42    /// Funding of a payment channel to a given destination with a given amount.
43    FundChannel(Address, HoprBalance),
44    /// Payment channel closure initiation with the given ID.
45    InitializeChannelClosure(ChannelId),
46    /// Payment channel closure finalization with the given ID.
47    FinalizeChannelClosure(ChannelId),
48    /// Incoming payment channel closure with the given ID.
49    IncomingChannelClosure(ChannelId),
50    /// Redemption of ticket.
51    RedeemTicket {
52        /// ID of the channel the ticket was issued on.
53        channel_id: ChannelId,
54        /// Index of the ticket within the channel.
55        ticket_index: u64,
56        /// Amount HOPR tokens on the ticket (value to be redeemed).
57        ticket_amount: HoprBalance,
58    },
59}
60
61impl ParsedHoprChainAction {
62    /// Attempts to parse a signed EIP-2718 transaction previously generated via a
63    /// [`PayloadGenerator`](crate::payload::PayloadGenerator).
64    pub fn parse_from_eip2718(
65        signed_tx: &[u8],
66        module: &Address,
67        contract_addresses: &ContractAddresses,
68    ) -> Result<(Self, Address), ChainTypesError> {
69        let tx = hopr_bindings::exports::alloy::consensus::TxEnvelope::decode_2718_exact(signed_tx)
70            .map_err(|e| ChainTypesError::ParseError(e.into()))?
71            .into_signed();
72
73        let signer = Address::from(
74            tx.recover_signer()
75                .map_err(|e| ChainTypesError::ParseError(e.into()))?
76                .0
77                .0,
78        );
79
80        let tx_target = tx
81            .to()
82            .map(|to| Address::from(to.0.0))
83            .ok_or(ChainTypesError::ParseError(anyhow::anyhow!(
84                "transaction has no recipient"
85            )))?;
86
87        let (target_contract, input, module_call) = if &tx_target == module {
88            let module_call = execTransactionFromModuleCall::abi_decode(tx.input().as_ref())
89                .map_err(|e| ChainTypesError::ParseError(e.into()))?;
90            (module_call.to.0.0.into(), module_call.data, true)
91        } else if contract_addresses.into_iter().any(|addr| addr == a2al(tx_target)) {
92            (tx_target, tx.input().clone(), false)
93        } else if tx.value() > 0 {
94            return Ok((
95                Self::WithdrawNative(tx_target, XDaiBalance::from_be_bytes(tx.value().to_be_bytes::<32>())),
96                signer,
97            ));
98        } else {
99            return Err(ChainTypesError::ParseError(anyhow::anyhow!(
100                "failed to determine type of transaction"
101            )));
102        };
103
104        let target_contract = a2al(target_contract);
105
106        if target_contract == contract_addresses.node_safe_registry {
107            let register_call = registerSafeByNodeCall::abi_decode(input.as_ref())
108                .map_err(|e| ChainTypesError::ParseError(e.into()))?;
109
110            Ok((Self::RegisterSafeAddress(register_call.safeAddr.0.0.into()), signer))
111        } else if target_contract == contract_addresses.channels && module_call {
112            if let Ok(fund) = fundChannelSafeCall::abi_decode(input.as_ref()) {
113                return Ok((
114                    Self::FundChannel(
115                        fund.account.0.0.into(),
116                        HoprBalance::from_be_bytes(fund.amount.to_be_bytes::<12>()),
117                    ),
118                    signer,
119                ));
120            }
121
122            if let Ok(initiate) = initiateOutgoingChannelClosureSafeCall::abi_decode(input.as_ref()) {
123                return Ok((
124                    Self::InitializeChannelClosure(generate_channel_id(&signer, &initiate.destination.0.0.into())),
125                    signer,
126                ));
127            }
128
129            if let Ok(finalize) = finalizeOutgoingChannelClosureSafeCall::abi_decode(input.as_ref()) {
130                return Ok((
131                    Self::FinalizeChannelClosure(generate_channel_id(&signer, &finalize.destination.0.0.into())),
132                    signer,
133                ));
134            }
135
136            if let Ok(close_incoming) = closeIncomingChannelSafeCall::abi_decode(input.as_ref()) {
137                return Ok((
138                    Self::IncomingChannelClosure(generate_channel_id(&close_incoming.source.0.0.into(), &signer)),
139                    signer,
140                ));
141            }
142
143            if let Ok(redeem) = redeemTicketSafeCall::abi_decode(input.as_ref()) {
144                let ticket_data = redeem.redeemable.data;
145                return Ok((
146                    Self::RedeemTicket {
147                        channel_id: ticket_data.channelId.0.into(),
148                        ticket_index: U256::from_be_bytes(ticket_data.ticketIndex.to_be_bytes::<6>()).as_u64(),
149                        ticket_amount: HoprBalance::from_be_bytes(ticket_data.amount.to_be_bytes::<12>()),
150                    },
151                    signer,
152                ));
153            }
154
155            Err(ChainTypesError::ParseError(anyhow::anyhow!(
156                "channel transaction has invalid type"
157            )))?
158        } else if target_contract == contract_addresses.channels && !module_call {
159            if let Ok(fund) = fundChannelCall::abi_decode(input.as_ref()) {
160                return Ok((
161                    Self::FundChannel(
162                        fund.account.0.0.into(),
163                        HoprBalance::from_be_bytes(fund.amount.to_be_bytes::<12>()),
164                    ),
165                    signer,
166                ));
167            }
168
169            if let Ok(initiate) = initiateOutgoingChannelClosureCall::abi_decode(input.as_ref()) {
170                return Ok((
171                    Self::InitializeChannelClosure(generate_channel_id(&signer, &initiate.destination.0.0.into())),
172                    signer,
173                ));
174            }
175
176            if let Ok(finalize) = finalizeOutgoingChannelClosureCall::abi_decode(input.as_ref()) {
177                return Ok((
178                    Self::FinalizeChannelClosure(generate_channel_id(&signer, &finalize.destination.0.0.into())),
179                    signer,
180                ));
181            }
182
183            if let Ok(close_incoming) = closeIncomingChannelCall::abi_decode(input.as_ref()) {
184                return Ok((
185                    Self::IncomingChannelClosure(generate_channel_id(&close_incoming.source.0.0.into(), &signer)),
186                    signer,
187                ));
188            }
189
190            if let Ok(redeem) = redeemTicketCall::abi_decode(input.as_ref()) {
191                let ticket_data = redeem.redeemable.data;
192                return Ok((
193                    Self::RedeemTicket {
194                        channel_id: ticket_data.channelId.0.into(),
195                        ticket_index: U256::from_be_bytes(ticket_data.ticketIndex.to_be_bytes::<6>()).as_u64(),
196                        ticket_amount: HoprBalance::from_be_bytes(ticket_data.amount.to_be_bytes::<12>()),
197                    },
198                    signer,
199                ));
200            }
201            Err(ChainTypesError::ParseError(anyhow::anyhow!(
202                "channel transaction has invalid type"
203            )))?
204        } else if target_contract == contract_addresses.token {
205            if let Ok(send) = sendCall::abi_decode(input.as_ref()) {
206                if send.recipient == contract_addresses.announcements {
207                    let mut data = vec![0u8; 32 + send.data.len()];
208                    data[31] = 32;
209                    data[32..].copy_from_slice(&send.data);
210
211                    let kb = KeyBindAndAnnouncePayload::abi_decode(&data)
212                        .map_err(|e| ChainTypesError::ParseError(e.into()))?;
213
214                    return Ok((
215                        Self::Announce {
216                            packet_key: kb.ed25519_pub_key.0.try_into().map_err(|_| {
217                                ChainTypesError::ParseError(anyhow::anyhow!("failed to parse packet key"))
218                            })?,
219                            multiaddress: if kb.multiaddress.is_empty() {
220                                None
221                            } else {
222                                Some(kb.multiaddress.parse().map_err(|_| {
223                                    ChainTypesError::ParseError(anyhow::anyhow!("failed to parse multiaddress"))
224                                })?)
225                            },
226                        },
227                        signer,
228                    ));
229                } else {
230                    Err(ChainTypesError::ParseError(anyhow::anyhow!(
231                        "token send transaction transaction has invalid type"
232                    )))?
233                }
234            }
235
236            let transfer =
237                transferCall::abi_decode(input.as_ref()).map_err(|e| ChainTypesError::ParseError(e.into()))?;
238
239            Ok((
240                Self::WithdrawToken(
241                    transfer.recipient.0.0.into(),
242                    HoprBalance::from_be_bytes(transfer.amount.to_be_bytes::<32>()),
243                ),
244                signer,
245            ))
246        } else {
247            Err(ChainTypesError::ParseError(anyhow::anyhow!(
248                "transaction has invalid contract address"
249            )))?
250        }
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use hex_literal::hex;
257    use hopr_crypto_types::{
258        crypto_traits::Randomizable,
259        prelude::{ChainKeypair, HalfKey, Hash, Keypair, OffchainKeypair, Response},
260    };
261    use hopr_internal_types::prelude::{AnnouncementData, KeyBinding, TicketBuilder};
262
263    use super::*;
264    use crate::payload::{
265        BasicPayloadGenerator, PayloadGenerator, SafePayloadGenerator, SignableTransaction, tests::CONTRACT_ADDRS,
266    };
267
268    const PRIVATE_KEY_1: [u8; 32] = hex!("c14b8faa0a9b8a5fa4453664996f23a7e7de606d42297d723fc4a794f375e260");
269    const PRIVATE_KEY_2: [u8; 32] = hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775");
270
271    #[tokio::test]
272    async fn announce_safe_action_should_decode() -> anyhow::Result<()> {
273        let cp = ChainKeypair::from_secret(&PRIVATE_KEY_1)?;
274        let ocp = OffchainKeypair::random();
275
276        let ad = AnnouncementData::new(
277            KeyBinding::new(cp.public().to_address(), &ocp),
278            Some("/ip4/127.0.0.1/tcp/10000".parse()?),
279        )?;
280
281        let safe_gen = SafePayloadGenerator::new(&cp, *CONTRACT_ADDRS, [1u8; Address::SIZE].into());
282        let signed_tx = safe_gen
283            .announce(ad, 10_u32.into())?
284            .sign_and_encode_to_eip2718(1, 1, None, &cp)
285            .await?;
286
287        let (action, signer) =
288            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
289        assert_eq!(
290            action,
291            ParsedHoprChainAction::Announce {
292                packet_key: *ocp.public(),
293                multiaddress: Some("/ip4/127.0.0.1/tcp/10000".parse()?),
294            }
295        );
296        assert_eq!(signer, cp.public().to_address());
297
298        Ok(())
299    }
300
301    #[tokio::test]
302    async fn fund_channel_action_should_decode() -> anyhow::Result<()> {
303        let cp = ChainKeypair::from_secret(&PRIVATE_KEY_1)?;
304
305        let basic_gen = BasicPayloadGenerator::new(cp.public().to_address(), *CONTRACT_ADDRS);
306        let signed_tx = basic_gen
307            .fund_channel([2u8; Address::SIZE].into(), 123_u32.into())?
308            .sign_and_encode_to_eip2718(1, 1, None, &cp)
309            .await?;
310
311        let (action, signer) =
312            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
313        assert_eq!(
314            action,
315            ParsedHoprChainAction::FundChannel([2u8; Address::SIZE].into(), 123_u32.into())
316        );
317        assert_eq!(signer, cp.public().to_address());
318
319        let safe_gen = SafePayloadGenerator::new(&cp, *CONTRACT_ADDRS, [1u8; Address::SIZE].into());
320        let signed_tx = safe_gen
321            .fund_channel([2u8; Address::SIZE].into(), 123_u32.into())?
322            .sign_and_encode_to_eip2718(1, 1, None, &cp)
323            .await?;
324
325        let (action, signer) =
326            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
327        assert_eq!(
328            action,
329            ParsedHoprChainAction::FundChannel([2u8; Address::SIZE].into(), 123_u32.into())
330        );
331        assert_eq!(signer, cp.public().to_address());
332
333        Ok(())
334    }
335
336    #[tokio::test]
337    async fn initiate_channel_closure_action_should_decode() -> anyhow::Result<()> {
338        let cp = ChainKeypair::from_secret(&PRIVATE_KEY_1)?;
339
340        let basic_gen = BasicPayloadGenerator::new(cp.public().to_address(), *CONTRACT_ADDRS);
341        let signed_tx = basic_gen
342            .initiate_outgoing_channel_closure([2u8; Address::SIZE].into())?
343            .sign_and_encode_to_eip2718(1, 1, None, &cp)
344            .await?;
345
346        let channel_id = generate_channel_id(&cp.public().to_address(), &[2u8; Address::SIZE].into());
347
348        let (action, signer) =
349            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
350        assert_eq!(action, ParsedHoprChainAction::InitializeChannelClosure(channel_id));
351        assert_eq!(signer, cp.public().to_address());
352
353        let safe_gen = SafePayloadGenerator::new(&cp, *CONTRACT_ADDRS, [1u8; Address::SIZE].into());
354        let signed_tx = safe_gen
355            .initiate_outgoing_channel_closure([2u8; Address::SIZE].into())?
356            .sign_and_encode_to_eip2718(1, 1, None, &cp)
357            .await?;
358
359        let (action, signer) =
360            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
361        assert_eq!(action, ParsedHoprChainAction::InitializeChannelClosure(channel_id));
362        assert_eq!(signer, cp.public().to_address());
363
364        Ok(())
365    }
366
367    #[tokio::test]
368    async fn finalize_channel_closure_action_should_decode() -> anyhow::Result<()> {
369        let cp = ChainKeypair::from_secret(&PRIVATE_KEY_1)?;
370
371        let basic_gen = BasicPayloadGenerator::new(cp.public().to_address(), *CONTRACT_ADDRS);
372        let signed_tx = basic_gen
373            .finalize_outgoing_channel_closure([2u8; Address::SIZE].into())?
374            .sign_and_encode_to_eip2718(1, 1, None, &cp)
375            .await?;
376
377        let channel_id = generate_channel_id(&cp.public().to_address(), &[2u8; Address::SIZE].into());
378
379        let (action, signer) =
380            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
381        assert_eq!(action, ParsedHoprChainAction::FinalizeChannelClosure(channel_id));
382        assert_eq!(signer, cp.public().to_address());
383
384        let safe_gen = SafePayloadGenerator::new(&cp, *CONTRACT_ADDRS, [1u8; Address::SIZE].into());
385        let signed_tx = safe_gen
386            .finalize_outgoing_channel_closure([2u8; Address::SIZE].into())?
387            .sign_and_encode_to_eip2718(1, 1, None, &cp)
388            .await?;
389
390        let (action, signer) =
391            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
392        assert_eq!(action, ParsedHoprChainAction::FinalizeChannelClosure(channel_id));
393        assert_eq!(signer, cp.public().to_address());
394
395        Ok(())
396    }
397
398    #[tokio::test]
399    async fn incoming_channel_closure_action_should_decode() -> anyhow::Result<()> {
400        let cp = ChainKeypair::from_secret(&PRIVATE_KEY_1)?;
401
402        let basic_gen = BasicPayloadGenerator::new(cp.public().to_address(), *CONTRACT_ADDRS);
403        let signed_tx = basic_gen
404            .close_incoming_channel([2u8; Address::SIZE].into())?
405            .sign_and_encode_to_eip2718(1, 1, None, &cp)
406            .await?;
407
408        let channel_id = generate_channel_id(&[2u8; Address::SIZE].into(), &cp.public().to_address());
409
410        let (action, signer) =
411            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
412        assert_eq!(action, ParsedHoprChainAction::IncomingChannelClosure(channel_id));
413        assert_eq!(signer, cp.public().to_address());
414
415        let safe_gen = SafePayloadGenerator::new(&cp, *CONTRACT_ADDRS, [1u8; Address::SIZE].into());
416        let signed_tx = safe_gen
417            .close_incoming_channel([2u8; Address::SIZE].into())?
418            .sign_and_encode_to_eip2718(1, 1, None, &cp)
419            .await?;
420
421        let (action, signer) =
422            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
423        assert_eq!(action, ParsedHoprChainAction::IncomingChannelClosure(channel_id));
424        assert_eq!(signer, cp.public().to_address());
425
426        Ok(())
427    }
428
429    #[tokio::test]
430    async fn register_safe_action_should_decode() -> anyhow::Result<()> {
431        let cp = ChainKeypair::from_secret(&PRIVATE_KEY_1)?;
432
433        let basic_gen = BasicPayloadGenerator::new(cp.public().to_address(), *CONTRACT_ADDRS);
434        let signed_tx = basic_gen
435            .register_safe_by_node([2u8; Address::SIZE].into())?
436            .sign_and_encode_to_eip2718(1, 1, None, &cp)
437            .await?;
438
439        let (action, signer) =
440            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
441        assert_eq!(
442            action,
443            ParsedHoprChainAction::RegisterSafeAddress([2u8; Address::SIZE].into())
444        );
445        assert_eq!(signer, cp.public().to_address());
446
447        let safe_gen = SafePayloadGenerator::new(&cp, *CONTRACT_ADDRS, [1u8; Address::SIZE].into());
448        let signed_tx = safe_gen
449            .register_safe_by_node([2u8; Address::SIZE].into())?
450            .sign_and_encode_to_eip2718(1, 1, None, &cp)
451            .await?;
452
453        let (action, signer) =
454            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
455        assert_eq!(
456            action,
457            ParsedHoprChainAction::RegisterSafeAddress([2u8; Address::SIZE].into())
458        );
459        assert_eq!(signer, cp.public().to_address());
460
461        Ok(())
462    }
463
464    #[tokio::test]
465    async fn redeem_ticket_safe_action_should_decode() -> anyhow::Result<()> {
466        let cp_1 = ChainKeypair::from_secret(&PRIVATE_KEY_1)?;
467        let cp_2 = ChainKeypair::from_secret(&PRIVATE_KEY_2)?;
468        let hk1 = HalfKey::random();
469        let hk2 = HalfKey::random();
470        let resp = Response::from_half_keys(&hk1, &hk2)?;
471
472        let ticket = TicketBuilder::default()
473            .counterparty(&cp_2)
474            .amount(123_u32)
475            .index(7)
476            .challenge(resp.to_challenge()?)
477            .build_signed(&cp_1, &Hash::default())?
478            .into_acknowledged(resp)
479            .into_redeemable(&cp_2, &Hash::default())?;
480
481        let basic_gen = BasicPayloadGenerator::new(cp_2.public().to_address(), *CONTRACT_ADDRS);
482        let signed_tx = basic_gen
483            .redeem_ticket(ticket.clone())?
484            .sign_and_encode_to_eip2718(1, 1, None, &cp_2)
485            .await?;
486
487        let (action, signer) =
488            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
489        assert_eq!(
490            action,
491            ParsedHoprChainAction::RedeemTicket {
492                channel_id: generate_channel_id(&cp_1.public().to_address(), &cp_2.public().to_address()),
493                ticket_index: 7,
494                ticket_amount: 123_u32.into()
495            }
496        );
497        assert_eq!(signer, cp_2.public().to_address());
498
499        let safe_gen = SafePayloadGenerator::new(&cp_2, *CONTRACT_ADDRS, [1u8; Address::SIZE].into());
500        let signed_tx = safe_gen
501            .redeem_ticket(ticket.clone())?
502            .sign_and_encode_to_eip2718(1, 1, None, &cp_2)
503            .await?;
504
505        let (action, signer) =
506            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
507        assert_eq!(
508            action,
509            ParsedHoprChainAction::RedeemTicket {
510                channel_id: generate_channel_id(&cp_1.public().to_address(), &cp_2.public().to_address()),
511                ticket_index: 7,
512                ticket_amount: 123_u32.into()
513            }
514        );
515        assert_eq!(signer, cp_2.public().to_address());
516
517        Ok(())
518    }
519
520    #[tokio::test]
521    async fn withdraw_native_action_should_decode() -> anyhow::Result<()> {
522        let cp = ChainKeypair::from_secret(&PRIVATE_KEY_1)?;
523
524        let basic_gen = BasicPayloadGenerator::new(cp.public().to_address(), *CONTRACT_ADDRS);
525        let signed_tx = basic_gen
526            .transfer::<XDai>([2u8; Address::SIZE].into(), 123_u32.into())?
527            .sign_and_encode_to_eip2718(1, 1, None, &cp)
528            .await?;
529
530        let (action, signer) =
531            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
532        assert_eq!(
533            action,
534            ParsedHoprChainAction::WithdrawNative([2u8; Address::SIZE].into(), 123_u32.into())
535        );
536        assert_eq!(signer, cp.public().to_address());
537
538        let safe_gen = SafePayloadGenerator::new(&cp, *CONTRACT_ADDRS, [1u8; Address::SIZE].into());
539        let signed_tx = safe_gen
540            .transfer::<XDai>([2u8; Address::SIZE].into(), 123_u32.into())?
541            .sign_and_encode_to_eip2718(1, 1, None, &cp)
542            .await?;
543
544        let (action, signer) =
545            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
546        assert_eq!(
547            action,
548            ParsedHoprChainAction::WithdrawNative([2u8; Address::SIZE].into(), 123_u32.into())
549        );
550        assert_eq!(signer, cp.public().to_address());
551
552        Ok(())
553    }
554
555    #[tokio::test]
556    async fn withdraw_token_safe_action_should_decode() -> anyhow::Result<()> {
557        let cp = ChainKeypair::from_secret(&PRIVATE_KEY_1)?;
558
559        let basic_gen = BasicPayloadGenerator::new(cp.public().to_address(), *CONTRACT_ADDRS);
560        let signed_tx = basic_gen
561            .transfer::<WxHOPR>([2u8; Address::SIZE].into(), 123_u32.into())?
562            .sign_and_encode_to_eip2718(1, 1, None, &cp)
563            .await?;
564
565        let (action, signer) =
566            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
567        assert_eq!(
568            action,
569            ParsedHoprChainAction::WithdrawToken([2u8; Address::SIZE].into(), 123_u32.into())
570        );
571        assert_eq!(signer, cp.public().to_address());
572
573        let safe_gen = SafePayloadGenerator::new(&cp, *CONTRACT_ADDRS, [1u8; Address::SIZE].into());
574        let signed_tx = safe_gen
575            .transfer::<WxHOPR>([2u8; Address::SIZE].into(), 123_u32.into())?
576            .sign_and_encode_to_eip2718(1, 1, None, &cp)
577            .await?;
578
579        let (action, signer) =
580            ParsedHoprChainAction::parse_from_eip2718(&signed_tx, &[1u8; Address::SIZE].into(), &CONTRACT_ADDRS)?;
581        assert_eq!(
582            action,
583            ParsedHoprChainAction::WithdrawToken([2u8; Address::SIZE].into(), 123_u32.into())
584        );
585        assert_eq!(signer, cp.public().to_address());
586
587        Ok(())
588    }
589}