diff --git a/.github/workflows/dispatch_to_docs.yml b/.github/workflows/dispatch_to_docs.yml new file mode 100644 index 000000000..766fac860 --- /dev/null +++ b/.github/workflows/dispatch_to_docs.yml @@ -0,0 +1,17 @@ +name: Send event to rebuild docs + +on: + push: + branches: [dev, master] + +jobs: + dispatch: + runs-on: ubuntu-latest + steps: + - name: Dispatch event to docs repo + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.DOCS_REPO_ACCESS_TOKEN }} + repository: holaplex/marketplace-api-docs + event-type: api_update + client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}' diff --git a/crates/core/migrations/2022-04-07-154737_remove_duplicate_attributes/down.sql b/crates/core/migrations/2022-04-07-154737_remove_duplicate_attributes/down.sql new file mode 100644 index 000000000..ee1b7e592 --- /dev/null +++ b/crates/core/migrations/2022-04-07-154737_remove_duplicate_attributes/down.sql @@ -0,0 +1,5 @@ +drop table attributes; + +alter table temp_attributes rename to attributes; + +drop function create_temp_attributes_table(); \ No newline at end of file diff --git a/crates/core/migrations/2022-04-07-154737_remove_duplicate_attributes/up.sql b/crates/core/migrations/2022-04-07-154737_remove_duplicate_attributes/up.sql new file mode 100644 index 000000000..d7c2bf78e --- /dev/null +++ b/crates/core/migrations/2022-04-07-154737_remove_duplicate_attributes/up.sql @@ -0,0 +1,34 @@ +create function create_temp_attributes_table() + returns void + language plpgsql as +$func$ +begin + if not exists (select from pg_catalog.pg_tables + where schemaname = 'public' + and tablename = 'temp_attributes') then + + alter table attributes rename to temp_attributes; + + create table attributes ( + metadata_address varchar(48) not null, + value text, + trait_type text, + id uuid primary key default gen_random_uuid(), + first_verified_creator varchar(48) null, + unique (metadata_address, value, trait_type) + ); + + create index if not exists attr_metadata_address_index on + attributes using hash (metadata_address); + + create index if not exists attr_first_verified_creator_index + on attributes (first_verified_creator); + + create index if not exists attr_trait_type_value_index + on attributes (trait_type, value); + + end if; +end +$func$; + +select create_temp_attributes_table(); \ No newline at end of file diff --git a/crates/core/migrations/2022-04-08-134331_add_buyer_index_to_bid_receipts/down.sql b/crates/core/migrations/2022-04-08-134331_add_buyer_index_to_bid_receipts/down.sql new file mode 100644 index 000000000..649a43e10 --- /dev/null +++ b/crates/core/migrations/2022-04-08-134331_add_buyer_index_to_bid_receipts/down.sql @@ -0,0 +1 @@ +drop index if exists buyer_bid_receipts_idx; \ No newline at end of file diff --git a/crates/core/migrations/2022-04-08-134331_add_buyer_index_to_bid_receipts/up.sql b/crates/core/migrations/2022-04-08-134331_add_buyer_index_to_bid_receipts/up.sql new file mode 100644 index 000000000..699838e1b --- /dev/null +++ b/crates/core/migrations/2022-04-08-134331_add_buyer_index_to_bid_receipts/up.sql @@ -0,0 +1,2 @@ +create index if not exists buyer_bid_receipts_idx on + bid_receipts using hash (buyer); \ No newline at end of file diff --git a/crates/core/migrations/2022-04-08-134927_add_seller_index_to_listing_receipts/down.sql b/crates/core/migrations/2022-04-08-134927_add_seller_index_to_listing_receipts/down.sql new file mode 100644 index 000000000..e936222be --- /dev/null +++ b/crates/core/migrations/2022-04-08-134927_add_seller_index_to_listing_receipts/down.sql @@ -0,0 +1 @@ +drop index if exists seller_listing_receipts_idx; \ No newline at end of file diff --git a/crates/core/migrations/2022-04-08-134927_add_seller_index_to_listing_receipts/up.sql b/crates/core/migrations/2022-04-08-134927_add_seller_index_to_listing_receipts/up.sql new file mode 100644 index 000000000..bc11af084 --- /dev/null +++ b/crates/core/migrations/2022-04-08-134927_add_seller_index_to_listing_receipts/up.sql @@ -0,0 +1,2 @@ +create index if not exists seller_listing_receipts_idx on + listing_receipts using hash (seller); \ No newline at end of file diff --git a/crates/core/src/db/mod.rs b/crates/core/src/db/mod.rs index b28ef8555..bcc698eab 100644 --- a/crates/core/src/db/mod.rs +++ b/crates/core/src/db/mod.rs @@ -16,7 +16,7 @@ use std::env; pub use diesel::{ backend::Backend, - debug_query, delete, insert_into, + debug_query, delete, expression, insert_into, pg::{upsert::excluded, Pg}, query_dsl, result::Error, diff --git a/crates/core/src/db/queries/mod.rs b/crates/core/src/db/queries/mod.rs index 36ee2227a..5448bba63 100644 --- a/crates/core/src/db/queries/mod.rs +++ b/crates/core/src/db/queries/mod.rs @@ -4,6 +4,7 @@ pub mod graph_connection; pub mod listing_denylist; pub mod metadata_edition; pub mod metadatas; +pub mod nft_count; pub mod stats; pub mod store_denylist; pub mod twitter_handle_name_service; diff --git a/crates/core/src/db/queries/nft_count.rs b/crates/core/src/db/queries/nft_count.rs new file mode 100644 index 000000000..dbfe82116 --- /dev/null +++ b/crates/core/src/db/queries/nft_count.rs @@ -0,0 +1,219 @@ +//! Query utilities for looking up nft counts + +use diesel::{ + expression::{operators::Eq, AsExpression, NonAggregate}, + pg::Pg, + prelude::*, + query_builder::QueryFragment, + query_source::joins::{Inner, Join, JoinOn}, + serialize::ToSql, + sql_types::Text, + AppearsOnTable, +}; + +use crate::{ + db::{ + any, + tables::{bid_receipts, listing_receipts, metadata_creators, metadatas, token_accounts}, + Connection, + }, + error::prelude::*, +}; + +/// Handles queries for total nfts count +/// +/// # Errors +/// returns an error when the underlying queries throw an error +pub fn total>(conn: &Connection, creators: &[C]) -> Result { + metadatas::table + .inner_join( + metadata_creators::table.on(metadatas::address.eq(metadata_creators::metadata_address)), + ) + .filter(metadata_creators::creator_address.eq(any(creators))) + .filter(metadata_creators::verified.eq(true)) + .count() + .get_result(conn) + .context("failed to load total nfts count") +} + +/// Handles queries for listed nfts count +/// +/// # Errors +/// returns an error when the underlying queries throw an error +pub fn listed, L: ToSql>( + conn: &Connection, + creators: &[C], + listed: Option<&[L]>, +) -> Result { + let mut query = metadatas::table + .inner_join( + metadata_creators::table.on(metadatas::address.eq(metadata_creators::metadata_address)), + ) + .inner_join(listing_receipts::table.on(metadatas::address.eq(listing_receipts::metadata))) + .into_boxed(); + + if let Some(listed) = listed { + query = query.filter(listing_receipts::auction_house.eq(any(listed))); + } + + query + .filter(metadata_creators::creator_address.eq(any(creators))) + .filter(metadata_creators::verified.eq(true)) + .filter(listing_receipts::purchase_receipt.is_null()) + .filter(listing_receipts::canceled_at.is_null()) + .count() + .get_result(conn) + .context("failed to load listed nfts count") +} + +/// Handles queries for owned nfts count +/// +/// # Errors +/// returns an error when the underlying queries throw an error +pub fn owned, C: ToSql>( + conn: &Connection, + wallet: W, + creators: Option<&[C]>, +) -> Result +where + W::Expression: NonAggregate + + QueryFragment + + AppearsOnTable< + JoinOn< + Join< + JoinOn< + Join, + Eq, + >, + token_accounts::table, + Inner, + >, + Eq, + >, + >, +{ + let mut query = metadatas::table + .inner_join( + metadata_creators::table.on(metadatas::address.eq(metadata_creators::metadata_address)), + ) + .inner_join( + token_accounts::table.on(metadatas::mint_address.eq(token_accounts::mint_address)), + ) + .into_boxed(); + + if let Some(creators) = creators { + query = query.filter(metadata_creators::creator_address.eq(any(creators))); + } + + query + .filter(metadata_creators::verified.eq(true)) + .filter(token_accounts::amount.eq(1)) + .filter(token_accounts::owner_address.eq(wallet)) + .count() + .get_result(conn) + .context("failed to load owned nfts count") +} + +/// Handles queries for nfts count for a wallet with optional creators and auction house filters +/// +/// # Errors +/// returns an error when the underlying queries throw an error +pub fn offered, C: ToSql, H: ToSql>( + conn: &Connection, + wallet: W, + creators: Option<&[C]>, + auction_houses: Option<&[H]>, +) -> Result +where + W::Expression: NonAggregate + + QueryFragment + + AppearsOnTable< + JoinOn< + Join< + JoinOn< + Join, + Eq, + >, + bid_receipts::table, + Inner, + >, + Eq, + >, + >, +{ + let mut query = metadatas::table + .inner_join( + metadata_creators::table.on(metadatas::address.eq(metadata_creators::metadata_address)), + ) + .inner_join(bid_receipts::table.on(metadatas::address.eq(bid_receipts::metadata))) + .into_boxed(); + + if let Some(auction_houses) = auction_houses { + query = query.filter(bid_receipts::auction_house.eq(any(auction_houses))); + } + + if let Some(creators) = creators { + query = query.filter(metadata_creators::creator_address.eq(any(creators))); + } + + query + .filter(metadata_creators::verified.eq(true)) + .filter(bid_receipts::buyer.eq(wallet)) + .filter(bid_receipts::purchase_receipt.is_null()) + .filter(bid_receipts::canceled_at.is_null()) + .count() + .get_result(conn) + .context("failed to load nfts count of open offers for a wallet") +} + +/// Handles queries for wallet listed nfts count +/// +/// # Errors +/// returns an error when the underlying queries throw an error +pub fn wallet_listed, C: ToSql, L: ToSql>( + conn: &Connection, + wallet: W, + creators: Option<&[C]>, + listed: Option<&[L]>, +) -> Result +where + W::Expression: NonAggregate + + QueryFragment + + AppearsOnTable< + JoinOn< + Join< + JoinOn< + Join, + Eq, + >, + listing_receipts::table, + Inner, + >, + Eq, + >, + >, +{ + let mut query = metadatas::table + .inner_join( + metadata_creators::table.on(metadatas::address.eq(metadata_creators::metadata_address)), + ) + .inner_join(listing_receipts::table.on(metadatas::address.eq(listing_receipts::metadata))) + .into_boxed(); + + if let Some(listed) = listed { + query = query.filter(listing_receipts::auction_house.eq(any(listed))); + } + + if let Some(creators) = creators { + query = query.filter(metadata_creators::creator_address.eq(any(creators))); + } + + query + .filter(metadata_creators::verified.eq(true)) + .filter(listing_receipts::purchase_receipt.is_null()) + .filter(listing_receipts::canceled_at.is_null()) + .filter(listing_receipts::seller.eq(wallet)) + .count() + .get_result(conn) + .context("failed to load listed nfts count") +} diff --git a/crates/core/src/db/queries/twitter_handle_name_service.rs b/crates/core/src/db/queries/twitter_handle_name_service.rs index 83bb16d6b..4416555c8 100644 --- a/crates/core/src/db/queries/twitter_handle_name_service.rs +++ b/crates/core/src/db/queries/twitter_handle_name_service.rs @@ -1,6 +1,12 @@ //! Query utilities for `graph_connections` table. -use diesel::OptionalExtension; +use diesel::{ + expression::{AsExpression, NonAggregate}, + pg::Pg, + query_builder::{QueryFragment, QueryId}, + sql_types::Text, + AppearsOnTable, OptionalExtension, +}; use crate::{ db::{tables::twitter_handle_name_services, Connection}, @@ -12,11 +18,17 @@ use crate::{ /// /// # Errors /// This function fails if the underlying query fails to execute. -pub fn get(conn: &Connection, address: String) -> Result> { +pub fn get>(conn: &Connection, address: A) -> Result> +where + A::Expression: NonAggregate + + QueryId + + QueryFragment + + AppearsOnTable, +{ twitter_handle_name_services::table .filter(twitter_handle_name_services::wallet_address.eq(address)) .select(twitter_handle_name_services::twitter_handle) - .first::(conn) + .first(conn) .optional() .context("Failed to load twitter handle") } diff --git a/crates/core/src/db/schema.rs b/crates/core/src/db/schema.rs index da74ce0bf..d8c7ade0f 100644 --- a/crates/core/src/db/schema.rs +++ b/crates/core/src/db/schema.rs @@ -763,6 +763,20 @@ table! { } } +table! { + use diesel::sql_types::*; + use diesel_full_text_search::{TsVector as Tsvector, TsQuery as Tsquery}; + use crate::db::custom_types::{SettingType as Settingtype, Mode, TokenStandard as Token_standard}; + + temp_attributes (id) { + metadata_address -> Varchar, + value -> Nullable, + trait_type -> Nullable, + id -> Uuid, + first_verified_creator -> Nullable, + } +} + table! { use diesel::sql_types::*; use diesel_full_text_search::{TsVector as Tsvector, TsQuery as Tsquery}; @@ -916,6 +930,7 @@ allow_tables_to_appear_in_same_query!( storefronts, stores, sub_account_infos, + temp_attributes, token_accounts, transactions, twitter_handle_name_services, diff --git a/crates/graphql/src/schema/dataloaders/mod.rs b/crates/graphql/src/schema/dataloaders/mod.rs index 9c9a65596..794a8d804 100644 --- a/crates/graphql/src/schema/dataloaders/mod.rs +++ b/crates/graphql/src/schema/dataloaders/mod.rs @@ -19,7 +19,8 @@ pub(self) mod prelude { pub(super) use super::{ super::prelude::*, batcher::{ - BatchIter, BatchMap, BatchResult, Batcher, TryBatchFn, TryBatchMap, TwitterBatcher, + BatchIter, BatchMap, BatchResult, Batcher, Error, TryBatchFn, TryBatchMap, + TwitterBatcher, }, }; } diff --git a/crates/graphql/src/schema/dataloaders/wallet.rs b/crates/graphql/src/schema/dataloaders/wallet.rs index 65b8c1ca3..c8ae6c43d 100644 --- a/crates/graphql/src/schema/dataloaders/wallet.rs +++ b/crates/graphql/src/schema/dataloaders/wallet.rs @@ -2,7 +2,7 @@ use futures_util::future::join_all; use itertools::Either; use objects::profile::{TwitterProfile, TwitterUserProfileResponse}; -use super::{batcher::Error, prelude::*}; +use super::prelude::*; const TWITTER_SCREEN_NAME_CHUNKS: usize = 100; diff --git a/crates/graphql/src/schema/objects/auction_house.rs b/crates/graphql/src/schema/objects/auction_house.rs index d19890f94..1bda70753 100644 --- a/crates/graphql/src/schema/objects/auction_house.rs +++ b/crates/graphql/src/schema/objects/auction_house.rs @@ -3,8 +3,10 @@ use objects::stats::MintStats; use super::prelude::*; #[derive(Debug, Clone)] +/// A Metaplex auction house pub struct AuctionHouse { pub address: String, + /// Mint address of the token in which fees are vendored pub treasury_mint: String, pub auction_house_treasury: String, pub treasury_withdrawal_destination: String, @@ -17,6 +19,7 @@ pub struct AuctionHouse { pub seller_fee_basis_points: i32, pub requires_sign_off: bool, pub can_change_sale_price: bool, + /// Account for which fees are paid out to pub auction_house_fee_account: String, } diff --git a/crates/graphql/src/schema/objects/creator.rs b/crates/graphql/src/schema/objects/creator.rs index fa366d41e..ef139dd87 100644 --- a/crates/graphql/src/schema/objects/creator.rs +++ b/crates/graphql/src/schema/objects/creator.rs @@ -3,12 +3,13 @@ use std::collections::HashMap; use indexer_core::{db::queries::stats, prelude::*}; use itertools::Itertools; use objects::{auction_house::AuctionHouse, profile::TwitterProfile, stats::MintStats}; +use scalars::PublicKey; use tables::{attributes, metadata_creators}; use super::prelude::*; -use crate::schema::scalars::PublicKey; #[derive(Debug, Clone)] +/// A creator associated with a marketplace pub struct Creator { pub address: String, pub twitter_handle: Option, @@ -42,11 +43,11 @@ impl CreatorCounts { fn creations(&self, context: &AppContext) -> FieldResult { let conn = context.db_pool.get()?; - let count = metadata_creators::table + let count: i64 = metadata_creators::table .filter(metadata_creators::creator_address.eq(&self.creator.address)) .filter(metadata_creators::verified.eq(true)) .count() - .get_result::(&conn)?; + .get_result(&conn)?; Ok(count.try_into()?) } @@ -130,13 +131,10 @@ impl Creator { } pub async fn profile(&self, ctx: &AppContext) -> FieldResult> { - let twitter_handle = self.twitter_handle.clone(); - - if twitter_handle.is_none() { - return Ok(None); - } - - let twitter_handle = twitter_handle.unwrap(); + let twitter_handle = match self.twitter_handle { + Some(ref t) => t.clone(), + None => return Ok(None), + }; ctx.twitter_profile_loader .load(twitter_handle) diff --git a/crates/graphql/src/schema/objects/denylist.rs b/crates/graphql/src/schema/objects/denylist.rs index 5bdbcde65..989201e21 100644 --- a/crates/graphql/src/schema/objects/denylist.rs +++ b/crates/graphql/src/schema/objects/denylist.rs @@ -5,6 +5,7 @@ use scalars::PublicKey; use super::prelude::*; #[derive(Debug, Clone, Copy)] +/// Deny-list for Holaplex storefronts and listings pub struct Denylist; #[graphql_object(Context = AppContext)] diff --git a/crates/graphql/src/schema/objects/graph_connection.rs b/crates/graphql/src/schema/objects/graph_connection.rs index 92c4a1d03..4f80ec69f 100644 --- a/crates/graphql/src/schema/objects/graph_connection.rs +++ b/crates/graphql/src/schema/objects/graph_connection.rs @@ -25,10 +25,8 @@ impl GraphConnection { } } -impl TryFrom for GraphConnection { - type Error = std::num::TryFromIntError; - - fn try_from( +impl From for GraphConnection { + fn from( models::TwitterEnrichedGraphConnection { connection_address, from_account, @@ -36,11 +34,11 @@ impl TryFrom for GraphConnection { from_twitter_handle, to_twitter_handle, }: models::TwitterEnrichedGraphConnection, - ) -> Result { - Ok(Self { + ) -> Self { + Self { address: connection_address, - from: Wallet::new(from_account, from_twitter_handle), - to: Wallet::new(to_account, to_twitter_handle), - }) + from: Wallet::new(from_account.into(), from_twitter_handle), + to: Wallet::new(to_account.into(), to_twitter_handle), + } } } diff --git a/crates/graphql/src/schema/objects/listing.rs b/crates/graphql/src/schema/objects/listing.rs index fe0defd38..62e2e7507 100644 --- a/crates/graphql/src/schema/objects/listing.rs +++ b/crates/graphql/src/schema/objects/listing.rs @@ -5,6 +5,7 @@ use tables::{auction_caches, auction_datas, auction_datas_ext}; use super::prelude::*; #[derive(Debug, Clone)] +/// A bid on an NFT listing pub struct Bid { pub listing_address: String, pub bidder_address: String, @@ -89,6 +90,7 @@ pub type ListingRow = ( ); #[derive(Debug, Clone)] +/// A listing of for sale of an NFT pub struct Listing { pub address: String, pub ext_address: String, diff --git a/crates/graphql/src/schema/objects/listing_receipt.rs b/crates/graphql/src/schema/objects/listing_receipt.rs index a49ef56f4..1c1c2ecf3 100644 --- a/crates/graphql/src/schema/objects/listing_receipt.rs +++ b/crates/graphql/src/schema/objects/listing_receipt.rs @@ -1,6 +1,7 @@ use super::prelude::*; #[derive(Debug, Clone, GraphQLObject)] +#[graphql(description = "An NFT listing receipt")] pub struct ListingReceipt { pub address: String, pub trade_state: String, @@ -19,6 +20,7 @@ pub struct ListingReceipt { impl<'a> TryFrom> for ListingReceipt { type Error = std::num::TryFromIntError; + fn try_from( models::ListingReceipt { address, @@ -48,7 +50,7 @@ impl<'a> TryFrom> for ListingReceipt { canceled_at: canceled_at.map(|c| DateTime::from_utc(c, Utc)), bookkeeper: bookkeeper.into_owned(), purchase_receipt: purchase_receipt.map(Cow::into_owned), - token_size: token_size.try_into().unwrap(), + token_size: token_size.try_into()?, bump: bump.into(), }) } diff --git a/crates/graphql/src/schema/objects/marketplace.rs b/crates/graphql/src/schema/objects/marketplace.rs index 00660f31c..115b9ee03 100644 --- a/crates/graphql/src/schema/objects/marketplace.rs +++ b/crates/graphql/src/schema/objects/marketplace.rs @@ -3,6 +3,7 @@ use objects::{auction_house::AuctionHouse, stats::MarketStats, store_creator::St use super::prelude::*; #[derive(Debug, Clone)] +/// An Holaplex marketplace pub struct Marketplace { pub config_address: String, pub subdomain: String, diff --git a/crates/graphql/src/schema/objects/nft.rs b/crates/graphql/src/schema/objects/nft.rs index b81e1ad7f..392ea2207 100644 --- a/crates/graphql/src/schema/objects/nft.rs +++ b/crates/graphql/src/schema/objects/nft.rs @@ -1,13 +1,14 @@ use base64::display::Base64Display; use indexer_core::{ assets::{AssetHint, AssetIdentifier, ImageSize}, - db::models, + db::queries, }; use objects::{ - bid_receipt::BidReceipt, listing_receipt::ListingReceipt, profile::TwitterProfile, - purchase_receipt::PurchaseReceipt, + auction_house::AuctionHouse, bid_receipt::BidReceipt, listing_receipt::ListingReceipt, + profile::TwitterProfile, purchase_receipt::PurchaseReceipt, }; use reqwest::Url; +use scalars::PublicKey; use super::prelude::*; @@ -57,6 +58,7 @@ impl<'a> TryFrom> for NftAttribute { } #[derive(Debug, Clone)] +/// An NFT creator pub struct NftCreator { pub address: String, pub metadata_address: String, @@ -93,13 +95,10 @@ impl NftCreator { } pub async fn profile(&self, ctx: &AppContext) -> FieldResult> { - let twitter_handle = self.twitter_handle.clone(); - - if twitter_handle.is_none() { - return Ok(None); - } - - let twitter_handle = twitter_handle.unwrap(); + let twitter_handle = match self.twitter_handle { + Some(ref t) => t.clone(), + None => return Ok(None), + }; ctx.twitter_profile_loader .load(twitter_handle) @@ -154,13 +153,10 @@ impl NftOwner { } pub async fn profile(&self, ctx: &AppContext) -> FieldResult> { - let twitter_handle = self.twitter_handle.clone(); - - if twitter_handle.is_none() { - return Ok(None); - } - - let twitter_handle = twitter_handle.unwrap(); + let twitter_handle = match self.twitter_handle { + Some(ref t) => t.clone(), + None => return Ok(None), + }; ctx.twitter_profile_loader .load(twitter_handle) @@ -207,6 +203,7 @@ impl TryFrom for NftActivity { } #[derive(Debug, Clone)] +/// An NFT pub struct Nft { pub address: String, pub name: String, @@ -290,9 +287,8 @@ If no value is provided, it will return XSmall")))] id.fingerprint(Some(hint)) .unwrap_or_else(|| unreachable!()) .as_ref(), - ) - .to_vec()[0] - .rem_euclid(shared.asset_proxy_count); + )[0] + .rem_euclid(shared.asset_proxy_count); let assets_cdn = &shared.asset_proxy_endpoint; let mut url = Url::parse(&assets_cdn.replace( @@ -402,3 +398,39 @@ If no value is provided, it will return XSmall")))] .map_err(Into::into) } } + +#[derive(Debug, Clone)] +pub struct NftCount { + creators: Vec>, +} + +impl NftCount { + #[must_use] + pub fn new(creators: Vec>) -> Self { + Self { creators } + } +} + +#[graphql_object(Context = AppContext)] +impl NftCount { + fn total(&self, context: &AppContext) -> FieldResult { + let conn = context.db_pool.get()?; + + let count = queries::nft_count::total(&conn, &self.creators)?; + + Ok(count.try_into()?) + } + + #[graphql(arguments(auction_houses(description = "a list of auction house public keys")))] + fn listed( + &self, + context: &AppContext, + auction_houses: Option>>, + ) -> FieldResult { + let conn = context.db_pool.get()?; + + let count = queries::nft_count::listed(&conn, &self.creators, auction_houses.as_deref())?; + + Ok(count.try_into()?) + } +} diff --git a/crates/graphql/src/schema/objects/profile.rs b/crates/graphql/src/schema/objects/profile.rs index 776c21f0f..1fe81aa03 100644 --- a/crates/graphql/src/schema/objects/profile.rs +++ b/crates/graphql/src/schema/objects/profile.rs @@ -1,4 +1,5 @@ use serde::Deserialize; +use tables::twitter_handle_name_services; use super::prelude::*; @@ -63,6 +64,21 @@ pub struct TwitterUserProfileResponse { #[graphql_object(Context = AppContext)] impl Profile { + fn wallet_address(&self, ctx: &AppContext) -> FieldResult> { + let db_conn = ctx.db_pool.get()?; + let result: Vec = twitter_handle_name_services::table + .select(twitter_handle_name_services::all_columns) + .limit(1) + .filter(twitter_handle_name_services::twitter_handle.eq(&self.handle)) + .load(&db_conn) + .context("Failed to load wallet address")?; + if result.is_empty() { + return Ok(None); + } + let matching_item = result.get(0).unwrap(); + let wallet_address = &matching_item.wallet_address; + Ok(Some(wallet_address.to_string())) + } fn handle(&self) -> &str { &self.handle } diff --git a/crates/graphql/src/schema/objects/store_creator.rs b/crates/graphql/src/schema/objects/store_creator.rs index b595ee9a7..cff5b20d6 100644 --- a/crates/graphql/src/schema/objects/store_creator.rs +++ b/crates/graphql/src/schema/objects/store_creator.rs @@ -1,4 +1,6 @@ -use super::{nft::Nft, prelude::*}; +use objects::nft::Nft; + +use super::prelude::*; #[derive(Debug, Clone)] pub struct StoreCreator { diff --git a/crates/graphql/src/schema/objects/wallet.rs b/crates/graphql/src/schema/objects/wallet.rs index c6531c7dc..6cc2969a3 100644 --- a/crates/graphql/src/schema/objects/wallet.rs +++ b/crates/graphql/src/schema/objects/wallet.rs @@ -1,16 +1,20 @@ -use objects::{listing::Bid, profile::TwitterProfile}; -use tables::bids; +use indexer_core::db::queries; +use objects::{ + auction_house::AuctionHouse, listing::Bid, nft::NftCreator, profile::TwitterProfile, +}; +use scalars::PublicKey; +use tables::{bids, graph_connections}; use super::prelude::*; #[derive(Debug, Clone)] pub struct Wallet { - pub address: String, + pub address: PublicKey, pub twitter_handle: Option, } impl Wallet { - pub fn new(address: String, twitter_handle: Option) -> Self { + pub fn new(address: PublicKey, twitter_handle: Option) -> Self { Self { address, twitter_handle, @@ -18,15 +22,74 @@ impl Wallet { } } +#[derive(Debug, Clone)] +pub struct WalletNftCount { + wallet: PublicKey, + creators: Option>>, +} + +impl WalletNftCount { + #[must_use] + pub fn new(wallet: PublicKey, creators: Option>>) -> Self { + Self { wallet, creators } + } +} + +#[graphql_object(Context = AppContext)] +impl WalletNftCount { + fn owned(&self, context: &AppContext) -> FieldResult { + let conn = context.db_pool.get()?; + + let count = queries::nft_count::owned(&conn, &self.wallet, self.creators.as_deref())?; + + Ok(count.try_into()?) + } + + #[graphql(arguments(auction_houses(description = "auction houses to scope wallet counts")))] + fn offered( + &self, + context: &AppContext, + auction_houses: Option>>, + ) -> FieldResult { + let conn = context.db_pool.get()?; + + let count = queries::nft_count::offered( + &conn, + &self.wallet, + self.creators.as_deref(), + auction_houses.as_deref(), + )?; + + Ok(count.try_into()?) + } + + #[graphql(arguments(auction_houses(description = "auction houses to scope wallet counts")))] + fn listed( + &self, + context: &AppContext, + auction_houses: Option>>, + ) -> FieldResult { + let conn = context.db_pool.get()?; + + let count = queries::nft_count::wallet_listed( + &conn, + &self.wallet, + self.creators.as_deref(), + auction_houses.as_deref(), + )?; + + Ok(count.try_into()?) + } +} + #[graphql_object(Context = AppContext)] impl Wallet { - pub fn address(&self) -> &str { + pub fn address(&self) -> &PublicKey { &self.address } pub fn bids(&self, ctx: &AppContext) -> FieldResult> { let db_conn = ctx.db_pool.get()?; - let rows: Vec = bids::table .select(bids::all_columns) .filter(bids::bidder_address.eq(&self.address)) @@ -41,17 +104,60 @@ impl Wallet { } pub async fn profile(&self, ctx: &AppContext) -> FieldResult> { - let twitter_handle = self.twitter_handle.clone(); - - if twitter_handle.is_none() { - return Ok(None); - } - - let twitter_handle = twitter_handle.unwrap(); + let twitter_handle = match self.twitter_handle { + Some(ref t) => t.clone(), + None => return Ok(None), + }; ctx.twitter_profile_loader .load(twitter_handle) .await .map_err(Into::into) } + + pub fn connection_counts(&self) -> FieldResult { + Ok(ConnectionCounts { + address: self.address.clone(), + }) + } + + #[graphql(arguments(creators(description = "a list of auction house public keys")))] + pub fn nft_counts( + &self, + _ctx: &AppContext, + creators: Option>>, + ) -> WalletNftCount { + WalletNftCount::new(self.address.clone(), creators) + } +} + +pub struct ConnectionCounts { + pub address: PublicKey, +} + +#[graphql_object(Context = AppContext)] +impl ConnectionCounts { + pub fn from_count(&self, ctx: &AppContext) -> FieldResult { + let db_conn = ctx.db_pool.get()?; + + let count: i64 = graph_connections::table + .filter(graph_connections::from_account.eq(&self.address)) + .count() + .get_result(&db_conn) + .context("Failed to count from_connections")?; + + Ok(count.try_into()?) + } + + pub fn to_count(&self, ctx: &AppContext) -> FieldResult { + let db_conn = ctx.db_pool.get()?; + + let count: i64 = graph_connections::table + .filter(graph_connections::to_account.eq(&self.address)) + .count() + .get_result(&db_conn) + .context("Failed to count to_connections")?; + + Ok(count.try_into()?) + } } diff --git a/crates/graphql/src/schema/query_root.rs b/crates/graphql/src/schema/query_root.rs index 8ee9d7cd9..c983c2aa5 100644 --- a/crates/graphql/src/schema/query_root.rs +++ b/crates/graphql/src/schema/query_root.rs @@ -6,7 +6,7 @@ use objects::{ graph_connection::GraphConnection, listing::{Listing, ListingColumns, ListingRow}, marketplace::Marketplace, - nft::Nft, + nft::{Nft, NftCount, NftCreator}, profile::{Profile, TwitterProfilePictureResponse, TwitterShowResponse}, storefront::{Storefront, StorefrontColumns}, wallet::Wallet, @@ -35,6 +35,11 @@ impl From for queries::metadatas::AttributeFilter { #[graphql_object(Context = AppContext)] impl QueryRoot { + #[graphql(arguments(creators(description = "creators of nfts"),))] + fn nft_counts(&self, creators: Vec>) -> FieldResult { + Ok(NftCount::new(creators)) + } + async fn profile( &self, ctx: &AppContext, @@ -121,7 +126,7 @@ impl QueryRoot { ) -> FieldResult { let conn = context.db_pool.get().context("failed to connect to db")?; - let twitter_handle = queries::twitter_handle_name_service::get(&conn, address.clone())?; + let twitter_handle = queries::twitter_handle_name_service::get(&conn, &address)?; Ok(Creator { address, @@ -170,11 +175,11 @@ impl QueryRoot { fn wallet( &self, context: &AppContext, - #[graphql(description = "Address of the wallet")] address: String, + #[graphql(description = "Address of the wallet")] address: PublicKey, ) -> FieldResult { let conn = context.db_pool.get()?; - let twitter_handle = queries::twitter_handle_name_service::get(&conn, address.clone())?; + let twitter_handle = queries::twitter_handle_name_service::get(&conn, &address)?; Ok(Wallet::new(address, twitter_handle)) } diff --git a/crates/graphql/src/schema/scalars/public_key.rs b/crates/graphql/src/schema/scalars/public_key.rs index d75f79779..318d96405 100644 --- a/crates/graphql/src/schema/scalars/public_key.rs +++ b/crates/graphql/src/schema/scalars/public_key.rs @@ -117,6 +117,28 @@ where } } +impl indexer_core::db::expression::AsExpression for PublicKey +where + String: indexer_core::db::expression::AsExpression, +{ + type Expression = >::Expression; + + fn as_expression(self) -> Self::Expression { + self.0.as_expression() + } +} + +impl<'a, T, U> indexer_core::db::expression::AsExpression for &'a PublicKey +where + &'a String: indexer_core::db::expression::AsExpression, +{ + type Expression = <&'a String as indexer_core::db::expression::AsExpression>::Expression; + + fn as_expression(self) -> Self::Expression { + (&self.0).as_expression() + } +} + //// BUG: juniper v0.15 does not support implementing GraphQLScalar on structs //// with generic parameters. As a result the expansion of this macro has //// been manually included in the code and tweaked to compile, and should diff --git a/crates/indexer/src/http/metadata_json.rs b/crates/indexer/src/http/metadata_json.rs index a27ef7cba..df1fef406 100644 --- a/crates/indexer/src/http/metadata_json.rs +++ b/crates/indexer/src/http/metadata_json.rs @@ -396,7 +396,13 @@ fn process_attributes( insert_into(attributes::table) .values(&row) - .on_conflict_do_nothing() + .on_conflict(( + attributes::metadata_address, + attributes::value, + attributes::trait_type, + )) + .do_update() + .set(&row) .execute(db) .context("Failed to insert attribute!")?; }