1use ethabi::{decode, ParamType, Token};
4use sha3::{Digest, Keccak256};
5use thiserror::Error;
6
7pub fn note_added_topic0_hex() -> String {
9 format!(
10 "0x{}",
11 hex::encode(Keccak256::digest(
12 b"NoteAdded(bytes32,bytes,bytes,bytes32,bytes32,bytes32)"
13 ))
14 )
15}
16
17pub fn note_added_legacy_topic0_hex() -> String {
19 format!(
20 "0x{}",
21 hex::encode(Keccak256::digest(
22 b"NoteAdded(bytes32,bytes,bytes32,bytes32)"
23 ))
24 )
25}
26
27pub fn note_added_topic0_alternatives() -> Vec<String> {
29 vec![
30 note_added_topic0_hex(),
31 note_added_legacy_topic0_hex(),
32 ]
33}
34
35pub fn note_confirmed_topic0_hex() -> String {
37 format!(
38 "0x{}",
39 hex::encode(Keccak256::digest(b"NoteConfirmed(bytes32,bytes32,uint256)"))
40 )
41}
42
43pub fn shield_completed_topic0_hex() -> String {
45 format!(
46 "0x{}",
47 hex::encode(Keccak256::digest(b"ShieldCompleted(bytes32,uint256)"))
48 )
49}
50
51pub fn perc20_created_topic0_hex() -> String {
53 format!(
54 "0x{}",
55 hex::encode(Keccak256::digest(b"Perc20Created(address,address,string,string,uint8)"))
56 )
57}
58
59pub fn shielded_topic0_hex() -> String {
61 format!("0x{}", hex::encode(Keccak256::digest(b"Shielded(address,uint256,uint256)")))
62}
63
64pub fn unshielded_topic0_hex() -> String {
66 format!("0x{}", hex::encode(Keccak256::digest(b"Unshielded(address,uint256,uint256)")))
67}
68
69pub fn wrapped_created_topic0_hex() -> String {
71 format!(
72 "0x{}",
73 hex::encode(Keccak256::digest(
74 b"WrappedCreated(address,address,uint256,string,string,uint8)"
75 ))
76 )
77}
78
79pub fn wrapped_deployed_topic0_hex() -> String {
81 format!(
82 "0x{}",
83 hex::encode(Keccak256::digest(b"WrappedDeployed(address,address,address,uint256)"))
84 )
85}
86
87#[derive(Debug, Clone)]
91pub struct DecodedShielded {
92 pub actor: [u8; 20],
94 pub amount_units: u128,
96 pub wei_amount: u128,
98}
99
100#[derive(Debug, Clone)]
103pub struct DecodedWrappedCreated {
104 pub pool: [u8; 20],
105 pub underlying: [u8; 20],
106 pub scale: u128,
107 pub name: String,
108 pub symbol: String,
109 pub decimals: u8,
110}
111
112#[derive(Debug, Clone)]
113pub struct DecodedNoteAdded {
114 pub cmx: [u8; 32],
115 pub enc_ciphertext: Vec<u8>,
116 pub out_ciphertext: Vec<u8>,
118 pub epk: [u8; 32],
119 pub nf_old: [u8; 32],
120 pub cv_net_x: Option<[u8; 32]>,
122}
123
124#[derive(Debug, Error)]
125pub enum LogDecodeError {
126 #[error("invalid topics/data for NoteAdded")]
127 BadNoteAdded,
128 #[error("invalid topics/data for NoteConfirmed")]
129 BadNoteConfirmed,
130 #[error("invalid topics/data for ShieldCompleted")]
131 BadShieldCompleted,
132 #[error("invalid topics/data for Shielded/Unshielded")]
133 BadShielded,
134 #[error("invalid topics/data for WrappedCreated")]
135 BadWrappedCreated,
136 #[error("ethabi decode: {0}")]
137 EthAbi(String),
138}
139
140fn topic_to_address(topic: &str) -> Option<[u8; 20]> {
142 let b = topic_to_bytes32(topic)?;
143 Some(b[12..32].try_into().ok()?)
144}
145
146fn topic_to_bytes32(topic: &str) -> Option<[u8; 32]> {
147 let t = topic.strip_prefix("0x").unwrap_or(topic);
148 let b = hex::decode(t).ok()?;
149 if b.len() != 32 {
150 return None;
151 }
152 Some(b.try_into().ok()?)
153}
154
155fn decode_note_added_v2(data: &[u8]) -> Result<DecodedNoteAdded, LogDecodeError> {
156 let tokens = decode(
157 &[
158 ParamType::Bytes,
159 ParamType::Bytes,
160 ParamType::FixedBytes(32),
161 ParamType::FixedBytes(32),
162 ParamType::FixedBytes(32),
163 ],
164 data,
165 )
166 .map_err(|e| LogDecodeError::EthAbi(e.to_string()))?;
167 if tokens.len() != 5 {
168 return Err(LogDecodeError::BadNoteAdded);
169 }
170 let enc = match &tokens[0] {
171 Token::Bytes(b) => b.clone(),
172 _ => return Err(LogDecodeError::BadNoteAdded),
173 };
174 let out = match &tokens[1] {
175 Token::Bytes(b) => b.clone(),
176 _ => return Err(LogDecodeError::BadNoteAdded),
177 };
178 let epk = token_bytes32(&tokens[2])?;
179 let nf_old = token_bytes32(&tokens[3])?;
180 let cv_net_x = token_bytes32(&tokens[4])?;
181 Ok(DecodedNoteAdded {
182 cmx: [0u8; 32], enc_ciphertext: enc,
184 out_ciphertext: out,
185 epk,
186 nf_old,
187 cv_net_x: Some(cv_net_x),
188 })
189}
190
191fn decode_note_added_legacy(data: &[u8]) -> Result<DecodedNoteAdded, LogDecodeError> {
192 let tokens = decode(
193 &[
194 ParamType::Bytes,
195 ParamType::FixedBytes(32),
196 ParamType::FixedBytes(32),
197 ],
198 data,
199 )
200 .map_err(|e| LogDecodeError::EthAbi(e.to_string()))?;
201 if tokens.len() != 3 {
202 return Err(LogDecodeError::BadNoteAdded);
203 }
204 let enc = match &tokens[0] {
205 Token::Bytes(b) => b.clone(),
206 _ => return Err(LogDecodeError::BadNoteAdded),
207 };
208 let epk = token_bytes32(&tokens[1])?;
209 let nf_old = token_bytes32(&tokens[2])?;
210 Ok(DecodedNoteAdded {
211 cmx: [0u8; 32],
212 enc_ciphertext: enc,
213 out_ciphertext: Vec::new(),
214 epk,
215 nf_old,
216 cv_net_x: None,
217 })
218}
219
220pub fn decode_note_added_log(topics: &[String], data_hex: &str) -> Result<DecodedNoteAdded, LogDecodeError> {
225 let cmx = topics
226 .get(1)
227 .and_then(|t| topic_to_bytes32(t))
228 .ok_or(LogDecodeError::BadNoteAdded)?;
229 let raw = hex::decode(data_hex.strip_prefix("0x").unwrap_or(data_hex))
230 .map_err(|_| LogDecodeError::BadNoteAdded)?;
231
232 let norm = |s: &str| {
233 s.strip_prefix("0x")
234 .unwrap_or(s)
235 .to_ascii_lowercase()
236 };
237 let topic0 = topics.first().map(|s| norm(s));
238 let legacy = norm(¬e_added_legacy_topic0_hex());
239 let current = norm(¬e_added_topic0_hex());
240 let mut decoded = if topic0.as_deref() == Some(current.as_str()) {
241 decode_note_added_v2(&raw)?
242 } else if topic0.as_deref() == Some(legacy.as_str()) {
243 decode_note_added_v2(&raw).or_else(|_| decode_note_added_legacy(&raw))?
246 } else {
247 decode_note_added_v2(&raw).or_else(|_| decode_note_added_legacy(&raw))?
248 };
249 decoded.cmx = cmx;
250 Ok(decoded)
251}
252
253fn token_bytes32(t: &Token) -> Result<[u8; 32], LogDecodeError> {
254 match t {
255 Token::FixedBytes(b) if b.len() == 32 => Ok(b[..].try_into().unwrap()),
256 Token::Bytes(b) if b.len() == 32 => Ok(b[..].try_into().unwrap()),
257 _ => Err(LogDecodeError::BadNoteAdded),
258 }
259}
260
261pub fn decode_note_confirmed_log(topics: &[String], data_hex: &str) -> Result<([u8; 32], [u8; 32], u64), LogDecodeError> {
263 let cmx = topics
264 .get(1)
265 .and_then(|t| topic_to_bytes32(t))
266 .ok_or(LogDecodeError::BadNoteConfirmed)?;
267 let raw = hex::decode(data_hex.strip_prefix("0x").unwrap_or(data_hex))
268 .map_err(|_| LogDecodeError::BadNoteConfirmed)?;
269 let tokens = decode(&[ParamType::FixedBytes(32), ParamType::Uint(256)], &raw)
271 .map_err(|e| LogDecodeError::EthAbi(e.to_string()))?;
272 let root = token_bytes32_confirmed(tokens.first().ok_or(LogDecodeError::BadNoteConfirmed)?)?;
273 let position = match tokens.get(1) {
274 Some(Token::Uint(u)) => u64::try_from(*u).unwrap_or(u64::MAX),
275 _ => return Err(LogDecodeError::BadNoteConfirmed),
276 };
277 Ok((cmx, root, position))
278}
279
280fn token_bytes32_confirmed(t: &Token) -> Result<[u8; 32], LogDecodeError> {
281 match t {
282 Token::FixedBytes(b) if b.len() == 32 => Ok(b[..].try_into().unwrap()),
283 _ => Err(LogDecodeError::BadNoteConfirmed),
284 }
285}
286
287pub fn decode_shield_completed_log(topics: &[String], data_hex: &str) -> Result<([u8; 32], u128), LogDecodeError> {
288 let cmx = topics
289 .get(1)
290 .and_then(|t| topic_to_bytes32(t))
291 .ok_or(LogDecodeError::BadShieldCompleted)?;
292 let raw = hex::decode(data_hex.strip_prefix("0x").unwrap_or(data_hex))
293 .map_err(|_| LogDecodeError::BadShieldCompleted)?;
294 let tokens =
295 decode(&[ParamType::Uint(256)], &raw).map_err(|e| LogDecodeError::EthAbi(e.to_string()))?;
296 let amt = match tokens.get(0) {
297 Some(Token::Uint(u)) => u128::try_from(*u).map_err(|_| LogDecodeError::BadShieldCompleted)?,
298 _ => return Err(LogDecodeError::BadShieldCompleted),
299 };
300 Ok((cmx, amt))
301}
302
303fn decode_shielded_like(
304 topics: &[String],
305 data_hex: &str,
306) -> Result<DecodedShielded, LogDecodeError> {
307 let actor = topics
308 .get(1)
309 .and_then(|t| topic_to_address(t))
310 .ok_or(LogDecodeError::BadShielded)?;
311 let raw = hex::decode(data_hex.strip_prefix("0x").unwrap_or(data_hex))
312 .map_err(|_| LogDecodeError::BadShielded)?;
313 let tokens = decode(&[ParamType::Uint(256), ParamType::Uint(256)], &raw)
315 .map_err(|e| LogDecodeError::EthAbi(e.to_string()))?;
316 let amount_units = match tokens.first() {
317 Some(Token::Uint(u)) => u128::try_from(*u).map_err(|_| LogDecodeError::BadShielded)?,
318 _ => return Err(LogDecodeError::BadShielded),
319 };
320 let wei_amount = match tokens.get(1) {
321 Some(Token::Uint(u)) => u128::try_from(*u).map_err(|_| LogDecodeError::BadShielded)?,
322 _ => return Err(LogDecodeError::BadShielded),
323 };
324 Ok(DecodedShielded { actor, amount_units, wei_amount })
325}
326
327pub fn decode_shielded_log(topics: &[String], data_hex: &str) -> Result<DecodedShielded, LogDecodeError> {
329 decode_shielded_like(topics, data_hex)
330}
331
332pub fn decode_unshielded_log(topics: &[String], data_hex: &str) -> Result<DecodedShielded, LogDecodeError> {
334 decode_shielded_like(topics, data_hex)
335}
336
337pub fn decode_wrapped_created_log(
340 topics: &[String],
341 data_hex: &str,
342) -> Result<DecodedWrappedCreated, LogDecodeError> {
343 let pool = topics
344 .get(1)
345 .and_then(|t| topic_to_address(t))
346 .ok_or(LogDecodeError::BadWrappedCreated)?;
347 let underlying = topics
348 .get(2)
349 .and_then(|t| topic_to_address(t))
350 .ok_or(LogDecodeError::BadWrappedCreated)?;
351 let raw = hex::decode(data_hex.strip_prefix("0x").unwrap_or(data_hex))
352 .map_err(|_| LogDecodeError::BadWrappedCreated)?;
353 let tokens = decode(
354 &[
355 ParamType::Uint(256),
356 ParamType::String,
357 ParamType::String,
358 ParamType::Uint(8),
359 ],
360 &raw,
361 )
362 .map_err(|e| LogDecodeError::EthAbi(e.to_string()))?;
363 let scale = match tokens.first() {
364 Some(Token::Uint(u)) => u128::try_from(*u).map_err(|_| LogDecodeError::BadWrappedCreated)?,
365 _ => return Err(LogDecodeError::BadWrappedCreated),
366 };
367 let name = match &tokens[1] {
368 Token::String(s) => s.clone(),
369 _ => return Err(LogDecodeError::BadWrappedCreated),
370 };
371 let symbol = match &tokens[2] {
372 Token::String(s) => s.clone(),
373 _ => return Err(LogDecodeError::BadWrappedCreated),
374 };
375 let decimals = match tokens.get(3) {
376 Some(Token::Uint(u)) => u8::try_from(*u).map_err(|_| LogDecodeError::BadWrappedCreated)?,
377 _ => return Err(LogDecodeError::BadWrappedCreated),
378 };
379 Ok(DecodedWrappedCreated { pool, underlying, scale, name, symbol, decimals })
380}
381
382#[cfg(test)]
383mod tests {
384 use ethabi::{encode, Token};
385
386 use super::*;
387
388 #[test]
389 fn topic0_lengths() {
390 assert_eq!(
391 note_added_topic0_hex().len(),
392 2 + 64,
393 "topic0 is 32 bytes hex"
394 );
395 assert_ne!(
396 note_added_topic0_hex(),
397 note_added_legacy_topic0_hex(),
398 "new NoteAdded changes topic0"
399 );
400 }
401
402 #[test]
403 fn note_added_v2_roundtrip() {
404 let cmx = [0x11u8; 32];
405 let enc = vec![0xABu8; 580];
406 let out = vec![0xCDu8; 80];
407 let epk = [0x22u8; 32];
408 let nf = [0x33u8; 32];
409 let cv = [0x44u8; 32];
410 let data = encode(&[
411 Token::Bytes(enc.clone()),
412 Token::Bytes(out.clone()),
413 Token::FixedBytes(epk.to_vec()),
414 Token::FixedBytes(nf.to_vec()),
415 Token::FixedBytes(cv.to_vec()),
416 ]);
417 let topics = vec![
418 note_added_topic0_hex(),
419 format!("0x{}", hex::encode(cmx)),
420 ];
421 let decoded =
422 decode_note_added_log(&topics, &format!("0x{}", hex::encode(&data))).unwrap();
423 assert_eq!(decoded.cmx, cmx);
424 assert_eq!(decoded.enc_ciphertext, enc);
425 assert_eq!(decoded.out_ciphertext, out);
426 assert_eq!(decoded.epk, epk);
427 assert_eq!(decoded.nf_old, nf);
428 assert_eq!(decoded.cv_net_x, Some(cv));
429 }
430
431 #[test]
432 fn note_added_legacy_roundtrip() {
433 let cmx = [0x55u8; 32];
434 let enc = vec![0x01u8; 580];
435 let epk = [0x66u8; 32];
436 let nf = [0x77u8; 32];
437 let data = encode(&[
438 Token::Bytes(enc.clone()),
439 Token::FixedBytes(epk.to_vec()),
440 Token::FixedBytes(nf.to_vec()),
441 ]);
442 let topics = vec![
443 note_added_legacy_topic0_hex(),
444 format!("0x{}", hex::encode(cmx)),
445 ];
446 let decoded =
447 decode_note_added_log(&topics, &format!("0x{}", hex::encode(&data))).unwrap();
448 assert_eq!(decoded.cmx, cmx);
449 assert_eq!(decoded.enc_ciphertext, enc);
450 assert!(decoded.out_ciphertext.is_empty());
451 assert_eq!(decoded.cv_net_x, None);
452 }
453
454 #[test]
455 fn new_topic0s_are_distinct_and_well_formed() {
456 for t in [
457 shielded_topic0_hex(),
458 unshielded_topic0_hex(),
459 wrapped_created_topic0_hex(),
460 wrapped_deployed_topic0_hex(),
461 perc20_created_topic0_hex(),
462 ] {
463 assert_eq!(t.len(), 2 + 64);
464 }
465 assert_ne!(shielded_topic0_hex(), unshielded_topic0_hex());
466 assert_ne!(wrapped_created_topic0_hex(), wrapped_deployed_topic0_hex());
467 }
468
469 #[test]
470 fn shielded_roundtrip() {
471 let actor = [0xABu8; 20];
472 let mut actor_topic = [0u8; 32];
473 actor_topic[12..].copy_from_slice(&actor);
474 let data = encode(&[
475 Token::Uint(1_000u64.into()),
476 Token::Uint(1_000_000_000_000u64.into()),
477 ]);
478 let topics = vec![shielded_topic0_hex(), format!("0x{}", hex::encode(actor_topic))];
479 let d = decode_shielded_log(&topics, &format!("0x{}", hex::encode(&data))).unwrap();
480 assert_eq!(d.actor, actor);
481 assert_eq!(d.amount_units, 1_000);
482 assert_eq!(d.wei_amount, 1_000_000_000_000);
483 }
484
485 #[test]
486 fn wrapped_created_roundtrip() {
487 let pool = [0x11u8; 20];
488 let underlying = [0x22u8; 20];
489 let mut pool_t = [0u8; 32];
490 pool_t[12..].copy_from_slice(&pool);
491 let mut und_t = [0u8; 32];
492 und_t[12..].copy_from_slice(&underlying);
493 let data = encode(&[
494 Token::Uint(1_000_000u64.into()),
495 Token::String("Wrapped USDC".to_string()),
496 Token::String("wUSDC".to_string()),
497 Token::Uint(6u8.into()),
498 ]);
499 let topics = vec![
500 wrapped_created_topic0_hex(),
501 format!("0x{}", hex::encode(pool_t)),
502 format!("0x{}", hex::encode(und_t)),
503 ];
504 let d = decode_wrapped_created_log(&topics, &format!("0x{}", hex::encode(&data))).unwrap();
505 assert_eq!(d.pool, pool);
506 assert_eq!(d.underlying, underlying);
507 assert_eq!(d.scale, 1_000_000);
508 assert_eq!(d.name, "Wrapped USDC");
509 assert_eq!(d.symbol, "wUSDC");
510 assert_eq!(d.decimals, 6);
511 }
512
513 #[test]
514 fn note_added_legacy_topic_v2_body_roundtrip() {
515 let cmx = [0x88u8; 32];
516 let enc = vec![0xABu8; 580];
517 let out = vec![0xCDu8; 80];
518 let epk = [0x22u8; 32];
519 let nf = [0x33u8; 32];
520 let cv = [0x44u8; 32];
521 let data = encode(&[
522 Token::Bytes(enc.clone()),
523 Token::Bytes(out.clone()),
524 Token::FixedBytes(epk.to_vec()),
525 Token::FixedBytes(nf.to_vec()),
526 Token::FixedBytes(cv.to_vec()),
527 ]);
528 let topics = vec![
529 note_added_legacy_topic0_hex(),
530 format!("0x{}", hex::encode(cmx)),
531 ];
532 let decoded =
533 decode_note_added_log(&topics, &format!("0x{}", hex::encode(&data))).unwrap();
534 assert_eq!(decoded.out_ciphertext, out);
535 assert_eq!(decoded.cv_net_x, Some(cv));
536 }
537}