1use alloy_primitives::{keccak256, Address, B256, I256, U256};
12use serde::Deserialize;
13use std::collections::HashMap;
14use std::path::Path;
15
16#[derive(Debug, thiserror::Error)]
18pub enum LayoutError {
19 #[error("json: {0}")]
21 Json(String),
22 #[error("io: {0}")]
24 Io(#[from] std::io::Error),
25 #[error("artifact has no storageLayout (compile with `extra_output = [\"storageLayout\"]` / outputSelection)")]
27 NoStorageLayout,
28 #[error("unknown field `{0}`")]
30 UnknownField(String),
31 #[error("unknown type id `{0}` in layout")]
33 UnknownType(String),
34 #[error("`{path}` is a {what}; expected {expected}")]
36 Shape {
37 path: String,
39 what: &'static str,
41 expected: &'static str,
43 },
44 #[error("bad key `{0}` for {1}")]
46 BadKey(String, String),
47 #[error("bad path syntax near `{0}`")]
49 Syntax(String),
50 #[error("mapping keys of type {0} (dynamic) are not supported yet")]
52 DynamicKey(String),
53}
54
55pub type Result<T> = std::result::Result<T, LayoutError>;
57
58const MAX_NESTING: usize = 32;
61
62#[derive(Debug, Clone, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct StorageEntry {
66 pub label: String,
68 #[serde(deserialize_with = "de_u256_str")]
70 pub slot: U256,
71 pub offset: usize,
73 #[serde(rename = "type")]
75 pub type_id: String,
76}
77
78#[derive(Debug, Clone, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct TypeInfo {
82 pub encoding: Encoding,
84 pub label: String,
86 #[serde(deserialize_with = "de_usize_str")]
88 pub number_of_bytes: usize,
89 pub key: Option<String>,
91 pub value: Option<String>,
93 pub base: Option<String>,
95 pub members: Option<Vec<StorageEntry>>,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum Encoding {
103 Inplace,
105 Mapping,
107 DynamicArray,
109 Bytes,
111}
112
113#[derive(Debug, Clone, Deserialize)]
114struct RawLayout {
115 storage: Vec<StorageEntry>,
116 types: HashMap<String, TypeInfo>,
117}
118
119fn de_u256_str<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<U256, D::Error> {
120 let s = String::deserialize(d)?;
121 s.parse::<U256>().map_err(serde::de::Error::custom)
122}
123
124fn de_usize_str<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<usize, D::Error> {
125 let s = String::deserialize(d)?;
126 s.parse::<usize>().map_err(serde::de::Error::custom)
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct Location {
132 pub slot: B256,
134 pub offset: usize,
136 pub size: usize,
138 pub type_id: String,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum Value {
147 Uint(U256),
149 Int(I256),
151 Bool(bool),
153 Address(Address),
155 FixedBytes(Vec<u8>),
157 Raw(B256),
159}
160
161impl std::fmt::Display for Value {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 match self {
164 Value::Uint(u) => write!(f, "{u}"),
165 Value::Int(i) => write!(f, "{i}"),
166 Value::Bool(b) => write!(f, "{b}"),
167 Value::Address(a) => write!(f, "{a}"),
168 Value::FixedBytes(b) => write!(f, "0x{}", alloy_primitives::hex::encode(b)),
169 Value::Raw(w) => write!(f, "{w}"),
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum ValueKind {
177 Uint,
179 Int,
181 Bool,
183 Address,
185 Bytes,
187 Raw,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum PathKind {
194 Value(ValueKind),
196 Struct,
198 Mapping,
200 Array,
202 FixedArray,
204}
205
206fn value_kind(label: &str) -> ValueKind {
207 if label == "bool" {
208 ValueKind::Bool
209 } else if label == "address" || label == "address payable" || label.starts_with("contract ") {
210 ValueKind::Address
211 } else if label.starts_with("uint") || label.starts_with("enum ") {
212 ValueKind::Uint
213 } else if label.starts_with("int") {
214 ValueKind::Int
215 } else if label.starts_with("bytes") {
216 ValueKind::Bytes
217 } else {
218 ValueKind::Raw
219 }
220}
221
222pub struct Layout {
224 storage: Vec<StorageEntry>,
225 types: HashMap<String, TypeInfo>,
226}
227
228#[derive(Debug)]
229enum Seg {
230 Field(String),
231 Index(String),
232}
233
234fn parse_path(path: &str) -> Result<Vec<Seg>> {
235 let mut out = Vec::new();
236 let mut rest = path.trim();
237 if rest.is_empty() {
238 return Err(LayoutError::Syntax(path.into()));
239 }
240 let mut first = true;
241 while !rest.is_empty() {
242 if let Some(r) = rest.strip_prefix('[') {
243 let end = r
244 .find(']')
245 .ok_or_else(|| LayoutError::Syntax(rest.into()))?;
246 out.push(Seg::Index(r[..end].trim().to_string()));
247 rest = &r[end + 1..];
248 } else {
249 let r = if first {
250 rest
251 } else {
252 rest.strip_prefix('.')
253 .ok_or_else(|| LayoutError::Syntax(rest.into()))?
254 };
255 let end = r.find(['.', '[']).unwrap_or(r.len());
256 if end == 0 {
257 return Err(LayoutError::Syntax(rest.into()));
258 }
259 out.push(Seg::Field(r[..end].to_string()));
260 rest = &r[end..];
261 }
262 first = false;
263 }
264 Ok(out)
265}
266
267fn parse_key(key: &str, key_type: &str) -> Result<B256> {
270 let k = key.trim();
271 let bad = || LayoutError::BadKey(key.into(), key_type.into());
272 let u = if let Some(h) = k.strip_prefix("0x") {
273 U256::from_str_radix(h, 16).map_err(|_| bad())?
274 } else if k == "true" {
275 U256::from(1)
276 } else if k == "false" {
277 U256::ZERO
278 } else if let Some(n) = k.strip_prefix('-') {
279 let n: U256 = n.parse().map_err(|_| bad())?;
280 U256::ZERO.wrapping_sub(n)
281 } else {
282 k.parse::<U256>().map_err(|_| bad())?
283 };
284 Ok(B256::from(u.to_be_bytes::<32>()))
285}
286
287fn slot_b(u: U256) -> B256 {
288 B256::from(u.to_be_bytes::<32>())
289}
290
291impl Layout {
292 pub fn from_json(s: &str) -> Result<Self> {
295 let v: serde_json::Value =
296 serde_json::from_str(s).map_err(|e| LayoutError::Json(e.to_string()))?;
297 let raw = if v.get("storage").is_some() {
298 v
299 } else if let Some(l) = v.get("storageLayout") {
300 l.clone()
301 } else {
302 return Err(LayoutError::NoStorageLayout);
303 };
304 let raw: RawLayout =
305 serde_json::from_value(raw).map_err(|e| LayoutError::Json(e.to_string()))?;
306 Ok(Self {
307 storage: raw.storage,
308 types: raw.types,
309 })
310 }
311
312 pub fn from_artifact(path: impl AsRef<Path>) -> Result<Self> {
314 Self::from_json(&std::fs::read_to_string(path)?)
315 }
316
317 pub fn fields(&self) -> impl Iterator<Item = &StorageEntry> {
319 self.storage.iter()
320 }
321
322 fn ty(&self, id: &str) -> Result<&TypeInfo> {
323 self.types
324 .get(id)
325 .ok_or_else(|| LayoutError::UnknownType(id.into()))
326 }
327
328 pub fn locate(&self, path: &str) -> Result<Location> {
330 let segs = parse_path(path)?;
331 let Some(Seg::Field(name)) = segs.first() else {
332 return Err(LayoutError::Syntax(path.into()));
333 };
334 let top = self
335 .storage
336 .iter()
337 .find(|e| &e.label == name)
338 .ok_or_else(|| LayoutError::UnknownField(name.clone()))?;
339 let mut slot = top.slot;
340 let mut offset = top.offset;
341 let mut type_id = top.type_id.clone();
342 let mut walked = name.clone();
343
344 for seg in &segs[1..] {
345 let t = self.ty(&type_id)?;
346 match (t.encoding, seg, t.base.as_deref(), t.members.as_deref()) {
347 (Encoding::Mapping, Seg::Index(k), _, _) => {
348 let key_ty = t
349 .key
350 .as_deref()
351 .ok_or_else(|| LayoutError::UnknownType(type_id.clone()))?;
352 let kt = self.ty(key_ty)?;
353 if kt.encoding == Encoding::Bytes {
354 return Err(LayoutError::DynamicKey(kt.label.clone()));
355 }
356 let key = parse_key(k, &kt.label)?;
357 let mut buf = [0u8; 64];
358 buf[..32].copy_from_slice(key.as_slice());
359 buf[32..].copy_from_slice(&slot.to_be_bytes::<32>());
360 slot = U256::from_be_bytes(keccak256(buf).0);
361 offset = 0;
362 type_id = t
363 .value
364 .clone()
365 .ok_or_else(|| LayoutError::UnknownType(type_id.clone()))?;
366 walked = format!("{walked}[{k}]");
367 }
368 (Encoding::DynamicArray, Seg::Field(m), _, _) if m == "length" => {
369 return Ok(Location {
370 slot: slot_b(slot),
371 offset: 0,
372 size: 32,
373 type_id: "t_uint256".into(),
374 });
375 }
376 (Encoding::DynamicArray, Seg::Index(i), Some(base_ty), _) => {
377 let idx = U256::from_be_bytes(parse_key(i, "index")?.0);
378 let data = U256::from_be_bytes(keccak256(slot.to_be_bytes::<32>()).0);
379 let (s, o) = self.element_at(base_ty, data, idx)?;
380 slot = s;
381 offset = o;
382 type_id = base_ty.to_string();
383 walked = format!("{walked}[{i}]");
384 }
385 (Encoding::Inplace, Seg::Index(i), Some(base_ty), _) => {
386 let idx = U256::from_be_bytes(parse_key(i, "index")?.0);
388 let (s, o) = self.element_at(base_ty, slot, idx)?;
389 slot = s;
390 offset = o;
391 type_id = base_ty.to_string();
392 walked = format!("{walked}[{i}]");
393 }
394 (Encoding::Inplace, Seg::Field(m), _, Some(members)) => {
395 let member = members
396 .iter()
397 .find(|e| &e.label == m)
398 .ok_or_else(|| LayoutError::UnknownField(format!("{walked}.{m}")))?;
399 slot += member.slot;
400 offset = member.offset;
401 type_id = member.type_id.clone();
402 walked = format!("{walked}.{m}");
403 }
404 (enc, _, _, _) => {
405 let (what, expected) = match enc {
406 Encoding::Mapping => ("mapping", "[key]"),
407 Encoding::DynamicArray => ("dynamic array", "[index] or .length"),
408 Encoding::Bytes => ("bytes/string", "no further path"),
409 Encoding::Inplace => ("value", "no further path"),
410 };
411 return Err(LayoutError::Shape {
412 path: walked,
413 what,
414 expected,
415 });
416 }
417 }
418 }
419 let size = self.ty(&type_id)?.number_of_bytes;
420 Ok(Location {
421 slot: slot_b(slot),
422 offset,
423 size,
424 type_id,
425 })
426 }
427
428 fn element_at(&self, base_ty: &str, data: U256, idx: U256) -> Result<(U256, usize)> {
430 let size = self.ty(base_ty)?.number_of_bytes;
431 if size == 0 {
432 return Err(LayoutError::UnknownType(base_ty.into()));
433 }
434 if size >= 32 {
437 let per_elem = U256::from(size.div_ceil(32));
438 Ok((data.wrapping_add(idx.wrapping_mul(per_elem)), 0))
439 } else {
440 let per_slot = U256::from(32 / size);
441 let slot = data.wrapping_add(idx / per_slot);
442 let offset = (idx % per_slot).to::<usize>() * size;
443 Ok((slot, offset))
444 }
445 }
446
447 pub fn decode(&self, loc: &Location, word: B256) -> Value {
449 let size = loc.size.clamp(1, 32);
450 let end = 32usize.saturating_sub(loc.offset).max(1);
451 let start = end.saturating_sub(size);
452 let bytes = &word.0[start..end];
453 let Some(t) = self.types.get(&loc.type_id) else {
454 return Value::Raw(word);
455 };
456 let label = t.label.as_str();
457 if t.encoding != Encoding::Inplace || t.members.is_some() || t.base.is_some() {
458 return Value::Raw(word);
459 }
460 if label == "bool" {
461 return Value::Bool(bytes.iter().any(|b| *b != 0));
462 }
463 if label == "address" || label == "address payable" || label.starts_with("contract ") {
464 let n = bytes.len();
465 return Value::Address(Address::from_slice(&bytes[n.saturating_sub(20)..]));
466 }
467 if label.starts_with("uint") || label.starts_with("enum ") {
468 return Value::Uint(U256::from_be_slice(bytes));
469 }
470 if label.starts_with("int") {
471 let fill = if bytes[0] & 0x80 != 0 { 0xFF } else { 0x00 };
472 let mut w = [fill; 32];
473 w[32 - bytes.len()..].copy_from_slice(bytes);
474 return Value::Int(I256::from_be_bytes(w));
475 }
476 if label.starts_with("bytes") && size <= 32 {
477 return Value::FixedBytes(bytes.to_vec());
479 }
480 Value::Raw(word)
481 }
482
483 pub fn kind_of(&self, path: &str) -> Result<PathKind> {
486 let loc = self.locate(path)?;
487 if path.trim_end().ends_with(".length") {
488 return Ok(PathKind::Value(ValueKind::Uint));
489 }
490 let t = self.ty(&loc.type_id)?;
491 Ok(match (t.encoding, t.members.is_some(), t.base.is_some()) {
492 (Encoding::Mapping, _, _) => PathKind::Mapping,
493 (Encoding::DynamicArray, _, _) => PathKind::Array,
494 (Encoding::Inplace, true, _) => PathKind::Struct,
495 (Encoding::Inplace, false, true) => PathKind::FixedArray,
496 (Encoding::Bytes, _, _) => PathKind::Value(ValueKind::Raw),
497 (Encoding::Inplace, false, false) => PathKind::Value(value_kind(&t.label)),
498 })
499 }
500
501 pub fn decode_typed(&self, loc: &Location, word: B256) -> (ValueKind, Value) {
504 let v = self.decode(loc, word);
505 let k = match &v {
506 Value::Uint(_) => ValueKind::Uint,
507 Value::Int(_) => ValueKind::Int,
508 Value::Bool(_) => ValueKind::Bool,
509 Value::Address(_) => ValueKind::Address,
510 Value::FixedBytes(_) => ValueKind::Bytes,
511 Value::Raw(_) => ValueKind::Raw,
512 };
513 (k, v)
514 }
515
516 pub fn typescript(&self, name: &str) -> String {
521 let mut out = String::new();
522 out.push_str("// Generated by balq from a solc storageLayout. Do not edit.\n");
523 out.push_str(&format!("export interface {name} {{\n"));
524 for e in &self.storage {
525 out.push_str(&format!(
526 " readonly {}: {};\n",
527 e.label,
528 self.ts_type(&e.type_id, 1)
529 ));
530 }
531 out.push_str("}\n");
532 out
533 }
534
535 fn ts_type(&self, type_id: &str, depth: usize) -> String {
536 if depth > MAX_NESTING {
539 return "unknown".into();
540 }
541 let Ok(t) = self.ty(type_id) else {
542 return "string".into();
543 };
544 let pad = " ".repeat(depth + 1);
545 let close = " ".repeat(depth);
546 match (t.encoding, t.members.as_deref(), t.base.as_deref()) {
547 (Encoding::Mapping, _, _) => {
548 let v = t
549 .value
550 .as_deref()
551 .map(|v| self.ts_type(v, depth + 1))
552 .unwrap_or_else(|| "string".into());
553 format!("{{ readonly [key: string]: {v} }}")
554 }
555 (Encoding::DynamicArray, _, base) => {
556 let v = base
557 .map(|b| self.ts_type(b, depth + 1))
558 .unwrap_or_else(|| "string".into());
559 format!("{{ readonly [index: number]: {v}; readonly length: bigint }}")
560 }
561 (Encoding::Inplace, Some(members), _) => {
562 let mut s = String::from("{\n");
563 for m in members {
564 s.push_str(&format!(
565 "{pad}readonly {}: {};\n",
566 m.label,
567 self.ts_type(&m.type_id, depth + 1)
568 ));
569 }
570 s.push_str(&format!("{close}}}"));
571 s
572 }
573 (Encoding::Inplace, None, Some(base)) => {
574 format!(
575 "{{ readonly [index: number]: {} }}",
576 self.ts_type(base, depth + 1)
577 )
578 }
579 (Encoding::Bytes, _, _) => "string".into(),
580 (Encoding::Inplace, None, None) => match value_kind(&t.label) {
581 ValueKind::Uint | ValueKind::Int => "bigint".into(),
582 ValueKind::Bool => "boolean".into(),
583 ValueKind::Address | ValueKind::Bytes | ValueKind::Raw => "string".into(),
584 },
585 }
586 }
587
588 pub fn describe_slot(&self, slot: B256, array_probe: u64) -> Vec<(String, Location)> {
592 let target = U256::from_be_bytes(slot.0);
593 let mut out = Vec::new();
594 for e in &self.storage {
595 self.describe_in(
596 &e.label,
597 e.slot,
598 e.offset,
599 &e.type_id,
600 target,
601 array_probe,
602 &mut out,
603 );
604 }
605 out
606 }
607
608 #[allow(clippy::too_many_arguments)]
609 fn describe_in(
610 &self,
611 name: &str,
612 base: U256,
613 offset: usize,
614 type_id: &str,
615 target: U256,
616 probe: u64,
617 out: &mut Vec<(String, Location)>,
618 ) {
619 if name.matches(['.', '[']).count() > MAX_NESTING {
621 return;
622 }
623 let Ok(t) = self.ty(type_id) else { return };
624 match (t.encoding, t.members.as_deref(), t.base.as_deref()) {
625 (Encoding::Inplace, Some(members), _) => {
626 for m in members {
627 self.describe_in(
628 &format!("{name}.{}", m.label),
629 base + m.slot,
630 m.offset,
631 &m.type_id,
632 target,
633 probe,
634 out,
635 );
636 }
637 }
638 (Encoding::Inplace, None, Some(base_ty)) => {
639 let words = U256::from(t.number_of_bytes.div_ceil(32));
640 if target < base || target >= base + words {
641 return;
642 }
643 self.describe_elements(name, base, base_ty, target, probe, out);
644 }
645 (Encoding::Inplace, None, None) | (Encoding::Bytes, _, _) => {
646 let words = U256::from(t.number_of_bytes.div_ceil(32).max(1));
647 if target >= base && target < base + words {
648 out.push((
649 name.to_string(),
650 Location {
651 slot: slot_b(target),
652 offset,
653 size: t.number_of_bytes,
654 type_id: type_id.to_string(),
655 },
656 ));
657 }
658 }
659 (Encoding::DynamicArray, _, base_ty) => {
660 if target == base {
661 out.push((
662 format!("{name}.length"),
663 Location {
664 slot: slot_b(base),
665 offset: 0,
666 size: 32,
667 type_id: "t_uint256".into(),
668 },
669 ));
670 return;
671 }
672 let data = U256::from_be_bytes(keccak256(base.to_be_bytes::<32>()).0);
673 if target < data || target - data >= U256::from(probe) {
674 return;
675 }
676 let Some(base_ty) = base_ty else { return };
677 self.describe_elements(name, data, base_ty, target, probe, out);
678 }
679 (Encoding::Mapping, _, _) => {}
680 }
681 }
682
683 fn describe_elements(
687 &self,
688 name: &str,
689 data: U256,
690 base_ty: &str,
691 target: U256,
692 probe: u64,
693 out: &mut Vec<(String, Location)>,
694 ) {
695 let Ok(bt) = self.ty(base_ty) else { return };
696 let size = bt.number_of_bytes;
697 if size == 0 {
698 return; }
700 if size >= 32 {
701 let per_elem = size.div_ceil(32) as u64;
702 let i = (target - data).to::<u64>() / per_elem;
703 self.describe_in(
704 &format!("{name}[{i}]"),
705 data + U256::from(i * per_elem),
706 0,
707 base_ty,
708 target,
709 probe,
710 out,
711 );
712 } else {
713 let per = (32 / size) as u64;
714 let first = (target - data).to::<u64>() * per;
715 for i in first..first + per {
716 out.push((
717 format!("{name}[{i}]"),
718 Location {
719 slot: slot_b(target),
720 offset: ((i % per) as usize) * size,
721 size,
722 type_id: base_ty.to_string(),
723 },
724 ));
725 }
726 }
727 }
728}
729
730#[cfg(test)]
731mod tests {
732 use super::*;
733
734 const PLAYGROUND: &str = include_str!("../tests/fixtures/Playground.layout.json");
735
736 fn layout() -> Layout {
737 Layout::from_json(PLAYGROUND).unwrap()
738 }
739
740 fn s(n: u64) -> B256 {
741 slot_b(U256::from(n))
742 }
743
744 #[test]
745 fn flat_and_packed() {
746 let l = layout();
747 assert_eq!(
748 l.locate("counter").unwrap(),
749 Location {
750 slot: s(0),
751 offset: 0,
752 size: 32,
753 type_id: "t_uint256".into()
754 }
755 );
756 let b = l.locate("b").unwrap();
757 assert_eq!((b.slot, b.offset, b.size), (s(1), 16, 8));
758 let c = l.locate("c").unwrap();
759 assert_eq!((c.slot, c.offset, c.size), (s(1), 24, 1));
760 let word = slot_b(U256::from(5) | (U256::from(7) << 128) | (U256::from(1) << 192));
762 assert_eq!(
763 l.decode(&l.locate("a").unwrap(), word),
764 Value::Uint(U256::from(5))
765 );
766 assert_eq!(l.decode(&b, word), Value::Uint(U256::from(7)));
767 assert_eq!(l.decode(&c, word), Value::Bool(true));
768 }
769
770 #[test]
771 fn struct_members() {
772 let l = layout();
773 let idx = l.locate("totals.index").unwrap();
774 assert_eq!((idx.slot, idx.offset, idx.size), (s(2), 8, 24));
775 assert!(matches!(
776 l.locate("totals.nope"),
777 Err(LayoutError::UnknownField(_))
778 ));
779 }
780
781 #[test]
782 fn mappings() {
783 let l = layout();
784 let addr = "0x000000000000000000000000000000000000dEaD";
785 let loc = l.locate(&format!("balances[{addr}]")).unwrap();
786 let mut buf = [0u8; 64];
787 buf[12..32].copy_from_slice(&alloy_primitives::hex::decode(&addr[2..]).unwrap());
788 buf[63] = 3;
789 assert_eq!(loc.slot, keccak256(buf));
790 let inner = l.locate(&format!("nested[{addr}][7]")).unwrap();
791 buf[63] = 4;
792 let first = keccak256(buf);
793 let mut buf2 = [0u8; 64];
794 buf2[31] = 7;
795 buf2[32..].copy_from_slice(first.as_slice());
796 assert_eq!(inner.slot, keccak256(buf2));
797 assert!(matches!(
798 l.locate("balances.x"),
799 Err(LayoutError::Shape { .. })
800 ));
801 }
802
803 #[test]
804 fn dynamic_array_and_describe() {
805 let l = layout();
806 assert_eq!(l.locate("items.length").unwrap().slot, s(5));
807 let data = U256::from_be_bytes(keccak256(U256::from(5).to_be_bytes::<32>()).0);
808 assert_eq!(
809 l.locate("items[2]").unwrap().slot,
810 slot_b(data + U256::from(2))
811 );
812
813 let names = |slot: B256| -> Vec<String> {
814 l.describe_slot(slot, 64)
815 .into_iter()
816 .map(|(n, _)| n)
817 .collect()
818 };
819 assert_eq!(names(s(1)), vec!["a", "b", "c"]);
820 assert_eq!(names(s(2)), vec!["totals.lastTime", "totals.index"]);
821 assert_eq!(names(slot_b(data + U256::from(3))), vec!["items[3]"]);
822 assert!(names(s(99)).is_empty());
823 }
824
825 #[test]
826 fn kinds_and_typescript() {
827 let l = layout();
828 assert_eq!(
829 l.kind_of("counter").unwrap(),
830 PathKind::Value(ValueKind::Uint)
831 );
832 assert_eq!(l.kind_of("c").unwrap(), PathKind::Value(ValueKind::Bool));
833 assert_eq!(
834 l.kind_of("lastPoker").unwrap(),
835 PathKind::Value(ValueKind::Address)
836 );
837 assert_eq!(l.kind_of("totals").unwrap(), PathKind::Struct);
838 assert_eq!(l.kind_of("balances").unwrap(), PathKind::Mapping);
839 assert_eq!(l.kind_of("nested[0x1]").unwrap(), PathKind::Mapping);
840 assert_eq!(l.kind_of("items").unwrap(), PathKind::Array);
841 assert_eq!(
842 l.kind_of("items.length").unwrap(),
843 PathKind::Value(ValueKind::Uint)
844 );
845 assert_eq!(
846 l.kind_of("items[2]").unwrap(),
847 PathKind::Value(ValueKind::Uint)
848 );
849
850 let ts = l.typescript("PlaygroundView");
851 assert!(ts.contains("export interface PlaygroundView {"));
852 assert!(ts.contains("readonly counter: bigint;"));
853 assert!(ts.contains("readonly c: boolean;"));
854 assert!(ts.contains("readonly lastPoker: string;"));
855 assert!(ts.contains("readonly balances: { readonly [key: string]: bigint };"));
856 assert!(ts.contains(
857 "readonly nested: { readonly [key: string]: { readonly [key: string]: bigint } };"
858 ));
859 assert!(ts.contains(
860 "readonly items: { readonly [index: number]: bigint; readonly length: bigint };"
861 ));
862 assert!(ts.contains(
863 "readonly totals: {\n readonly lastTime: bigint;\n readonly index: bigint;\n };"
864 ));
865 }
866
867 #[test]
870 fn self_referential_layout_terminates() {
871 let json = r#"{
872 "storage": [{"label":"a","slot":"0","offset":0,"type":"t_struct(A)"}],
873 "types": {
874 "t_struct(A)": {"encoding":"inplace","label":"struct A","numberOfBytes":"64",
875 "members":[{"label":"inner","slot":"0","offset":0,"type":"t_struct(A)"},
876 {"label":"n","slot":"1","offset":0,"type":"t_uint256"}]},
877 "t_uint256": {"encoding":"inplace","label":"uint256","numberOfBytes":"32"}
878 }}"#;
879 let l = Layout::from_json(json).unwrap();
880 let ts = l.typescript("Evil");
881 assert!(ts.contains("unknown"), "recursion must be cut, got:\n{ts}");
882
883 let json2 = r#"{
886 "storage": [
887 {"label":"m","slot":"0","offset":0,"type":"t_m"},
888 {"label":"d","slot":"1","offset":0,"type":"t_d"},
889 {"label":"f","slot":"2","offset":0,"type":"t_f"},
890 {"label":"z","slot":"3","offset":0,"type":"t_z"}],
891 "types": {
892 "t_m": {"encoding":"mapping","label":"mapping(uint256 => m)","numberOfBytes":"32","key":"t_uint256","value":"t_m"},
893 "t_d": {"encoding":"dynamic_array","label":"d[]","numberOfBytes":"32","base":"t_d"},
894 "t_f": {"encoding":"inplace","label":"f[2]","numberOfBytes":"64","base":"t_f"},
895 "t_z": {"encoding":"dynamic_array","label":"zero[]","numberOfBytes":"32","base":"t_zero"},
896 "t_zero": {"encoding":"inplace","label":"uint0","numberOfBytes":"0"},
897 "t_uint256": {"encoding":"inplace","label":"uint256","numberOfBytes":"32"}
898 }}"#;
899 let l2 = Layout::from_json(json2).unwrap();
900 let _ = l2.typescript("Evil2");
901 let data = U256::from_be_bytes(keccak256(U256::from(3).to_be_bytes::<32>()).0);
902 let _ = l2.describe_slot(slot_b(data), 16); let _ = l2.describe_slot(s(2), 16);
904 let names = l.describe_slot(s(1), 16);
905 assert!(names.iter().any(|(n, _)| n.ends_with(".n")));
906 assert!(l.locate("a.inner.inner.n").is_ok());
908 }
909
910 #[test]
911 fn decode_address_and_never_panics() {
912 let l = layout();
913 let loc = l.locate("lastPoker").unwrap();
914 let w = slot_b(U256::from_be_slice(&[0xAB; 20]));
915 assert_eq!(
916 l.decode(&loc, w),
917 Value::Address(Address::repeat_byte(0xAB))
918 );
919 let bogus = Location {
920 slot: s(0),
921 offset: 40,
922 size: 64,
923 type_id: "t_uint256".into(),
924 };
925 let _ = l.decode(&bogus, B256::ZERO);
926 }
927}