diff --git a/src/controller.rs b/src/controller.rs index fa143a3..a82bb32 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -172,9 +172,22 @@ impl Controller { } async fn validate_and_insert(&self, premint: PremintTypes) -> eyre::Result<()> { + let metadata = premint.metadata(); + let existing = match self + .store + .get_for_id_and_kind(metadata.id, metadata.kind) + .await + { + Ok(existing) => Some(existing), + Err(report) => match report.downcast_ref::() { + Some(sqlx::Error::RowNotFound) => None, + _ => return Err(report), + }, + }; + let evaluation = self .rules - .evaluate(premint.clone(), RuleContext::new(self.store.clone())) + .evaluate(&premint, &RuleContext::new(self.store.clone(), existing)) .await; if evaluation.is_accept() { diff --git a/src/premints/zora_premint_v2/rules.rs b/src/premints/zora_premint_v2/rules.rs index 1ac54ec..e0c2a0a 100644 --- a/src/premints/zora_premint_v2/rules.rs +++ b/src/premints/zora_premint_v2/rules.rs @@ -1,34 +1,23 @@ use std::str::FromStr; use alloy_primitives::Signature; -use alloy_sol_macro::sol; use alloy_sol_types::SolStruct; use crate::chain::contract_call; use crate::chain_list::CHAINS; -use crate::premints::zora_premint_v2::types::ZoraPremintV2; +use crate::premints::zora_premint_v2::types::{IZoraPremintV2, ZoraPremintV2}; use crate::rules::Evaluation::{Accept, Reject}; use crate::rules::{Evaluation, Rule, RuleContext}; use crate::typed_rule; -use crate::types::{Premint, PremintTypes}; - -sol! { - contract PremintExecutor { - function isAuthorizedToCreatePremint( - address signer, - address premintContractConfigContractAdmin, - address contractAddress - ) external view returns (bool isAuthorized); - } -} +use crate::types::PremintTypes; // create premint v2 rule implementations here pub async fn is_authorized_to_create_premint( - premint: ZoraPremintV2, - _context: RuleContext, + premint: &ZoraPremintV2, + _context: &RuleContext, ) -> eyre::Result { - let call = PremintExecutor::isAuthorizedToCreatePremintCall { + let call = IZoraPremintV2::isAuthorizedToCreatePremintCall { contractAddress: premint.collection_address, signer: premint.collection.contractAdmin, premintContractConfigContractAdmin: premint.collection.contractAdmin, @@ -43,13 +32,50 @@ pub async fn is_authorized_to_create_premint( } } +pub async fn not_minted( + premint: &ZoraPremintV2, + _context: &RuleContext, +) -> eyre::Result { + let call = IZoraPremintV2::premintStatusCall { + contractAddress: premint.collection_address, + uid: premint.premint.uid, + }; + + let provider = CHAINS.get_rpc(premint.chain_id).await?; + let result = contract_call(call, provider).await?; + + match result.contractCreated && !result.tokenIdForPremint.is_zero() { + false => Ok(Accept), + true => Ok(Reject("Premint already minted".to_string())), + } +} + +pub async fn premint_version_supported( + premint: &ZoraPremintV2, + _context: &RuleContext, +) -> eyre::Result { + let call = IZoraPremintV2::supportedPremintSignatureVersionsCall { + contractAddress: premint.collection_address, + }; + + let provider = CHAINS.get_rpc(premint.chain_id).await?; + let result = contract_call(call, provider).await?; + + match result.versions.contains(&"2".to_string()) { + true => Ok(Accept), + false => Ok(Reject( + "Premint version 2 not supported by contract".to_string(), + )), + } +} + // * signatureIsValid ( this can be performed entirely offline ) // * check if the signature is valid // * check if the signature is equal to the proposed contract admin pub async fn is_valid_signature( - premint: ZoraPremintV2, - _context: RuleContext, + premint: &ZoraPremintV2, + _context: &RuleContext, ) -> eyre::Result { // * 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 @@ -71,8 +97,8 @@ pub async fn is_valid_signature( } async fn is_chain_supported( - premint: ZoraPremintV2, - _context: RuleContext, + premint: &ZoraPremintV2, + _context: &RuleContext, ) -> eyre::Result { let supported_chains: Vec = vec![7777777, 999999999, 8453]; let chain_id = premint.chain_id; @@ -88,14 +114,17 @@ pub fn all_rules() -> Vec> { typed_rule!(PremintTypes::ZoraV2, is_authorized_to_create_premint), typed_rule!(PremintTypes::ZoraV2, is_valid_signature), typed_rule!(PremintTypes::ZoraV2, is_chain_supported), + typed_rule!(PremintTypes::ZoraV2, not_minted), + typed_rule!(PremintTypes::ZoraV2, premint_version_supported), ] } #[cfg(test)] mod test { - use super::*; use crate::rules::Evaluation::Ignore; + use super::*; + const PREMINT_JSON: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/data/valid_zora_v2_premint.json" @@ -105,7 +134,7 @@ mod test { async fn test_is_valid_signature() { let premint: ZoraPremintV2 = serde_json::from_str(PREMINT_JSON).unwrap(); let context = RuleContext::test_default().await; - let result = is_valid_signature(premint, context).await; + let result = is_valid_signature(&premint, &context).await; match result { Ok(Accept) => {} @@ -119,7 +148,7 @@ mod test { async fn test_is_authorized_to_create_premint() { let premint: ZoraPremintV2 = serde_json::from_str(PREMINT_JSON).unwrap(); let context = RuleContext::test_default().await; - let result = is_authorized_to_create_premint(premint, context).await; + let result = is_authorized_to_create_premint(&premint, &context).await; match result { Ok(Accept) => {} diff --git a/src/rules.rs b/src/rules.rs index 50cc11c..5c25b15 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -57,11 +57,12 @@ impl Results { #[derive(Clone)] pub struct RuleContext { pub storage: PremintStorage, + pub existing: Option, } impl RuleContext { - pub fn new(storage: PremintStorage) -> Self { - RuleContext { storage } + pub fn new(storage: PremintStorage, existing: Option) -> Self { + RuleContext { storage, existing } } #[cfg(test)] pub async fn test_default() -> Self { @@ -69,38 +70,39 @@ impl RuleContext { RuleContext { storage: PremintStorage::new(&config).await, + existing: None, } } } #[async_trait] pub trait Rule: Send + Sync { - async fn check(&self, item: PremintTypes, context: RuleContext) -> eyre::Result; + async fn check(&self, item: &PremintTypes, context: &RuleContext) -> eyre::Result; fn rule_name(&self) -> &'static str; } -pub struct FnRule(pub &'static str, pub T); +#[macro_export] +macro_rules! rule { + ($fn:tt) => {{ + struct SimpleRule; -#[async_trait] -impl Rule for FnRule -where - T: Fn(PremintTypes, RuleContext) -> Fut + Send + Sync + 'static, - Fut: std::future::Future> + Send, -{ - async fn check(&self, item: PremintTypes, context: RuleContext) -> eyre::Result { - self.1(item, context).await - } + #[async_trait::async_trait] + impl $crate::rules::Rule for SimpleRule { + async fn check( + &self, + item: &$crate::types::PremintTypes, + context: &$crate::rules::RuleContext, + ) -> eyre::Result { + $fn(item, context).await + } - fn rule_name(&self) -> &'static str { - self.0 - } -} + fn rule_name(&self) -> &'static str { + concat!(stringify!($fn)) + } + } -#[macro_export] -macro_rules! rule { - ($fn:tt) => { - std::boxed::Box::new($crate::rules::FnRule(stringify!($fn), $fn)) - }; + std::boxed::Box::new(SimpleRule {}) + }}; } #[macro_export] @@ -112,10 +114,10 @@ macro_rules! metadata_rule { impl $crate::rules::Rule for MetadataRule { async fn check( &self, - item: $crate::types::PremintTypes, - context: $crate::rules::RuleContext, + item: &$crate::types::PremintTypes, + context: &$crate::rules::RuleContext, ) -> eyre::Result { - $fn(item.metadata(), context).await + $fn(&item.metadata(), context).await } fn rule_name(&self) -> &'static str { @@ -136,11 +138,11 @@ macro_rules! typed_rule { impl $crate::rules::Rule for TypedRule { async fn check( &self, - item: $crate::types::PremintTypes, - context: $crate::rules::RuleContext, + item: &$crate::types::PremintTypes, + context: &$crate::rules::RuleContext, ) -> eyre::Result<$crate::rules::Evaluation> { match item { - $t(premint) => $fn(premint, context).await, + $t(premint) => $fn(&premint, context).await, _ => Ok($crate::rules::Evaluation::Ignore), } } @@ -177,11 +179,11 @@ impl RulesEngine { pub fn add_default_rules(&mut self) { self.rules.extend(all_rules()); } - pub async fn evaluate(&self, item: PremintTypes, context: RuleContext) -> Results { + pub async fn evaluate(&self, item: &PremintTypes, context: &RuleContext) -> Results { let results: Vec<_> = self .rules .iter() - .map(|rule| rule.check(item.clone(), context.clone())) + .map(|rule| rule.check(&item, &context)) .collect(); let all_checks = join_all(results).await; @@ -199,7 +201,7 @@ impl RulesEngine { } mod general { - use crate::rules::Evaluation::{Accept, Reject}; + use crate::rules::Evaluation::{Accept, Ignore, Reject}; use crate::rules::{Evaluation, Rule, RuleContext}; use crate::types::PremintMetadata; @@ -207,12 +209,14 @@ mod general { vec![ metadata_rule!(token_uri_length), metadata_rule!(existing_token_uri), + metadata_rule!(signer_matches), + metadata_rule!(version_is_higher), ] } pub async fn token_uri_length( - meta: PremintMetadata, - _context: RuleContext, + meta: &PremintMetadata, + _context: &RuleContext, ) -> eyre::Result { let max_allowed = if meta.uri.starts_with("data:") { // allow some more data for data uris @@ -233,10 +237,10 @@ mod general { } pub async fn existing_token_uri( - meta: PremintMetadata, - context: RuleContext, + meta: &PremintMetadata, + context: &RuleContext, ) -> eyre::Result { - let existing = context.storage.get_for_token_uri(meta.uri).await; + let existing = context.storage.get_for_token_uri(&meta.uri).await; match existing { Err(report) => match report.downcast_ref::() { @@ -259,12 +263,45 @@ mod general { } } } + + pub async fn signer_matches( + meta: &PremintMetadata, + context: &RuleContext, + ) -> eyre::Result { + match &context.existing { + None => Ok(Ignore), + Some(existing) => { + if existing.metadata().signer == meta.signer { + Ok(Accept) + } else { + Ok(Reject("Signer does not match".to_string())) + } + } + } + } + + pub async fn version_is_higher( + meta: &PremintMetadata, + context: &RuleContext, + ) -> eyre::Result { + match &context.existing { + None => Ok(Ignore), + Some(existing) => { + if meta.version > existing.metadata().version { + Ok(Accept) + } else { + Ok(Reject(format!( + "Existing premint with higher version {} exists", + existing.metadata().version + ))) + } + } + } + } } #[cfg(test)] mod test { - use alloy_primitives::U256; - use crate::premints::zora_premint_v2::types::ZoraPremintV2; use crate::rules::general::existing_token_uri; use crate::rules::Evaluation::{Accept, Reject}; @@ -280,13 +317,13 @@ mod test { (re, storage) } - async fn simple_rule(item: PremintTypes, context: RuleContext) -> eyre::Result { + async fn simple_rule(item: &PremintTypes, context: &RuleContext) -> eyre::Result { Ok(Accept) } async fn conditional_rule( - item: PremintTypes, - _context: RuleContext, + item: &PremintTypes, + _context: &RuleContext, ) -> eyre::Result { match item { PremintTypes::Simple(s) => { @@ -301,15 +338,15 @@ mod test { } async fn simple_typed_rule( - _item: SimplePremint, - _context: RuleContext, + _item: &SimplePremint, + _context: &RuleContext, ) -> eyre::Result { Ok(Accept) } async fn simple_typed_zora_rule( - _item: ZoraPremintV2, - _context: RuleContext, + _item: &ZoraPremintV2, + _context: &RuleContext, ) -> eyre::Result { Ok(Accept) } @@ -319,7 +356,7 @@ mod test { let context = RuleContext::test_default().await; let rule = rule!(simple_rule); let result = rule - .check(PremintTypes::Simple(Default::default()), context) + .check(&PremintTypes::Simple(Default::default()), &context) .await .unwrap(); assert!(matches!(result, Accept)); @@ -334,7 +371,7 @@ mod test { engine.add_rule(rule!(conditional_rule)); let result = engine - .evaluate(PremintTypes::Simple(Default::default()), context) + .evaluate(&PremintTypes::Simple(Default::default()), &context) .await; assert!(result.is_accept()); @@ -358,7 +395,7 @@ mod test { engine.add_rule(rule2); let result = engine - .evaluate(PremintTypes::Simple(Default::default()), context) + .evaluate(&PremintTypes::Simple(Default::default()), &context) .await; assert!(result.is_accept()); @@ -369,9 +406,12 @@ mod test { let storage = PremintStorage::new(&Config::test_default()).await; let premint = PremintTypes::Simple(SimplePremint::default()); - let evaluation = existing_token_uri(premint.metadata(), RuleContext::new(storage.clone())) - .await - .expect("Rule execution should not fail"); + let evaluation = existing_token_uri( + &premint.metadata(), + &RuleContext::new(storage.clone(), None), + ) + .await + .expect("Rule execution should not fail"); assert!(matches!(evaluation, Accept)); @@ -381,9 +421,12 @@ mod test { .await .expect("Simple premint should be stored"); - let evaluation = existing_token_uri(premint.metadata(), RuleContext::new(storage.clone())) - .await - .expect("Rule execution should not fail"); + let evaluation = existing_token_uri( + &premint.metadata(), + &RuleContext::new(storage.clone(), None), + ) + .await + .expect("Rule execution should not fail"); // rule should still pass, because it's the same token id assert!(matches!(evaluation, Accept)); @@ -397,9 +440,12 @@ mod test { premint.metadata().uri, ); - let evaluation = existing_token_uri(premint2.metadata(), RuleContext::new(storage.clone())) - .await - .expect("Rule execution should not fail"); + let evaluation = existing_token_uri( + &premint2.metadata(), + &RuleContext::new(storage.clone(), None), + ) + .await + .expect("Rule execution should not fail"); assert!(matches!(evaluation, Reject(_))); } diff --git a/src/storage.rs b/src/storage.rs index aca8c02..fbefa3f 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -160,7 +160,7 @@ impl PremintStorage { PremintTypes::from_json(json) } - pub async fn get_for_token_uri(&self, uri: String) -> eyre::Result { + pub async fn get_for_token_uri(&self, uri: &String) -> eyre::Result { let row = sqlx::query("SELECT json FROM premints WHERE token_uri = ?") .bind(uri) .fetch_one(&self.db)