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::{
18        Bytes,
19        FixedBytes,
20    },
21    providers::Provider,
22};
23use include_dir::{
24    include_dir,
25    Dir,
26};
27
28use crate::error::{
29    Error,
30    Result,
31};
32
33/// The vendored artifacts, embedded at compile time.
34static EMBEDDED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/artifacts");
35
36/// Every deployable contract the crate covers, as `(file, contract)` — the
37/// artifact lives at `<file>.sol/<contract>.json`. Interfaces (`IBank`) are
38/// vendored for their `methodIdentifiers` but carry no bytecode, so they are
39/// not listed here. Keep in sync with `scripts/vendor-artifacts.sh`.
40pub const COVERED: &[(&str, &str)] = &[
41    // login
42    ("Registry", "Registry"),
43    ("Notary", "Notary"),
44    ("WalletFactory", "WalletFactory"),
45    ("WebWallet", "WebWallet"),
46    ("ERC1967Proxy", "ERC1967Proxy"),
47    ("XZkVerifier", "XZkVerifier"),
48    ("XHonkVerifier", "XHonkVerifier"),
49    // oidc — the Google OIDC circuit verifier is generated into `Verifier.sol`
50    // under the contract name `HonkVerifier`
51    ("Verifier", "HonkVerifier"),
52    ("GoogleOidcVerifier", "GoogleOidcVerifier"),
53    // transfer (bank diamond)
54    ("Diamond", "Diamond"),
55    ("DiamondCutFacet", "DiamondCutFacet"),
56    ("DiamondLoupeFacet", "DiamondLoupeFacet"),
57    ("OwnershipFacet", "OwnershipFacet"),
58    ("AdminFacet", "AdminFacet"),
59    ("VaultFacet", "VaultFacet"),
60    ("TransferFacet", "TransferFacet"),
61    ("BankInit", "BankInit"),
62    ("MockERC20", "MockERC20"),
63    ("WTIA9", "WTIA9"),
64    // identity
65    ("IdentityNames", "IdentityNames"),
66    ("GitHubIdentityVerifier", "GitHubIdentityVerifier"),
67    ("GoogleIdentityVerifier", "GoogleIdentityVerifier"),
68    ("XIdentityVerifier", "XIdentityVerifier"),
69    ("IdentityJwksRoots", "IdentityJwksRoots"),
70    // factory
71    ("LibidFactory", "LibidFactory"),
72];
73
74enum Source {
75    Embedded,
76    Dir(PathBuf),
77}
78
79/// A source of compiled contract artifacts.
80pub struct Artifacts {
81    source: Source,
82}
83
84impl Artifacts {
85    /// The artifacts vendored into the crate. The default, filesystem-free
86    /// path.
87    pub const fn embedded() -> Self {
88        Self {
89            source: Source::Embedded,
90        }
91    }
92
93    /// Artifacts read from a directory laid out as `<File>.sol/<Name>.json`
94    /// (a forge `out/` directory qualifies).
95    pub fn from_dir(dir: impl Into<PathBuf>) -> Self {
96        Self {
97            source: Source::Dir(dir.into()),
98        }
99    }
100
101    /// The raw artifact JSON for `out/<file>.sol/<contract>.json`.
102    pub fn raw(&self, file: &str, contract: &str) -> Result<serde_json::Value> {
103        let rel = format!("{file}.sol/{contract}.json");
104        let contents = match &self.source {
105            Source::Embedded => EMBEDDED
106                .get_file(&rel)
107                .and_then(|f| f.contents_utf8())
108                .map(str::to_owned)
109                .ok_or_else(|| Error::Artifact {
110                    detail: format!("no embedded artifact {rel}"),
111                })?,
112            Source::Dir(dir) => {
113                let path = dir.join(&rel);
114                std::fs::read_to_string(&path).map_err(|e| Error::Artifact {
115                    detail: format!("failed to read artifact {}: {e}", path.display()),
116                })?
117            }
118        };
119        serde_json::from_str(&contents).map_err(|e| Error::Artifact {
120            detail: format!("failed to parse artifact {rel}: {e}"),
121        })
122    }
123
124    /// Creation bytecode of a contract whose `.sol` file name matches the
125    /// contract name. Fails on artifacts with unresolved link references —
126    /// deploy those through [`Self::linked_bytecode`].
127    pub fn bytecode(&self, contract: &str) -> Result<Bytes> {
128        self.bytecode_named(contract, contract)
129    }
130
131    /// Creation bytecode where the source file and contract names differ
132    /// (`Verifier.sol` → `HonkVerifier`).
133    pub fn bytecode_named(&self, file: &str, contract: &str) -> Result<Bytes> {
134        let hex_str = self.bytecode_hex(file, contract)?;
135        if hex_str.contains("__$") {
136            return Err(Error::Artifact {
137                detail: format!(
138                    "{file}.sol:{contract} has unresolved link references; deploy it \
139                     via linked_bytecode"
140                ),
141            });
142        }
143        let bytes = hex::decode(&hex_str).map_err(|e| Error::Artifact {
144            detail: format!("invalid bytecode hex for {file}.sol:{contract}: {e}"),
145        })?;
146        Ok(Bytes::from(bytes))
147    }
148
149    /// Creation bytecode with every external library it references deployed
150    /// (recursively) through `provider` and linked in. Mirrors what forge does
151    /// automatically: the generated UltraHonk verifiers link `ZKTranscriptLib`.
152    /// For artifacts with no link references this behaves like
153    /// [`Self::bytecode_named`] (no transaction is sent).
154    ///
155    /// `sender` opts into explicit nonce management (see
156    /// [`deploy_contract_from`](crate::deploy::deploy_contract_from)).
157    pub async fn linked_bytecode<P: Provider>(
158        &self,
159        provider: &P,
160        file: &str,
161        contract: &str,
162        sender: Option<alloy::primitives::Address>,
163    ) -> Result<Bytes> {
164        crate::deploy::load_linked_bytecode(provider, self, file, contract, sender).await
165    }
166
167    /// The artifact's `methodIdentifiers`: `"sig(args)" -> 4-byte selector`
168    /// (8 hex chars, no `0x`).
169    pub fn method_identifiers(&self, contract: &str) -> Result<BTreeMap<String, String>> {
170        let json = self.raw(contract, contract)?;
171        let methods =
172            json["methodIdentifiers"]
173                .as_object()
174                .ok_or_else(|| Error::Artifact {
175                    detail: format!(
176                        "no methodIdentifiers in {contract}.sol/{contract}.json"
177                    ),
178                })?;
179        methods
180            .iter()
181            .map(|(sig, value)| {
182                let sel = value.as_str().ok_or_else(|| Error::Artifact {
183                    detail: format!("non-string selector for {sig} in {contract}"),
184                })?;
185                Ok((sig.clone(), sel.to_owned()))
186            })
187            .collect()
188    }
189
190    /// All external function selectors of a facet, decoded from its
191    /// `methodIdentifiers`. Mirrors `BankDiamondDeployer._selectors`.
192    pub fn facet_selectors(&self, contract: &str) -> Result<Vec<FixedBytes<4>>> {
193        let methods = self.method_identifiers(contract)?;
194        let mut selectors = Vec::with_capacity(methods.len());
195        for (sig, hex_sel) in &methods {
196            let bytes = hex::decode(hex_sel).map_err(|e| Error::Artifact {
197                detail: format!("invalid selector hex for {sig}: {e}"),
198            })?;
199            let arr: [u8; 4] =
200                bytes.as_slice().try_into().map_err(|_| Error::Artifact {
201                    detail: format!(
202                        "selector for {sig} is {} bytes, expected 4",
203                        bytes.len()
204                    ),
205                })?;
206            selectors.push(FixedBytes::<4>::from(arr));
207        }
208        Ok(selectors)
209    }
210
211    /// The raw `bytecode.object` hex (no `0x`), link placeholders intact.
212    pub(crate) fn bytecode_hex(&self, file: &str, contract: &str) -> Result<String> {
213        let json = self.raw(file, contract)?;
214        let raw = json["bytecode"]["object"]
215            .as_str()
216            .ok_or_else(|| Error::Artifact {
217                detail: format!("no bytecode.object in {file}.sol/{contract}.json"),
218            })?;
219        Ok(raw.strip_prefix("0x").unwrap_or(raw).to_owned())
220    }
221
222    /// The artifact's `bytecode.linkReferences` object, empty when absent.
223    pub(crate) fn link_references(
224        &self,
225        file: &str,
226        contract: &str,
227    ) -> Result<serde_json::Map<String, serde_json::Value>> {
228        let json = self.raw(file, contract)?;
229        Ok(json["bytecode"]["linkReferences"]
230            .as_object()
231            .cloned()
232            .unwrap_or_default())
233    }
234}
235
236impl Default for Artifacts {
237    fn default() -> Self {
238        Self::embedded()
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    /// Every covered contract's creation bytecode is present and non-empty.
247    /// The Honk verifiers carry link placeholders, so only the hex is checked
248    /// here; linking is exercised by the anvil tests.
249    #[test]
250    fn every_covered_contract_has_bytecode() {
251        let artifacts = Artifacts::embedded();
252        for &(file, contract) in COVERED {
253            let hex_str = artifacts
254                .bytecode_hex(file, contract)
255                .unwrap_or_else(|e| panic!("{file}.sol:{contract}: {e}"));
256            assert!(
257                !hex_str.is_empty(),
258                "{file}.sol:{contract} has empty bytecode"
259            );
260        }
261    }
262
263    /// Contracts without link references decode straight to bytes.
264    #[test]
265    fn unlinked_contracts_decode() {
266        let artifacts = Artifacts::embedded();
267        for &(file, contract) in COVERED {
268            if artifacts
269                .link_references(file, contract)
270                .unwrap()
271                .is_empty()
272            {
273                let bytecode = artifacts
274                    .bytecode_named(file, contract)
275                    .unwrap_or_else(|e| panic!("{file}.sol:{contract}: {e}"));
276                assert!(!bytecode.is_empty());
277            }
278        }
279    }
280
281    /// The Honk verifiers link `ZKTranscriptLib`, and the library artifact
282    /// they resolve against is vendored alongside them.
283    #[test]
284    fn honk_verifiers_link_the_transcript_lib() {
285        let artifacts = Artifacts::embedded();
286        for (file, contract) in [
287            ("XHonkVerifier", "XHonkVerifier"),
288            ("Verifier", "HonkVerifier"),
289        ] {
290            let refs = artifacts.link_references(file, contract).unwrap();
291            assert!(!refs.is_empty(), "{contract} should carry link references");
292            for (lib_path, libs) in &refs {
293                let stem = std::path::Path::new(lib_path)
294                    .file_stem()
295                    .and_then(|s| s.to_str())
296                    .unwrap();
297                for lib_name in libs.as_object().unwrap().keys() {
298                    let lib_hex = artifacts.bytecode_hex(stem, lib_name).unwrap();
299                    assert!(!lib_hex.is_empty(), "{lib_name} library missing");
300                }
301            }
302        }
303    }
304
305    /// Facet selector extraction: every Bank facet exposes selectors, they are
306    /// 4 bytes each, and none repeats across facets (a diamondCut would revert
307    /// on a duplicate).
308    #[test]
309    fn facet_selectors_extract_and_are_disjoint() {
310        let artifacts = Artifacts::embedded();
311        let mut all = std::collections::BTreeSet::new();
312        for facet in [
313            "DiamondCutFacet",
314            "DiamondLoupeFacet",
315            "OwnershipFacet",
316            "AdminFacet",
317            "VaultFacet",
318            "TransferFacet",
319        ] {
320            let selectors = artifacts.facet_selectors(facet).unwrap();
321            assert!(!selectors.is_empty(), "{facet} has no selectors");
322            for sel in selectors {
323                assert!(all.insert(sel), "duplicate selector {sel} in {facet}");
324            }
325        }
326        // diamondCut(FacetCut[],address,bytes) — the one selector the Diamond
327        // constructor wires in itself.
328        let cut = artifacts.method_identifiers("DiamondCutFacet").unwrap();
329        assert!(cut.keys().any(|sig| sig.starts_with("diamondCut(")));
330    }
331}