Skip to content

Commit

Permalink
Return a proper Evaluation type (#13)
Browse files Browse the repository at this point in the history
This helps distinguishing errors from rule violations. It also allows the caller to decide  whether to accept a premint based on the specific results.
  • Loading branch information
ligustah authored Apr 6, 2024
1 parent 28e36f4 commit 093fb30
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 46 deletions.
25 changes: 17 additions & 8 deletions src/premints/zora_premint_v2/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use alloy_primitives::Signature;
use alloy_sol_types::SolStruct;

use crate::premints::zora_premint_v2::types::ZoraPremintV2;
use crate::rules::{Rule, RuleContext};
use crate::rules::Evaluation::{Accept, Reject};
use crate::rules::{Evaluation, Rule, RuleContext};
use crate::typed_rule;
use crate::types::{Premint, PremintTypes};

Expand All @@ -13,11 +14,11 @@ use crate::types::{Premint, PremintTypes};
pub async fn is_authorized_to_create_premint(
premint: ZoraPremintV2,
context: RuleContext,
) -> eyre::Result<bool> {
) -> eyre::Result<Evaluation> {
// * if contract exists, check if the signer is the contract admin
// * if contract does not exist, check if the signer is the proposed contract admin
// * this logic exists as a function on the premint executor contract
Ok(true)
Ok(Accept)
}

// * signatureIsValid ( this can be performed entirely offline )
Expand All @@ -27,7 +28,7 @@ pub async fn is_authorized_to_create_premint(
pub async fn is_valid_signature(
premint: ZoraPremintV2,
context: RuleContext,
) -> eyre::Result<bool> {
) -> eyre::Result<Evaluation> {
// * if contract exists, check if the signer is the contract admin
// * if contract does not exist, check if the signer is the proposed contract admin

Expand All @@ -37,7 +38,14 @@ pub async fn is_valid_signature(
let hash = premint.premint.eip712_signing_hash(&domain);
let signer = signature.recover_address_from_prehash(&hash)?;

Ok(signer == premint.collection.contractAdmin)
if signer != premint.collection.contractAdmin {
return Ok(Reject(format!(
"Invalid signature for contract admin {}",
premint.collection.contractAdmin
)));
}

Ok(Accept)
}

pub fn all_rules() -> Vec<Box<dyn Rule>> {
Expand Down Expand Up @@ -88,8 +96,9 @@ mod test {
#[tokio::test]
async fn test_is_valid_signature() {
let premint: ZoraPremintV2 = serde_json::from_str(PREMINT_JSON).unwrap();
assert!(is_valid_signature(premint, RuleContext {})
.await
.expect("failed to check signature"));
assert!(matches!(
is_valid_signature(premint, RuleContext {}).await,
Ok(Accept)
));
}
}
126 changes: 88 additions & 38 deletions src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,46 @@ use futures::future::join_all;

use crate::types::{Premint, PremintTypes};

#[derive(Debug)]
pub enum Evaluation {
Accept,
Ignore,
Reject(String),
}

#[derive(Debug)]
pub struct RuleResult {
pub rule_name: &'static str,
pub result: eyre::Result<Evaluation>,
}

#[derive(Debug)]
pub struct Results(Vec<RuleResult>);

impl Results {
pub fn is_accept(&self) -> bool {
!self.is_reject()
}

pub fn is_reject(&self) -> bool {
!self.is_err()
&& self
.0
.iter()
.any(|r| matches!(r.result, Ok(Evaluation::Reject(_))))
}

pub fn is_err(&self) -> bool {
self.0.iter().any(|r| matches!(r.result, Err(_)))
}
}

#[derive(Clone)]
pub struct RuleContext {}

#[async_trait]
pub trait Rule: Send + Sync {
async fn check(&self, item: PremintTypes, context: RuleContext) -> eyre::Result<bool>;
async fn check(&self, item: PremintTypes, context: RuleContext) -> eyre::Result<Evaluation>;
fn rule_name(&self) -> &'static str;
}

Expand All @@ -18,9 +52,9 @@ pub struct FnRule<T>(pub &'static str, pub T);
impl<T, Fut> Rule for FnRule<T>
where
T: Fn(PremintTypes, RuleContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = eyre::Result<bool>> + Send,
Fut: std::future::Future<Output = eyre::Result<Evaluation>> + Send,
{
async fn check(&self, item: PremintTypes, context: RuleContext) -> eyre::Result<bool> {
async fn check(&self, item: PremintTypes, context: RuleContext) -> eyre::Result<Evaluation> {
self.1(item, context).await
}

Expand All @@ -47,7 +81,7 @@ macro_rules! metadata_rule {
&self,
item: crate::types::PremintTypes,
context: crate::rules::RuleContext,
) -> eyre::Result<bool> {
) -> eyre::Result<crate::rules::Evaluation> {
$fn(item.metadata(), context).await
}

Expand All @@ -71,10 +105,10 @@ macro_rules! typed_rule {
&self,
item: crate::types::PremintTypes,
context: crate::rules::RuleContext,
) -> eyre::Result<bool> {
) -> eyre::Result<crate::rules::Evaluation> {
match item {
$t(premint) => $fn(premint, context).await,
_ => Ok(true),
_ => Ok(crate::rules::Evaluation::Ignore),
}
}

Expand Down Expand Up @@ -108,35 +142,30 @@ impl RulesEngine {
self.rules.push(Box::new(rule));
}

pub async fn evaluate(&self, item: PremintTypes, context: RuleContext) -> eyre::Result<bool> {
pub async fn evaluate(&self, item: PremintTypes, context: RuleContext) -> Results {
let results: Vec<_> = self
.rules
.iter()
.map(|rule| rule.check(item.clone(), context.clone()))
.collect();
let all_checks = join_all(results).await;

// TODO: ideally we'd want to return a list of all errors
// so that a caller could determine which rules failed and why
for error in all_checks.into_iter() {
match error {
Err(e) => {
return Err(e);
}
Ok(pass) => {
if !pass {
return Ok(false);
}
}
}
}

Ok(true)
Results(
all_checks
.into_iter()
.zip(self.rules.iter())
.map(|(result, rule)| RuleResult {
rule_name: rule.rule_name(),
result,
})
.collect(),
)
}
}

mod general {
use crate::rules::{Rule, RuleContext};
use crate::rules::Evaluation::{Accept, Reject};
use crate::rules::{Evaluation, Rule, RuleContext};
use crate::types::PremintMetadata;

pub fn all_rules() -> Vec<Box<dyn Rule>> {
Expand All @@ -146,15 +175,23 @@ mod general {
pub async fn token_uri_length(
meta: PremintMetadata,
context: RuleContext,
) -> eyre::Result<bool> {
) -> eyre::Result<Evaluation> {
let max_allowed = if meta.uri.starts_with("data:") {
// allow some more data for data uris
8 * 1024
} else {
2 * 1024
};

Ok(meta.uri.len() <= max_allowed)
Ok(match meta.uri.len() {
0 => Reject("Token URI is empty".to_string()),
_ if meta.uri.len() > max_allowed => Reject(format!(
"Token URI is too long: {} > {}",
meta.uri.len(),
max_allowed
)),
_ => Accept,
})
}
}

Expand All @@ -163,30 +200,43 @@ mod test {
use alloy_primitives::U256;

use crate::premints::zora_premint_v2::types::ZoraPremintV2;
use crate::rules::Evaluation::{Accept, Reject};
use crate::types::SimplePremint;

use super::*;

async fn simple_rule(item: PremintTypes, context: RuleContext) -> eyre::Result<bool> {
Ok(true)
async fn simple_rule(item: PremintTypes, context: RuleContext) -> eyre::Result<Evaluation> {
Ok(Accept)
}

async fn conditional_rule(item: PremintTypes, context: RuleContext) -> eyre::Result<bool> {
async fn conditional_rule(
item: PremintTypes,
context: RuleContext,
) -> eyre::Result<Evaluation> {
match item {
PremintTypes::Simple(s) => Ok(s.metadata().chain_id == U256::default()),
_ => Ok(true),
PremintTypes::Simple(s) => {
if s.metadata().chain_id == U256::default() {
Ok(Accept)
} else {
Ok(Reject("Chain ID is not default".to_string()))
}
}
_ => Ok(Accept),
}
}

async fn simple_typed_rule(item: SimplePremint, context: RuleContext) -> eyre::Result<bool> {
Ok(true)
async fn simple_typed_rule(
item: SimplePremint,
context: RuleContext,
) -> eyre::Result<Evaluation> {
Ok(Accept)
}

async fn simple_typed_zora_rule(
item: ZoraPremintV2,
context: RuleContext,
) -> eyre::Result<bool> {
Ok(true)
) -> eyre::Result<Evaluation> {
Ok(Accept)
}

#[tokio::test]
Expand All @@ -197,7 +247,7 @@ mod test {
.check(PremintTypes::Simple(Default::default()), context)
.await
.unwrap();
assert!(result);
assert!(matches!(result, Accept));
}

#[tokio::test]
Expand All @@ -211,7 +261,7 @@ mod test {
.evaluate(PremintTypes::Simple(Default::default()), context)
.await;

assert!(result.unwrap());
assert!(result.is_accept());
}

#[tokio::test]
Expand All @@ -235,6 +285,6 @@ mod test {
.evaluate(PremintTypes::Simple(Default::default()), context)
.await;

assert!(result.unwrap());
assert!(result.is_accept());
}
}

0 comments on commit 093fb30

Please sign in to comment.