diff --git a/crates/core/src/db/queries/listings.rs b/crates/core/src/db/queries/listings.rs new file mode 100644 index 000000000..09eaafa91 --- /dev/null +++ b/crates/core/src/db/queries/listings.rs @@ -0,0 +1,183 @@ +//! Query utilities for looking up metadatas +use diesel::prelude::*; +use sea_query::{Condition, Expr, Iden, Order, PostgresQueryBuilder, Query, Value}; + +use crate::{ + db::{models::Nft, Connection}, + error::prelude::*, + prelude::Utc, +}; + +/// Format for incoming filters on attributes +#[derive(Debug)] +pub struct AttributeFilter { + /// name of trait + pub trait_type: String, + /// array of trait values + pub values: Vec, +} + +#[derive(Iden)] +enum Metadatas { + Table, + Address, + Name, + MintAddress, + PrimarySaleHappened, + SellerFeeBasisPoints, + UpdateAuthorityAddress, + Uri, + Slot, + BurnedAt, +} + +#[derive(Iden)] +enum CollectionMints { + Table, + CollectionId, + Mint, +} + +#[derive(Iden)] +enum MetadataJsons { + Table, + MetadataAddress, + Description, + Image, + AnimationUrl, + ExternalUrl, + Category, + Model, +} + +#[derive(Iden)] +enum CurrentMetadataOwners { + Table, + OwnerAddress, + MintAddress, + TokenAccountAddress, +} + +#[derive(Iden)] +enum Listings { + Table, + Price, + Metadata, + AuctionHouse, + Seller, + PurchaseId, + CanceledAt, + Expiry, +} + +/// Input parameters for the `collection_nfts` query. +#[derive(Debug)] +pub struct CollectionListedNftOptions { + /// Collection address + pub collection: String, + /// Auction house of the collection + pub auction_house: Option, + /// Limit the number of returned rows + pub limit: u64, + /// Skip the first `n` resulting rows + pub offset: u64, +} + +/// Handles queries for a Collection Listed Nfts +/// +/// # Errors +/// returns an error when the underlying queries throw an error +#[allow(clippy::too_many_lines)] +pub fn list>( + conn: &Connection, + options: CollectionListedNftOptions, + opensea_auction_house: O, +) -> Result> { + let CollectionListedNftOptions { + collection, + auction_house, + limit, + offset, + } = options; + + let current_time = Utc::now().naive_utc(); + + let query = Query::select() + .columns(vec![ + (Metadatas::Table, Metadatas::Address), + (Metadatas::Table, Metadatas::Name), + (Metadatas::Table, Metadatas::SellerFeeBasisPoints), + (Metadatas::Table, Metadatas::UpdateAuthorityAddress), + (Metadatas::Table, Metadatas::MintAddress), + (Metadatas::Table, Metadatas::PrimarySaleHappened), + (Metadatas::Table, Metadatas::Uri), + (Metadatas::Table, Metadatas::Slot), + ]) + .columns(vec![ + (MetadataJsons::Table, MetadataJsons::Description), + (MetadataJsons::Table, MetadataJsons::Image), + (MetadataJsons::Table, MetadataJsons::AnimationUrl), + (MetadataJsons::Table, MetadataJsons::ExternalUrl), + (MetadataJsons::Table, MetadataJsons::Category), + (MetadataJsons::Table, MetadataJsons::Model), + ]) + .columns(vec![( + CurrentMetadataOwners::Table, + CurrentMetadataOwners::TokenAccountAddress, + )]) + .from(MetadataJsons::Table) + .inner_join( + Metadatas::Table, + Expr::tbl(MetadataJsons::Table, MetadataJsons::MetadataAddress) + .equals(Metadatas::Table, Metadatas::Address), + ) + .inner_join( + CurrentMetadataOwners::Table, + Expr::tbl(Metadatas::Table, Metadatas::MintAddress).equals( + CurrentMetadataOwners::Table, + CurrentMetadataOwners::MintAddress, + ), + ) + .inner_join( + CollectionMints::Table, + Expr::tbl(CollectionMints::Table, CollectionMints::Mint) + .equals(Metadatas::Table, Metadatas::MintAddress), + ) + .left_join( + Listings::Table, + Condition::all() + .add( + Expr::tbl(Listings::Table, Listings::Metadata) + .equals(Metadatas::Table, Metadatas::Address), + ) + .add(Expr::tbl(Listings::Table, Listings::Seller).equals( + CurrentMetadataOwners::Table, + CurrentMetadataOwners::OwnerAddress, + )) + .add(Expr::tbl(Listings::Table, Listings::PurchaseId).is_null()) + .add(Expr::tbl(Listings::Table, Listings::CanceledAt).is_null()) + .add(Expr::tbl(Listings::Table, Listings::AuctionHouse).ne(opensea_auction_house)) + .add( + Expr::tbl(Listings::Table, Listings::Expiry) + .is_null() + .or(Expr::tbl(Listings::Table, Listings::Expiry).gt(current_time)), + ) + .add_option(auction_house.map(|auction_house| { + Expr::col((Listings::Table, Listings::AuctionHouse)).eq(auction_house) + })), + ) + .and_where(Expr::col(Metadatas::BurnedAt).is_null()) + .and_where( + Expr::col((CollectionMints::Table, CollectionMints::CollectionId)).eq(collection), + ) + .limit(limit) + .offset(offset) + .order_by((Listings::Table, Listings::Price), Order::Asc) + .take(); + + let query = query.to_string(PostgresQueryBuilder); + + diesel::sql_query(query) + .load(conn) + .context("Failed to load collection listed nft(s)") +} diff --git a/crates/core/src/db/queries/mod.rs b/crates/core/src/db/queries/mod.rs index 447f51a0f..444dfb027 100644 --- a/crates/core/src/db/queries/mod.rs +++ b/crates/core/src/db/queries/mod.rs @@ -9,6 +9,7 @@ pub mod feed_event; pub mod genopets; pub mod graph_connection; pub mod listing_denylist; +pub mod listings; pub mod metadata_edition; pub mod metadatas; pub mod nft_count; diff --git a/crates/graphql/src/schema/objects/nft.rs b/crates/graphql/src/schema/objects/nft.rs index 459eb85d7..06e517b69 100644 --- a/crates/graphql/src/schema/objects/nft.rs +++ b/crates/graphql/src/schema/objects/nft.rs @@ -2,7 +2,7 @@ use indexer_core::{ assets::{proxy_url, AssetIdentifier, ImageSize}, bigdecimal::ToPrimitive, db::{ - queries::{self, metadatas::CollectionNftOptions}, + queries::{self, listings::CollectionListedNftOptions, metadatas::CollectionNftOptions}, sql_query, sql_types::Text, tables::{ @@ -883,6 +883,313 @@ impl TryFrom for CollectionNFT { } } +// TODO: use collection identifier for the data loader instead of string +#[derive(Debug, Clone)] +pub struct CollectionIdentifier(pub String); + +#[derive(Debug, Clone)] +pub struct CollectionTrend { + pub collection: String, + pub floor_price: U64, + pub nft_count: i32, + pub one_day_volume: U64, + pub seven_day_volume: U64, + pub thirty_day_volume: U64, + pub one_day_sales_count: U64, + pub seven_day_sales_count: U64, + pub thirty_day_sales_count: U64, + pub prev_one_day_volume: U64, + pub prev_seven_day_volume: U64, + pub prev_thirty_day_volume: U64, + pub prev_one_day_sales_count: U64, + pub prev_seven_day_sales_count: U64, + pub prev_thirty_day_sales_count: U64, + pub prev_one_day_floor_price: U64, + pub prev_seven_day_floor_price: U64, + pub prev_thirty_day_floor_price: U64, + pub prev_one_day_marketcap: U64, + pub prev_seven_day_marketcap: U64, + pub prev_thirty_day_marketcap: U64, + pub one_day_volume_change: i32, + pub seven_day_volume_change: i32, + pub thirty_day_volume_change: i32, + pub one_day_floor_price_change: i32, + pub seven_day_floor_price_change: i32, + pub thirty_day_floor_price_change: i32, + pub one_day_sales_count_change: i32, + pub seven_day_sales_count_change: i32, + pub thirty_day_sales_count_change: i32, + pub one_day_marketcap: U64, + pub seven_day_marketcap: U64, + pub thirty_day_marketcap: U64, + pub one_day_marketcap_change: i32, + pub seven_day_marketcap_change: i32, + pub thirty_day_marketcap_change: i32, +} + +impl TryFrom for CollectionTrend { + type Error = std::num::TryFromIntError; + + fn try_from( + models::CollectionTrend { + collection, + floor_price, + nft_count, + one_day_volume, + seven_day_volume, + thirty_day_volume, + one_day_sales_count, + seven_day_sales_count, + thirty_day_sales_count, + prev_one_day_volume, + prev_seven_day_volume, + prev_thirty_day_volume, + prev_one_day_sales_count, + prev_seven_day_sales_count, + prev_thirty_day_sales_count, + prev_one_day_floor_price, + prev_seven_day_floor_price, + prev_thirty_day_floor_price, + prev_one_day_marketcap, + prev_seven_day_marketcap, + prev_thirty_day_marketcap, + one_day_volume_change, + seven_day_volume_change, + thirty_day_volume_change, + one_day_floor_price_change, + seven_day_floor_price_change, + thirty_day_floor_price_change, + one_day_sales_count_change, + seven_day_sales_count_change, + thirty_day_sales_count_change, + one_day_marketcap, + one_day_marketcap_change, + seven_day_marketcap, + seven_day_marketcap_change, + thirty_day_marketcap, + thirty_day_marketcap_change, + }: models::CollectionTrend, + ) -> Result { + Ok(Self { + collection, + floor_price: floor_price.try_into().unwrap_or_default(), + nft_count: nft_count.try_into()?, + one_day_volume: one_day_volume.try_into().unwrap_or_default(), + seven_day_volume: seven_day_volume.try_into().unwrap_or_default(), + thirty_day_volume: thirty_day_volume.try_into().unwrap_or_default(), + one_day_sales_count: one_day_sales_count.try_into().unwrap_or_default(), + seven_day_sales_count: seven_day_sales_count.try_into().unwrap_or_default(), + thirty_day_sales_count: thirty_day_sales_count.try_into().unwrap_or_default(), + prev_one_day_volume: prev_one_day_volume.try_into().unwrap_or_default(), + prev_seven_day_volume: prev_seven_day_volume.try_into().unwrap_or_default(), + prev_thirty_day_volume: prev_thirty_day_volume.try_into().unwrap_or_default(), + prev_one_day_sales_count: prev_one_day_sales_count.try_into().unwrap_or_default(), + prev_seven_day_sales_count: prev_seven_day_sales_count + .to_u64() + .unwrap_or_default() + .into(), + prev_thirty_day_sales_count: prev_thirty_day_sales_count + .to_u64() + .unwrap_or_default() + .into(), + prev_one_day_floor_price: prev_one_day_floor_price.try_into().unwrap_or_default(), + prev_seven_day_floor_price: prev_seven_day_floor_price + .to_u64() + .unwrap_or_default() + .into(), + prev_thirty_day_floor_price: prev_thirty_day_floor_price + .to_u64() + .unwrap_or_default() + .into(), + prev_one_day_marketcap: prev_one_day_marketcap.to_u64().unwrap_or_default().into(), + prev_seven_day_marketcap: prev_seven_day_marketcap.to_u64().unwrap_or_default().into(), + prev_thirty_day_marketcap: prev_thirty_day_marketcap + .to_u64() + .unwrap_or_default() + .into(), + one_day_volume_change: one_day_volume_change.try_into()?, + seven_day_volume_change: seven_day_volume_change.try_into()?, + thirty_day_volume_change: thirty_day_volume_change.try_into()?, + one_day_floor_price_change: one_day_floor_price_change.try_into()?, + seven_day_floor_price_change: seven_day_floor_price_change.try_into()?, + thirty_day_floor_price_change: thirty_day_floor_price_change.try_into()?, + one_day_sales_count_change: one_day_sales_count_change.try_into()?, + seven_day_sales_count_change: seven_day_sales_count_change.try_into()?, + thirty_day_sales_count_change: thirty_day_sales_count_change.try_into()?, + one_day_marketcap: one_day_marketcap.try_into().unwrap_or_default(), + seven_day_marketcap: seven_day_marketcap.try_into().unwrap_or_default(), + thirty_day_marketcap: thirty_day_marketcap.try_into().unwrap_or_default(), + one_day_marketcap_change: one_day_marketcap_change.try_into()?, + seven_day_marketcap_change: seven_day_marketcap_change.try_into()?, + thirty_day_marketcap_change: thirty_day_marketcap_change.try_into()?, + }) + } +} + +#[graphql_object(Context = AppContext)] +impl CollectionTrend { + pub fn floor_price(&self) -> U64 { + self.floor_price + } + + pub fn nft_count(&self) -> i32 { + self.nft_count + } + + pub fn one_day_volume(&self) -> U64 { + self.one_day_volume + } + + pub fn seven_day_volume(&self) -> U64 { + self.seven_day_volume + } + + pub fn thirty_day_volume(&self) -> U64 { + self.thirty_day_volume + } + + pub fn one_day_sales_count(&self) -> U64 { + self.one_day_sales_count + } + + pub fn seven_day_sales_count(&self) -> U64 { + self.seven_day_sales_count + } + + pub fn thirty_day_sales_count(&self) -> U64 { + self.thirty_day_sales_count + } + + pub fn prev_one_day_volume(&self) -> U64 { + self.prev_one_day_volume + } + + pub fn prev_seven_day_volume(&self) -> U64 { + self.prev_seven_day_volume + } + + pub fn prev_thirty_day_volume(&self) -> U64 { + self.prev_thirty_day_volume + } + + pub fn prev_one_day_sales_count(&self) -> U64 { + self.prev_one_day_sales_count + } + + pub fn prev_seven_day_sales_count(&self) -> U64 { + self.prev_seven_day_sales_count + } + + pub fn prev_thirty_day_sales_count(&self) -> U64 { + self.prev_thirty_day_sales_count + } + + pub fn prev_one_day_floor_price(&self) -> U64 { + self.prev_one_day_floor_price + } + + pub fn prev_seven_day_floor_price(&self) -> U64 { + self.prev_seven_day_floor_price + } + + pub fn prev_thirty_day_floor_price(&self) -> U64 { + self.prev_thirty_day_floor_price + } + + pub fn one_day_volume_change(&self) -> i32 { + self.one_day_volume_change + } + + pub fn seven_day_volume_change(&self) -> i32 { + self.seven_day_volume_change + } + + pub fn thirty_day_volume_change(&self) -> i32 { + self.thirty_day_volume_change + } + + pub fn one_day_floor_price_change(&self) -> i32 { + self.one_day_floor_price_change + } + + pub fn seven_day_floor_price_change(&self) -> i32 { + self.seven_day_floor_price_change + } + + pub fn thirty_day_floor_price_change(&self) -> i32 { + self.thirty_day_floor_price_change + } + + pub fn one_day_sales_count_change(&self) -> i32 { + self.one_day_sales_count_change + } + + pub fn seven_day_sales_count_change(&self) -> i32 { + self.seven_day_sales_count_change + } + + pub fn thirty_day_sales_count_change(&self) -> i32 { + self.thirty_day_sales_count_change + } + + pub fn one_day_marketcap(&self) -> U64 { + self.one_day_marketcap + } + + pub fn seven_day_marketcap(&self) -> U64 { + self.one_day_marketcap + } + + pub fn thirty_day_marketcap(&self) -> U64 { + self.one_day_marketcap + } + + pub fn one_day_marketcap_change(&self) -> i32 { + self.one_day_marketcap_change + } + + pub fn seven_day_marketcap_change(&self) -> i32 { + self.one_day_marketcap_change + } + + pub fn thirty_day_marketcap_change(&self) -> i32 { + self.one_day_marketcap_change + } + + pub async fn listings( + &self, + ctx: &AppContext, + limit: i32, + offset: i32, + auction_house: Option, + ) -> FieldResult> { + let conn = ctx.shared.db.get()?; + + let nfts = queries::listings::list( + &conn, + CollectionListedNftOptions { + collection: self.collection.clone(), + auction_house, + limit: limit.try_into()?, + offset: offset.try_into()?, + }, + pubkeys::OPENSEA_AUCTION_HOUSE.to_string(), + )?; + + nfts.into_iter() + .map(TryInto::try_into) + .collect::>() + .map_err(Into::into) + } + + pub async fn collection(&self, ctx: &AppContext) -> FieldResult> { + ctx.generic_collection_loader + .load(self.collection.clone()) + .await + .map_err(Into::into) + } +} + #[derive(Debug, Clone)] pub struct NftCount { creators: Vec>,