use std::{
collections::BTreeMap,
path::PathBuf,
};
use alloy::{
hex,
primitives::{
Bytes,
FixedBytes,
},
providers::Provider,
};
use include_dir::{
include_dir,
Dir,
};
use crate::error::{
Error,
Result,
};
static EMBEDDED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/artifacts");
pub const COVERED: &[(&str, &str)] = &[
("Registry", "Registry"),
("Notary", "Notary"),
("WalletFactory", "WalletFactory"),
("WebWallet", "WebWallet"),
("ERC1967Proxy", "ERC1967Proxy"),
("XZkVerifier", "XZkVerifier"),
("XHonkVerifier", "XHonkVerifier"),
("Verifier", "HonkVerifier"),
("GoogleOidcVerifier", "GoogleOidcVerifier"),
("Diamond", "Diamond"),
("DiamondCutFacet", "DiamondCutFacet"),
("DiamondLoupeFacet", "DiamondLoupeFacet"),
("OwnershipFacet", "OwnershipFacet"),
("AdminFacet", "AdminFacet"),
("VaultFacet", "VaultFacet"),
("TransferFacet", "TransferFacet"),
("BankInit", "BankInit"),
("MockERC20", "MockERC20"),
("WTIA9", "WTIA9"),
("IdentityNames", "IdentityNames"),
("GitHubIdentityVerifier", "GitHubIdentityVerifier"),
("GoogleIdentityVerifier", "GoogleIdentityVerifier"),
("XIdentityVerifier", "XIdentityVerifier"),
("IdentityJwksRoots", "IdentityJwksRoots"),
("LibidFactory", "LibidFactory"),
];
enum Source {
Embedded,
Dir(PathBuf),
}
pub struct Artifacts {
source: Source,
}
impl Artifacts {
pub const fn embedded() -> Self {
Self {
source: Source::Embedded,
}
}
pub fn from_dir(dir: impl Into<PathBuf>) -> Self {
Self {
source: Source::Dir(dir.into()),
}
}
pub fn raw(&self, file: &str, contract: &str) -> Result<serde_json::Value> {
let rel = format!("{file}.sol/{contract}.json");
let contents = match &self.source {
Source::Embedded => EMBEDDED
.get_file(&rel)
.and_then(|f| f.contents_utf8())
.map(str::to_owned)
.ok_or_else(|| Error::Artifact {
detail: format!("no embedded artifact {rel}"),
})?,
Source::Dir(dir) => {
let path = dir.join(&rel);
std::fs::read_to_string(&path).map_err(|e| Error::Artifact {
detail: format!("failed to read artifact {}: {e}", path.display()),
})?
}
};
serde_json::from_str(&contents).map_err(|e| Error::Artifact {
detail: format!("failed to parse artifact {rel}: {e}"),
})
}
pub fn bytecode(&self, contract: &str) -> Result<Bytes> {
self.bytecode_named(contract, contract)
}
pub fn bytecode_named(&self, file: &str, contract: &str) -> Result<Bytes> {
let hex_str = self.bytecode_hex(file, contract)?;
if hex_str.contains("__$") {
return Err(Error::Artifact {
detail: format!(
"{file}.sol:{contract} has unresolved link references; deploy it \
via linked_bytecode"
),
});
}
let bytes = hex::decode(&hex_str).map_err(|e| Error::Artifact {
detail: format!("invalid bytecode hex for {file}.sol:{contract}: {e}"),
})?;
Ok(Bytes::from(bytes))
}
pub async fn linked_bytecode<P: Provider>(
&self,
provider: &P,
file: &str,
contract: &str,
sender: Option<alloy::primitives::Address>,
) -> Result<Bytes> {
crate::deploy::load_linked_bytecode(provider, self, file, contract, sender).await
}
pub fn method_identifiers(&self, contract: &str) -> Result<BTreeMap<String, String>> {
let json = self.raw(contract, contract)?;
let methods =
json["methodIdentifiers"]
.as_object()
.ok_or_else(|| Error::Artifact {
detail: format!(
"no methodIdentifiers in {contract}.sol/{contract}.json"
),
})?;
methods
.iter()
.map(|(sig, value)| {
let sel = value.as_str().ok_or_else(|| Error::Artifact {
detail: format!("non-string selector for {sig} in {contract}"),
})?;
Ok((sig.clone(), sel.to_owned()))
})
.collect()
}
pub fn facet_selectors(&self, contract: &str) -> Result<Vec<FixedBytes<4>>> {
let methods = self.method_identifiers(contract)?;
let mut selectors = Vec::with_capacity(methods.len());
for (sig, hex_sel) in &methods {
let bytes = hex::decode(hex_sel).map_err(|e| Error::Artifact {
detail: format!("invalid selector hex for {sig}: {e}"),
})?;
let arr: [u8; 4] =
bytes.as_slice().try_into().map_err(|_| Error::Artifact {
detail: format!(
"selector for {sig} is {} bytes, expected 4",
bytes.len()
),
})?;
selectors.push(FixedBytes::<4>::from(arr));
}
Ok(selectors)
}
pub(crate) fn bytecode_hex(&self, file: &str, contract: &str) -> Result<String> {
let json = self.raw(file, contract)?;
let raw = json["bytecode"]["object"]
.as_str()
.ok_or_else(|| Error::Artifact {
detail: format!("no bytecode.object in {file}.sol/{contract}.json"),
})?;
Ok(raw.strip_prefix("0x").unwrap_or(raw).to_owned())
}
pub(crate) fn link_references(
&self,
file: &str,
contract: &str,
) -> Result<serde_json::Map<String, serde_json::Value>> {
let json = self.raw(file, contract)?;
Ok(json["bytecode"]["linkReferences"]
.as_object()
.cloned()
.unwrap_or_default())
}
}
impl Default for Artifacts {
fn default() -> Self {
Self::embedded()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_covered_contract_has_bytecode() {
let artifacts = Artifacts::embedded();
for &(file, contract) in COVERED {
let hex_str = artifacts
.bytecode_hex(file, contract)
.unwrap_or_else(|e| panic!("{file}.sol:{contract}: {e}"));
assert!(
!hex_str.is_empty(),
"{file}.sol:{contract} has empty bytecode"
);
}
}
#[test]
fn unlinked_contracts_decode() {
let artifacts = Artifacts::embedded();
for &(file, contract) in COVERED {
if artifacts
.link_references(file, contract)
.unwrap()
.is_empty()
{
let bytecode = artifacts
.bytecode_named(file, contract)
.unwrap_or_else(|e| panic!("{file}.sol:{contract}: {e}"));
assert!(!bytecode.is_empty());
}
}
}
#[test]
fn honk_verifiers_link_the_transcript_lib() {
let artifacts = Artifacts::embedded();
for (file, contract) in [
("XHonkVerifier", "XHonkVerifier"),
("Verifier", "HonkVerifier"),
] {
let refs = artifacts.link_references(file, contract).unwrap();
assert!(!refs.is_empty(), "{contract} should carry link references");
for (lib_path, libs) in &refs {
let stem = std::path::Path::new(lib_path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap();
for lib_name in libs.as_object().unwrap().keys() {
let lib_hex = artifacts.bytecode_hex(stem, lib_name).unwrap();
assert!(!lib_hex.is_empty(), "{lib_name} library missing");
}
}
}
}
#[test]
fn facet_selectors_extract_and_are_disjoint() {
let artifacts = Artifacts::embedded();
let mut all = std::collections::BTreeSet::new();
for facet in [
"DiamondCutFacet",
"DiamondLoupeFacet",
"OwnershipFacet",
"AdminFacet",
"VaultFacet",
"TransferFacet",
] {
let selectors = artifacts.facet_selectors(facet).unwrap();
assert!(!selectors.is_empty(), "{facet} has no selectors");
for sel in selectors {
assert!(all.insert(sel), "duplicate selector {sel} in {facet}");
}
}
let cut = artifacts.method_identifiers("DiamondCutFacet").unwrap();
assert!(cut.keys().any(|sig| sig.starts_with("diamondCut(")));
}
}