Skip to content

Commit

Permalink
Merge pull request #38 from ourzora/erik/back-2727-rule-api-customiza…
Browse files Browse the repository at this point in the history
…tion-example

[back 2727] Library usage example
  • Loading branch information
erikreppel authored Apr 19, 2024
2 parents 543a23f + f7977dd commit 58f1707
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 23 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,28 @@ jobs:
with:
command: fmt
args: -- --check

examples-compile:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true

- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: seed
run: cargo install just && just ci

- name: Build
run: cargo build --examples
11 changes: 11 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Examples

Mintpool can be used as either a library or a binary. When using it as a library you can do things
like add API routes to support different query patterns, and rules to modify which messages your
node stores.

To run examples you can use cargo, ex:

```shell
cargo run --example extra_rules_and_routes
```
135 changes: 135 additions & 0 deletions examples/extra_rules_and_routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use async_trait::async_trait;
use axum::extract::State;
use axum::routing::get;
use axum::Json;
use mintpool::api::{start_api, AppState};
use mintpool::metadata_rule;
use mintpool::rules::{Evaluation, Rule, RuleContext, RulesEngine};
use mintpool::storage::Reader;
use mintpool::types::{PremintMetadata, PremintTypes};
use reqwest::StatusCode;
use serde_json::Value;
use sqlx::{Executor, Row};
use tokio::signal::unix::{signal, SignalKind};
use tracing_subscriber::EnvFilter;

#[tokio::main]
async fn main() -> eyre::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.pretty()
.init();

let config = mintpool::config::init();

// Add some custom rules in addition to the defaults
let mut rules = RulesEngine::new_with_default_rules(&config);
rules.add_rule(metadata_rule!(only_odd_token_ids));
rules.add_rule(Box::new(MustStartWithA {}));

let ctl = mintpool::run::start_p2p_services(&config, rules).await?;

// Add some custom routes in addition to the defaults. You could also add middleware or anything else you can do with axum.
let mut router = mintpool::api::router_with_defaults().await;
router = router
.route("/simple", get(my_simple_route))
.route("/count", get(query_route));

start_api(&config, ctl.clone(), router).await?;

let mut sigint = signal(SignalKind::interrupt())?;
let mut sigterm = signal(SignalKind::terminate())?;

tokio::select! {
_ = sigint.recv() => {
tracing::info!("Received SIGINT, shutting down");
}
_ = sigterm.recv() => {
tracing::info!("Received SIGTERM, shutting down");
}
}
Ok(())
}

// rules can be made a few different ways

// This is the most basic way to create a rule, implement the Rule trait for a struct
struct MustStartWithA;

#[async_trait]
impl<T: Reader> Rule<T> for MustStartWithA {
async fn check(
&self,
item: &PremintTypes,
_context: &RuleContext<T>,
) -> eyre::Result<Evaluation> {
match item {
PremintTypes::ZoraV2(premint) => {
if premint
.collection_address
.to_string()
.to_lowercase()
.starts_with("0xa")
{
Ok(Evaluation::Accept)
} else {
Ok(Evaluation::Reject(
"collection address must start with 0xa".to_string(),
))
}
}
_ => Ok(Evaluation::Ignore("not a zora v2 premint".to_string())),
}
}

fn rule_name(&self) -> &'static str {
"collection address must start with 0xa"
}
}

// if you only want your rule to act on metadata, you can use the metadata_rule! macro and write a function that takes a PremintMetadata and RuleContext
async fn only_odd_token_ids<T: Reader>(
metadata: &PremintMetadata,
_context: &RuleContext<T>,
) -> eyre::Result<Evaluation> {
if metadata.token_id.to::<u128>() % 2 == 1 {
Ok(Evaluation::Accept)
} else {
Ok(Evaluation::Reject("token id must be odd".to_string()))
}
}

async fn my_simple_route() -> &'static str {
"wow so simple"
}

// Routes are just axum routes, so you can use the full power of axum to define them.
// routes can use AppState which gives access to the commands channel, and a db connection for queries
// AppState is connected when `start_api(config, controller, router)` is called.
async fn query_route(State(state): State<AppState>) -> (StatusCode, Json<Value>) {
let res = state
.db
.fetch_one("SELECT count(*) as count FROM premints")
.await;

let row = match res {
Ok(row) => row,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": format!("failed to get count, {}", e)})),
)
}
};

match row.try_get::<i64, _>("count") {
Ok(count) => (StatusCode::OK, Json(serde_json::json!({"count": count}))),
Err(e) => {
tracing::error!("Failed to get count: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": format!("failed to get count, {}", e)})),
)
}
}
}
30 changes: 21 additions & 9 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,33 @@ pub struct AppState {
pub db: SqlitePool,
pub controller: ControllerInterface,
}
pub async fn make_router(_config: &Config, controller: ControllerInterface) -> Router {
let (snd, recv) = tokio::sync::oneshot::channel();
controller
.send_command(ControllerCommands::Query(DBQuery::Direct(snd)))
.await
.unwrap();
let db = recv.await.unwrap().expect("Failed to get db");

impl AppState {
pub async fn from(controller: ControllerInterface) -> Self {
let (snd, recv) = tokio::sync::oneshot::channel();
controller
.send_command(ControllerCommands::Query(DBQuery::Direct(snd)))
.await
.unwrap();
let db = recv.await.unwrap().expect("Failed to get db");
Self { db, controller }
}
}

pub async fn router_with_defaults() -> Router<AppState> {
Router::new()
.route("/health", get(health))
.route("/list-all", get(list_all))
.route("/submit-premint", post(submit_premint))
.with_state(AppState { db, controller })
}

pub async fn start_api(config: &Config, router: Router) -> eyre::Result<()> {
pub async fn start_api(
config: &Config,
controller: ControllerInterface,
router: Router<AppState>,
) -> eyre::Result<()> {
let app_state = AppState::from(controller).await;
let router = router.with_state(app_state);
let addr = format!("{}:{}", config.initial_network_ip(), config.api_port);
let listener = TcpListener::bind(addr.clone()).await.unwrap();

Expand Down
11 changes: 7 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use clap::Parser;
use mintpool::api;
use mintpool::premints::zora_premint_v2::types::ZoraPremintV2;
use mintpool::run::{start_services, start_watch_chain};
use mintpool::rules::RulesEngine;
use mintpool::run::{start_p2p_services, start_watch_chain};
use mintpool::stdin::watch_stdin;
use tokio::signal::unix::{signal, SignalKind};
use tracing_subscriber::EnvFilter;
Expand All @@ -20,10 +21,12 @@ async fn main() -> eyre::Result<()> {

tracing::info!("Starting mintpool with config: {:?}", config);

let ctl = start_services(&config).await?;
let mut rules = RulesEngine::new(&config);
rules.add_default_rules();
let ctl = start_p2p_services(&config, rules).await?;

let router = api::make_router(&config, ctl.clone()).await;
api::start_api(&config, router).await?;
let router = api::router_with_defaults().await;
api::start_api(&config, ctl.clone(), router).await?;

start_watch_chain::<ZoraPremintV2>(&config, ctl.clone()).await;
if config.interactive {
Expand Down
1 change: 0 additions & 1 deletion src/premints/zora_premint_v2/rules.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use alloy::hex;
use std::str::FromStr;

use alloy_primitives::Signature;
Expand Down
11 changes: 10 additions & 1 deletion src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ macro_rules! metadata_rule {
&self,
item: &$crate::types::PremintTypes,
context: &$crate::rules::RuleContext<T>,
) -> eyre::Result<crate::rules::Evaluation> {
) -> eyre::Result<$crate::rules::Evaluation> {
$fn(&item.metadata(), context).await
}

Expand Down Expand Up @@ -288,12 +288,21 @@ impl<T: Reader> RulesEngine<T> {
use_rpc: config.enable_rpc,
}
}

pub fn add_rule(&mut self, rule: Box<dyn Rule<T>>) {
self.rules.push(rule);
}

pub fn add_default_rules(&mut self) {
self.rules.extend(all_rules());
}

pub fn new_with_default_rules(config: &Config) -> Self {
let mut engine = Self::new(config);
engine.add_default_rules();
engine
}

pub async fn evaluate(&self, item: &PremintTypes, store: T) -> eyre::Result<Results> {
let metadata = item.metadata();
let existing = match store.get_for_id_and_kind(&metadata.id, metadata.kind).await {
Expand Down
11 changes: 5 additions & 6 deletions src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ use crate::config::{ChainInclusionMode, Config};
use crate::controller::{Controller, ControllerInterface};
use crate::p2p::SwarmController;
use crate::rules::RulesEngine;
use crate::storage::PremintStorage;
use crate::storage::{PremintStorage, Reader};
use crate::types::Premint;

/// Starts the libp2p swarm, the controller, and the checkers if applicable.
/// Returns an interface for interacting with the controller.
/// All interactions with the controller should be done through `ControllerInterface` for memory safety.
pub async fn start_services(config: &Config) -> eyre::Result<ControllerInterface> {
pub async fn start_p2p_services(
config: &Config,
rules: RulesEngine<PremintStorage>,
) -> eyre::Result<ControllerInterface> {
let mut bytes = [0u8; 32];
bytes[0] = config.seed as u8;

Expand All @@ -25,10 +28,6 @@ pub async fn start_services(config: &Config) -> eyre::Result<ControllerInterface

let store = PremintStorage::new(config).await;

// configure rules
let mut rules = RulesEngine::new(config);
rules.add_default_rules();

let mut swarm_controller = SwarmController::new(id_keys, config, swrm_recv, event_send);
let mut controller = Controller::new(swrm_cmd_send, event_recv, ext_cmd_recv, store, rules);
let controller_interface = ControllerInterface::new(ext_cmd_send);
Expand Down
9 changes: 8 additions & 1 deletion tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod factories;
pub mod mintpool_build {
use mintpool::config::{ChainInclusionMode, Config};
use mintpool::controller::{ControllerCommands, ControllerInterface};
use mintpool::rules::RulesEngine;
use tokio::time;

pub async fn announce_all(nodes: Vec<ControllerInterface>) {
Expand Down Expand Up @@ -53,7 +54,13 @@ pub mod mintpool_build {
let mut nodes = Vec::new();
for i in 0..num_nodes {
let config = make_config(start_port + i, peer_limit);
let ctl = mintpool::run::start_services(&config).await.unwrap();

let ctl = mintpool::run::start_p2p_services(
&config,
RulesEngine::new_with_default_rules(&config),
)
.await
.unwrap();
nodes.push(ctl);
}
nodes
Expand Down
5 changes: 4 additions & 1 deletion tests/e2e_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use mintpool::premints::zora_premint_v2::types::IZoraPremintV2::MintArguments;
use mintpool::premints::zora_premint_v2::types::{
IZoraPremintV2, ZoraPremintV2, PREMINT_FACTORY_ADDR,
};
use mintpool::rules::RulesEngine;
use mintpool::run;
use mintpool::types::PremintTypes;
use std::env;
Expand Down Expand Up @@ -66,7 +67,9 @@ async fn test_zora_premint_v2_e2e() {
// set this so CHAINS will use the anvil rpc rather than the one in chains.json
env::set_var("CHAIN_7777777_RPC_WSS", anvil.ws_endpoint());

let ctl = run::start_services(&config).await.unwrap();
let ctl = run::start_p2p_services(&config, RulesEngine::new_with_default_rules(&config))
.await
.unwrap();
run::start_watch_chain::<ZoraPremintV2>(&config, ctl.clone()).await;

// ============================================================================================
Expand Down

0 comments on commit 58f1707

Please sign in to comment.