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
42fn a2h(a: hopr_primitive_types::prelude::Address) -> alloy::primitives::Address {
44 alloy::primitives::Address::from_slice(a.as_ref())
45}
46
47sol! {
48 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 }
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#[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 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(), }
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#[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 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
501fn convert_vrf_parameters(
506 off_chain: &VrfParameters,
507 signer: &Address,
508 ticket_hash: &Hash,
509 domain_separator: &Hash,
510) -> VRFParameters {
511 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 .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
535fn 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..]), 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 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 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}