From f9b2fc55dc2c875b3723aa68a45fbe55d7697c3f Mon Sep 17 00:00:00 2001 From: Anshul Goel Date: Thu, 24 Mar 2022 15:01:41 +0530 Subject: [PATCH 1/8] purchase receipt dataloader added, send purchases info in nft data --- crates/graphql/src/schema/context.rs | 5 ++- crates/graphql/src/schema/dataloaders/mod.rs | 1 + .../schema/dataloaders/purchase_receipt.rs | 31 +++++++++++++++++++ crates/graphql/src/schema/objects/nft.rs | 9 +++++- 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 crates/graphql/src/schema/dataloaders/purchase_receipt.rs diff --git a/crates/graphql/src/schema/context.rs b/crates/graphql/src/schema/context.rs index e65b7ded6..769c1d296 100644 --- a/crates/graphql/src/schema/context.rs +++ b/crates/graphql/src/schema/context.rs @@ -4,6 +4,7 @@ use objects::{ bid_receipt::BidReceipt, listing::{Bid, Listing}, listing_receipt::ListingReceipt, + purchase_receipt::PurchaseReceipt, nft::{Nft, NftAttribute, NftCreator, NftOwner}, stats::{MarketStats, MintStats}, store_creator::StoreCreator, @@ -11,7 +12,7 @@ use objects::{ }; use scalars::{markers::StoreConfig, PublicKey}; -use super::prelude::*; +use super::{prelude::*}; #[derive(Clone)] pub struct AppContext { @@ -30,6 +31,7 @@ pub struct AppContext { pub nft_owner_loader: Loader, Option>, pub storefront_loader: Loader, Option>, pub listing_receipts_loader: Loader, Vec>, + pub purchase_receipts_loader: Loader, Vec>, pub bid_receipts_loader: Loader, Vec>, pub store_creator_loader: Loader, Vec>, pub collection_loader: Loader, Vec>, @@ -53,6 +55,7 @@ impl AppContext { nft_owner_loader: Loader::new(batcher.clone()), storefront_loader: Loader::new(batcher.clone()), listing_receipts_loader: Loader::new(batcher.clone()), + purchase_receipts_loader: Loader::new(batcher.clone()), bid_receipts_loader: Loader::new(batcher.clone()), store_creator_loader: Loader::new(batcher.clone()), collection_loader: Loader::new(batcher), diff --git a/crates/graphql/src/schema/dataloaders/mod.rs b/crates/graphql/src/schema/dataloaders/mod.rs index 80dfb045b..65aed1adc 100644 --- a/crates/graphql/src/schema/dataloaders/mod.rs +++ b/crates/graphql/src/schema/dataloaders/mod.rs @@ -9,6 +9,7 @@ pub mod store_creator; pub mod storefront; pub(self) mod batcher; +pub mod purchase_receipt; pub(self) mod prelude { pub use async_trait::async_trait; diff --git a/crates/graphql/src/schema/dataloaders/purchase_receipt.rs b/crates/graphql/src/schema/dataloaders/purchase_receipt.rs new file mode 100644 index 000000000..63ed0312c --- /dev/null +++ b/crates/graphql/src/schema/dataloaders/purchase_receipt.rs @@ -0,0 +1,31 @@ +use objects::{purchase_receipt::PurchaseReceipt, nft::Nft}; +use scalars::PublicKey; +use tables::{purchase_receipts, metadatas, token_accounts}; + +use super::prelude::*; + +#[async_trait] +impl TryBatchFn, Vec> for Batcher { + async fn load( + &mut self, + addresses: &[PublicKey], + ) -> TryBatchMap, Vec> { + let conn = self.db()?; + + let rows: Vec = purchase_receipts::table + .inner_join(metadatas::table.on(metadatas::address.eq(purchase_receipts::metadata))) + .inner_join( + token_accounts::table.on(token_accounts::mint_address.eq(metadatas::mint_address)), + ) + .select(purchase_receipts::all_columns) + .filter(token_accounts::amount.eq(1)) + .filter(purchase_receipts::metadata.eq(any(addresses))) + .load(&conn) + .context("Failed to load purchase receipts")?; + + Ok(rows + .into_iter() + .map(|purchase| (purchase.metadata.clone(), purchase.try_into())) + .batch(addresses)) + } +} diff --git a/crates/graphql/src/schema/objects/nft.rs b/crates/graphql/src/schema/objects/nft.rs index 5d8e77103..760a695e1 100644 --- a/crates/graphql/src/schema/objects/nft.rs +++ b/crates/graphql/src/schema/objects/nft.rs @@ -4,7 +4,7 @@ use objects::{bid_receipt::BidReceipt, listing_receipt::ListingReceipt}; use regex::Regex; use reqwest::Url; -use super::prelude::*; +use super::{prelude::*, purchase_receipt::PurchaseReceipt}; #[derive(Debug, Clone)] pub struct NftAttribute { @@ -213,6 +213,13 @@ impl Nft { .map_err(Into::into) } + pub async fn purchases(&self, ctx: &AppContext) -> FieldResult> { + ctx.purchase_receipts_loader + .load(self.address.clone().into()) + .await + .map_err(Into::into) + } + pub async fn offers(&self, ctx: &AppContext) -> FieldResult> { ctx.bid_receipts_loader .load(self.address.clone().into()) From 9f58a63345541fe43181219b446cf1fc8f5e2a66 Mon Sep 17 00:00:00 2001 From: Kyle Espinola Date: Thu, 24 Mar 2022 13:09:37 -0700 Subject: [PATCH 2/8] feat(graphql): query purchase history for an nft --- crates/graphql/src/schema/context.rs | 4 +- .../src/schema/dataloaders/listing_receipt.rs | 33 ---------- crates/graphql/src/schema/dataloaders/mod.rs | 2 - crates/graphql/src/schema/dataloaders/nft.rs | 61 ++++++++++++++++++- .../schema/dataloaders/purchase_receipt.rs | 31 ---------- crates/graphql/src/schema/objects/nft.rs | 6 +- 6 files changed, 65 insertions(+), 72 deletions(-) delete mode 100644 crates/graphql/src/schema/dataloaders/listing_receipt.rs delete mode 100644 crates/graphql/src/schema/dataloaders/purchase_receipt.rs diff --git a/crates/graphql/src/schema/context.rs b/crates/graphql/src/schema/context.rs index 769c1d296..7a6c76004 100644 --- a/crates/graphql/src/schema/context.rs +++ b/crates/graphql/src/schema/context.rs @@ -4,15 +4,15 @@ use objects::{ bid_receipt::BidReceipt, listing::{Bid, Listing}, listing_receipt::ListingReceipt, - purchase_receipt::PurchaseReceipt, nft::{Nft, NftAttribute, NftCreator, NftOwner}, + purchase_receipt::PurchaseReceipt, stats::{MarketStats, MintStats}, store_creator::StoreCreator, storefront::Storefront, }; use scalars::{markers::StoreConfig, PublicKey}; -use super::{prelude::*}; +use super::prelude::*; #[derive(Clone)] pub struct AppContext { diff --git a/crates/graphql/src/schema/dataloaders/listing_receipt.rs b/crates/graphql/src/schema/dataloaders/listing_receipt.rs deleted file mode 100644 index 476b68b4c..000000000 --- a/crates/graphql/src/schema/dataloaders/listing_receipt.rs +++ /dev/null @@ -1,33 +0,0 @@ -use objects::{listing_receipt::ListingReceipt, nft::Nft}; -use scalars::PublicKey; -use tables::{listing_receipts, metadatas, token_accounts}; - -use super::prelude::*; - -#[async_trait] -impl TryBatchFn, Vec> for Batcher { - async fn load( - &mut self, - addresses: &[PublicKey], - ) -> TryBatchMap, Vec> { - let conn = self.db()?; - - let rows: Vec = listing_receipts::table - .inner_join(metadatas::table.on(metadatas::address.eq(listing_receipts::metadata))) - .inner_join( - token_accounts::table.on(token_accounts::mint_address.eq(metadatas::mint_address)), - ) - .select(listing_receipts::all_columns) - .filter(token_accounts::amount.eq(1)) - .filter(listing_receipts::canceled_at.is_null()) - .filter(listing_receipts::purchase_receipt.is_null()) - .filter(listing_receipts::metadata.eq(any(addresses))) - .load(&conn) - .context("Failed to load listing receipts")?; - - Ok(rows - .into_iter() - .map(|listing| (listing.metadata.clone(), listing.try_into())) - .batch(addresses)) - } -} diff --git a/crates/graphql/src/schema/dataloaders/mod.rs b/crates/graphql/src/schema/dataloaders/mod.rs index 65aed1adc..3565dccac 100644 --- a/crates/graphql/src/schema/dataloaders/mod.rs +++ b/crates/graphql/src/schema/dataloaders/mod.rs @@ -2,14 +2,12 @@ pub mod auction_house; pub mod bid_receipt; pub mod collection; pub mod listing; -pub mod listing_receipt; pub mod nft; pub mod stats; pub mod store_creator; pub mod storefront; pub(self) mod batcher; -pub mod purchase_receipt; pub(self) mod prelude { pub use async_trait::async_trait; diff --git a/crates/graphql/src/schema/dataloaders/nft.rs b/crates/graphql/src/schema/dataloaders/nft.rs index 68a9684b0..cbd2d9848 100644 --- a/crates/graphql/src/schema/dataloaders/nft.rs +++ b/crates/graphql/src/schema/dataloaders/nft.rs @@ -1,6 +1,12 @@ -use objects::nft::{Nft, NftAttribute, NftCreator, NftOwner}; +use objects::{ + listing_receipt::ListingReceipt, + nft::{Nft, NftAttribute, NftCreator, NftOwner}, + purchase_receipt::PurchaseReceipt, +}; use scalars::PublicKey; -use tables::{attributes, metadata_creators, token_accounts}; +use tables::{ + attributes, listing_receipts, metadata_creators, metadatas, purchase_receipts, token_accounts, +}; use super::prelude::*; @@ -76,3 +82,54 @@ impl TryBatchFn, Option> for Batcher { .batch(mint_addresses)) } } + +#[async_trait] +impl TryBatchFn, Vec> for Batcher { + async fn load( + &mut self, + addresses: &[PublicKey], + ) -> TryBatchMap, Vec> { + let conn = self.db()?; + + let rows: Vec = purchase_receipts::table + .inner_join(metadatas::table.on(metadatas::address.eq(purchase_receipts::metadata))) + .select(purchase_receipts::all_columns) + .filter(purchase_receipts::metadata.eq(any(addresses))) + .order(purchase_receipts::created_at.desc()) + .load(&conn) + .context("Failed to load purchase receipts")?; + + Ok(rows + .into_iter() + .map(|purchase| (purchase.metadata.clone(), purchase.try_into())) + .batch(addresses)) + } +} + +#[async_trait] +impl TryBatchFn, Vec> for Batcher { + async fn load( + &mut self, + addresses: &[PublicKey], + ) -> TryBatchMap, Vec> { + let conn = self.db()?; + + let rows: Vec = listing_receipts::table + .inner_join(metadatas::table.on(metadatas::address.eq(listing_receipts::metadata))) + .inner_join( + token_accounts::table.on(token_accounts::mint_address.eq(metadatas::mint_address)), + ) + .select(listing_receipts::all_columns) + .filter(token_accounts::amount.eq(1)) + .filter(listing_receipts::canceled_at.is_null()) + .filter(listing_receipts::purchase_receipt.is_null()) + .filter(listing_receipts::metadata.eq(any(addresses))) + .load(&conn) + .context("Failed to load listing receipts")?; + + Ok(rows + .into_iter() + .map(|listing| (listing.metadata.clone(), listing.try_into())) + .batch(addresses)) + } +} diff --git a/crates/graphql/src/schema/dataloaders/purchase_receipt.rs b/crates/graphql/src/schema/dataloaders/purchase_receipt.rs deleted file mode 100644 index 63ed0312c..000000000 --- a/crates/graphql/src/schema/dataloaders/purchase_receipt.rs +++ /dev/null @@ -1,31 +0,0 @@ -use objects::{purchase_receipt::PurchaseReceipt, nft::Nft}; -use scalars::PublicKey; -use tables::{purchase_receipts, metadatas, token_accounts}; - -use super::prelude::*; - -#[async_trait] -impl TryBatchFn, Vec> for Batcher { - async fn load( - &mut self, - addresses: &[PublicKey], - ) -> TryBatchMap, Vec> { - let conn = self.db()?; - - let rows: Vec = purchase_receipts::table - .inner_join(metadatas::table.on(metadatas::address.eq(purchase_receipts::metadata))) - .inner_join( - token_accounts::table.on(token_accounts::mint_address.eq(metadatas::mint_address)), - ) - .select(purchase_receipts::all_columns) - .filter(token_accounts::amount.eq(1)) - .filter(purchase_receipts::metadata.eq(any(addresses))) - .load(&conn) - .context("Failed to load purchase receipts")?; - - Ok(rows - .into_iter() - .map(|purchase| (purchase.metadata.clone(), purchase.try_into())) - .batch(addresses)) - } -} diff --git a/crates/graphql/src/schema/objects/nft.rs b/crates/graphql/src/schema/objects/nft.rs index 760a695e1..220399dda 100644 --- a/crates/graphql/src/schema/objects/nft.rs +++ b/crates/graphql/src/schema/objects/nft.rs @@ -1,10 +1,12 @@ use base64::display::Base64Display; use indexer_core::assets::{AssetIdentifier, ImageSize}; -use objects::{bid_receipt::BidReceipt, listing_receipt::ListingReceipt}; +use objects::{ + bid_receipt::BidReceipt, listing_receipt::ListingReceipt, purchase_receipt::PurchaseReceipt, +}; use regex::Regex; use reqwest::Url; -use super::{prelude::*, purchase_receipt::PurchaseReceipt}; +use super::prelude::*; #[derive(Debug, Clone)] pub struct NftAttribute { From 1de3eb5e329c547e7fbfa0b238375f29df94aa7d Mon Sep 17 00:00:00 2001 From: Abdul Basit <45506001+imabdulbasit@users.noreply.github.com> Date: Fri, 25 Mar 2022 00:50:55 +0500 Subject: [PATCH 3/8] Abdul/tk slot indexing (#267) * slot col for tk * updated_at removed from model * nft dataloader edited * formatting * fmt * added required changes * on_conflict() added * brrr --- .../down.sql | 4 + .../up.sql | 18 +++++ crates/core/src/db/models.rs | 5 +- crates/core/src/db/schema.rs | 1 + crates/graphql/src/schema/dataloaders/nft.rs | 7 ++ .../indexer/src/accountsdb/accounts/token.rs | 75 ++++++++++++++----- .../indexer/src/accountsdb/programs/token.rs | 2 +- 7 files changed, 92 insertions(+), 20 deletions(-) create mode 100644 crates/core/migrations/2022-03-22-214148_add_slot_col_and_trigger_to_token_account/down.sql create mode 100644 crates/core/migrations/2022-03-22-214148_add_slot_col_and_trigger_to_token_account/up.sql diff --git a/crates/core/migrations/2022-03-22-214148_add_slot_col_and_trigger_to_token_account/down.sql b/crates/core/migrations/2022-03-22-214148_add_slot_col_and_trigger_to_token_account/down.sql new file mode 100644 index 000000000..2df54ab59 --- /dev/null +++ b/crates/core/migrations/2022-03-22-214148_add_slot_col_and_trigger_to_token_account/down.sql @@ -0,0 +1,4 @@ +alter table token_accounts +drop column slot; +drop trigger set_token_account_updated_at on token_accounts; +drop function trigger_set_updated_at_timestamp(); \ No newline at end of file diff --git a/crates/core/migrations/2022-03-22-214148_add_slot_col_and_trigger_to_token_account/up.sql b/crates/core/migrations/2022-03-22-214148_add_slot_col_and_trigger_to_token_account/up.sql new file mode 100644 index 000000000..6aaad88b5 --- /dev/null +++ b/crates/core/migrations/2022-03-22-214148_add_slot_col_and_trigger_to_token_account/up.sql @@ -0,0 +1,18 @@ +alter table token_accounts +add column slot bigint null; + +alter table token_accounts alter updated_at set default now(); + +create function trigger_set_updated_at_timestamp() +returns trigger as $$ +begin + new.updated_at = now(); + return new; +end; +$$ language 'plpgsql'; + +create trigger set_token_account_updated_at +before update on token_accounts +for each row +execute procedure trigger_set_updated_at_timestamp(); + diff --git a/crates/core/src/db/models.rs b/crates/core/src/db/models.rs index 9b07c75ce..1493427b8 100644 --- a/crates/core/src/db/models.rs +++ b/crates/core/src/db/models.rs @@ -168,8 +168,9 @@ pub struct TokenAccount<'a> { pub owner_address: Cow<'a, str>, /// The amount of the token, often 1 pub amount: i64, - /// updated_at - pub updated_at: NaiveDateTime, + /// Solana slot number + /// The period of time for which each leader ingests transactions and produces a block. + pub slot: Option, } /// A row in the `metadatas` table diff --git a/crates/core/src/db/schema.rs b/crates/core/src/db/schema.rs index aca127850..725d05bfd 100644 --- a/crates/core/src/db/schema.rs +++ b/crates/core/src/db/schema.rs @@ -513,6 +513,7 @@ table! { owner_address -> Varchar, amount -> Int8, updated_at -> Timestamp, + slot -> Nullable, } } diff --git a/crates/graphql/src/schema/dataloaders/nft.rs b/crates/graphql/src/schema/dataloaders/nft.rs index bf6b97582..68a9684b0 100644 --- a/crates/graphql/src/schema/dataloaders/nft.rs +++ b/crates/graphql/src/schema/dataloaders/nft.rs @@ -56,6 +56,13 @@ impl TryBatchFn, Option> for Batcher { let rows: Vec = token_accounts::table .filter(token_accounts::mint_address.eq(any(mint_addresses))) .filter(token_accounts::amount.eq(1)) + .select(( + token_accounts::address, + token_accounts::mint_address, + token_accounts::owner_address, + token_accounts::amount, + token_accounts::slot, + )) .load(&conn) .context("Failed to load NFT owners")?; diff --git a/crates/indexer/src/accountsdb/accounts/token.rs b/crates/indexer/src/accountsdb/accounts/token.rs index a08bd8c43..e163cb89f 100644 --- a/crates/indexer/src/accountsdb/accounts/token.rs +++ b/crates/indexer/src/accountsdb/accounts/token.rs @@ -1,6 +1,5 @@ -use chrono::offset::Local; use indexer_core::{ - db::{insert_into, models::TokenAccount as TokenAccountModel, tables::token_accounts}, + db::{insert_into, models::TokenAccount as TokenAccountModel, tables::token_accounts, update}, prelude::*, }; use spl_token::state::Account as TokenAccount; @@ -8,10 +7,14 @@ use spl_token::state::Account as TokenAccount; use super::Client; use crate::prelude::*; -pub async fn process(client: &Client, key: Pubkey, token_account: TokenAccount) -> Result<()> { +pub async fn process( + client: &Client, + key: Pubkey, + token_account: TokenAccount, + slot: u64, +) -> Result<()> { let pubkey = key.to_string(); - let now = Local::now().naive_utc(); let amount: i64 = token_account .amount .try_into() @@ -22,26 +25,64 @@ pub async fn process(client: &Client, key: Pubkey, token_account: TokenAccount) return Ok(()); } + let rows = client + .db() + .run(move |db| { + token_accounts::table + .select(( + token_accounts::address, + token_accounts::mint_address, + token_accounts::owner_address, + token_accounts::amount, + token_accounts::slot, + )) + .filter(token_accounts::address.eq(key.to_string())) + .load::(db) + }) + .await + .context("failed to load token accounts!")?; + let values = TokenAccountModel { address: Owned(pubkey), amount, mint_address: Owned(token_account.mint.to_string()), owner_address: Owned(owner), - updated_at: now, + slot: Some(slot.try_into()?), }; - client - .db() - .run(move |db| { - insert_into(token_accounts::table) - .values(&values) - .on_conflict(token_accounts::address) - .do_update() - .set(&values) - .execute(db) - }) - .await - .context("failed to insert token account")?; + let incoming_slot: i64 = slot.try_into()?; + + match rows.get(0).and_then(|r| r.slot) { + Some(indexed_slot) if incoming_slot > indexed_slot => { + client + .db() + .run(move |db| { + update( + token_accounts::table + .filter(token_accounts::address.eq(values.clone().address)), + ) + .set(&values) + .execute(db) + }) + .await + .context("failed to update token account")?; + }, + Some(_) => (), + None => { + client + .db() + .run(move |db| { + insert_into(token_accounts::table) + .values(&values) + .on_conflict(token_accounts::address) + .do_update() + .set(&values) + .execute(db) + }) + .await + .context("failed to insert token account")?; + }, + } Ok(()) } diff --git a/crates/indexer/src/accountsdb/programs/token.rs b/crates/indexer/src/accountsdb/programs/token.rs index ef9a39003..fc9f5e86f 100644 --- a/crates/indexer/src/accountsdb/programs/token.rs +++ b/crates/indexer/src/accountsdb/programs/token.rs @@ -7,7 +7,7 @@ use crate::prelude::*; async fn process_token(client: &Client, update: AccountUpdate) -> Result<()> { let token_account = TokenAccount::unpack_unchecked(&update.data) .context("Failed to deserialize token account data!")?; - token::process(client, update.key, token_account).await + token::process(client, update.key, token_account, update.slot).await } pub(crate) async fn process(client: &Client, update: AccountUpdate) -> Result<()> { From b57adaf032c992be1a90c7690f743fe27f5e468a Mon Sep 17 00:00:00 2001 From: raykast Date: Thu, 24 Mar 2022 14:56:24 -0700 Subject: [PATCH 4/8] Remove configurable API version, lock at v1. --- crates/graphql/src/main.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/graphql/src/main.rs b/crates/graphql/src/main.rs index 966ce2cdb..e023caa37 100644 --- a/crates/graphql/src/main.rs +++ b/crates/graphql/src/main.rs @@ -32,9 +32,6 @@ struct Opts { #[clap(long, env)] asset_proxy_count: u8, - - #[clap(long, env, default_value = "0")] - api_version: String, } fn graphiql(uri: String) -> impl Fn() -> HttpResponse + Clone { @@ -85,7 +82,6 @@ fn main() { twitter_bearer_token, asset_proxy_endpoint, asset_proxy_count, - api_version, } = Opts::parse(); let (addr,) = server.into_parts(); @@ -103,13 +99,10 @@ fn main() { db::connect(db::ConnectMode::Read).context("Failed to connect to Postgres")?; let db_pool = Arc::new(db_pool); - let version_extension = format!( - "/v{}", - percent_encoding::utf8_percent_encode(&api_version, percent_encoding::NON_ALPHANUMERIC,) - ); + let version_extension = "/v1"; // Should look something like "/..." - let graphiql_uri = version_extension.clone(); + let graphiql_uri = version_extension.to_owned(); assert!(graphiql_uri.starts_with('/')); let schema = Arc::new(schema::create()); @@ -132,7 +125,7 @@ fn main() { .max_age(3600), ) .service( - web::resource(&version_extension) + web::resource(version_extension) .route(web::post().to(graphql(db_pool.clone(), shared.clone()))), ) .service( From 4a0e3c257f298f03200266d477c38bb1af621d10 Mon Sep 17 00:00:00 2001 From: Ryan S <6175424+ray-kast@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:54:22 -0700 Subject: [PATCH 5/8] Expose full ends-at data for listings. (#292) --- crates/graphql/src/schema/objects/listing.rs | 21 +++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/graphql/src/schema/objects/listing.rs b/crates/graphql/src/schema/objects/listing.rs index b4b769f5a..fe0defd38 100644 --- a/crates/graphql/src/schema/objects/listing.rs +++ b/crates/graphql/src/schema/objects/listing.rs @@ -95,6 +95,7 @@ pub struct Listing { pub cache_address: String, pub store_address: String, pub token_mint: Option, + pub ends_at: Option>, pub ended: bool, } @@ -116,19 +117,21 @@ impl Listing { ): ListingRow, now: NaiveDateTime, ) -> Result { + let (ends_at, ended) = indexer_core::util::get_end_info( + ends_at, + gap_time.map(|i| chrono::Duration::seconds(i.into())), + last_bid_time, + now, + )?; + Ok(Self { address, ext_address, cache_address, store_address, token_mint, - ended: indexer_core::util::get_end_info( - ends_at, - gap_time.map(|i| chrono::Duration::seconds(i.into())), - last_bid_time, - now, - )? - .1, + ends_at: ends_at.map(|t| DateTime::from_utc(t, Utc)), + ended, }) } } @@ -151,6 +154,10 @@ impl Listing { &self.store_address } + pub fn ends_at(&self) -> Option> { + self.ends_at + } + pub fn ended(&self) -> bool { self.ended } From 17d3d31a1f15651f23874708750d07f3cb8834d7 Mon Sep 17 00:00:00 2001 From: raykast Date: Thu, 24 Mar 2022 11:22:04 -0700 Subject: [PATCH 6/8] Added basic path handling to the IPFS parser. --- Cargo.lock | 1 - crates/core/src/assets.rs | 39 +++++++--- crates/graphql/Cargo.toml | 1 - crates/graphql/src/schema/objects/nft.rs | 74 +++++++++++-------- .../indexer/src/bin/metaplex-indexer-http.rs | 4 + crates/indexer/src/http/client.rs | 10 ++- crates/indexer/src/http/metadata_json.rs | 6 +- 7 files changed, 85 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8c006ece..ba1b6acc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2945,7 +2945,6 @@ dependencies = [ "md5", "metaplex-indexer-core", "percent-encoding", - "regex", "reqwest", "serde", "serde_json", diff --git a/crates/core/src/assets.rs b/crates/core/src/assets.rs index 9bc1b8c45..1c9769d9a 100644 --- a/crates/core/src/assets.rs +++ b/crates/core/src/assets.rs @@ -7,10 +7,10 @@ use url::Url; pub struct ArTxid(pub [u8; 32]); /// Struct to hold tx ids -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct AssetIdentifier { /// ipfs cid - pub ipfs: Option, + pub ipfs: Option<(Cid, String)>, /// Arweave tx id pub arweave: Option, } @@ -40,21 +40,26 @@ impl From for ImageSize { } impl AssetIdentifier { - fn visit_url(url: &Url, mut f: impl FnMut(&str)) { + fn visit_url(url: &Url, mut f: impl FnMut(&str, Option)) { Some(url.scheme()) .into_iter() .chain(url.domain().into_iter().flat_map(|s| s.split('.'))) .chain(Some(url.username())) .chain(url.password()) - .chain(Some(url.path())) - .chain(url.path_segments().into_iter().flatten()) - .chain(url.query()) - .chain(url.fragment().into_iter().flat_map(|s| s.split('/'))) - .for_each(&mut f); + .map(|s| (s, Some(0))) + .chain(Some((url.path(), None))) + .chain( + url.path_segments() + .into_iter() + .flat_map(|s| s.into_iter().enumerate().map(|(i, s)| (s, Some(i + 1)))), + ) + .chain(url.query().map(|q| (q, Some(0)))) + .chain(url.fragment().map(|f| (f, Some(0)))) + .for_each(|(s, i)| f(s, i)); url.query_pairs().for_each(|(k, v)| { - f(k.as_ref()); - f(v.as_ref()); + f(k.as_ref(), Some(0)); + f(v.as_ref(), Some(0)); }); } @@ -80,7 +85,9 @@ impl AssetIdentifier { fn advance_heuristic(state: &mut Result, ()>, value: T) { match state { + // We found a match Ok(None) => *state = Ok(Some(value)), + // We found two matches, convert to error due to ambiguity Ok(Some(_)) => *state = Err(()), Err(()) => (), } @@ -92,9 +99,17 @@ impl AssetIdentifier { let mut ipfs = Ok(None); let mut arweave = Ok(None); - Self::visit_url(url, |s| { + Self::visit_url(url, |s, i| { if let Some(c) = Self::try_ipfs(s) { - Self::advance_heuristic(&mut ipfs, c); + let path = i + .and_then(|i| url.path_segments().map(|s| (i, s))) + .map_or_else(String::new, |(i, s)| { + s.skip(i) + .flat_map(|s| Some("/").into_iter().chain(Some(s))) + .collect() + }); + + Self::advance_heuristic(&mut ipfs, (c, path)); } if let Some(t) = Self::try_arweave(s) { diff --git a/crates/graphql/Cargo.toml b/crates/graphql/Cargo.toml index 1aa1ccf2b..a104621fb 100644 --- a/crates/graphql/Cargo.toml +++ b/crates/graphql/Cargo.toml @@ -27,7 +27,6 @@ serde_json = "1.0.70" thiserror = "1.0.30" base64 = "0.13.0" md5 = "0.7.0" -regex = "1.4.2" [dependencies.indexer-core] package = "metaplex-indexer-core" diff --git a/crates/graphql/src/schema/objects/nft.rs b/crates/graphql/src/schema/objects/nft.rs index 220399dda..2c5f0c590 100644 --- a/crates/graphql/src/schema/objects/nft.rs +++ b/crates/graphql/src/schema/objects/nft.rs @@ -3,7 +3,6 @@ use indexer_core::assets::{AssetIdentifier, ImageSize}; use objects::{ bid_receipt::BidReceipt, listing_receipt::ListingReceipt, purchase_receipt::PurchaseReceipt, }; -use regex::Regex; use reqwest::Url; use super::prelude::*; @@ -148,42 +147,55 @@ impl Nft { &self.description } - #[graphql(arguments(width( - description = "Image width possible values are:\n- 0 (Original size)\n- 100 (Tiny)\n- 400 (XSmall)\n- 600 (Small)\n- 800 (Medium)\n- 1400 (Large)\n\n Any other value will return the original image size.\n\n If no value is provided, it will return XSmall" - ),))] - pub fn image(&self, width: Option, ctx: &AppContext) -> FieldResult { - let width = ImageSize::from(width.unwrap_or(ImageSize::XSmall as i32)); - let cdn_count = ctx.shared.asset_proxy_count; - let assets_cdn = &ctx.shared.asset_proxy_endpoint; - let asset = AssetIdentifier::new(&Url::parse(&self.image).context("couldnt parse url")?); + #[graphql(arguments(width(description = r"Image width possible values are: +- 0 (Original size) +- 100 (Tiny) +- 400 (XSmall) +- 600 (Small) +- 800 (Medium) +- 1400 (Large) - let re = Regex::new(r"nftstorage\.link").unwrap(); +Any other value will return the original image size. - Ok(if re.is_match(&self.image) { - self.image.clone() - } else if asset.arweave.is_some() && asset.ipfs.is_none() { - let cid = - Base64Display::with_config(&asset.arweave.unwrap().0, base64::URL_SAFE_NO_PAD) - .to_string(); +If no value is provided, it will return XSmall")))] + pub fn image(&self, width: Option, ctx: &AppContext) -> FieldResult { + fn get_asset_cdn(id: impl AsRef<[u8]>, shared: &SharedData) -> String { + let rem = md5::compute(id).to_vec()[0].rem_euclid(shared.asset_proxy_count); + let assets_cdn = &shared.asset_proxy_endpoint; - let rem = md5::compute(&cid).to_vec()[0].rem_euclid(cdn_count); - let assets_cdn = if rem == 0 { + if rem == 0 { assets_cdn.replace("[n]", "") } else { assets_cdn.replace("[n]", &rem.to_string()) - }; - format!("{}arweave/{}?width={}", assets_cdn, cid, width as i32) - } else if asset.ipfs.is_some() && asset.arweave.is_none() { - let cid = asset.ipfs.unwrap().to_string(); - let rem = md5::compute(&cid).to_vec()[0].rem_euclid(cdn_count); - let assets_cdn = if rem == 0 { - assets_cdn.replace("[n]", "") - } else { - assets_cdn.replace("[n]", &rem.to_string()) - }; - format!("{}ipfs/{}?width={}", assets_cdn, cid, width as i32) - } else { - self.image.clone() + } + } + + let width = ImageSize::from(width.unwrap_or(ImageSize::XSmall as i32)); + let id = AssetIdentifier::new(&Url::parse(&self.image).context("couldnt parse url")?); + + Ok(match (id.arweave, id.ipfs) { + (Some(_), Some(_)) | (None, None) => self.image.clone(), + (Some(txid), None) => { + let txid = Base64Display::with_config(&txid.0, base64::URL_SAFE_NO_PAD).to_string(); + + format!( + "{}arweave/{}?width={}", + get_asset_cdn(&txid, &ctx.shared), + txid, + width as i32 + ) + }, + (None, Some((cid, path))) => { + let cid = cid.to_string(); + + format!( + "{}ipfs/{}{}?width={}", + get_asset_cdn(&cid, &ctx.shared), + cid, + path, + width as i32 + ) + }, }) } diff --git a/crates/indexer/src/bin/metaplex-indexer-http.rs b/crates/indexer/src/bin/metaplex-indexer-http.rs index 6d4304994..a34ad528f 100644 --- a/crates/indexer/src/bin/metaplex-indexer-http.rs +++ b/crates/indexer/src/bin/metaplex-indexer-http.rs @@ -53,6 +53,10 @@ async fn run( client, } = args; + if cfg!(debug_assertions) && queue_suffix.is_none() { + bail!("Debug builds must specify a RabbitMQ queue suffix!"); + } + let conn = metaplex_indexer::amqp_connect(amqp_url).await?; let client = Client::new_rc(db, client).context("Failed to construct Client")?; diff --git a/crates/indexer/src/http/client.rs b/crates/indexer/src/http/client.rs index cf8d4684e..86c71b634 100644 --- a/crates/indexer/src/http/client.rs +++ b/crates/indexer/src/http/client.rs @@ -86,8 +86,14 @@ impl Client { /// /// # Errors /// This function fails if the CID provided is not URL safe. - pub fn ipfs_link(&self, cid: &Cid) -> Result { - self.ipfs_cdn.join(&cid.to_string()).map_err(Into::into) + pub fn ipfs_link(&self, cid: &Cid, path: &str) -> Result { + let mut ret = self.ipfs_cdn.join(&cid.to_string())?; + + if !path.is_empty() { + ret = ret.join(path)?; + } + + Ok(ret) } /// Construct an Arweave link from a valid Arweave transaction ID diff --git a/crates/indexer/src/http/metadata_json.rs b/crates/indexer/src/http/metadata_json.rs index 35d73c440..d42721901 100644 --- a/crates/indexer/src/http/metadata_json.rs +++ b/crates/indexer/src/http/metadata_json.rs @@ -1,6 +1,5 @@ use std::fmt::{self, Debug, Display}; -use cid::Cid; use indexer_core::{ assets::AssetIdentifier, db::{ @@ -170,7 +169,8 @@ async fn try_locate_json( for (url, fingerprint) in id .ipfs - .map(|c| (client.ipfs_link(&c), c.to_bytes())) + .as_ref() + .map(|(c, p)| (client.ipfs_link(c, p), c.to_bytes())) .into_iter() .chain(id.arweave.map(|t| (client.arweave_link(&t), t.0.to_vec()))) { @@ -440,7 +440,7 @@ pub async fn process<'a>( let possible_fingerprints: Vec<_> = id .ipfs .iter() - .map(Cid::to_bytes) + .map(|(c, _)| c.to_bytes()) .chain(id.arweave.map(|a| a.0.to_vec())) .collect(); let addr = bs58::encode(meta_key).into_string(); From 00bd25fd6955f5fa0a3196abb8d4043b307ffb4f Mon Sep 17 00:00:00 2001 From: raykast Date: Fri, 25 Mar 2022 11:51:47 -0700 Subject: [PATCH 7/8] Updated asset fingerprints to be IPFS-folder-aware --- .env | 2 +- crates/core/src/assets.rs | 111 ++++++++++++++++++----- crates/core/src/lib.rs | 2 + crates/graphql/src/schema/objects/nft.rs | 76 +++++++++++----- crates/indexer/src/http/client.rs | 14 ++- crates/indexer/src/http/metadata_json.rs | 29 +++--- 6 files changed, 172 insertions(+), 62 deletions(-) diff --git a/.env b/.env index 5de325c44..6f43afd70 100644 --- a/.env +++ b/.env @@ -2,5 +2,5 @@ ARWEAVE_URL=https://arweave.net/ IPFS_CDN=https://ipfs.cache.holaplex.com/ ARWEAVE_CDN=https://arweave.net/ HTTP_INDEXER_TIMEOUT=10 -ASSET_PROXY_ENDPOINT=https://assets[n].holaplex.com/ +ASSET_PROXY_ENDPOINT=https://assets[n].holaplex.tools/ ASSET_PROXY_COUNT=5 diff --git a/crates/core/src/assets.rs b/crates/core/src/assets.rs index 1c9769d9a..607ed7aa1 100644 --- a/crates/core/src/assets.rs +++ b/crates/core/src/assets.rs @@ -1,4 +1,7 @@ //! ``AssetIdentifier`` utils - Parse and capture tx and cid + +use std::borrow::Cow; + use cid::Cid; use url::Url; @@ -15,6 +18,15 @@ pub struct AssetIdentifier { pub arweave: Option, } +/// An unambiguous asset-type hint +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AssetHint { + /// The asset is expected to be an IPFS CID + Ipfs, + /// The asset is expected to be an Arweave transaction + Arweave, +} + /// Supported width sizes for asset proxy #[derive(Debug, Clone, Copy, strum::FromRepr)] #[repr(i32)] @@ -40,6 +52,43 @@ impl From for ImageSize { } impl AssetIdentifier { + /// Attempt to parse IPFS or Arweave asset IDs from a URL. + /// + /// Parsing occurs as follows: + /// - If the URL contains a CID in any segment, it is considered to be an + /// IPFS URL. + /// - If the URL contains a base64-encoded 256-bit digest, it is + /// considered to be an Arweave transaction. + /// - If both of the above are found, the URL is considered ambiguous but + /// usable and both Arweave and IPFS parse results are stored. + /// - If more than one valid IPFS parse result is found, the IPFS result is + /// considered ambiguous and unusable and no IPFS data is returned. The + /// same holds for the Arweave parse result. + #[must_use] + pub fn new(url: &Url) -> Self { + let mut ipfs = Ok(None); + let mut arweave = Ok(None); + + Self::visit_url(url, |s, i| { + if let Some(c) = Self::try_ipfs(s) { + let path = i + .and_then(|i| url.path_segments().map(|s| (i, s))) + .map_or_else(String::new, |(i, s)| s.skip(i).intersperse("/").collect()); + + Self::advance_heuristic(&mut ipfs, (c, path)); + } + + if let Some(t) = Self::try_arweave(s) { + Self::advance_heuristic(&mut arweave, t); + } + }); + + Self { + ipfs: ipfs.ok().flatten(), + arweave: arweave.ok().flatten(), + } + } + fn visit_url(url: &Url, mut f: impl FnMut(&str, Option)) { Some(url.scheme()) .into_iter() @@ -93,33 +142,51 @@ impl AssetIdentifier { } } - /// parse cid from url + /// Generate a binary fingerprint for this asset ID. + /// + /// For ambiguous cases, a type hint must be provided for disambiguation + /// otherwise no result is returned. #[must_use] - pub fn new(url: &Url) -> Self { - let mut ipfs = Ok(None); - let mut arweave = Ok(None); + pub fn fingerprint(&self, hint: Option) -> Option> { + match (self.ipfs.as_ref(), self.arweave.as_ref(), hint) { + (Some((cid, path)), Some(_), Some(AssetHint::Ipfs)) | (Some((cid, path)), None, _) => { + Some(Cow::Owned(Self::fingerprint_ipfs(cid, path))) + }, + (Some(_), Some(txid), Some(AssetHint::Arweave)) | (None, Some(txid), _) => { + Some(Cow::Borrowed(Self::fingerprint_arweave(txid))) + }, + (Some(_), Some(_), None) | (None, None, _) => None, + } + } - Self::visit_url(url, |s, i| { - if let Some(c) = Self::try_ipfs(s) { - let path = i - .and_then(|i| url.path_segments().map(|s| (i, s))) - .map_or_else(String::new, |(i, s)| { - s.skip(i) - .flat_map(|s| Some("/").into_iter().chain(Some(s))) - .collect() - }); + /// Return all possible fingerprints for this asset ID. + pub fn fingerprints(&self) -> impl Iterator> { + self.ipfs + .iter() + .map(|(c, p)| Cow::Owned(Self::fingerprint_ipfs(c, p))) + .chain( + self.arweave + .iter() + .map(|t| Cow::Borrowed(Self::fingerprint_arweave(t))), + ) + } - Self::advance_heuristic(&mut ipfs, (c, path)); - } + fn fingerprint_ipfs(cid: &Cid, path: &str) -> Vec { + if path.is_empty() { + use cid::multihash::StatefulHasher; - if let Some(t) = Self::try_arweave(s) { - Self::advance_heuristic(&mut arweave, t); - } - }); + let mut h = cid::multihash::Sha2_256::default(); - Self { - ipfs: ipfs.ok().flatten(), - arweave: arweave.ok().flatten(), + cid.write_bytes(&mut h).unwrap_or_else(|_| unreachable!()); + h.update(path.as_bytes()); + + h.finalize().as_ref().to_vec() + } else { + cid.to_bytes() } } + + fn fingerprint_arweave(txid: &ArTxid) -> &[u8] { + &txid.0 + } } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index c153e4988..881583e1a 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -7,6 +7,7 @@ missing_copy_implementations )] #![warn(clippy::pedantic, clippy::cargo, missing_docs)] +#![feature(iter_intersperse)] // TODO: #[macro_use] is somewhat deprecated, but diesel still relies on it #[macro_use] @@ -16,6 +17,7 @@ extern crate diesel_migrations; pub extern crate chrono; pub extern crate clap; +pub extern crate url; pub mod assets; pub mod db; diff --git a/crates/graphql/src/schema/objects/nft.rs b/crates/graphql/src/schema/objects/nft.rs index 2c5f0c590..c1b7921ec 100644 --- a/crates/graphql/src/schema/objects/nft.rs +++ b/crates/graphql/src/schema/objects/nft.rs @@ -1,5 +1,5 @@ use base64::display::Base64Display; -use indexer_core::assets::{AssetIdentifier, ImageSize}; +use indexer_core::assets::{AssetHint, AssetIdentifier, ImageSize}; use objects::{ bid_receipt::BidReceipt, listing_receipt::ListingReceipt, purchase_receipt::PurchaseReceipt, }; @@ -159,42 +159,76 @@ Any other value will return the original image size. If no value is provided, it will return XSmall")))] pub fn image(&self, width: Option, ctx: &AppContext) -> FieldResult { - fn get_asset_cdn(id: impl AsRef<[u8]>, shared: &SharedData) -> String { - let rem = md5::compute(id).to_vec()[0].rem_euclid(shared.asset_proxy_count); + fn format_cdn_url<'a>( + shared: &SharedData, + id: &AssetIdentifier, + hint: AssetHint, + path: impl IntoIterator, + query: impl IntoIterator, + ) -> Url { + let rem = md5::compute( + id.fingerprint(Some(hint)) + .unwrap_or_else(|| unreachable!()) + .as_ref(), + ) + .to_vec()[0] + .rem_euclid(shared.asset_proxy_count); let assets_cdn = &shared.asset_proxy_endpoint; - if rem == 0 { - assets_cdn.replace("[n]", "") - } else { - assets_cdn.replace("[n]", &rem.to_string()) - } + let mut url = Url::parse(&assets_cdn.replace( + "[n]", + &if rem == 0 { + String::new() + } else { + rem.to_string() + }, + )) + .unwrap_or_else(|_| unreachable!()); + + url.path_segments_mut() + .unwrap_or_else(|_| unreachable!()) + .extend(path); + url.query_pairs_mut().extend_pairs(query); + + url } let width = ImageSize::from(width.unwrap_or(ImageSize::XSmall as i32)); - let id = AssetIdentifier::new(&Url::parse(&self.image).context("couldnt parse url")?); + let width_str = (width as i32).to_string(); + let id = + AssetIdentifier::new(&Url::parse(&self.image).context("Couldn't parse asset URL")?); - Ok(match (id.arweave, id.ipfs) { + Ok(match (id.arweave, &id.ipfs) { (Some(_), Some(_)) | (None, None) => self.image.clone(), (Some(txid), None) => { let txid = Base64Display::with_config(&txid.0, base64::URL_SAFE_NO_PAD).to_string(); - format!( - "{}arweave/{}?width={}", - get_asset_cdn(&txid, &ctx.shared), - txid, - width as i32 + format_cdn_url( + &ctx.shared, + &id, + AssetHint::Arweave, + ["arweave", &txid], + Some(("width", &*width_str)), ) + .to_string() }, (None, Some((cid, path))) => { let cid = cid.to_string(); - format!( - "{}ipfs/{}{}?width={}", - get_asset_cdn(&cid, &ctx.shared), - cid, - path, - width as i32 + format_cdn_url( + &ctx.shared, + &id, + AssetHint::Ipfs, + ["ipfs", &cid], + Some(("width", &*width_str)) + .into_iter() + .chain(if path.is_empty() { + None + } else { + Some(("path", &**path)) + }), ) + .to_string() }, }) } diff --git a/crates/indexer/src/http/client.rs b/crates/indexer/src/http/client.rs index 86c71b634..31db594ce 100644 --- a/crates/indexer/src/http/client.rs +++ b/crates/indexer/src/http/client.rs @@ -87,10 +87,18 @@ impl Client { /// # Errors /// This function fails if the CID provided is not URL safe. pub fn ipfs_link(&self, cid: &Cid, path: &str) -> Result { - let mut ret = self.ipfs_cdn.join(&cid.to_string())?; + let mut ret = self.ipfs_cdn.clone(); - if !path.is_empty() { - ret = ret.join(path)?; + { + let mut parts = ret + .path_segments_mut() + .map_err(|_| anyhow!("Invalid IPFS CDN URL"))?; + + parts.push(&cid.to_string()); + + if !path.is_empty() { + parts.extend(path.split('/')); + } } Ok(ret) diff --git a/crates/indexer/src/http/metadata_json.rs b/crates/indexer/src/http/metadata_json.rs index d42721901..048cda598 100644 --- a/crates/indexer/src/http/metadata_json.rs +++ b/crates/indexer/src/http/metadata_json.rs @@ -1,7 +1,7 @@ use std::fmt::{self, Debug, Display}; use indexer_core::{ - assets::AssetIdentifier, + assets::{AssetHint, AssetIdentifier}, db::{ insert_into, models::{ @@ -167,14 +167,18 @@ async fn try_locate_json( ) -> Result<(MetadataJsonResult, Vec)> { let mut resp = None; - for (url, fingerprint) in id + for (url, hint) in id .ipfs - .as_ref() - .map(|(c, p)| (client.ipfs_link(c, p), c.to_bytes())) - .into_iter() - .chain(id.arweave.map(|t| (client.arweave_link(&t), t.0.to_vec()))) + .iter() + .map(|(c, p)| (client.ipfs_link(c, p), AssetHint::Ipfs)) + .chain( + id.arweave + .iter() + .map(|t| (client.arweave_link(t), AssetHint::Arweave)), + ) { let url_str = url.as_ref().map_or("???", Url::as_str).to_owned(); + let fingerprint = id.fingerprint(Some(hint)).unwrap_or_else(|| unreachable!()); match fetch_json(client, meta_key, url).await { Ok(j) => { @@ -188,8 +192,8 @@ async fn try_locate_json( } } - Ok(if let Some(r) = resp { - r + Ok(if let Some((res, fingerprint)) = resp { + (res, fingerprint.into_owned()) } else { // Set to true for fallback const TRY_LAST_RESORT: bool = true; @@ -209,7 +213,7 @@ async fn try_locate_json( ) } else { bail!( - "Cached metadata fetch {:?} for {} failed (not tryiing last-resort)", + "Cached metadata fetch {:?} for {} failed (not trying last-resort)", url.as_str(), meta_key ) @@ -437,12 +441,7 @@ pub async fn process<'a>( let url = Url::parse(&uri_str).context("Couldn't parse metadata JSON URL")?; let id = AssetIdentifier::new(&url); - let possible_fingerprints: Vec<_> = id - .ipfs - .iter() - .map(|(c, _)| c.to_bytes()) - .chain(id.arweave.map(|a| a.0.to_vec())) - .collect(); + let possible_fingerprints: Vec<_> = id.fingerprints().map(Cow::into_owned).collect(); let addr = bs58::encode(meta_key).into_string(); let is_present = client From dae0b03279875a8f42a4a9629a91429d2e199c53 Mon Sep 17 00:00:00 2001 From: raykast Date: Fri, 25 Mar 2022 12:41:42 -0700 Subject: [PATCH 8/8] Added 301 from /v0 to /v1. --- crates/graphql/src/main.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/graphql/src/main.rs b/crates/graphql/src/main.rs index e023caa37..d9fd98981 100644 --- a/crates/graphql/src/main.rs +++ b/crates/graphql/src/main.rs @@ -44,6 +44,20 @@ fn graphiql(uri: String) -> impl Fn() -> HttpResponse + Clone { } } +fn redirect_version( + route: &'static str, + version: &'static str, +) -> impl Fn() -> HttpResponse + Clone { + move || { + HttpResponse::MovedPermanently() + .insert_header(("Location", version)) + .body(format!( + "API route {} deprecated, please use {}", + route, version + )) + } +} + fn graphql( db_pool: Arc, shared: Arc, @@ -128,6 +142,9 @@ fn main() { web::resource(version_extension) .route(web::post().to(graphql(db_pool.clone(), shared.clone()))), ) + .service( + web::resource("/v0").to(redirect_version("/v0", version_extension)), + ) .service( web::resource("/graphiql") .route(web::get().to(graphiql(graphiql_uri.clone()))),