1use aztec_core::types::{AztecAddress, Fr};
7use sha3::{Digest, Keccak256};
8
9#[derive(Clone, Debug)]
15pub struct L1ContractAddresses {
16 pub inbox: String,
17 pub outbox: String,
18 pub rollup: String,
19 pub fee_juice_portal: String,
20 pub fee_juice: Option<String>,
22 pub fee_asset_handler: Option<String>,
24}
25
26impl L1ContractAddresses {
27 pub fn from_json(json: &serde_json::Value) -> Option<Self> {
29 Some(Self {
30 inbox: json.get("inboxAddress")?.as_str()?.to_owned(),
31 outbox: json.get("outboxAddress")?.as_str()?.to_owned(),
32 rollup: json.get("rollupAddress")?.as_str()?.to_owned(),
33 fee_juice_portal: json.get("feeJuicePortalAddress")?.as_str()?.to_owned(),
34 fee_juice: json
35 .get("feeJuiceAddress")
36 .and_then(|v| v.as_str())
37 .map(|s| s.to_owned()),
38 fee_asset_handler: json
39 .get("feeAssetHandlerAddress")
40 .and_then(|v| v.as_str())
41 .map(|s| s.to_owned()),
42 })
43 }
44}
45
46#[derive(Clone, Debug)]
52pub struct L1ToL2MessageSentResult {
53 pub msg_hash: Fr,
55 pub global_leaf_index: Fr,
57 pub tx_hash: String,
59}
60
61pub struct EthClient {
67 url: String,
68 client: reqwest::Client,
69}
70
71impl EthClient {
72 pub fn new(url: &str) -> Self {
73 Self {
74 url: url.to_owned(),
75 client: reqwest::Client::new(),
76 }
77 }
78
79 pub fn default_url() -> String {
81 std::env::var("ETHEREUM_HOST").unwrap_or_else(|_| "http://localhost:8545".to_owned())
82 }
83
84 pub async fn rpc_call(
85 &self,
86 method: &str,
87 params: serde_json::Value,
88 ) -> Result<serde_json::Value, aztec_core::Error> {
89 let body = serde_json::json!({
90 "jsonrpc": "2.0",
91 "method": method,
92 "params": params,
93 "id": 1,
94 });
95 let resp = self
96 .client
97 .post(&self.url)
98 .json(&body)
99 .send()
100 .await
101 .map_err(|e| aztec_core::Error::InvalidData(format!("L1 RPC error: {e}")))?;
102
103 let json: serde_json::Value = resp
104 .json()
105 .await
106 .map_err(|e| aztec_core::Error::InvalidData(format!("L1 RPC parse error: {e}")))?;
107
108 if let Some(err) = json.get("error") {
109 return Err(aztec_core::Error::InvalidData(format!(
110 "L1 RPC error: {}",
111 err
112 )));
113 }
114
115 Ok(json["result"].clone())
116 }
117
118 pub async fn get_account(&self) -> Result<String, aztec_core::Error> {
120 let result = self.rpc_call("eth_accounts", serde_json::json!([])).await?;
121 result
122 .as_array()
123 .and_then(|a| a.first())
124 .and_then(|v| v.as_str())
125 .map(|s| s.to_owned())
126 .ok_or_else(|| aztec_core::Error::InvalidData("no L1 accounts available".into()))
127 }
128
129 pub async fn send_transaction(
131 &self,
132 to: &str,
133 data: &str,
134 from: &str,
135 ) -> Result<String, aztec_core::Error> {
136 let result = self
137 .rpc_call(
138 "eth_sendTransaction",
139 serde_json::json!([{
140 "from": from,
141 "to": to,
142 "data": data,
143 "gas": "0xf4240", }]),
145 )
146 .await?;
147
148 result
149 .as_str()
150 .map(|s| s.to_owned())
151 .ok_or_else(|| aztec_core::Error::InvalidData("no tx hash in response".into()))
152 }
153
154 pub async fn wait_for_receipt(
156 &self,
157 tx_hash: &str,
158 ) -> Result<serde_json::Value, aztec_core::Error> {
159 for _ in 0..60 {
160 let result = self
161 .rpc_call("eth_getTransactionReceipt", serde_json::json!([tx_hash]))
162 .await?;
163 if !result.is_null() {
164 return Ok(result);
165 }
166 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
167 }
168 Err(aztec_core::Error::Timeout(
169 "L1 tx receipt not available after 30s".into(),
170 ))
171 }
172}
173
174fn encode_send_l2_message(
183 recipient: &AztecAddress,
184 rollup_version: u64,
185 content: &Fr,
186 secret_hash: &Fr,
187) -> String {
188 let sig = b"sendL2Message((bytes32,uint256),bytes32,bytes32)";
190 let mut hasher = Keccak256::new();
191 hasher.update(sig);
192 let selector = &hasher.finalize()[..4];
193
194 let actor_bytes = recipient.0.to_be_bytes();
200 let mut version_bytes = [0u8; 32];
201 version_bytes[24..32].copy_from_slice(&rollup_version.to_be_bytes());
202 let content_bytes = content.to_be_bytes();
203 let secret_hash_bytes = secret_hash.to_be_bytes();
204
205 let mut calldata = Vec::with_capacity(4 + 128);
206 calldata.extend_from_slice(selector);
207 calldata.extend_from_slice(&actor_bytes);
208 calldata.extend_from_slice(&version_bytes);
209 calldata.extend_from_slice(&content_bytes);
210 calldata.extend_from_slice(&secret_hash_bytes);
211
212 format!("0x{}", hex::encode(&calldata))
213}
214
215pub async fn send_l1_to_l2_message(
219 eth_client: &EthClient,
220 inbox_address: &str,
221 recipient: &AztecAddress,
222 rollup_version: u64,
223 content: &Fr,
224 secret_hash: &Fr,
225) -> Result<L1ToL2MessageSentResult, aztec_core::Error> {
226 let from = eth_client.get_account().await?;
227 let calldata = encode_send_l2_message(recipient, rollup_version, content, secret_hash);
228
229 let tx_hash = eth_client
230 .send_transaction(inbox_address, &calldata, &from)
231 .await?;
232
233 let receipt = eth_client.wait_for_receipt(&tx_hash).await?;
234
235 let status = receipt
237 .get("status")
238 .and_then(|v| v.as_str())
239 .unwrap_or("0x0");
240 if status != "0x1" {
241 return Err(aztec_core::Error::InvalidData(format!(
242 "L1 tx {tx_hash} failed with status {status}"
243 )));
244 }
245
246 let event_sig = {
252 let mut hasher = Keccak256::new();
253 hasher.update(b"MessageSent(uint256,uint256,bytes32,bytes16)");
254 format!("0x{}", hex::encode(hasher.finalize()))
255 };
256
257 let logs = receipt
258 .get("logs")
259 .and_then(|v| v.as_array())
260 .ok_or_else(|| aztec_core::Error::InvalidData("no logs in L1 receipt".into()))?;
261
262 for log in logs {
263 let topics = log
264 .get("topics")
265 .and_then(|v| v.as_array())
266 .unwrap_or(&Vec::new())
267 .clone();
268 if topics.len() >= 3 {
269 let topic0 = topics[0].as_str().unwrap_or("");
270 if topic0 == event_sig {
271 let hash_hex = topics[2].as_str().unwrap_or("0x0");
274 let hash_bytes = hex::decode(hash_hex.strip_prefix("0x").unwrap_or(hash_hex))
275 .unwrap_or_default();
276
277 let mut hsh = [0u8; 32];
278 let start = 32usize.saturating_sub(hash_bytes.len());
279 hsh[start..].copy_from_slice(&hash_bytes);
280
281 let data_hex = log.get("data").and_then(|v| v.as_str()).unwrap_or("0x");
283 let data_bytes = hex::decode(data_hex.strip_prefix("0x").unwrap_or(data_hex))
284 .unwrap_or_default();
285 let mut idx = [0u8; 32];
286 if data_bytes.len() >= 32 {
287 idx.copy_from_slice(&data_bytes[..32]);
288 }
289
290 return Ok(L1ToL2MessageSentResult {
291 msg_hash: Fr::from(hsh),
292 global_leaf_index: Fr::from(idx),
293 tx_hash,
294 });
295 }
296 }
297 }
298
299 Err(aztec_core::Error::InvalidData(
300 "no MessageSent event found in L1 receipt".into(),
301 ))
302}
303
304fn keccak_selector(sig: &[u8]) -> [u8; 4] {
309 let mut hasher = Keccak256::new();
310 hasher.update(sig);
311 let hash = hasher.finalize();
312 let mut sel = [0u8; 4];
313 sel.copy_from_slice(&hash[..4]);
314 sel
315}
316
317fn encode_mint(to: &str) -> String {
319 let sel = keccak_selector(b"mint(address)");
320 let mut calldata = Vec::with_capacity(4 + 32);
321 calldata.extend_from_slice(&sel);
322 let addr_bytes = hex::decode(to.strip_prefix("0x").unwrap_or(to)).unwrap_or_default();
324 let mut padded = [0u8; 32];
325 let start = 32usize.saturating_sub(addr_bytes.len());
326 padded[start..].copy_from_slice(&addr_bytes);
327 calldata.extend_from_slice(&padded);
328 format!("0x{}", hex::encode(&calldata))
329}
330
331fn encode_approve(spender: &str, amount: u128) -> String {
333 let sel = keccak_selector(b"approve(address,uint256)");
334 let mut calldata = Vec::with_capacity(4 + 64);
335 calldata.extend_from_slice(&sel);
336 let addr_bytes = hex::decode(spender.strip_prefix("0x").unwrap_or(spender)).unwrap_or_default();
337 let mut padded = [0u8; 32];
338 let start = 32usize.saturating_sub(addr_bytes.len());
339 padded[start..].copy_from_slice(&addr_bytes);
340 calldata.extend_from_slice(&padded);
341 let mut amt = [0u8; 32];
342 amt[16..32].copy_from_slice(&amount.to_be_bytes());
343 calldata.extend_from_slice(&amt);
344 format!("0x{}", hex::encode(&calldata))
345}
346
347fn encode_deposit_to_aztec_public(to: &AztecAddress, amount: u128, secret_hash: &Fr) -> String {
349 let sel = keccak_selector(b"depositToAztecPublic(bytes32,uint256,bytes32)");
350 let mut calldata = Vec::with_capacity(4 + 96);
351 calldata.extend_from_slice(&sel);
352 calldata.extend_from_slice(&to.0.to_be_bytes());
353 let mut amt = [0u8; 32];
354 amt[16..32].copy_from_slice(&amount.to_be_bytes());
355 calldata.extend_from_slice(&amt);
356 calldata.extend_from_slice(&secret_hash.to_be_bytes());
357 format!("0x{}", hex::encode(&calldata))
358}
359
360fn encode_mint_amount() -> String {
365 let sel = keccak_selector(b"mintAmount()");
366 format!("0x{}", hex::encode(sel))
367}
368
369#[derive(Clone, Debug)]
371pub struct FeeJuiceBridgeResult {
372 pub claim_amount: u128,
374 pub claim_secret: Fr,
376 pub message_leaf_index: u64,
378 pub message_hash: Fr,
380}
381
382pub async fn prepare_fee_juice_on_l1(
393 eth_client: &EthClient,
394 l1_addresses: &L1ContractAddresses,
395 recipient: &AztecAddress,
396) -> Result<FeeJuiceBridgeResult, aztec_core::Error> {
397 let fee_juice_address = l1_addresses
398 .fee_juice
399 .as_deref()
400 .ok_or_else(|| aztec_core::Error::InvalidData("feeJuiceAddress not in node info".into()))?;
401 let handler_address = l1_addresses.fee_asset_handler.as_deref().ok_or_else(|| {
402 aztec_core::Error::InvalidData("feeAssetHandlerAddress not in node info".into())
403 })?;
404 let portal_address = &l1_addresses.fee_juice_portal;
405 let from = eth_client.get_account().await?;
406
407 let mint_amount = {
409 let data = encode_mint_amount();
410 let result = eth_client
411 .rpc_call(
412 "eth_call",
413 serde_json::json!([{ "to": handler_address, "data": data }, "latest"]),
414 )
415 .await
416 .map_err(|e| {
417 aztec_core::Error::InvalidData(format!("mintAmount() call failed: {e}"))
418 })?;
419 let hex_str = result.as_str().unwrap_or("0x0");
420 let bytes = hex::decode(hex_str.strip_prefix("0x").unwrap_or(hex_str)).unwrap_or_default();
421 if bytes.len() >= 32 {
422 let mut amt_bytes = [0u8; 16];
423 amt_bytes.copy_from_slice(&bytes[16..32]);
424 u128::from_be_bytes(amt_bytes)
425 } else {
426 return Err(aztec_core::Error::InvalidData(format!(
427 "mintAmount() returned invalid data ({} bytes)",
428 bytes.len()
429 )));
430 }
431 };
432
433 let mint_data = encode_mint(&from);
435 let tx_hash = eth_client
436 .send_transaction(handler_address, &mint_data, &from)
437 .await
438 .map_err(|e| aztec_core::Error::InvalidData(format!("mint tx send failed: {e}")))?;
439 let receipt = eth_client.wait_for_receipt(&tx_hash).await?;
440 let status = receipt
441 .get("status")
442 .and_then(|v| v.as_str())
443 .unwrap_or("0x0");
444 if status != "0x1" {
445 return Err(aztec_core::Error::InvalidData(format!(
446 "mint tx reverted: {status}"
447 )));
448 }
449
450 let approve_data = encode_approve(portal_address, mint_amount);
452 let tx_hash = eth_client
453 .send_transaction(fee_juice_address, &approve_data, &from)
454 .await
455 .map_err(|e| aztec_core::Error::InvalidData(format!("approve tx send failed: {e}")))?;
456 let receipt = eth_client.wait_for_receipt(&tx_hash).await?;
457 let status = receipt
458 .get("status")
459 .and_then(|v| v.as_str())
460 .unwrap_or("0x0");
461 if status != "0x1" {
462 return Err(aztec_core::Error::InvalidData(format!(
463 "approve tx reverted: {status}"
464 )));
465 }
466
467 let (claim_secret, claim_secret_hash) = super::messaging::generate_claim_secret();
469
470 let deposit_data = encode_deposit_to_aztec_public(recipient, mint_amount, &claim_secret_hash);
472 let tx_hash = eth_client
473 .send_transaction(portal_address, &deposit_data, &from)
474 .await
475 .map_err(|e| {
476 aztec_core::Error::InvalidData(format!("depositToAztecPublic tx send failed: {e}"))
477 })?;
478 let receipt = eth_client.wait_for_receipt(&tx_hash).await?;
479 let status = receipt
480 .get("status")
481 .and_then(|v| v.as_str())
482 .unwrap_or("0x0");
483 if status != "0x1" {
484 return Err(aztec_core::Error::InvalidData(format!(
485 "depositToAztecPublic tx reverted: {status}"
486 )));
487 }
488
489 let message_sent_sig = {
502 let mut hasher = Keccak256::new();
503 hasher.update(b"MessageSent(uint256,uint256,bytes32,bytes16)");
504 format!("0x{}", hex::encode(hasher.finalize()))
505 };
506
507 let logs = receipt
508 .get("logs")
509 .and_then(|v| v.as_array())
510 .ok_or_else(|| {
511 aztec_core::Error::InvalidData("no logs in depositToAztecPublic receipt".into())
512 })?;
513
514 for log in logs {
515 let topics = log
516 .get("topics")
517 .and_then(|v| v.as_array())
518 .unwrap_or(&Vec::new())
519 .clone();
520 if topics.len() >= 3 && topics[0].as_str().unwrap_or("") == message_sent_sig {
521 let hash_hex = topics[2].as_str().unwrap_or("0x0");
523 let hash_bytes =
524 hex::decode(hash_hex.strip_prefix("0x").unwrap_or(hash_hex)).unwrap_or_default();
525 let mut msg_hash = [0u8; 32];
526 let start = 32usize.saturating_sub(hash_bytes.len());
527 msg_hash[start..].copy_from_slice(&hash_bytes);
528
529 let data_hex = log.get("data").and_then(|v| v.as_str()).unwrap_or("0x");
531 let data_bytes =
532 hex::decode(data_hex.strip_prefix("0x").unwrap_or(data_hex)).unwrap_or_default();
533 let message_leaf_index = if data_bytes.len() >= 32 {
534 let mut idx = [0u8; 8];
535 idx.copy_from_slice(&data_bytes[24..32]);
536 u64::from_be_bytes(idx)
537 } else {
538 0
539 };
540
541 return Ok(FeeJuiceBridgeResult {
542 claim_amount: mint_amount,
543 claim_secret,
544 message_leaf_index,
545 message_hash: Fr::from(msg_hash),
546 });
547 }
548 }
549
550 Err(aztec_core::Error::InvalidData(
551 "no MessageSent event found in deposit receipt".into(),
552 ))
553}