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