From ccbbafe28d170748df4640888df69e0b3f21b345 Mon Sep 17 00:00:00 2001 From: Kyle Espinola Date: Wed, 11 Oct 2023 22:27:42 +0200 Subject: [PATCH] feat: use cache method for total_mints and supply on collections --- api/src/dataloaders/collection.rs | 192 ++++++++++++++++++++++++- api/src/dataloaders/collection_drop.rs | 27 +--- api/src/dataloaders/drop.rs | 34 ++--- api/src/dataloaders/drops.rs | 14 +- api/src/dataloaders/mod.rs | 7 +- api/src/entities/collection_mints.rs | 8 ++ api/src/entities/collections.rs | 1 - api/src/events.rs | 14 -- api/src/handlers.rs | 8 +- api/src/lib.rs | 28 +++- api/src/main.rs | 3 +- api/src/mutations/drop.rs | 47 ++---- api/src/mutations/mint.rs | 53 ++++--- api/src/objects/collection.rs | 45 +++--- api/src/objects/drop.rs | 132 ++++++++++++----- api/src/objects/project.rs | 2 +- 16 files changed, 422 insertions(+), 193 deletions(-) diff --git a/api/src/dataloaders/collection.rs b/api/src/dataloaders/collection.rs index 59d4df2..bb5569a 100644 --- a/api/src/dataloaders/collection.rs +++ b/api/src/dataloaders/collection.rs @@ -2,9 +2,17 @@ use std::collections::HashMap; use async_graphql::{dataloader::Loader as DataLoader, FieldError, Result}; use poem::async_trait; -use sea_orm::prelude::*; +use redis::{AsyncCommands, Client as Redis}; +use sea_orm::{prelude::*, FromQueryResult, QueryFilter, QuerySelect}; -use crate::{db::Connection, entities::collections, objects::Collection}; +use crate::{ + db::Connection, + entities::{ + collection_mints, collections, drops, + sea_orm_active_enums::{CreationStatus, DropType}, + }, + objects::Collection, +}; #[derive(Debug, Clone)] pub struct Loader { @@ -35,3 +43,183 @@ impl DataLoader for Loader { .collect() } } + +#[derive(FromQueryResult)] +struct CollectionTotalMintsCount { + id: Uuid, + count: i64, +} + +#[derive(Debug, Clone)] +pub struct TotalMintsLoader { + pub db: Connection, + pub redis: Redis, +} + +impl TotalMintsLoader { + #[must_use] + pub fn new(db: Connection, redis: Redis) -> Self { + Self { db, redis } + } +} + +#[async_trait] +impl DataLoader for TotalMintsLoader { + type Error = FieldError; + type Value = i64; + + async fn load(&self, keys: &[Uuid]) -> Result, Self::Error> { + let mut results: HashMap = HashMap::new(); + let mut missing_keys: Vec = Vec::new(); + + let mut redis_connection = self.redis.get_async_connection().await?; + + for key in keys { + let redis_key = format!("collection:{key}:total_mints"); + match redis_connection.get::<_, i64>(&redis_key).await { + Ok(value) => { + results.insert(*key, value); + }, + Err(_) => { + missing_keys.push(*key); + }, + } + } + + if missing_keys.is_empty() { + return Ok(results); + } + + let conn = self.db.get(); + let count_results = collection_mints::Entity::find() + .select_only() + .column_as(collection_mints::Column::Id.count(), "count") + .column_as(collection_mints::Column::CollectionId, "id") + .filter( + collection_mints::Column::CollectionId + .is_in(missing_keys.iter().map(ToOwned::to_owned)) + .and(collection_mints::Column::CreationStatus.ne(CreationStatus::Queued)), + ) + .group_by(collection_mints::Column::CollectionId) + .into_model::() + .all(conn) + .await?; + + for count_result in count_results { + let redis_key = format!("collection:{}:total_mints", count_result.id); + + let count = redis_connection + .set::<_, i64, i64>(&redis_key, count_result.count) + .await?; + + results.insert(count_result.id, count); + } + + Ok(results) + } +} + +#[derive(FromQueryResult)] +struct CollectionSupplyCount { + id: Uuid, + count: i64, +} + +#[derive(Debug, Clone)] +pub struct SupplyLoader { + pub db: Connection, + pub redis: Redis, +} + +impl SupplyLoader { + #[must_use] + pub fn new(db: Connection, redis: Redis) -> Self { + Self { db, redis } + } +} + +#[async_trait] +impl DataLoader for SupplyLoader { + type Error = FieldError; + type Value = Option; + + async fn load(&self, keys: &[Uuid]) -> Result, Self::Error> { + let mut results: HashMap = HashMap::new(); + let mut missing_keys: Vec = Vec::new(); + + let mut redis_connection = self.redis.get_async_connection().await?; + + for key in keys { + let redis_key = format!("collection:{key}:supply"); + match redis_connection.get::<_, Option>(&redis_key).await { + Ok(value) => { + results.insert(*key, value); + }, + Err(_) => { + missing_keys.push(*key); + }, + } + } + + if missing_keys.is_empty() { + return Ok(results); + } + + let conn = self.db.get(); + let mut computed_supplies: Vec = Vec::new(); + + let collection_with_drops = collections::Entity::find() + .filter(collections::Column::Id.is_in(missing_keys.iter().map(ToOwned::to_owned))) + .inner_join(drops::Entity) + .select_also(drops::Entity) + .all(conn) + .await?; + + for (collection, drop) in collection_with_drops { + if let Some(drop) = drop { + if drop.drop_type == DropType::Open { + computed_supplies.push(collection.id); + continue; + } + continue; + } + + let redis_key = format!("collection:{}:supply", collection.id); + + let supply = redis_connection + .set::<_, Option, Option>(&redis_key, collection.supply) + .await?; + + results.insert(collection.id, supply); + } + + let count_results = collection_mints::Entity::find() + .select_only() + .column_as(collection_mints::Column::Id.count(), "count") + .column_as(collection_mints::Column::CollectionId, "id") + .filter( + collection_mints::Column::CollectionId + .is_in(computed_supplies.iter().map(ToOwned::to_owned)), + ) + .group_by(collection_mints::Column::CollectionId) + .into_model::() + .all(conn) + .await? + .into_iter() + .map(|result| (result.id, result.count)) + .collect::>(); + + for key in computed_supplies { + let count = count_results.get(&key).copied().unwrap_or_default(); + let redis_key = format!("collection:{key}:supply"); + + let count = redis_connection + .set::<_, Option, Option>(&redis_key, Some(count)) + .await?; + + results.insert(key, count); + } + + Ok(results) + } +} diff --git a/api/src/dataloaders/collection_drop.rs b/api/src/dataloaders/collection_drop.rs index 49642a7..4f12237 100644 --- a/api/src/dataloaders/collection_drop.rs +++ b/api/src/dataloaders/collection_drop.rs @@ -2,13 +2,9 @@ use std::collections::HashMap; use async_graphql::{dataloader::Loader as DataLoader, FieldError, Result}; use poem::async_trait; -use sea_orm::{prelude::*, JoinType, QuerySelect}; +use sea_orm::prelude::*; -use crate::{ - db::Connection, - entities::{collections, drops}, - objects::Drop, -}; +use crate::{db::Connection, entities::drops, objects::Drop}; #[derive(Debug, Clone)] pub struct Loader { @@ -29,26 +25,13 @@ impl DataLoader for Loader { async fn load(&self, keys: &[Uuid]) -> Result, Self::Error> { let drops = drops::Entity::find() - .join(JoinType::InnerJoin, drops::Relation::Collections.def()) - .select_also(collections::Entity) .filter(drops::Column::CollectionId.is_in(keys.iter().map(ToOwned::to_owned))) .all(self.db.get()) .await?; - drops + Ok(drops .into_iter() - .map(|(drop, collection)| { - Ok(( - drop.collection_id, - Drop::new( - drop.clone(), - collection.ok_or(FieldError::new(format!( - "no collection for the drop {}", - drop.id - )))?, - ), - )) - }) - .collect::>>() + .map(|drop| (drop.collection_id, drop.into())) + .collect::>()) } } diff --git a/api/src/dataloaders/drop.rs b/api/src/dataloaders/drop.rs index 3cacca8..118ac9d 100644 --- a/api/src/dataloaders/drop.rs +++ b/api/src/dataloaders/drop.rs @@ -1,21 +1,17 @@ use std::collections::HashMap; -use async_graphql::{dataloader::Loader as DataLoader, FieldError, Result}; +use async_graphql::{dataloader::Loader, FieldError, Result}; use poem::async_trait; -use sea_orm::{prelude::*, JoinType, QuerySelect}; +use sea_orm::prelude::*; -use crate::{ - db::Connection, - entities::{collections, drops}, - objects::Drop, -}; +use crate::{db::Connection, entities::drops, objects::Drop}; #[derive(Debug, Clone)] -pub struct Loader { +pub struct DropLoader { pub db: Connection, } -impl Loader { +impl DropLoader { #[must_use] pub fn new(db: Connection) -> Self { Self { db } @@ -23,31 +19,19 @@ impl Loader { } #[async_trait] -impl DataLoader for Loader { +impl Loader for DropLoader { type Error = FieldError; type Value = Drop; async fn load(&self, keys: &[Uuid]) -> Result, Self::Error> { let drops = drops::Entity::find() - .join(JoinType::InnerJoin, drops::Relation::Collections.def()) - .select_also(collections::Entity) .filter(drops::Column::Id.is_in(keys.iter().map(ToOwned::to_owned))) .all(self.db.get()) .await?; - drops + Ok(drops .into_iter() - .map(|(drop, collection)| { - Ok(( - drop.id, - Drop::new( - drop.clone(), - collection.ok_or_else(|| { - FieldError::new(format!("no collection for the drop {}", drop.id)) - })?, - ), - )) - }) - .collect::>>() + .map(|drop| (drop.id, drop.into())) + .collect()) } } diff --git a/api/src/dataloaders/drops.rs b/api/src/dataloaders/drops.rs index 3572f9e..936e56d 100644 --- a/api/src/dataloaders/drops.rs +++ b/api/src/dataloaders/drops.rs @@ -2,13 +2,9 @@ use std::collections::HashMap; use async_graphql::{dataloader::Loader as DataLoader, FieldError, Result}; use poem::async_trait; -use sea_orm::{prelude::*, JoinType, QuerySelect}; +use sea_orm::prelude::*; -use crate::{ - db::Connection, - entities::{collections, drops}, - objects::Drop, -}; +use crate::{db::Connection, entities::drops, objects::Drop}; #[derive(Debug, Clone)] pub struct ProjectLoader { @@ -29,17 +25,13 @@ impl DataLoader for ProjectLoader { async fn load(&self, keys: &[Uuid]) -> Result, Self::Error> { let drops = drops::Entity::find() - .join(JoinType::InnerJoin, drops::Relation::Collections.def()) - .select_also(collections::Entity) .filter(drops::Column::ProjectId.is_in(keys.iter().map(ToOwned::to_owned))) .all(self.db.get()) .await?; Ok(drops .into_iter() - .filter_map(|(drop, collection)| { - collection.map(|collection| (drop.project_id, Drop::new(drop, collection))) - }) + .map(|drop| (drop.project_id, drop.into())) .fold(HashMap::new(), |mut acc, (project, drop)| { acc.entry(project).or_insert_with(Vec::new); diff --git a/api/src/dataloaders/mod.rs b/api/src/dataloaders/mod.rs index f6648c7..c37100d 100644 --- a/api/src/dataloaders/mod.rs +++ b/api/src/dataloaders/mod.rs @@ -14,14 +14,17 @@ mod project_collections; mod switch_collection_histories; mod update_histories; -pub use collection::Loader as CollectionLoader; +pub use collection::{ + Loader as CollectionLoader, SupplyLoader as CollectionSupplyLoader, + TotalMintsLoader as CollectionTotalMintsLoader, +}; pub use collection_drop::Loader as CollectionDropLoader; pub use collection_mints::{ CollectionMintLoader, Loader as CollectionMintsLoader, OwnerLoader as CollectionMintsOwnerLoader, QueuedMintsLoader, }; pub use creators::Loader as CreatorsLoader; -pub use drop::Loader as DropLoader; +pub use drop::DropLoader; pub use drops::ProjectLoader as ProjectDropsLoader; pub use holders::Loader as HoldersLoader; pub use metadata_json::{ diff --git a/api/src/entities/collection_mints.rs b/api/src/entities/collection_mints.rs index 27dd451..0a8ad80 100644 --- a/api/src/entities/collection_mints.rs +++ b/api/src/entities/collection_mints.rs @@ -88,4 +88,12 @@ impl Entity { .select_also(collections::Entity) .filter(Column::Id.eq(id)) } + + pub fn filter_by_collection(id: Uuid) -> Select { + Self::find().filter( + Column::CollectionId + .eq(id) + .and(Column::CreationStatus.ne(CreationStatus::Queued)), + ) + } } diff --git a/api/src/entities/collections.rs b/api/src/entities/collections.rs index 2c87027..f72abc9 100644 --- a/api/src/entities/collections.rs +++ b/api/src/entities/collections.rs @@ -15,7 +15,6 @@ pub struct Model { #[sea_orm(nullable)] pub credits_deduction_id: Option, pub creation_status: CreationStatus, - pub total_mints: i64, #[sea_orm(column_type = "Text", nullable)] pub address: Option, #[sea_orm(nullable)] diff --git a/api/src/events.rs b/api/src/events.rs index 9cacdb5..5e4f71b 100644 --- a/api/src/events.rs +++ b/api/src/events.rs @@ -8,7 +8,6 @@ use hub_core::{ uuid::{self, Uuid}, }; use sea_orm::{ - sea_query::{Expr, SimpleExpr}, ActiveModelTrait, ColumnTrait, EntityTrait, JoinType, QueryFilter, QuerySelect, RelationTrait, Set, TransactionTrait, }; @@ -350,7 +349,6 @@ impl Processor { project_id: Set(Uuid::from_str(&project_id)?), credits_deduction_id: Set(None), creation_status: Set(CreationStatus::Created), - total_mints: Set(0), address: Set(Some(mint_address)), signature: Set(None), seller_fee_basis_points: Set(seller_fee_basis_points @@ -470,18 +468,6 @@ impl Processor { index_attributes(&self.db, json_model.id, attributes).await?; index_files(&self.db, json_model.id, files).await?; - let collection_id = Uuid::from_str(&collection_id)?; - - collections::Entity::update_many() - .col_expr( - collections::Column::TotalMints, - >::into(Expr::col(collections::Column::TotalMints)) - .add(SimpleExpr::Value(1.into())), - ) - .filter(collections::Column::Id.eq(collection_id)) - .exec(self.db.get()) - .await?; - Ok(()) } diff --git a/api/src/handlers.rs b/api/src/handlers.rs index f79bba5..0012726 100644 --- a/api/src/handlers.rs +++ b/api/src/handlers.rs @@ -31,7 +31,13 @@ pub async fn graphql_handler( balance: Balance, req: GraphQLRequest, ) -> Result { - let context = AppContext::new(state.connection.clone(), user_id, organization, balance); + let context = AppContext::new( + state.connection.clone(), + state.redis.clone(), + user_id, + organization, + balance, + ); Ok(state .schema diff --git a/api/src/lib.rs b/api/src/lib.rs index 9a3c1ee..a3a4de4 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -25,10 +25,11 @@ use blockchains::{polygon::Polygon, solana::Solana}; use dataloaders::{ CollectionDropLoader, CollectionLoader, CollectionMintHistoriesLoader, CollectionMintLoader, CollectionMintMintHistoryLoader, CollectionMintTransfersLoader, CollectionMintsLoader, - CollectionMintsOwnerLoader, CreatorsLoader, DropLoader, DropMintHistoryLoader, HoldersLoader, - MetadataJsonAttributesLoader, MetadataJsonLoader, MintCreatorsLoader, MinterMintHistoryLoader, - ProjectCollectionLoader, ProjectCollectionsLoader, ProjectDropsLoader, QueuedMintsLoader, - SwitchCollectionHistoryLoader, UpdateMintHistoryLoader, + CollectionMintsOwnerLoader, CollectionSupplyLoader, CollectionTotalMintsLoader, CreatorsLoader, + DropLoader, DropMintHistoryLoader, HoldersLoader, MetadataJsonAttributesLoader, + MetadataJsonLoader, MintCreatorsLoader, MinterMintHistoryLoader, ProjectCollectionLoader, + ProjectCollectionsLoader, ProjectDropsLoader, QueuedMintsLoader, SwitchCollectionHistoryLoader, + UpdateMintHistoryLoader, }; use db::Connection; use hub_core::{ @@ -46,6 +47,7 @@ use metrics::Metrics; use mutations::Mutation; use poem::{async_trait, FromRequest, Request, RequestBody}; use queries::Query; +use redis::Client as Redis; #[allow(clippy::pedantic)] pub mod proto { @@ -239,6 +241,7 @@ pub struct AppState { pub polygon: Polygon, pub asset_proxy: AssetProxy, pub metadata_json_upload_job_queue: JobQueue, + pub redis: Redis, } impl AppState { @@ -253,6 +256,7 @@ impl AppState { polygon: Polygon, asset_proxy: AssetProxy, metadata_json_upload_job_queue: JobQueue, + redis: Redis, ) -> Self { Self { schema, @@ -263,6 +267,7 @@ impl AppState { polygon, asset_proxy, metadata_json_upload_job_queue, + redis, } } } @@ -272,6 +277,7 @@ pub struct AppContext { user_id: UserID, organization_id: OrganizationId, balance: Balance, + redis: Redis, project_drops_loader: DataLoader, project_collections_loader: DataLoader, project_collection_loader: DataLoader, @@ -294,6 +300,8 @@ pub struct AppContext { collection_mint_transfers_loader: DataLoader, switch_collection_history_loader: DataLoader, queued_mints_loader: DataLoader, + collection_total_mints_loader: DataLoader, + collection_supply_loader: DataLoader, } impl AppContext { @@ -301,6 +309,7 @@ impl AppContext { #[allow(clippy::similar_names)] pub fn new( db: Connection, + redis: Redis, user_id: UserID, organization_id: OrganizationId, balance: Balance, @@ -346,12 +355,21 @@ impl AppContext { let switch_collection_history_loader = DataLoader::new(SwitchCollectionHistoryLoader::new(db.clone()), tokio::spawn); let queued_mints_loader = DataLoader::new(QueuedMintsLoader::new(db.clone()), tokio::spawn); + let collection_total_mints_loader = DataLoader::new( + CollectionTotalMintsLoader::new(db.clone(), redis.clone()), + tokio::spawn, + ); + let collection_supply_loader = DataLoader::new( + CollectionSupplyLoader::new(db.clone(), redis.clone()), + tokio::spawn, + ); Self { db, user_id, organization_id, balance, + redis, project_drops_loader, project_collections_loader, project_collection_loader, @@ -374,6 +392,8 @@ impl AppContext { collection_mint_transfers_loader, switch_collection_history_loader, queued_mints_loader, + collection_total_mints_loader, + collection_supply_loader, } } } diff --git a/api/src/main.rs b/api/src/main.rs index 0f8f2a1..e7743ac 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -64,7 +64,7 @@ pub fn main() { let metadata_json_upload_task_context = MetadataJsonUploadContext::new(hub_uploads, solana.clone(), polygon.clone()); - let job_queue = JobQueue::new(redis_client, connection.clone()); + let job_queue = JobQueue::new(redis_client.clone(), connection.clone()); let worker = Worker::::new( job_queue.clone(), connection.clone(), @@ -80,6 +80,7 @@ pub fn main() { polygon.clone(), common.asset_proxy, job_queue.clone(), + redis_client, ); let cons = common.consumer_cfg.build::().await?; diff --git a/api/src/mutations/drop.rs b/api/src/mutations/drop.rs index b782290..507189e 100644 --- a/api/src/mutations/drop.rs +++ b/api/src/mutations/drop.rs @@ -4,7 +4,7 @@ use hub_core::{ credits::{CreditsClient, TransactionId}, producer::Producer, }; -use sea_orm::{prelude::*, JoinType, ModelTrait, QuerySelect, Set, TransactionTrait}; +use sea_orm::{prelude::*, ModelTrait, Set, TransactionTrait}; use serde::{Deserialize, Serialize}; use super::collection::{validate_creators, validate_json, validate_solana_creator_verification}; @@ -67,7 +67,7 @@ impl Mutation { let owner_address = fetch_owner(conn, input.project, input.blockchain).await?; let supply = if input.drop_type == DropType::Open { - Some(0) + None } else { input.supply.map(TryInto::try_into).transpose()? }; @@ -161,7 +161,7 @@ impl Mutation { .await?; Ok(CreateDropPayload { - drop: Drop::new(drop_model, collection), + drop: drop_model.into(), }) } @@ -293,9 +293,7 @@ impl Mutation { drop_am.creation_status = Set(CreationStatus::Pending); let drop = drop_am.update(conn).await?; - Ok(CreateDropPayload { - drop: Drop::new(drop, collection), - }) + Ok(CreateDropPayload { drop: drop.into() }) } /// This mutation allows for the temporary blocking of the minting of editions and can be resumed by calling the resumeDrop mutation. pub async fn pause_drop( @@ -306,26 +304,19 @@ impl Mutation { let AppContext { db, .. } = ctx.data::()?; let conn = db.get(); - let (drop, collection) = Drops::find() - .join(JoinType::InnerJoin, drops::Relation::Collections.def()) - .select_also(Collections) + let drop = Drops::find() .filter(drops::Column::Id.eq(input.drop)) .one(conn) .await? .ok_or(Error::new("drop not found"))?; - let collection_model = collection.ok_or(Error::new(format!( - "no collection found for drop {}", - input.drop - )))?; - let mut drops_active_model: drops::ActiveModel = drop.into(); drops_active_model.paused_at = Set(Some(Utc::now().into())); let drop_model = drops_active_model.update(db.get()).await?; Ok(PauseDropPayload { - drop: Drop::new(drop_model, collection_model), + drop: drop_model.into(), }) } @@ -338,27 +329,20 @@ impl Mutation { let AppContext { db, .. } = ctx.data::()?; let conn = db.get(); - let (drop, collection) = Drops::find() - .join(JoinType::InnerJoin, drops::Relation::Collections.def()) - .select_also(Collections) + let drop = Drops::find() .filter(drops::Column::Id.eq(input.drop)) .one(conn) .await? .ok_or(Error::new("drop not found"))?; - let collection_model = collection.ok_or(Error::new(format!( - "no collection found for drop {}", - input.drop - )))?; - let mut drops_active_model: drops::ActiveModel = drop.into(); drops_active_model.paused_at = Set(None); - let drop_model = drops_active_model.update(db.get()).await?; + let drop_model = drops_active_model.update(conn).await?; Ok(ResumeDropPayload { - drop: Drop::new(drop_model, collection_model), + drop: drop_model.into(), }) } @@ -375,19 +359,12 @@ impl Mutation { let AppContext { db, .. } = ctx.data::()?; let conn = db.get(); - let (drop, collection) = Drops::find() - .join(JoinType::InnerJoin, drops::Relation::Collections.def()) - .select_also(Collections) + let drop = Drops::find() .filter(drops::Column::Id.eq(input.drop)) .one(conn) .await? .ok_or(Error::new("drop not found"))?; - let collection_model = collection.ok_or(Error::new(format!( - "no collection found for drop {}", - input.drop - )))?; - let mut drops_active_model: drops::ActiveModel = drop.into(); drops_active_model.shutdown_at = Set(Some(Utc::now().into())); @@ -395,7 +372,7 @@ impl Mutation { let drop_model = drops_active_model.update(db.get()).await?; Ok(ShutdownDropPayload { - drop: Drop::new(drop_model, collection_model), + drop: drop_model.into(), }) } @@ -598,7 +575,7 @@ impl Mutation { tx.commit().await?; Ok(PatchDropPayload { - drop: Drop::new(drop_model, collection), + drop: drop_model.into(), }) } } diff --git a/api/src/mutations/mint.rs b/api/src/mutations/mint.rs index 7af2012..8154127 100644 --- a/api/src/mutations/mint.rs +++ b/api/src/mutations/mint.rs @@ -6,6 +6,7 @@ use hub_core::{ credits::{CreditsClient, TransactionId}, producer::Producer, }; +use redis::AsyncCommands; use sea_orm::{ prelude::*, sea_query::{Func, SimpleExpr}, @@ -62,10 +63,12 @@ impl Mutation { user_id, organization_id, balance, + redis, .. } = ctx.data::()?; let credits = ctx.data::>()?; let conn = db.get(); + let mut redis_conn = redis.get_async_connection().await?; let solana = ctx.data::()?; let polygon = ctx.data::()?; let nfts_producer = ctx.data::>()?; @@ -89,11 +92,17 @@ impl Mutation { // Call check_drop_status to check that drop is currently running check_drop_status(&drop_model)?; - if collection.supply == Some(collection.total_mints) { + let total_mints = collection_mints::Entity::filter_by_collection(collection.id) + .count(conn) + .await?; + + let total_mints = i64::try_from(total_mints)?; + + if collection.supply == Some(total_mints) { return Err(Error::new("Collection is sold out")); } - let edition = collection.total_mints.add(1); + let edition = total_mints.add(1); let owner_address = fetch_owner(conn, collection.project_id, collection.blockchain).await?; @@ -123,10 +132,6 @@ impl Mutation { let collection_mint_model = collection_mint_active_model.insert(conn).await?; - let mut collection_am = collections::ActiveModel::from(collection.clone()); - collection_am.total_mints = Set(edition); - collection_am.update(&tx).await?; - // inserts a mint histories record in the database let mint_history_am = mint_histories::ActiveModel { mint_id: Set(collection_mint_model.id), @@ -220,6 +225,10 @@ impl Mutation { tx.commit().await?; + redis_conn + .del(format!("collection:{}:total_mints", collection.id)) + .await?; + let event_key = NftEventKey { id: collection_mint_model.id.to_string(), user_id: user_id.to_string(), @@ -407,10 +416,12 @@ impl Mutation { user_id, organization_id, balance, + redis, .. } = ctx.data::()?; let credits = ctx.data::>()?; let conn = db.get(); + let mut redis_conn = redis.get_async_connection().await?; let nfts_producer = ctx.data::>()?; let metadata_json_upload_job_queue = ctx.data::()?; @@ -494,10 +505,6 @@ impl Mutation { am.insert(&tx).await?; } - let mut collection_am = collections::ActiveModel::from(collection.clone()); - collection_am.total_mints = Set(collection.total_mints.add(1)); - collection_am.update(&tx).await?; - let mint_history_am = mint_histories::ActiveModel { mint_id: Set(collection_mint_model.id), wallet: Set(input.recipient), @@ -512,6 +519,10 @@ impl Mutation { tx.commit().await?; + redis_conn + .del(format!("collection:{}:total_mints", collection.id)) + .await?; + metadata_json_upload_job_queue .enqueue(MetadataJsonUploadTask { caller: MetadataJsonUploadCaller::MintToCollection( @@ -972,6 +983,7 @@ impl Mutation { user_id, organization_id, balance, + redis, .. } = ctx.data::()?; @@ -980,6 +992,7 @@ impl Mutation { let solana = ctx.data::()?; let conn = db.get(); + let mut redis_conn = redis.get_async_connection().await?; let UserID(id) = user_id; let OrganizationId(org) = organization_id; @@ -1049,12 +1062,6 @@ impl Mutation { let tx = conn.begin().await?; - let mut collection_am = collections::ActiveModel::from(collection.clone()); - - collection_am.total_mints = Set(collection.total_mints.add(1)); - - collection_am.update(&tx).await?; - let mut mint_am: collection_mints::ActiveModel = mint.into(); mint_am.creation_status = Set(CreationStatus::Pending); @@ -1079,6 +1086,10 @@ impl Mutation { tx.commit().await?; + redis_conn + .del(format!("collection:{}:total_mints", collection.id)) + .await?; + match collection.blockchain { BlockchainEnum::Solana => { solana @@ -1138,10 +1149,12 @@ impl Mutation { user_id, organization_id, balance, + redis, .. } = ctx.data::()?; let credits = ctx.data::>()?; let conn = db.get(); + let mut redis_conn = redis.get_async_connection().await?; let solana = ctx.data::()?; let nfts_producer = ctx.data::>()?; @@ -1210,10 +1223,6 @@ impl Mutation { let tx = conn.begin().await?; - let mut collection_am = collections::ActiveModel::from(collection.clone()); - collection_am.total_mints = Set(collection.total_mints.add(1)); - collection_am.update(&tx).await?; - let mut mint_am: collection_mints::ActiveModel = mint.into(); mint_am.creation_status = Set(CreationStatus::Pending); @@ -1238,6 +1247,10 @@ impl Mutation { tx.commit().await?; + redis_conn + .del(format!("collection:{}:total_mints", collection.id)) + .await?; + let event_key = NftEventKey { id: mint.id.to_string(), user_id: user_id.to_string(), diff --git a/api/src/objects/collection.rs b/api/src/objects/collection.rs index e0d2c3d..9399284 100644 --- a/api/src/objects/collection.rs +++ b/api/src/objects/collection.rs @@ -1,4 +1,4 @@ -use async_graphql::{Context, Object, Result}; +use async_graphql::{Context, Error, Object, Result}; use sea_orm::entity::prelude::*; use super::{metadata_json::MetadataJson, CollectionMint, Drop, Holder}; @@ -21,16 +21,12 @@ pub struct Collection { pub id: Uuid, /// The blockchain of the collection. pub blockchain: Blockchain, - /// The total supply of the collection. Setting to `null` implies unlimited minting. - pub supply: Option, /// The creation status of the collection. When the collection is in a `CREATED` status you can mint NFTs from the collection. pub creation_status: CreationStatus, /// The blockchain address of the collection used to view it in blockchain explorers. /// On Solana this is the mint address. /// On EVM chains it is the concatenation of the contract address and the token id `{contractAddress}:{tokenId}`. pub address: Option, - /// The current number of NFTs minted from the collection. - pub total_mints: i64, /// The transaction signature of the collection. pub signature: Option, /// The royalties assigned to mints belonging to the collection expressed in basis points. @@ -57,8 +53,18 @@ impl Collection { self.blockchain } /// The total supply of the collection. Setting to `null` implies unlimited minting. - async fn supply(&self) -> Option { - self.supply + async fn supply(&self, ctx: &Context<'_>) -> Result> { + let AppContext { + collection_supply_loader, + .. + } = ctx.data::()?; + + let supply = collection_supply_loader + .load_one(self.id) + .await? + .ok_or(Error::new("Unable to find collection supply"))?; + + Ok(supply) } /// The creation status of the collection. When the collection is in a `CREATED` status you can mint NFTs from the collection. @@ -91,11 +97,6 @@ impl Collection { self.address.clone() } - /// The current number of NFTs minted from the collection. - async fn total_mints(&self) -> i64 { - self.total_mints - } - /// The transaction signature of the collection. async fn signature(&self) -> Option { self.signature.clone() @@ -176,6 +177,21 @@ impl Collection { collection_drop_loader.load_one(self.id).await } + + /// The current number of NFTs minted from the collection. + async fn total_mints(&self, ctx: &Context<'_>) -> Result { + let AppContext { + collection_total_mints_loader, + .. + } = ctx.data::()?; + + let total_mints = collection_total_mints_loader + .load_one(self.id) + .await? + .ok_or(Error::new("Unable to find collection total mints"))?; + + Ok(total_mints) + } } impl From for Collection { @@ -183,9 +199,7 @@ impl From for Collection { Model { id, blockchain, - supply, creation_status, - total_mints, signature, seller_fee_basis_points, address, @@ -193,15 +207,14 @@ impl From for Collection { credits_deduction_id, created_at, created_by, + .. }: Model, ) -> Self { Self { id, blockchain, - supply, creation_status, address, - total_mints, signature, seller_fee_basis_points, project_id, diff --git a/api/src/objects/drop.rs b/api/src/objects/drop.rs index 500aed5..d4106fb 100644 --- a/api/src/objects/drop.rs +++ b/api/src/objects/drop.rs @@ -5,102 +5,121 @@ use sea_orm::entity::prelude::*; use super::{Collection, CollectionMint}; use crate::{ entities::{ - collections, drops, mint_histories, + drops, mint_histories, sea_orm_active_enums::{CreationStatus, DropType}, }, AppContext, }; + /// An NFT campaign that controls the minting rules for a collection, such as its start date and end date. #[derive(Clone, Debug)] pub struct Drop { - pub drop: drops::Model, - pub collection: collections::Model, -} - -impl Drop { - #[must_use] - pub fn new(drop: drops::Model, collection: collections::Model) -> Self { - Self { drop, collection } - } + pub id: Uuid, + pub drop_type: DropType, + pub project_id: Uuid, + pub collection_id: Uuid, + pub creation_status: CreationStatus, + pub start_time: Option, + pub end_time: Option, + pub price: i64, + pub created_by: Uuid, + pub created_at: DateTimeWithTimeZone, + pub paused_at: Option, + pub shutdown_at: Option, } #[Object] impl Drop { /// The unique identifier for the drop. async fn id(&self) -> Uuid { - self.drop.id + self.id } // The type of the drop. async fn drop_type(&self) -> DropType { - self.drop.drop_type + self.drop_type } /// The identifier of the project to which the drop is associated. async fn project_id(&self) -> Uuid { - self.drop.project_id + self.project_id } /// The creation status of the drop. async fn creation_status(&self) -> CreationStatus { - self.drop.creation_status + self.creation_status } /// The date and time in UTC when the drop is eligible for minting. A value of `null` means the drop can be minted immediately. async fn start_time(&self) -> Option { - self.drop.start_time + self.start_time } /// The end date and time in UTC for the drop. A value of `null` means the drop does not end until it is fully minted. async fn end_time(&self) -> Option { - self.drop.end_time + self.end_time } /// The cost to mint the drop in US dollars. When purchasing with crypto the user will be charged at the current conversion rate for the blockchain's native coin at the time of minting. async fn price(&self) -> i64 { - self.drop.price + self.price } /// The user id of the person who created the drop. async fn created_by_id(&self) -> Uuid { - self.drop.created_by + self.created_by } /// The date and time in UTC when the drop was created. async fn created_at(&self) -> DateTimeWithTimeZone { - self.drop.created_at + self.created_at } // The paused_at field represents the date and time in UTC when the drop was paused. // If it is null, the drop is currently not paused. async fn paused_at(&self) -> Option { - self.drop.paused_at + self.paused_at } /// The shutdown_at field represents the date and time in UTC when the drop was shutdown /// If it is null, the drop is currently not shutdown async fn shutdown_at(&self) -> Option { - self.drop.shutdown_at + self.shutdown_at } /// The collection for which the drop is managing mints. - async fn collection(&self) -> Result { - Ok(self.collection.clone().into()) + async fn collection(&self, ctx: &Context<'_>) -> Result> { + let AppContext { + collection_loader, .. + } = ctx.data::()?; + + collection_loader.load_one(self.collection_id).await } /// The current status of the drop. - async fn status(&self) -> Result { - let now = Utc::now(); - let scheduled = self.drop.start_time.map(|start_time| now < start_time); - let expired = self.drop.end_time.map(|end_time| now > end_time); - let paused_at = self.drop.paused_at; - let shutdown_at = self.drop.shutdown_at; + async fn status(&self, ctx: &Context<'_>) -> Result { + let AppContext { + collection_total_mints_loader, + collection_supply_loader, + .. + } = ctx.data::()?; - let total_mints = self.collection.total_mints; - let minted = self - .collection - .supply - .map(|supply| supply == total_mints && total_mints > 0); + let now = Utc::now(); + let scheduled = self.start_time.map(|start_time| now < start_time); + let expired = self.end_time.map(|end_time| now > end_time); + let paused_at = self.paused_at; + let shutdown_at = self.shutdown_at; + + let total_mints = collection_total_mints_loader + .load_one(self.collection_id) + .await? + .ok_or(Error::new("Unable to find collection total mints"))?; + let supply = collection_supply_loader + .load_one(self.collection_id) + .await? + .ok_or(Error::new("Unable to find collection supply"))?; + + let minted = supply.map(|supply| supply == total_mints && total_mints > 0); match ( scheduled, @@ -108,7 +127,7 @@ impl Drop { minted, paused_at, shutdown_at, - self.drop.creation_status, + self.creation_status, ) { (_, _, _, Some(_), ..) => Ok(DropStatus::Paused), (_, _, _, _, Some(_), _) => Ok(DropStatus::Shutdown), @@ -130,7 +149,9 @@ impl Drop { (_, _, Some(false), ..) | (_, _, None, _, _, CreationStatus::Created) => { Ok(DropStatus::Minting) }, - (_, _, _, _, _, CreationStatus::Queued) => Err(Error::new("Invalid Drop Status")), + (_, _, _, _, _, CreationStatus::Queued) => { + Err(Error::new("Unable to calculate drop status")) + }, } } @@ -140,7 +161,7 @@ impl Drop { .. } = ctx.data::()?; - queued_mints_loader.load_one(self.drop.id).await + queued_mints_loader.load_one(self.id).await } #[graphql(deprecation = "Use `mint_histories` under `Collection` Object instead.")] @@ -151,7 +172,42 @@ impl Drop { .. } = ctx.data::()?; - drop_mint_history_loader.load_one(self.drop.id).await + drop_mint_history_loader.load_one(self.id).await + } +} + +impl From for Drop { + fn from( + drops::Model { + id, + drop_type, + project_id, + collection_id, + creation_status, + start_time, + end_time, + price, + created_by, + created_at, + paused_at, + shutdown_at, + .. + }: drops::Model, + ) -> Self { + Self { + id, + drop_type, + project_id, + collection_id, + creation_status, + start_time, + end_time, + price, + created_by, + created_at, + paused_at, + shutdown_at, + } } } diff --git a/api/src/objects/project.rs b/api/src/objects/project.rs index c0d3715..8925dc3 100644 --- a/api/src/objects/project.rs +++ b/api/src/objects/project.rs @@ -32,7 +32,7 @@ impl Project { let drop = drop_loader.load_one(id).await?; if let Some(drop) = drop { - if drop.drop.project_id == self.id { + if drop.project_id == self.id { return Ok(Some(drop)); }