1use alloy::{
14 network::TransactionBuilder,
15 primitives::{
16 B256, U256,
17 aliases::{U24, U48, U56, U96},
18 },
19 rpc::types::TransactionRequest,
20 sol_types::SolCall,
21};
22use hopr_bindings::{
23 hoprannouncements::HoprAnnouncements::{
24 announceCall, announceSafeCall, bindKeysAnnounceCall, bindKeysAnnounceSafeCall,
25 },
26 hoprchannels::{
27 HoprChannels::{
28 RedeemableTicket as OnChainRedeemableTicket, TicketData, closeIncomingChannelCall,
29 closeIncomingChannelSafeCall, finalizeOutgoingChannelClosureCall, finalizeOutgoingChannelClosureSafeCall,
30 fundChannelCall, fundChannelSafeCall, initiateOutgoingChannelClosureCall,
31 initiateOutgoingChannelClosureSafeCall, redeemTicketCall, redeemTicketSafeCall,
32 },
33 HoprCrypto::{CompactSignature, VRFParameters},
34 },
35 hoprnodemanagementmodule::HoprNodeManagementModule::execTransactionFromModuleCall,
36 hoprnodesaferegistry::HoprNodeSafeRegistry::{deregisterNodeBySafeCall, registerSafeByNodeCall},
37 hoprtoken::HoprToken::{approveCall, transferCall},
38};
39use hopr_chain_types::ContractAddresses;
40use hopr_crypto_types::prelude::*;
41use hopr_internal_types::prelude::*;
42use hopr_primitive_types::prelude::*;
43
44use crate::errors::{
45 ChainActionsError::{InvalidArguments, InvalidState},
46 Result,
47};
48
49#[repr(u8)]
50#[derive(Copy, Clone, Debug, PartialEq, Eq)]
51enum Operation {
52 Call = 0,
53 }
55
56pub trait PayloadGenerator<T: Into<TransactionRequest>> {
58 fn approve(&self, spender: Address, amount: HoprBalance) -> Result<T>;
61
62 fn transfer<C: Currency>(&self, destination: Address, amount: Balance<C>) -> Result<T>;
64
65 fn announce(&self, announcement: AnnouncementData) -> Result<T>;
67
68 fn fund_channel(&self, dest: Address, amount: HoprBalance) -> Result<T>;
70
71 fn close_incoming_channel(&self, source: Address) -> Result<T>;
73
74 fn initiate_outgoing_channel_closure(&self, destination: Address) -> Result<T>;
78
79 fn finalize_outgoing_channel_closure(&self, destination: Address) -> Result<T>;
83
84 fn redeem_ticket(&self, acked_ticket: RedeemableTicket) -> Result<T>;
86
87 fn register_safe_by_node(&self, safe_addr: Address) -> Result<T>;
90
91 fn deregister_node_by_safe(&self) -> Result<T>;
94}
95
96fn channels_payload(hopr_channels: Address, call_data: Vec<u8>) -> Vec<u8> {
97 execTransactionFromModuleCall {
98 to: hopr_channels.into(),
99 value: U256::ZERO,
100 data: call_data.into(),
101 operation: Operation::Call as u8,
102 }
103 .abi_encode()
104}
105
106fn approve_tx(spender: Address, amount: HoprBalance) -> TransactionRequest {
107 TransactionRequest::default().with_input(
108 approveCall {
109 spender: spender.into(),
110 value: U256::from_be_bytes(amount.amount().to_be_bytes()),
111 }
112 .abi_encode(),
113 )
114}
115
116fn transfer_tx<C: Currency>(destination: Address, amount: Balance<C>) -> TransactionRequest {
117 let amount_u256 = U256::from_be_bytes(amount.amount().to_be_bytes());
118 let tx = TransactionRequest::default();
119 if WxHOPR::is::<C>() {
120 tx.with_input(
121 transferCall {
122 recipient: destination.into(),
123 amount: amount_u256,
124 }
125 .abi_encode(),
126 )
127 } else if XDai::is::<C>() {
128 tx.with_value(amount_u256)
129 } else {
130 unimplemented!("other currencies are currently not supported")
131 }
132}
133
134fn register_safe_tx(safe_addr: Address) -> TransactionRequest {
135 TransactionRequest::default().with_input(
136 registerSafeByNodeCall {
137 safeAddr: safe_addr.into(),
138 }
139 .abi_encode(),
140 )
141}
142
143#[derive(Debug, Clone)]
145pub struct BasicPayloadGenerator {
146 me: Address,
147 contract_addrs: ContractAddresses,
148}
149
150impl BasicPayloadGenerator {
151 pub fn new(me: Address, contract_addrs: ContractAddresses) -> Self {
152 Self { me, contract_addrs }
153 }
154}
155
156impl PayloadGenerator<TransactionRequest> for BasicPayloadGenerator {
157 fn approve(&self, spender: Address, amount: HoprBalance) -> Result<TransactionRequest> {
158 let tx = approve_tx(spender, amount).with_to(self.contract_addrs.token.into());
159 Ok(tx)
160 }
161
162 fn transfer<C: Currency>(&self, destination: Address, amount: Balance<C>) -> Result<TransactionRequest> {
163 let to = if XDai::is::<C>() {
164 destination
165 } else if WxHOPR::is::<C>() {
166 self.contract_addrs.token
167 } else {
168 return Err(InvalidArguments("invalid currency".into()));
169 };
170 let tx = transfer_tx(destination, amount).with_to(to.into());
171 Ok(tx)
172 }
173
174 fn announce(&self, announcement: AnnouncementData) -> Result<TransactionRequest> {
175 let payload = match &announcement.key_binding {
176 Some(binding) => {
177 let serialized_signature = binding.signature.as_ref();
178
179 bindKeysAnnounceCall {
180 ed25519_sig_0: B256::from_slice(&serialized_signature[0..32]),
181 ed25519_sig_1: B256::from_slice(&serialized_signature[32..64]),
182 ed25519_pub_key: B256::from_slice(binding.packet_key.as_ref()),
183 baseMultiaddr: announcement.multiaddress().to_string(),
184 }
185 .abi_encode()
186 }
187 None => announceCall {
188 baseMultiaddr: announcement.multiaddress().to_string(),
189 }
190 .abi_encode(),
191 };
192
193 let tx = TransactionRequest::default()
194 .with_input(payload)
195 .with_to(self.contract_addrs.announcements.into());
196 Ok(tx)
197 }
198
199 fn fund_channel(&self, dest: Address, amount: HoprBalance) -> Result<TransactionRequest> {
200 if dest.eq(&self.me) {
201 return Err(InvalidArguments("Cannot fund channel to self".into()));
202 }
203
204 let tx = TransactionRequest::default()
205 .with_input(
206 fundChannelCall {
207 account: dest.into(),
208 amount: U96::from_be_slice(&amount.amount().to_be_bytes()[32 - 12..]),
209 }
210 .abi_encode(),
211 )
212 .with_to(self.contract_addrs.channels.into());
213 Ok(tx)
214 }
215
216 fn close_incoming_channel(&self, source: Address) -> Result<TransactionRequest> {
217 if source.eq(&self.me) {
218 return Err(InvalidArguments("Cannot close incoming channel from self".into()));
219 }
220
221 let tx = TransactionRequest::default()
222 .with_input(closeIncomingChannelCall { source: source.into() }.abi_encode())
223 .with_to(self.contract_addrs.channels.into());
224 Ok(tx)
225 }
226
227 fn initiate_outgoing_channel_closure(&self, destination: Address) -> Result<TransactionRequest> {
228 if destination.eq(&self.me) {
229 return Err(InvalidArguments(
230 "Cannot initiate closure of incoming channel to self".into(),
231 ));
232 }
233
234 let tx = TransactionRequest::default()
235 .with_input(
236 initiateOutgoingChannelClosureCall {
237 destination: destination.into(),
238 }
239 .abi_encode(),
240 )
241 .with_to(self.contract_addrs.channels.into());
242 Ok(tx)
243 }
244
245 fn finalize_outgoing_channel_closure(&self, destination: Address) -> Result<TransactionRequest> {
246 if destination.eq(&self.me) {
247 return Err(InvalidArguments(
248 "Cannot initiate closure of incoming channel to self".into(),
249 ));
250 }
251
252 let tx = TransactionRequest::default()
253 .with_input(
254 finalizeOutgoingChannelClosureCall {
255 destination: destination.into(),
256 }
257 .abi_encode(),
258 )
259 .with_to(self.contract_addrs.channels.into());
260 Ok(tx)
261 }
262
263 fn redeem_ticket(&self, acked_ticket: RedeemableTicket) -> Result<TransactionRequest> {
264 let redeemable = convert_acknowledged_ticket(&acked_ticket)?;
265
266 let params = convert_vrf_parameters(
267 &acked_ticket.vrf_params,
268 &self.me,
269 acked_ticket.ticket.verified_hash(),
270 &acked_ticket.channel_dst,
271 );
272
273 let tx = TransactionRequest::default()
274 .with_input(redeemTicketCall { redeemable, params }.abi_encode())
275 .with_to(self.contract_addrs.channels.into());
276 Ok(tx)
277 }
278
279 fn register_safe_by_node(&self, safe_addr: Address) -> Result<TransactionRequest> {
280 let tx = register_safe_tx(safe_addr).with_to(self.contract_addrs.safe_registry.into());
281 Ok(tx)
282 }
283
284 fn deregister_node_by_safe(&self) -> Result<TransactionRequest> {
285 Err(InvalidState(
286 "Can only deregister an address if Safe is activated".into(),
287 ))
288 }
289}
290
291#[derive(Debug, Clone)]
293pub struct SafePayloadGenerator {
294 me: Address,
295 contract_addrs: ContractAddresses,
296 module: Address,
297}
298
299pub const DEFAULT_TX_GAS: u64 = 400_000;
300
301impl SafePayloadGenerator {
302 pub fn new(chain_keypair: &ChainKeypair, contract_addrs: ContractAddresses, module: Address) -> Self {
303 Self {
304 me: chain_keypair.into(),
305 contract_addrs,
306 module,
307 }
308 }
309}
310
311impl PayloadGenerator<TransactionRequest> for SafePayloadGenerator {
312 fn approve(&self, spender: Address, amount: HoprBalance) -> Result<TransactionRequest> {
313 let tx = approve_tx(spender, amount)
314 .with_to(self.contract_addrs.token.into())
315 .with_gas_limit(DEFAULT_TX_GAS);
316
317 Ok(tx)
318 }
319
320 fn transfer<C: Currency>(&self, destination: Address, amount: Balance<C>) -> Result<TransactionRequest> {
321 let to = if XDai::is::<C>() {
322 destination
323 } else if WxHOPR::is::<C>() {
324 self.contract_addrs.token
325 } else {
326 return Err(InvalidArguments("invalid currency".into()));
327 };
328 let tx = transfer_tx(destination, amount)
329 .with_to(to.into())
330 .with_gas_limit(DEFAULT_TX_GAS);
331
332 Ok(tx)
333 }
334
335 fn announce(&self, announcement: AnnouncementData) -> Result<TransactionRequest> {
336 let call_data = match &announcement.key_binding {
337 Some(binding) => {
338 let serialized_signature = binding.signature.as_ref();
339
340 bindKeysAnnounceSafeCall {
341 selfAddress: self.me.into(),
342 ed25519_sig_0: B256::from_slice(&serialized_signature[0..32]),
343 ed25519_sig_1: B256::from_slice(&serialized_signature[32..64]),
344 ed25519_pub_key: B256::from_slice(binding.packet_key.as_ref()),
345 baseMultiaddr: announcement.multiaddress().to_string(),
346 }
347 .abi_encode()
348 }
349 None => announceSafeCall {
350 selfAddress: self.me.into(),
351 baseMultiaddr: announcement.multiaddress().to_string(),
352 }
353 .abi_encode(),
354 };
355
356 let tx = TransactionRequest::default()
357 .with_input(
358 execTransactionFromModuleCall {
359 to: self.contract_addrs.announcements.into(),
360 value: U256::ZERO,
361 data: call_data.into(),
362 operation: Operation::Call as u8,
363 }
364 .abi_encode(),
365 )
366 .with_to(self.module.into())
367 .with_gas_limit(DEFAULT_TX_GAS);
368
369 Ok(tx)
370 }
371
372 fn fund_channel(&self, dest: Address, amount: HoprBalance) -> Result<TransactionRequest> {
373 if dest.eq(&self.me) {
374 return Err(InvalidArguments("Cannot fund channel to self".into()));
375 }
376
377 if amount.amount() > hopr_primitive_types::prelude::U256::from(ChannelEntry::MAX_CHANNEL_BALANCE) {
378 return Err(InvalidArguments(
379 "Cannot fund channel with amount larger than 96 bits".into(),
380 ));
381 }
382
383 let call_data = fundChannelSafeCall {
384 selfAddress: self.me.into(),
385 account: dest.into(),
386 amount: U96::from_be_slice(&amount.amount().to_be_bytes()[32 - 12..]),
387 }
388 .abi_encode();
389
390 let tx = TransactionRequest::default()
391 .with_input(channels_payload(self.contract_addrs.channels, call_data))
392 .with_to(self.module.into())
393 .with_gas_limit(DEFAULT_TX_GAS);
394
395 Ok(tx)
396 }
397
398 fn close_incoming_channel(&self, source: Address) -> Result<TransactionRequest> {
399 if source.eq(&self.me) {
400 return Err(InvalidArguments("Cannot close incoming channel from self".into()));
401 }
402
403 let call_data = closeIncomingChannelSafeCall {
404 selfAddress: self.me.into(),
405 source: source.into(),
406 }
407 .abi_encode();
408
409 let tx = TransactionRequest::default()
410 .with_input(channels_payload(self.contract_addrs.channels, call_data))
411 .with_to(self.module.into())
412 .with_gas_limit(DEFAULT_TX_GAS);
413
414 Ok(tx)
415 }
416
417 fn initiate_outgoing_channel_closure(&self, destination: Address) -> Result<TransactionRequest> {
418 if destination.eq(&self.me) {
419 return Err(InvalidArguments(
420 "Cannot initiate closure of incoming channel to self".into(),
421 ));
422 }
423
424 let call_data = initiateOutgoingChannelClosureSafeCall {
425 selfAddress: self.me.into(),
426 destination: destination.into(),
427 }
428 .abi_encode();
429
430 let tx = TransactionRequest::default()
431 .with_input(channels_payload(self.contract_addrs.channels, call_data))
432 .with_to(self.module.into())
433 .with_gas_limit(DEFAULT_TX_GAS);
434
435 Ok(tx)
436 }
437
438 fn finalize_outgoing_channel_closure(&self, destination: Address) -> Result<TransactionRequest> {
439 if destination.eq(&self.me) {
440 return Err(InvalidArguments(
441 "Cannot initiate closure of incoming channel to self".into(),
442 ));
443 }
444
445 let call_data = finalizeOutgoingChannelClosureSafeCall {
446 selfAddress: self.me.into(),
447 destination: destination.into(),
448 }
449 .abi_encode();
450
451 let tx = TransactionRequest::default()
452 .with_input(channels_payload(self.contract_addrs.channels, call_data))
453 .with_to(self.module.into())
454 .with_gas_limit(DEFAULT_TX_GAS);
455
456 Ok(tx)
457 }
458
459 fn redeem_ticket(&self, acked_ticket: RedeemableTicket) -> Result<TransactionRequest> {
460 let redeemable = convert_acknowledged_ticket(&acked_ticket)?;
461
462 let params = convert_vrf_parameters(
463 &acked_ticket.vrf_params,
464 &self.me,
465 acked_ticket.ticket.verified_hash(),
466 &acked_ticket.channel_dst,
467 );
468
469 let call_data = redeemTicketSafeCall {
470 selfAddress: self.me.into(),
471 redeemable,
472 params,
473 }
474 .abi_encode();
475
476 let tx = TransactionRequest::default()
477 .with_input(channels_payload(self.contract_addrs.channels, call_data))
478 .with_to(self.module.into())
479 .with_gas_limit(DEFAULT_TX_GAS);
480
481 Ok(tx)
482 }
483
484 fn register_safe_by_node(&self, safe_addr: Address) -> Result<TransactionRequest> {
485 let tx = register_safe_tx(safe_addr)
486 .with_to(self.contract_addrs.safe_registry.into())
487 .with_gas_limit(DEFAULT_TX_GAS);
488
489 Ok(tx)
490 }
491
492 fn deregister_node_by_safe(&self) -> Result<TransactionRequest> {
493 let tx = TransactionRequest::default()
494 .with_input(
495 deregisterNodeBySafeCall {
496 nodeAddr: self.me.into(),
497 }
498 .abi_encode(),
499 )
500 .with_to(self.module.into())
501 .with_gas_limit(DEFAULT_TX_GAS);
502
503 Ok(tx)
504 }
505}
506
507pub fn convert_vrf_parameters(
512 off_chain: &VrfParameters,
513 signer: &Address,
514 ticket_hash: &Hash,
515 domain_separator: &Hash,
516) -> VRFParameters {
517 let v = off_chain.V.as_uncompressed();
519 let s_b = off_chain
520 .get_s_b_witness(signer, &ticket_hash.into(), domain_separator.as_ref())
521 .expect("ticket hash exceeded hash2field boundaries or encoding to unsupported curve");
526
527 let h_v = off_chain.get_h_v_witness();
528
529 VRFParameters {
530 vx: U256::from_be_slice(&v.as_bytes()[1..33]),
531 vy: U256::from_be_slice(&v.as_bytes()[33..65]),
532 s: U256::from_be_slice(&off_chain.s.to_bytes()),
533 h: U256::from_be_slice(&off_chain.h.to_bytes()),
534 sBx: U256::from_be_slice(&s_b.as_bytes()[1..33]),
535 sBy: U256::from_be_slice(&s_b.as_bytes()[33..65]),
536 hVx: U256::from_be_slice(&h_v.as_bytes()[1..33]),
537 hVy: U256::from_be_slice(&h_v.as_bytes()[33..65]),
538 }
539}
540
541pub fn convert_acknowledged_ticket(off_chain: &RedeemableTicket) -> Result<OnChainRedeemableTicket> {
546 if let Some(ref signature) = off_chain.verified_ticket().signature {
547 let serialized_signature = signature.as_ref();
548
549 println!(
550 "off_chain.verified_ticket().amount.amount() {:?}",
551 off_chain.verified_ticket().amount.amount()
552 );
553
554 Ok(OnChainRedeemableTicket {
555 data: TicketData {
556 channelId: B256::from_slice(off_chain.verified_ticket().channel_id.as_ref()),
557 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..]),
559 indexOffset: off_chain.verified_ticket().index_offset,
560 epoch: U24::from_be_slice(&off_chain.verified_ticket().channel_epoch.to_be_bytes()[4 - 3..]),
561 winProb: U56::from_be_slice(&off_chain.verified_ticket().encoded_win_prob),
562 },
563 signature: CompactSignature {
564 r: B256::from_slice(&serialized_signature[0..32]),
565 vs: B256::from_slice(&serialized_signature[32..64]),
566 },
567 porSecret: U256::from_be_slice(off_chain.response.as_ref()),
568 })
569 } else {
570 Err(InvalidArguments("Acknowledged ticket must be signed".into()))
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use std::str::FromStr;
577
578 use alloy::{primitives::U256, providers::Provider};
579 use anyhow::Context;
580 use hex_literal::hex;
581 use hopr_chain_rpc::client::create_rpc_client_to_anvil;
582 use hopr_chain_types::ContractInstances;
583 use hopr_crypto_types::prelude::*;
584 use hopr_internal_types::prelude::*;
585 use hopr_primitive_types::prelude::HoprBalance;
586 use multiaddr::Multiaddr;
587
588 use super::{BasicPayloadGenerator, PayloadGenerator};
589
590 const PRIVATE_KEY: [u8; 32] = hex!("c14b8faa0a9b8a5fa4453664996f23a7e7de606d42297d723fc4a794f375e260");
591 const RESPONSE_TO_CHALLENGE: [u8; 32] = hex!("b58f99c83ae0e7dd6a69f755305b38c7610c7687d2931ff3f70103f8f92b90bb");
592
593 #[tokio::test]
594 async fn test_announce() -> anyhow::Result<()> {
595 let test_multiaddr = Multiaddr::from_str("/ip4/1.2.3.4/tcp/56")?;
596
597 let anvil = hopr_chain_types::utils::create_anvil(None);
598 let chain_key_0 = ChainKeypair::from_secret(anvil.keys()[0].to_bytes().as_ref())?;
599 let client = create_rpc_client_to_anvil(&anvil, &chain_key_0);
600
601 let contract_instances = ContractInstances::deploy_for_testing(client.clone(), &chain_key_0)
603 .await
604 .context("could not deploy contracts")?;
605
606 let generator = BasicPayloadGenerator::new((&chain_key_0).into(), (&contract_instances).into());
607
608 let ad = AnnouncementData::new(
609 test_multiaddr,
610 Some(KeyBinding::new(
611 (&chain_key_0).into(),
612 &OffchainKeypair::from_secret(&PRIVATE_KEY)?,
613 )),
614 )?;
615
616 let tx = generator.announce(ad)?;
617
618 assert!(client.send_transaction(tx).await?.get_receipt().await?.status());
619
620 let test_multiaddr_reannounce = Multiaddr::from_str("/ip4/5.6.7.8/tcp/99")?;
621
622 let ad_reannounce = AnnouncementData::new(test_multiaddr_reannounce, None)?;
623 let reannounce_tx = generator.announce(ad_reannounce)?;
624
625 assert!(
626 client
627 .send_transaction(reannounce_tx)
628 .await?
629 .get_receipt()
630 .await?
631 .status()
632 );
633
634 Ok(())
635 }
636
637 #[tokio::test]
638 async fn redeem_ticket() -> anyhow::Result<()> {
639 let anvil = hopr_chain_types::utils::create_anvil(None);
640 let chain_key_alice = ChainKeypair::from_secret(anvil.keys()[0].to_bytes().as_ref())?;
641 let chain_key_bob = ChainKeypair::from_secret(anvil.keys()[1].to_bytes().as_ref())?;
642 let client = create_rpc_client_to_anvil(&anvil, &chain_key_alice);
643
644 let contract_instances = ContractInstances::deploy_for_testing(client.clone(), &chain_key_alice).await?;
646
647 let _ = hopr_chain_types::utils::mint_tokens(contract_instances.token.clone(), U256::from(1000_u128)).await;
649
650 let domain_separator: Hash = contract_instances.channels.domainSeparator().call().await?.0.into();
651
652 let _ = hopr_chain_types::utils::fund_channel(
654 (&chain_key_bob).into(),
655 contract_instances.token.clone(),
656 contract_instances.channels.clone(),
657 U256::from(1_u128),
658 )
659 .await;
660
661 let _ = hopr_chain_types::utils::fund_node(
663 (&chain_key_bob).into(),
664 U256::from(1000000000000000000_u128),
665 U256::from(10_u128),
666 contract_instances.token.clone(),
667 )
668 .await;
669
670 let response = Response::try_from(RESPONSE_TO_CHALLENGE.as_ref())?;
671
672 let ticket = TicketBuilder::default()
674 .addresses(&chain_key_alice, &chain_key_bob)
675 .amount(1)
676 .index(1)
677 .index_offset(1)
678 .win_prob(1.0.try_into()?)
679 .channel_epoch(1)
680 .challenge(response.to_challenge().into())
681 .build_signed(&chain_key_alice, &domain_separator)?;
682
683 let acked_ticket = ticket
685 .into_acknowledged(response)
686 .into_redeemable(&chain_key_bob, &domain_separator)?;
687
688 let generator = BasicPayloadGenerator::new((&chain_key_bob).into(), (&contract_instances).into());
690 let redeem_ticket_tx = generator.redeem_ticket(acked_ticket)?;
691 let client = create_rpc_client_to_anvil(&anvil, &chain_key_bob);
692
693 assert!(
694 client
695 .send_transaction(redeem_ticket_tx)
696 .await?
697 .get_receipt()
698 .await?
699 .status()
700 );
701
702 Ok(())
703 }
704
705 #[tokio::test]
706 async fn withdraw_token() -> anyhow::Result<()> {
707 let anvil = hopr_chain_types::utils::create_anvil(None);
708 let chain_key_alice = ChainKeypair::from_secret(anvil.keys()[0].to_bytes().as_ref())?;
709 let chain_key_bob = ChainKeypair::from_secret(anvil.keys()[1].to_bytes().as_ref())?;
710 let client = create_rpc_client_to_anvil(&anvil, &chain_key_alice);
711
712 let contract_instances = ContractInstances::deploy_for_testing(client.clone(), &chain_key_alice).await?;
714 let generator = BasicPayloadGenerator::new((&chain_key_alice).into(), (&contract_instances).into());
715
716 let _ = hopr_chain_types::utils::mint_tokens(contract_instances.token.clone(), U256::from(1000_u128)).await;
718
719 let balance = contract_instances
721 .token
722 .balanceOf(hopr_primitive_types::primitives::Address::from(&chain_key_alice).into())
723 .call()
724 .await?;
725 assert_eq!(balance, U256::from(1000_u128));
726
727 let tx = generator.transfer((&chain_key_bob).into(), HoprBalance::from(100))?;
729
730 assert!(client.send_transaction(tx).await?.get_receipt().await?.status());
731
732 let balance = contract_instances
734 .token
735 .balanceOf(hopr_primitive_types::primitives::Address::from(&chain_key_alice).into())
736 .call()
737 .await?;
738 assert_eq!(balance, U256::from(900_u128));
739
740 Ok(())
741 }
742}