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];
71
72enum Source {
73 Embedded,
74 Dir(PathBuf),
75}
76
77pub struct Artifacts {
79 source: Source,
80}
81
82impl Artifacts {
83 pub const fn embedded() -> Self {
86 Self {
87 source: Source::Embedded,
88 }
89 }
90
91 pub fn from_dir(dir: impl Into<PathBuf>) -> Self {
94 Self {
95 source: Source::Dir(dir.into()),
96 }
97 }
98
99 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 pub fn bytecode(&self, contract: &str) -> Result<Bytes> {
126 self.bytecode_named(contract, contract)
127 }
128
129 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 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 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 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 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 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 #[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 #[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 #[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 #[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 let cut = artifacts.method_identifiers("DiamondCutFacet").unwrap();
327 assert!(cut.keys().any(|sig| sig.starts_with("diamondCut(")));
328 }
329}