hopr_chain_types/
utils.rs

1//! Chain utilities used for testing.
2//!
3//! This used in unit and integration tests.
4
5#![allow(clippy::too_many_arguments)]
6
7use std::str::FromStr;
8
9use SafeContract::SafeContractInstance;
10use alloy::{
11    contract::{Error as ContractError, Result as ContractResult},
12    network::{ReceiptResponse, TransactionBuilder},
13    primitives::{self, Bytes, U256, address, aliases, keccak256},
14    rpc::types::TransactionRequest,
15    signers::{Signer, local::PrivateKeySigner},
16    sol,
17    sol_types::{SolCall, SolValue},
18};
19use hopr_bindings::{
20    hoprchannels::HoprChannels::HoprChannelsInstance,
21    hoprnodemanagementmodule::HoprNodeManagementModule,
22    hoprnodestakefactory::HoprNodeStakeFactory,
23    hoprtoken::HoprToken::{self, HoprTokenInstance},
24};
25use hopr_crypto_types::prelude::*;
26use hopr_primitive_types::primitives::Address;
27use tracing::debug;
28
29use crate::{
30    ContractInstances, constants,
31    errors::{ChainTypesError, Result as ChainTypesResult},
32};
33
34// define basic safe abi
35sol!(
36    #![sol(abi)]
37    #![sol(rpc)]
38    // #[allow(dead_code)]
39    contract SafeContract {
40        function nonce() view returns (uint256);
41        function getTransactionHash( address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 _nonce) public view returns (bytes32);
42        function execTransaction(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes memory signatures) public returns (bool);
43    }
44);
45
46lazy_static::lazy_static! {
47    static ref MINTER_ROLE_VALUE: primitives::FixedBytes<32> = keccak256("MINTER_ROLE");
48}
49
50/// Creates local Anvil instance.
51///
52/// Used for testing. When block time is given, new blocks are mined periodically.
53/// Otherwise, a new block is mined per transaction.
54///
55/// Uses a fixed mnemonic to make generated accounts deterministic.
56pub fn create_anvil(block_time: Option<std::time::Duration>) -> alloy::node_bindings::AnvilInstance {
57    let mut anvil = alloy::node_bindings::Anvil::new()
58        .mnemonic("gentle wisdom move brush express similar canal dune emotion series because parrot");
59
60    if let Some(bt) = block_time {
61        anvil = anvil.block_time(bt.as_secs());
62    }
63
64    anvil.spawn()
65}
66
67/// Mints specified amount of HOPR tokens to the contract deployer wallet.
68/// Assumes that the `hopr_token` contract is associated with a RPC client that also deployed the contract.
69/// Returns the block number at which the minting transaction was confirmed.
70pub async fn mint_tokens<P, N>(hopr_token: HoprTokenInstance<P, N>, amount: U256) -> ContractResult<Option<u64>>
71where
72    P: alloy::contract::private::Provider<N>,
73    N: alloy::providers::Network,
74{
75    let deployer = hopr_token
76        .provider()
77        .get_accounts()
78        .await
79        .expect("client must have a signer")[0];
80
81    hopr_token
82        .grantRole(*MINTER_ROLE_VALUE, deployer)
83        .send()
84        .await?
85        .watch()
86        .await?;
87
88    let tx_receipt = hopr_token
89        .mint(deployer, amount, Bytes::new(), Bytes::new())
90        .send()
91        .await?
92        .get_receipt()
93        .await?;
94
95    Ok(tx_receipt.block_number())
96}
97
98/// Creates a transaction that transfers the given `amount` of native tokens to the
99/// given destination.
100pub fn create_native_transfer<N>(to: Address, amount: U256) -> N::TransactionRequest
101where
102    N: alloy::providers::Network,
103{
104    N::TransactionRequest::default().with_to(to.into()).with_value(amount)
105}
106
107/// Funds the given wallet address with specified amount of native tokens and HOPR tokens.
108/// These must be present in the client's wallet.
109pub async fn fund_node<P, N>(
110    node: Address,
111    native_token: U256,
112    hopr_token: U256,
113    hopr_token_contract: HoprTokenInstance<P, N>,
114) -> ContractResult<()>
115where
116    P: alloy::contract::private::Provider<N>,
117    N: alloy::providers::Network,
118{
119    let native_transfer_tx = N::TransactionRequest::default()
120        .with_to(node.into())
121        .with_value(native_token);
122
123    // let native_transfer_tx = Eip1559TransactionRequest::new()
124    //     .to(NameOrAddress::Address(node.into()))
125    //     .value(native_token);
126
127    let provider = hopr_token_contract.provider();
128
129    provider.send_transaction(native_transfer_tx).await?.watch().await?;
130
131    hopr_token_contract
132        .transfer(node.into(), hopr_token)
133        .send()
134        .await?
135        .watch()
136        .await?;
137    Ok(())
138}
139
140/// Funds the channel to the counterparty with the given amount of HOPR tokens.
141/// The amount must be present in the wallet of the client.
142pub async fn fund_channel<P, N>(
143    counterparty: Address,
144    hopr_token: HoprTokenInstance<P, N>,
145    hopr_channels: HoprChannelsInstance<P, N>,
146    amount: U256,
147) -> ContractResult<()>
148where
149    P: alloy::contract::private::Provider<N>,
150    N: alloy::providers::Network,
151{
152    hopr_token
153        .approve(*hopr_channels.address(), amount)
154        .send()
155        .await?
156        .watch()
157        .await?;
158
159    hopr_channels
160        .fundChannel(counterparty.into(), aliases::U96::from(amount))
161        .send()
162        .await?
163        .watch()
164        .await?;
165
166    Ok(())
167}
168
169/// Funds the channel to the counterparty with the given amount of HOPR tokens, from a different client
170/// The amount must be present in the wallet of the client.
171pub async fn fund_channel_from_different_client<P, N>(
172    counterparty: Address,
173    hopr_token_address: Address,
174    hopr_channels_address: Address,
175    amount: U256,
176    new_client: P,
177) -> ContractResult<()>
178where
179    P: alloy::contract::private::Provider<N> + Clone,
180    N: alloy::providers::Network,
181{
182    let hopr_token_with_new_client: HoprTokenInstance<P, N> =
183        HoprTokenInstance::new(hopr_token_address.into(), new_client.clone());
184    let hopr_channels_with_new_client = HoprChannelsInstance::new(hopr_channels_address.into(), new_client.clone());
185    hopr_token_with_new_client
186        .approve(hopr_channels_address.into(), amount)
187        .send()
188        .await?
189        .watch()
190        .await?;
191
192    hopr_channels_with_new_client
193        .fundChannel(counterparty.into(), aliases::U96::from(amount))
194        .send()
195        .await?
196        .watch()
197        .await?;
198
199    Ok(())
200}
201
202/// Prepare a safe transaction
203pub async fn get_safe_tx<P, N>(
204    safe_contract: SafeContractInstance<P, N>,
205    target: Address,
206    inner_tx_data: Bytes,
207    wallet: PrivateKeySigner,
208) -> ChainTypesResult<N::TransactionRequest>
209where
210    P: alloy::contract::private::Provider<N>,
211    N: alloy::providers::Network,
212{
213    let nonce = safe_contract.nonce().call().await?;
214
215    let data_hash = safe_contract
216        .getTransactionHash(
217            target.into(),
218            U256::ZERO,
219            inner_tx_data.clone(),
220            0,
221            U256::ZERO,
222            U256::ZERO,
223            U256::ZERO,
224            primitives::Address::default(),
225            wallet.address(),
226            nonce,
227        )
228        .call()
229        .await?;
230
231    let signed_data_hash = wallet.sign_hash(&data_hash).await?;
232
233    let safe_tx_data = SafeContract::execTransactionCall {
234        to: target.into(),
235        value: U256::ZERO,
236        data: inner_tx_data,
237        operation: 0,
238        safeTxGas: U256::ZERO,
239        baseGas: U256::ZERO,
240        gasPrice: U256::ZERO,
241        gasToken: primitives::Address::default(),
242        refundReceiver: wallet.address(),
243        signatures: Bytes::from(signed_data_hash.as_bytes()),
244    }
245    .abi_encode();
246
247    // Outer tx payload: execute as safe tx
248    let safe_tx = N::TransactionRequest::default()
249        .with_to(*safe_contract.address())
250        .with_input(safe_tx_data);
251
252    Ok(safe_tx)
253}
254
255/// Send a Safe transaction to the module to include node to the module
256pub async fn include_node_to_module_by_safe<P, N>(
257    provider: P,
258    safe_address: Address,
259    module_address: Address,
260    node_address: Address,
261    deployer: &ChainKeypair, // also node address
262) -> Result<(), ChainTypesError>
263where
264    P: alloy::contract::private::Provider<N> + Clone,
265    N: alloy::providers::Network,
266{
267    // prepare default permission for node.
268    // - Clearance: Function
269    // - TargetType: SEND
270    // - TargetPermission: allow all
271    // - NodeDefaultPermission: None
272    let node_target_permission = format!("{node_address:?}010203000000000000000000");
273
274    // Inner tx payload: include node to the module
275    let inner_tx_data = HoprNodeManagementModule::includeNodeCall {
276        nodeDefaultTarget: U256::from_str(&node_target_permission).unwrap(),
277    }
278    .abi_encode();
279
280    let safe_contract = SafeContract::new(safe_address.into(), provider.clone());
281    let wallet = PrivateKeySigner::from_slice(deployer.secret().as_ref()).expect("failed to construct wallet");
282    let safe_tx = get_safe_tx(safe_contract, module_address, inner_tx_data.into(), wallet).await?;
283
284    provider
285        .send_transaction(safe_tx)
286        .await
287        .map_err(|e| ChainTypesError::ContractError(e.into()))?
288        .watch()
289        .await
290        .map_err(|e| ChainTypesError::ContractError(e.into()))?;
291
292    Ok(())
293}
294
295/// Send a Safe transaction to the module to include annoucement to the module
296pub async fn add_announcement_as_target<P, N>(
297    provider: P,
298    safe_address: Address,
299    module_address: Address,
300    announcement_contract_address: Address,
301    deployer: &ChainKeypair, // also node address
302) -> ContractResult<()>
303where
304    P: alloy::contract::private::Provider<N> + Clone,
305    N: alloy::providers::Network,
306{
307    // prepare default permission for announcement.
308    // - Clearance: Function
309    // - TargetType: TOKEN
310    // - TargetPermission: allow all
311    // - NodeDefaultPermission: None
312    let announcement_target_permission = format!("{announcement_contract_address:?}010003000000000000000000");
313
314    // Inner tx payload: include node to the module
315    let inner_tx_data = HoprNodeManagementModule::scopeTargetTokenCall {
316        defaultTarget: U256::from_str(&announcement_target_permission).unwrap(),
317    }
318    .abi_encode();
319
320    let safe_contract = SafeContract::new(safe_address.into(), provider.clone());
321    let wallet = PrivateKeySigner::from_slice(deployer.secret().as_ref()).expect("failed to construct wallet");
322    let safe_tx = get_safe_tx(safe_contract, module_address, inner_tx_data.into(), wallet)
323        .await
324        .unwrap();
325
326    provider.send_transaction(safe_tx).await?.watch().await?;
327
328    Ok(())
329}
330
331/// Send a Safe transaction to the token contract, to approve channels on behalf of safe.
332pub async fn approve_channel_transfer_from_safe<P, N>(
333    provider: P,
334    safe_address: Address,
335    token_address: Address,
336    channel_address: Address,
337    deployer: &ChainKeypair, // also node address
338) -> ContractResult<()>
339where
340    P: alloy::contract::private::Provider<N> + Clone,
341    N: alloy::providers::Network,
342{
343    // Inner tx payload: include node to the module
344    let inner_tx_data = HoprToken::approveCall {
345        spender: channel_address.into(),
346        value: U256::MAX,
347    }
348    .abi_encode();
349
350    let safe_contract = SafeContract::new(safe_address.into(), provider.clone());
351    let wallet = PrivateKeySigner::from_slice(deployer.secret().as_ref()).expect("failed to construct wallet");
352    let safe_tx = get_safe_tx(safe_contract, token_address, inner_tx_data.into(), wallet)
353        .await
354        .unwrap();
355
356    provider.send_transaction(safe_tx).await?.watch().await?;
357
358    Ok(())
359}
360
361/// Deploy a safe instance and a module instance.
362///
363/// Notice that to complete the on-boarding process,
364/// 1) node should be included to the module
365/// 2) announcement contract should be a target in the module
366///
367/// Notice that to be able to open channels, the deployed safe should have HOPR tokens and approve token transfer for
368/// Channels contract on the token contract.
369///
370/// Returns (module address, safe address)
371pub async fn deploy_one_safe_one_module_and_setup_for_testing<P>(
372    instances: &ContractInstances<P>,
373    provider: P,
374    deployer: &ChainKeypair,
375) -> ContractResult<(Address, Address)>
376where
377    P: alloy::providers::Provider + Clone,
378{
379    // Get deployer address
380    let self_address: Address = deployer.public().to_address();
381
382    // Check if safe suite has been deployed. If so, skip this step
383    let code = provider
384        .get_code_at(address!("914d7Fec6aaC8cd542e72Bca78B30650d45643d7"))
385        .await?;
386
387    // only deploy contracts when needed
388    if code.is_empty() {
389        debug!("deploying safe code");
390        // Deploy Safe diamond deployment proxy singleton
391        let safe_diamond_proxy_address = {
392            // Fund Safe singleton deployer 0.01 anvil-eth and deploy Safe singleton
393            let tx = TransactionRequest::default()
394                .with_to(address!("E1CB04A0fA36DdD16a06ea828007E35e1a3cBC37"))
395                .with_value(U256::from(10000000000000000u128));
396
397            provider.send_transaction(tx).await?.watch().await?;
398
399            let tx_receipt = provider
400                .send_raw_transaction(&constants::SAFE_DIAMOND_PROXY_SINGLETON_DEPLOY_CODE)
401                .await?
402                .get_receipt()
403                .await?;
404            tx_receipt.contract_address().unwrap()
405        };
406        debug!(%safe_diamond_proxy_address, "Safe diamond proxy singleton");
407
408        // Deploy minimum Safe suite
409        {
410            // 1. Safe proxy factory deploySafeProxyFactory();
411            let _tx_safe_proxy_factory = TransactionRequest::default()
412                .with_to(safe_diamond_proxy_address)
413                .with_input(constants::SAFE_PROXY_FACTORY_DEPLOY_CODE);
414
415            // 2. Handler: only CompatibilityFallbackHandler and omit TokenCallbackHandler as it's not used now
416            let _tx_safe_compatibility_fallback_handler = TransactionRequest::default()
417                .with_to(safe_diamond_proxy_address)
418                .with_input(constants::SAFE_COMPATIBILITY_FALLBACK_HANDLER_DEPLOY_CODE);
419            // 3. Library: only MultiSendCallOnly and omit MultiSendCall
420            let _tx_safe_multisend_call_only = TransactionRequest::default()
421                .with_to(safe_diamond_proxy_address)
422                .with_input(constants::SAFE_MULTISEND_CALL_ONLY_DEPLOY_CODE);
423            // 4. Safe singleton deploySafe();
424            let _tx_safe_singleton = TransactionRequest::default()
425                .with_to(safe_diamond_proxy_address)
426                .with_input(constants::SAFE_SINGLETON_DEPLOY_CODE);
427            // other omitted libs: SimulateTxAccessor, CreateCall, and SignMessageLib
428            // broadcast those transactions
429            provider.send_transaction(_tx_safe_proxy_factory).await?.watch().await?;
430            provider
431                .send_transaction(_tx_safe_compatibility_fallback_handler)
432                .await?
433                .watch()
434                .await?;
435            provider
436                .send_transaction(_tx_safe_multisend_call_only)
437                .await?
438                .watch()
439                .await?;
440            provider.send_transaction(_tx_safe_singleton).await?.watch().await?;
441        }
442    }
443
444    // create a salt from the nonce
445    let curr_nonce = provider
446        .get_transaction_count(self_address.into())
447        .pending()
448        //  Some(BlockNumber::Pending.into()))
449        .await
450        .unwrap();
451    debug!(%curr_nonce, "curr_nonce");
452
453    // FIXME: Check if this is the correct way to get the nonce
454    let nonce =
455        keccak256((Into::<primitives::Address>::into(self_address), U256::from(curr_nonce)).abi_encode_packed());
456    let default_target = format!("{:?}010103020202020202020202", instances.channels.address());
457
458    debug!(%self_address, "self_address");
459    debug!("nonce {:?}", U256::from_be_bytes(nonce.0).to_string());
460    debug!("default_target in bytes {:?}", default_target.bytes());
461    debug!("default_target in u256 {:?}", U256::from_str(&default_target).unwrap());
462
463    let typed_tx = HoprNodeStakeFactory::cloneCall {
464        moduleSingletonAddress: *instances.module_implementation.address(),
465        admins: vec![self_address.into()],
466        nonce: nonce.into(),
467        defaultTarget: U256::from_str(&default_target).unwrap().into(),
468    }
469    .abi_encode();
470
471    debug!("typed_tx {:?}", typed_tx);
472
473    // deploy one safe and one module
474    let instance_deployment_tx_receipt = instances
475        .stake_factory
476        .clone(
477            *instances.module_implementation.address(),
478            vec![self_address.into()],
479            nonce.into(),
480            U256::from_str(&default_target).unwrap().into(),
481        )
482        .send()
483        .await?
484        .get_receipt()
485        .await?;
486
487    // decode logs
488    let maybe_module_tx_log =
489        instance_deployment_tx_receipt.decoded_log::<HoprNodeStakeFactory::NewHoprNodeStakeModule>();
490    let deployed_module_address: primitives::Address = if let Some(module_tx_log) = maybe_module_tx_log {
491        let HoprNodeStakeFactory::NewHoprNodeStakeModule { instance, .. } = module_tx_log.data;
492        instance
493    } else {
494        return Err(ContractError::ContractNotDeployed);
495    };
496
497    let maybe_safe_tx_log = instance_deployment_tx_receipt.decoded_log::<HoprNodeStakeFactory::NewHoprNodeStakeSafe>();
498    let deployed_safe_address: primitives::Address = if let Some(safe_tx_log) = maybe_safe_tx_log {
499        let HoprNodeStakeFactory::NewHoprNodeStakeSafe { instance } = safe_tx_log.data;
500        instance
501    } else {
502        return Err(ContractError::ContractNotDeployed);
503    };
504
505    debug!("instance_deployment_tx module instance {:?}", deployed_module_address);
506    debug!("instance_deployment_tx safe instance {:?}", deployed_safe_address);
507
508    Ok((deployed_module_address.into(), deployed_safe_address.into()))
509}