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