1use 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
33static EMBEDDED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/artifacts");
35
36pub const COVERED: &[(&str, &str)] = &[
41 ("Registry", "Registry"),
43 ("Notary", "Notary"),
44 ("WalletFactory", "WalletFactory"),
45 ("WebWallet", "WebWallet"),
46 ("ERC1967Proxy", "ERC1967Proxy"),
47 ("XZkVerifier", "XZkVerifier"),
48 ("XHonkVerifier", "XHonkVerifier"),
49 ("Verifier", "HonkVerifier"),
52 ("GoogleOidcVerifier", "GoogleOidcVerifier"),
53 ("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 ("IdentityNames", "IdentityNames"),
66 ("GitHubIdentityVerifier", "GitHubIdentityVerifier"),
67 ("GoogleIdentityVerifier", "GoogleIdentityVerifier"),
68 ("XIdentityVerifier", "XIdentityVerifier"),
69 ("IdentityJwksRoots", "IdentityJwksRoots"),
70 ("LibidFactory", "LibidFactory"),
72];
73
74enum Source {
75 Embedded,
76 Dir(PathBuf),
77}
78
79pub struct Artifacts {
81 source: Source,
82}
83
84impl Artifacts {
85 pub const fn embedded() -> Self {
88 Self {
89 source: Source::Embedded,
90 }
91 }
92
93 pub fn from_dir(dir: impl Into<PathBuf>) -> Self {
96 Self {
97 source: Source::Dir(dir.into()),
98 }
99 }
100
101 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 pub fn bytecode(&self, contract: &str) -> Result<Bytes> {
128 self.bytecode_named(contract, contract)
129 }
130
131 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 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 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 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 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 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 #[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 #[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 #[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 #[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 let cut = artifacts.method_identifiers("DiamondCutFacet").unwrap();
329 assert!(cut.keys().any(|sig| sig.starts_with("diamondCut(")));
330 }
331}