use std::{
collections::BTreeMap,
path::PathBuf,
};
use alloy::{
hex,
primitives::Bytes,
};
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)] = &[
("NotaryService", "NotaryService"),
("CeremonyProofVerifier", "CeremonyProofVerifier"),
("ERC1967Proxy", "ERC1967Proxy"),
("GoogleJwtRoots", "GoogleJwtRoots"),
("XPlatformVerifier", "XPlatformVerifier"),
("GitHubPlatformVerifier", "GitHubPlatformVerifier"),
("GooglePlatformVerifier", "GooglePlatformVerifier"),
("BearerLinkHonkVerifier", "BearerLinkHonkVerifier"),
("OidcGoogleHonkVerifier", "OidcGoogleHonkVerifier"),
("IdentityRegistry", "IdentityRegistry"),
("HandleResolver", "HandleResolver"),
("HandleEscrow", "HandleEscrow"),
("LibidFactory", "LibidFactory"),
("WTIA9", "WTIA9"),
];
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> {
self.read_json(&format!("{file}.sol/{contract}.json"))
}
pub(crate) fn read_json(&self, rel: &str) -> Result<serde_json::Value> {
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)?;
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 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(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())
}
}
impl Default for Artifacts {
fn default() -> Self {
Self::embedded()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_covered_contract_has_deployable_bytecode() {
let artifacts = Artifacts::embedded();
for &(file, contract) in COVERED {
let bytecode = artifacts
.bytecode_named(file, contract)
.unwrap_or_else(|e| panic!("{file}.sol:{contract}: {e}"));
assert!(
!bytecode.is_empty(),
"{file}.sol:{contract} has empty bytecode"
);
}
}
}