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