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    providers::Provider,
19};
20use include_dir::{
21    include_dir,
22    Dir,
23};
24
25use crate::error::{
26    Error,
27    Result,
28};
29
30/// The vendored artifacts, embedded at compile time.
31static EMBEDDED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/artifacts");
32
33/// Every deployable contract the crate covers, as `(file, contract)` — the
34/// artifact lives at `<file>.sol/<contract>.json`. Keep in sync with
35/// `scripts/vendor-artifacts.sh`.
36pub const COVERED: &[(&str, &str)] = &[
37    // ceremony
38    ("NotaryService", "NotaryService"),
39    ("CeremonyProofVerifier", "CeremonyProofVerifier"),
40    ("ERC1967Proxy", "ERC1967Proxy"),
41    ("GoogleJwtRoots", "GoogleJwtRoots"),
42    // identity
43    ("IdentityNames", "IdentityNames"),
44    // ens (deployed once per network, not CREATE3-canonical)
45    ("HandleResolver", "HandleResolver"),
46    // factory
47    ("LibidFactory", "LibidFactory"),
48    ("WTIA9", "WTIA9"),
49];
50
51enum Source {
52    Embedded,
53    Dir(PathBuf),
54}
55
56/// A source of compiled contract artifacts.
57pub struct Artifacts {
58    source: Source,
59}
60
61impl Artifacts {
62    /// The artifacts vendored into the crate. The default, filesystem-free
63    /// path.
64    pub const fn embedded() -> Self {
65        Self {
66            source: Source::Embedded,
67        }
68    }
69
70    /// Artifacts read from a directory laid out as `<File>.sol/<Name>.json`
71    /// (a forge `out/` directory qualifies).
72    pub fn from_dir(dir: impl Into<PathBuf>) -> Self {
73        Self {
74            source: Source::Dir(dir.into()),
75        }
76    }
77
78    /// The raw artifact JSON for `out/<file>.sol/<contract>.json`.
79    pub fn raw(&self, file: &str, contract: &str) -> Result<serde_json::Value> {
80        let rel = format!("{file}.sol/{contract}.json");
81        let contents = match &self.source {
82            Source::Embedded => EMBEDDED
83                .get_file(&rel)
84                .and_then(|f| f.contents_utf8())
85                .map(str::to_owned)
86                .ok_or_else(|| Error::Artifact {
87                    detail: format!("no embedded artifact {rel}"),
88                })?,
89            Source::Dir(dir) => {
90                let path = dir.join(&rel);
91                std::fs::read_to_string(&path).map_err(|e| Error::Artifact {
92                    detail: format!("failed to read artifact {}: {e}", path.display()),
93                })?
94            }
95        };
96        serde_json::from_str(&contents).map_err(|e| Error::Artifact {
97            detail: format!("failed to parse artifact {rel}: {e}"),
98        })
99    }
100
101    /// Creation bytecode of a contract whose `.sol` file name matches the
102    /// contract name. Fails on artifacts with unresolved link references —
103    /// deploy those through [`Self::linked_bytecode`].
104    pub fn bytecode(&self, contract: &str) -> Result<Bytes> {
105        self.bytecode_named(contract, contract)
106    }
107
108    /// Creation bytecode where the source file and contract names differ
109    /// (a bb-generated `Verifier.sol` holding `HonkVerifier`, say).
110    pub fn bytecode_named(&self, file: &str, contract: &str) -> Result<Bytes> {
111        let hex_str = self.bytecode_hex(file, contract)?;
112        if hex_str.contains("__$") {
113            return Err(Error::Artifact {
114                detail: format!(
115                    "{file}.sol:{contract} has unresolved link references; deploy it \
116                     via linked_bytecode"
117                ),
118            });
119        }
120        let bytes = hex::decode(&hex_str).map_err(|e| Error::Artifact {
121            detail: format!("invalid bytecode hex for {file}.sol:{contract}: {e}"),
122        })?;
123        Ok(Bytes::from(bytes))
124    }
125
126    /// Creation bytecode with every external library it references deployed
127    /// (recursively) through `provider` and linked in. Mirrors what forge does
128    /// automatically. Nothing covered today links a library; the UltraHonk
129    /// verifiers the ceremony circuits bring link `ZKTranscriptLib`, and this
130    /// is the path they will deploy through. For artifacts with no link
131    /// references this behaves like [`Self::bytecode_named`] (no transaction
132    /// is sent).
133    ///
134    /// `sender` opts into explicit nonce management (see
135    /// [`deploy_contract_from`](crate::deploy::deploy_contract_from)).
136    pub async fn linked_bytecode<P: Provider>(
137        &self,
138        provider: &P,
139        file: &str,
140        contract: &str,
141        sender: Option<alloy::primitives::Address>,
142    ) -> Result<Bytes> {
143        crate::deploy::load_linked_bytecode(provider, self, file, contract, sender).await
144    }
145
146    /// The artifact's `methodIdentifiers`: `"sig(args)" -> 4-byte selector`
147    /// (8 hex chars, no `0x`).
148    pub fn method_identifiers(&self, contract: &str) -> Result<BTreeMap<String, String>> {
149        let json = self.raw(contract, contract)?;
150        let methods =
151            json["methodIdentifiers"]
152                .as_object()
153                .ok_or_else(|| Error::Artifact {
154                    detail: format!(
155                        "no methodIdentifiers in {contract}.sol/{contract}.json"
156                    ),
157                })?;
158        methods
159            .iter()
160            .map(|(sig, value)| {
161                let sel = value.as_str().ok_or_else(|| Error::Artifact {
162                    detail: format!("non-string selector for {sig} in {contract}"),
163                })?;
164                Ok((sig.clone(), sel.to_owned()))
165            })
166            .collect()
167    }
168
169    /// The raw `bytecode.object` hex (no `0x`), link placeholders intact.
170    pub(crate) fn bytecode_hex(&self, file: &str, contract: &str) -> Result<String> {
171        let json = self.raw(file, contract)?;
172        let raw = json["bytecode"]["object"]
173            .as_str()
174            .ok_or_else(|| Error::Artifact {
175                detail: format!("no bytecode.object in {file}.sol/{contract}.json"),
176            })?;
177        Ok(raw.strip_prefix("0x").unwrap_or(raw).to_owned())
178    }
179
180    /// The artifact's `bytecode.linkReferences` object, empty when absent.
181    pub(crate) fn link_references(
182        &self,
183        file: &str,
184        contract: &str,
185    ) -> Result<serde_json::Map<String, serde_json::Value>> {
186        let json = self.raw(file, contract)?;
187        Ok(json["bytecode"]["linkReferences"]
188            .as_object()
189            .cloned()
190            .unwrap_or_default())
191    }
192}
193
194impl Default for Artifacts {
195    fn default() -> Self {
196        Self::embedded()
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    /// Every covered contract's creation bytecode is present and non-empty.
205    /// Only the hex is checked here so a future artifact with link
206    /// placeholders still passes; linking is the anvil tests' business.
207    #[test]
208    fn every_covered_contract_has_bytecode() {
209        let artifacts = Artifacts::embedded();
210        for &(file, contract) in COVERED {
211            let hex_str = artifacts
212                .bytecode_hex(file, contract)
213                .unwrap_or_else(|e| panic!("{file}.sol:{contract}: {e}"));
214            assert!(
215                !hex_str.is_empty(),
216                "{file}.sol:{contract} has empty bytecode"
217            );
218        }
219    }
220
221    /// Contracts without link references decode straight to bytes — which is
222    /// every covered contract today, so this doubles as the check that none of
223    /// them silently grew a library dependency the vendor script must follow.
224    #[test]
225    fn unlinked_contracts_decode() {
226        let artifacts = Artifacts::embedded();
227        for &(file, contract) in COVERED {
228            if artifacts
229                .link_references(file, contract)
230                .unwrap()
231                .is_empty()
232            {
233                let bytecode = artifacts
234                    .bytecode_named(file, contract)
235                    .unwrap_or_else(|e| panic!("{file}.sol:{contract}: {e}"));
236                assert!(!bytecode.is_empty());
237            }
238        }
239    }
240}