Skip to main content

libid_contracts/
artifacts.rs

1//! Access to the compiled forge artifacts the crate ships.
2//!
3//! The default source is [`Artifacts::embedded`]: the pruned artifact JSONs
4//! vendored by `scripts/vendor-artifacts.sh` into `artifacts/` are compiled
5//! into the binary, so deployment has zero filesystem dependencies at runtime.
6//! [`Artifacts::from_dir`] reads the same `<File>.sol/<Name>.json` layout from
7//! disk instead — it accepts a raw forge `out/` directory too, since the crate
8//! only reads fields forge emits.
9
10use std::{
11    collections::BTreeMap,
12    path::PathBuf,
13};
14
15use alloy::{
16    hex,
17    primitives::Bytes,
18};
19use include_dir::{
20    include_dir,
21    Dir,
22};
23
24use crate::error::{
25    Error,
26    Result,
27};
28
29/// The vendored artifacts, embedded at compile time.
30static EMBEDDED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/artifacts");
31
32/// Every deployable contract the crate covers, as `(file, contract)` — the
33/// artifact lives at `<file>.sol/<contract>.json`. Keep in sync with
34/// `scripts/vendor-artifacts.sh`.
35pub const COVERED: &[(&str, &str)] = &[
36    // ceremony
37    ("NotaryService", "NotaryService"),
38    ("CeremonyProofVerifier", "CeremonyProofVerifier"),
39    ("ERC1967Proxy", "ERC1967Proxy"),
40    ("GoogleJwtRoots", "GoogleJwtRoots"),
41    // ceremony: the launch Platform Verifiers (one per profile)
42    ("XPlatformVerifier", "XPlatformVerifier"),
43    ("GitHubPlatformVerifier", "GitHubPlatformVerifier"),
44    ("GooglePlatformVerifier", "GooglePlatformVerifier"),
45    // circuits: the UltraHonk verifiers the Platform Verifiers pin, vendored
46    // from the libid-circuits release
47    ("BearerLinkHonkVerifier", "BearerLinkHonkVerifier"),
48    ("OidcGoogleHonkVerifier", "OidcGoogleHonkVerifier"),
49    // identity
50    ("IdentityRegistry", "IdentityRegistry"),
51    // ens (deployed once per network, not CREATE3-canonical)
52    ("HandleResolver", "HandleResolver"),
53    // escrow: value held against a handle nobody holds yet
54    ("HandleEscrow", "HandleEscrow"),
55    // factory
56    ("LibidFactory", "LibidFactory"),
57    ("WTIA9", "WTIA9"),
58];
59
60enum Source {
61    Embedded,
62    Dir(PathBuf),
63}
64
65/// A source of compiled contract artifacts.
66pub struct Artifacts {
67    source: Source,
68}
69
70impl Artifacts {
71    /// The artifacts vendored into the crate. The default, filesystem-free
72    /// path.
73    pub const fn embedded() -> Self {
74        Self {
75            source: Source::Embedded,
76        }
77    }
78
79    /// Artifacts read from a directory laid out as `<File>.sol/<Name>.json`
80    /// (a forge `out/` directory qualifies).
81    pub fn from_dir(dir: impl Into<PathBuf>) -> Self {
82        Self {
83            source: Source::Dir(dir.into()),
84        }
85    }
86
87    /// The raw artifact JSON for `out/<file>.sol/<contract>.json`.
88    pub fn raw(&self, file: &str, contract: &str) -> Result<serde_json::Value> {
89        self.read_json(&format!("{file}.sol/{contract}.json"))
90    }
91
92    /// Any JSON file at `rel` inside the source: an artifact, or the
93    /// `circuits.json` pin the vendor script copies in beside them.
94    pub(crate) fn read_json(&self, rel: &str) -> Result<serde_json::Value> {
95        let contents = match &self.source {
96            Source::Embedded => EMBEDDED
97                .get_file(rel)
98                .and_then(|f| f.contents_utf8())
99                .map(str::to_owned)
100                .ok_or_else(|| Error::Artifact {
101                    detail: format!("no embedded artifact {rel}"),
102                })?,
103            Source::Dir(dir) => {
104                let path = dir.join(rel);
105                std::fs::read_to_string(&path).map_err(|e| Error::Artifact {
106                    detail: format!("failed to read artifact {}: {e}", path.display()),
107                })?
108            }
109        };
110        serde_json::from_str(&contents).map_err(|e| Error::Artifact {
111            detail: format!("failed to parse artifact {rel}: {e}"),
112        })
113    }
114
115    /// Creation bytecode of a contract whose `.sol` file name matches the
116    /// contract name.
117    pub fn bytecode(&self, contract: &str) -> Result<Bytes> {
118        self.bytecode_named(contract, contract)
119    }
120
121    /// Creation bytecode where the source file and contract names differ
122    /// (a bb-generated `Verifier.sol` holding `HonkVerifier`, say).
123    pub fn bytecode_named(&self, file: &str, contract: &str) -> Result<Bytes> {
124        let hex_str = self.bytecode_hex(file, contract)?;
125        let bytes = hex::decode(&hex_str).map_err(|e| Error::Artifact {
126            detail: format!("invalid bytecode hex for {file}.sol:{contract}: {e}"),
127        })?;
128        Ok(Bytes::from(bytes))
129    }
130
131    /// The artifact's `methodIdentifiers`: `"sig(args)" -> 4-byte selector`
132    /// (8 hex chars, no `0x`).
133    pub fn method_identifiers(&self, contract: &str) -> Result<BTreeMap<String, String>> {
134        let json = self.raw(contract, contract)?;
135        let methods =
136            json["methodIdentifiers"]
137                .as_object()
138                .ok_or_else(|| Error::Artifact {
139                    detail: format!(
140                        "no methodIdentifiers in {contract}.sol/{contract}.json"
141                    ),
142                })?;
143        methods
144            .iter()
145            .map(|(sig, value)| {
146                let sel = value.as_str().ok_or_else(|| Error::Artifact {
147                    detail: format!("non-string selector for {sig} in {contract}"),
148                })?;
149                Ok((sig.clone(), sel.to_owned()))
150            })
151            .collect()
152    }
153
154    /// The raw `bytecode.object` hex, without `0x`.
155    pub(crate) fn bytecode_hex(&self, file: &str, contract: &str) -> Result<String> {
156        let json = self.raw(file, contract)?;
157        let raw = json["bytecode"]["object"]
158            .as_str()
159            .ok_or_else(|| Error::Artifact {
160                detail: format!("no bytecode.object in {file}.sol/{contract}.json"),
161            })?;
162        Ok(raw.strip_prefix("0x").unwrap_or(raw).to_owned())
163    }
164}
165
166impl Default for Artifacts {
167    fn default() -> Self {
168        Self::embedded()
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    /// Every covered contract's creation bytecode decodes as it is embedded
177    /// and is not empty: the crate links no libraries, so a contract that
178    /// needed one would carry a placeholder here and fail to decode.
179    #[test]
180    fn every_covered_contract_has_deployable_bytecode() {
181        let artifacts = Artifacts::embedded();
182        for &(file, contract) in COVERED {
183            let bytecode = artifacts
184                .bytecode_named(file, contract)
185                .unwrap_or_else(|e| panic!("{file}.sol:{contract}: {e}"));
186            assert!(
187                !bytecode.is_empty(),
188                "{file}.sol:{contract} has empty bytecode"
189            );
190        }
191    }
192}