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