Skip to main content

miden_client_integration_tests/tests/agglayer/
mod.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::{Context, Result};
4use miden_agglayer::create_bridge_account;
5use miden_client::Deserializable;
6use miden_client::account::{AccountFile, AccountId, AccountType};
7use miden_client::auth::RPO_FALCON_SCHEME_ID;
8use miden_client::crypto::FeltRng;
9use miden_client::keystore::Keystore;
10use miden_client::testing::common::{
11    FilesystemKeyStore,
12    TestClient,
13    insert_new_wallet,
14    wait_for_node,
15    wait_for_tx,
16};
17use miden_client::transaction::TransactionRequestBuilder;
18
19use crate::tests::config::ClientConfig;
20
21pub mod agglayer_bridge_in_out;
22mod agglayer_test_utils;
23pub mod ger;
24
25/// `AggLayer` network ID assigned to the Miden chain (the protocol's `MIDEN_NETWORK_ID` MASM
26/// constant). Claim validation compares the leaf's `destination_network` to this value, so it
27/// must match the `MIDEN_NETWORK_ID` constant in the foundry vectors
28/// (`foundry-vectors/test/ClaimAssetTestVectorsLocalTx.t.sol`).
29pub const MIDEN_AGGLAYER_NETWORK_ID: u32 = 77;
30
31// AGGLAYER CONFIG
32// ================================================================================================
33
34/// Configuration for agglayer tests when running against a node with pre-deployed
35/// agglayer accounts (e.g. complete genesis or devnet).
36///
37/// Loaded from `.mac` files in the directory specified by `AGGLAYER_ACCOUNTS_DIR` env var.
38/// Account IDs and keys are read from files, but the actual account state is fetched
39/// from the network to ensure it's up-to-date (idempotent across repeated runs).
40pub struct AgglayerConfig {
41    pub bridge_admin: AccountFile,
42    pub ger_manager: AccountFile,
43    pub bridge: AccountFile,
44    pub faucet: AccountFile,
45}
46
47impl AgglayerConfig {
48    /// File names matching the gen-genesis output (see the test-node-genesis crate).
49    const BRIDGE_ADMIN_FILE: &str = "bridge_admin.mac";
50    const GER_MANAGER_FILE: &str = "ger_manager.mac";
51    const BRIDGE_FILE: &str = "bridge.mac";
52    const FAUCET_FILE: &str = "agglayer_faucet.mac";
53
54    /// Tries to load agglayer config from the `AGGLAYER_ACCOUNTS_DIR` env var.
55    /// Returns `None` if the env var is not set.
56    pub fn from_env() -> Result<Option<Self>> {
57        match std::env::var("AGGLAYER_ACCOUNTS_DIR") {
58            Ok(dir) => {
59                let dir = PathBuf::from(dir);
60                let bridge_admin = Self::load_account_file(&dir, Self::BRIDGE_ADMIN_FILE)?;
61                let ger_manager = Self::load_account_file(&dir, Self::GER_MANAGER_FILE)?;
62                let bridge = Self::load_account_file(&dir, Self::BRIDGE_FILE)?;
63                let faucet = Self::load_account_file(&dir, Self::FAUCET_FILE)?;
64                Ok(Some(Self {
65                    bridge_admin,
66                    ger_manager,
67                    bridge,
68                    faucet,
69                }))
70            },
71            Err(_) => Ok(None),
72        }
73    }
74
75    pub fn bridge_admin_id(&self) -> AccountId {
76        self.bridge_admin.account.id()
77    }
78
79    pub fn ger_manager_id(&self) -> AccountId {
80        self.ger_manager.account.id()
81    }
82
83    pub fn bridge_id(&self) -> AccountId {
84        self.bridge.account.id()
85    }
86
87    pub fn faucet_id(&self) -> AccountId {
88        self.faucet.account.id()
89    }
90
91    /// Imports a single account (by ID) into the given client and keystore.
92    /// Fetches the latest state from the network. Adds any matching secret keys.
93    pub async fn import_account(
94        &self,
95        account_id: AccountId,
96        client: &mut TestClient,
97        keystore: &FilesystemKeyStore,
98    ) -> Result<()> {
99        let account_file = [&self.bridge_admin, &self.ger_manager, &self.bridge, &self.faucet]
100            .into_iter()
101            .find(|f| f.account.id() == account_id)
102            .with_context(|| format!("account {account_id} not found in agglayer config"))?;
103
104        client
105            .import_account_by_id(account_id)
106            .await
107            .with_context(|| format!("failed to import account {account_id} from network"))?;
108
109        for secret_key in &account_file.auth_secret_keys {
110            keystore.add_key(secret_key, account_id).await.with_context(|| {
111                format!("failed to add key for account {account_id} to keystore")
112            })?;
113        }
114        Ok(())
115    }
116
117    fn load_account_file(dir: &Path, filename: &str) -> Result<AccountFile> {
118        let path = dir.join(filename);
119        let bytes =
120            std::fs::read(&path).with_context(|| format!("failed to read {}", path.display()))?;
121        AccountFile::read_from_bytes(&bytes)
122            .map_err(|e| anyhow::anyhow!("failed to deserialize {}: {}", path.display(), e))
123    }
124}
125
126// SHARED TEST SETUP
127// ================================================================================================
128
129/// A client + keystore pair for a single test entity.
130pub struct ClientPair {
131    pub client: TestClient,
132    pub keystore: FilesystemKeyStore,
133}
134
135/// Account IDs produced by the core setup: `(bridge_admin_id, ger_manager_id, bridge_id)`.
136pub type CoreAccountIds = (AccountId, AccountId, AccountId);
137
138/// Creates three clients sharing the same RPC endpoint, for bridge admin, GER manager, and user.
139pub async fn create_agglayer_clients(
140    client_config: &ClientConfig,
141) -> Result<(ClientPair, ClientPair, ClientPair)> {
142    let (mut client, keystore) = client_config.clone().into_client().await?;
143    wait_for_node(&mut client).await;
144    client.sync_state().await?;
145    println!("[setup] Bridge admin client initialized");
146    let bridge_admin = ClientPair { client, keystore };
147
148    let (client, keystore) = ClientConfig::default()
149        .with_rpc_endpoint(client_config.rpc_endpoint())
150        .into_client()
151        .await?;
152    println!("[setup] GER manager client initialized");
153    let ger_manager = ClientPair { client, keystore };
154
155    let (client, keystore) = ClientConfig::default()
156        .with_rpc_endpoint(client_config.rpc_endpoint())
157        .into_client()
158        .await?;
159    println!("[setup] User client initialized");
160    let user = ClientPair { client, keystore };
161
162    Ok((bridge_admin, ger_manager, user))
163}
164
165/// Sets up the core agglayer accounts (bridge admin, GER manager, bridge) across 3 clients.
166///
167/// Two modes:
168/// - **Genesis** (`config` is `Some`, i.e. `AGGLAYER_ACCOUNTS_DIR` is set): the bridge admin, GER
169///   manager and bridge are pre-deployed at genesis and imported from `.mac` files.
170/// - **Runtime** (`config` is `None`): the bridge admin and GER manager wallets are created on
171///   their clients, and the bridge (`AuthNetworkAccount`) is created and deployed within the test;
172///   the faucet is registered against it later via the `CONFIG_AGG_BRIDGE` note.
173pub async fn setup_core_accounts(
174    config: Option<&AgglayerConfig>,
175    bridge_admin: &mut ClientPair,
176    ger_manager: &mut ClientPair,
177    user: &mut ClientPair,
178) -> Result<CoreAccountIds> {
179    if let Some(config) = config {
180        println!("[setup] Loading core accounts from genesis");
181        println!("[setup]   bridge admin:  {}", config.bridge_admin_id());
182        println!("[setup]   GER manager:   {}", config.ger_manager_id());
183        println!("[setup]   bridge:        {}", config.bridge_id());
184
185        config
186            .import_account(
187                config.bridge_admin_id(),
188                &mut bridge_admin.client,
189                &bridge_admin.keystore,
190            )
191            .await?;
192        config
193            .import_account(config.ger_manager_id(), &mut ger_manager.client, &ger_manager.keystore)
194            .await?;
195
196        for pair in [&mut *bridge_admin, &mut *ger_manager, &mut *user] {
197            config
198                .import_account(config.bridge_id(), &mut pair.client, &pair.keystore)
199                .await?;
200        }
201
202        return Ok((config.bridge_admin_id(), config.ger_manager_id(), config.bridge_id()));
203    }
204
205    println!("[setup] Creating core accounts at runtime");
206
207    // Bridge admin and GER manager are ordinary wallets, created on their own clients.
208    let (bridge_admin_account, ..) = insert_new_wallet(
209        &mut bridge_admin.client,
210        AccountType::Public,
211        &bridge_admin.keystore,
212        RPO_FALCON_SCHEME_ID,
213    )
214    .await?;
215    let (ger_manager_account, ..) = insert_new_wallet(
216        &mut ger_manager.client,
217        AccountType::Public,
218        &ger_manager.keystore,
219        RPO_FALCON_SCHEME_ID,
220    )
221    .await?;
222
223    // The bridge is an `AuthNetworkAccount`. Create it (unconfigured) and distribute it to all
224    // three clients so each can build transactions that reference it.
225    let bridge_seed = bridge_admin.client.rng().draw_word();
226    let bridge_account = create_bridge_account(
227        bridge_seed,
228        bridge_admin_account.id(),
229        ger_manager_account.id(),
230        MIDEN_AGGLAYER_NETWORK_ID,
231    );
232    println!("[setup]   bridge admin:  {}", bridge_admin_account.id());
233    println!("[setup]   GER manager:   {}", ger_manager_account.id());
234    println!("[setup]   bridge:        {}", bridge_account.id());
235
236    for pair in [&mut *bridge_admin, &mut *ger_manager, &mut *user] {
237        pair.client.add_account(&bridge_account, false).await?;
238    }
239
240    // Deploy the bridge account.
241    let deploy_tx = TransactionRequestBuilder::new().build()?;
242    let tx_id = bridge_admin
243        .client
244        .submit_new_transaction(bridge_account.id(), deploy_tx)
245        .await?;
246    wait_for_tx(&mut bridge_admin.client, tx_id).await?;
247    println!("[setup] Bridge account deployed on-chain");
248
249    Ok((bridge_admin_account.id(), ger_manager_account.id(), bridge_account.id()))
250}