extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use anyhow::{Context, Result, ensure};
use miden_agglayer::{
EthAddress,
EthAmount,
EthEmbeddedAccountId,
ExitRoot,
GlobalIndex,
Keccak256Output,
LeafData,
MetadataHash,
ProofData,
SmtNode,
};
use miden_client::utils::hex_to_bytes;
use miden_protocol::account::AccountId;
use serde::Deserialize;
fn deserialize_uint_to_string<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::String(s) => Ok(s),
serde_json::Value::Number(n) => Ok(n.to_string()),
_ => Err(serde::de::Error::custom("expected a number or string for amount")),
}
}
#[derive(Debug, Deserialize)]
pub struct LeafValueVector {
pub origin_network: u32,
pub origin_token_address: String,
pub destination_network: u32,
pub destination_address: String,
#[serde(deserialize_with = "deserialize_uint_to_string")]
pub amount: String,
pub metadata_hash: String,
#[allow(dead_code)]
pub leaf_value: String,
}
impl LeafValueVector {
pub fn to_leaf_data(&self) -> LeafData {
LeafData {
origin_network: self.origin_network,
origin_token_address: EthAddress::from_hex(&self.origin_token_address)
.expect("valid origin token address hex"),
destination_network: self.destination_network,
destination_address: EthAddress::from_hex(&self.destination_address)
.expect("valid destination address hex"),
amount: EthAmount::from_uint_str(&self.amount).expect("valid amount uint string"),
metadata_hash: MetadataHash::new(
hex_to_bytes(&self.metadata_hash).expect("valid metadata hash hex"),
),
}
}
}
#[derive(Debug, Deserialize)]
pub struct ProofValueVector {
pub smt_proof_local_exit_root: Vec<String>,
pub smt_proof_rollup_exit_root: Vec<String>,
pub global_index: String,
pub mainnet_exit_root: String,
pub rollup_exit_root: String,
#[allow(dead_code)]
pub global_exit_root: String,
}
impl ProofValueVector {
pub fn to_proof_data(&self) -> ProofData {
let smt_proof_local: [SmtNode; 32] = self
.smt_proof_local_exit_root
.iter()
.map(|s| SmtNode::new(hex_to_bytes(s).expect("valid smt proof hex")))
.collect::<Vec<_>>()
.try_into()
.expect("expected 32 SMT proof nodes for local exit root");
let smt_proof_rollup: [SmtNode; 32] = self
.smt_proof_rollup_exit_root
.iter()
.map(|s| SmtNode::new(hex_to_bytes(s).expect("valid smt proof hex")))
.collect::<Vec<_>>()
.try_into()
.expect("expected 32 SMT proof nodes for rollup exit root");
ProofData {
smt_proof_local_exit_root: smt_proof_local,
smt_proof_rollup_exit_root: smt_proof_rollup,
global_index: GlobalIndex::from_hex(&self.global_index)
.expect("valid global index hex"),
mainnet_exit_root: Keccak256Output::new(
hex_to_bytes(&self.mainnet_exit_root).expect("valid mainnet exit root hex"),
),
rollup_exit_root: Keccak256Output::new(
hex_to_bytes(&self.rollup_exit_root).expect("valid rollup exit root hex"),
),
}
}
}
#[derive(Debug, Deserialize)]
pub struct ClaimAssetVector {
#[serde(flatten)]
pub proof: ProofValueVector,
#[serde(flatten)]
pub leaf: LeafValueVector,
}
const MAX_DEPOSIT_OFFSET: u32 = 1000;
const FOUNDRY_PROJECT_SUBDIR: &str = "foundry-vectors";
const FOUNDRY_OUTPUT_JSON: &str = "test-vectors/claim_asset_vectors_local_tx.json";
pub fn generate_claim_data_for_account(
account_id: AccountId,
origin_token_address: Option<&EthAddress>,
) -> Result<(ProofData, LeafData, ExitRoot)> {
let destination_address: EthAddress = EthEmbeddedAccountId::from_account_id(account_id).into();
let destination_hex = destination_address.to_hex();
println!(
"[foundry] Generating claim data for account {:?} (eth address: {})",
account_id, destination_hex
);
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let foundry_dir = std::path::Path::new(manifest_dir).join(FOUNDRY_PROJECT_SUBDIR);
ensure!(
foundry_dir.join("foundry.toml").exists(),
"Foundry project not found at {}. Run `forge install` in that directory first.",
foundry_dir.display()
);
let output_dir = foundry_dir.join("test-vectors");
std::fs::create_dir_all(&output_dir).with_context(|| {
format!("failed to create test-vectors directory at {}", output_dir.display())
})?;
let deposit_offset: u32 = rand::random::<u32>() % MAX_DEPOSIT_OFFSET;
println!("[foundry] Using deposit offset: {}", deposit_offset);
let mut cmd = std::process::Command::new("forge");
cmd.arg("test")
.arg("-vv")
.arg("--match-contract")
.arg("ClaimAssetTestVectorsLocalTx")
.env("DESTINATION_ADDRESS", &destination_hex)
.env("DEPOSIT_OFFSET", deposit_offset.to_string())
.current_dir(&foundry_dir);
if let Some(addr) = origin_token_address {
let addr_hex = addr.to_hex();
println!("[foundry] Using origin token address: {}", addr_hex);
cmd.env("ORIGIN_TOKEN_ADDRESS", &addr_hex);
}
let output = cmd.output().context("failed to execute `forge test` - is foundry installed?")?;
ensure!(
output.status.success(),
"forge test failed!\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
println!(
"[foundry] forge test completed successfully:\n{}",
String::from_utf8_lossy(&output.stdout)
);
let json_path = foundry_dir.join(FOUNDRY_OUTPUT_JSON);
let json_content = std::fs::read_to_string(&json_path).with_context(|| {
format!("failed to read generated test vectors from {}", json_path.display())
})?;
let vector: ClaimAssetVector = serde_json::from_str(&json_content)
.context("failed to parse foundry-generated claim asset vectors JSON")?;
let ger = ExitRoot::new(
hex_to_bytes(&vector.proof.global_exit_root).context("invalid global exit root hex")?,
);
println!(
"[foundry] Claim data generated successfully for destination: {}",
destination_hex
);
Ok((vector.proof.to_proof_data(), vector.leaf.to_leaf_data(), ger))
}