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