hopr_chain_types/
utils.rs

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