From 0def7923011357c54bd5b9f0490928757fc4614a Mon Sep 17 00:00:00 2001 From: Kyle Espinola Date: Thu, 6 Jul 2023 13:06:55 +0200 Subject: [PATCH 1/9] refactor: switch from drops to collections --- api/src/entities/collections.rs | 3 + api/src/mutations/collection.rs | 771 ++++++++++++++++++++++++++++++++ api/src/mutations/drop.rs | 10 - api/src/mutations/mod.rs | 5 +- 4 files changed, 777 insertions(+), 12 deletions(-) create mode 100644 api/src/mutations/collection.rs diff --git a/api/src/entities/collections.rs b/api/src/entities/collections.rs index 5e3d3c3..713678b 100644 --- a/api/src/entities/collections.rs +++ b/api/src/entities/collections.rs @@ -11,6 +11,9 @@ pub struct Model { pub id: Uuid, pub blockchain: Blockchain, pub supply: Option, + pub project_id: Uuid, + #[sea_orm(nullable)] + pub credits_deduction_id: Option, pub creation_status: CreationStatus, pub total_mints: i64, #[sea_orm(column_type = "Text", nullable)] diff --git a/api/src/mutations/collection.rs b/api/src/mutations/collection.rs new file mode 100644 index 0000000..63bf3b0 --- /dev/null +++ b/api/src/mutations/collection.rs @@ -0,0 +1,771 @@ +use std::str::FromStr; + +use async_graphql::{Context, Error, InputObject, Object, Result, SimpleObject}; +use hub_core::{chrono::Utc, credits::CreditsClient, producer::Producer}; +use reqwest::Url; +use sea_orm::{prelude::*, JoinType, ModelTrait, QuerySelect, Set, TransactionTrait}; +use serde::{Deserialize, Serialize}; +use solana_program::pubkey::Pubkey; + +use crate::{ + blockchains::{polygon::Polygon, solana::Solana, Event}, + collection::Collection, + db::Connection, + entities::{ + collection_creators, collections, drops, metadata_jsons, + prelude::{CollectionCreators, Collections, Drops, MetadataJsons}, + project_wallets, + sea_orm_active_enums::{Blockchain as BlockchainEnum, CreationStatus}, + }, + metadata_json::MetadataJson, + objects::{Collection as CollectionObject, CollectionCreator, MetadataJsonInput}, + proto::{ + nft_events::Event as NftEvent, CreateEditionTransaction, + CreationStatus as NftCreationStatus, Creator as ProtoCreator, DropCreation, EditionInfo, + MasterEdition, MetaplexMasterEditionTransaction, NftEventKey, NftEvents, + }, + Actions, AppContext, NftStorageClient, OrganizationId, UpdateEdtionTransaction, UserID, +}; + +#[derive(Default)] +pub struct Mutation; + +#[Object(name = "CollectionMutation")] +impl Mutation { + /// This mutation creates a new NFT drop and its associated collection. The drop returns immediately with a creation status of CREATING. You can [set up a webhook](https://docs.holaplex.dev/hub/For%20Developers/webhooks-overview) to receive a notification when the drop is ready to be minted. + /// Error + /// If the drop cannot be saved to the database or fails to be emitted for submission to the desired blockchain, the mutation will result in an error. + pub async fn create_collection( + &self, + ctx: &Context<'_>, + input: CreateCollectionInput, + ) -> Result { + let AppContext { + db, + user_id, + organization_id, + balance, + .. + } = ctx.data::()?; + + let user_id = user_id.0.ok_or(Error::new("X-USER-ID header not found"))?; + let org_id = organization_id + .0 + .ok_or(Error::new("X-ORGANIZATION-ID header not found"))?; + let balance = balance + .0 + .ok_or(Error::new("X-CREDIT-BALANCE header not found"))?; + + let conn = db.get(); + let credits = ctx.data::>()?; + let solana = ctx.data::()?; + let polygon = ctx.data::()?; + let nft_storage = ctx.data::()?; + let nfts_producer = ctx.data::>()?; + + let owner_address = fetch_owner(conn, input.project, input.blockchain).await?; + + input.validate()?; + + if input.blockchain == BlockchainEnum::Solana { + validate_solana_creator_verification(&owner_address, &input.creators)?; + } + + let seller_fee_basis_points = input.seller_fee_basis_points.unwrap_or_default(); + + let collection_am = collections::ActiveModel { + blockchain: Set(input.blockchain), + supply: Set(input.supply.map(TryFrom::try_from).transpose()?), + creation_status: Set(CreationStatus::Pending), + seller_fee_basis_points: Set(seller_fee_basis_points.try_into()?), + ..Default::default() + }; + + let collection = Collection::new(collection_am) + .creators(input.creators.clone()) + .save(db) + .await?; + + let metadata_json = MetadataJson::new(input.metadata_json) + .upload(nft_storage) + .await? + .save(collection.id, db) + .await?; + + let event_key = NftEventKey { + id: collection.id.to_string(), + user_id: user_id.to_string(), + project_id: input.project.to_string(), + }; + + match input.blockchain { + BlockchainEnum::Solana => { + solana + .event() + .create_drop( + event_key, + MetaplexMasterEditionTransaction { + master_edition: Some(MasterEdition { + owner_address, + supply: input.supply.map(TryInto::try_into).transpose()?, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: seller_fee_basis_points.into(), + creators: input + .creators + .into_iter() + .map(TryFrom::try_from) + .collect::>()?, + }), + }, + ) + .await?; + }, + BlockchainEnum::Polygon => { + let amount = input.supply.ok_or(Error::new("supply is required"))?; + polygon + .create_drop( + event_key, + CreateEditionTransaction { + amount: amount.try_into()?, + edition_info: Some(EditionInfo { + creator: input + .creators + .get(0) + .ok_or(Error::new("creator is required"))? + .clone() + .address, + collection: metadata_json.name, + uri: metadata_json.uri, + description: metadata_json.description, + image_uri: metadata_json.image, + }), + fee_receiver: owner_address.clone(), + fee_numerator: seller_fee_basis_points.into(), + }, + ) + .await?; + }, + BlockchainEnum::Ethereum => { + return Err(Error::new("blockchain not supported as this time")); + }, + }; + + submit_pending_deduction( + credits, + db, + DeductionParams { + user_id, + org_id, + balance, + collection: collection.id, + blockchain: input.blockchain, + action: Actions::CreateDrop, + }, + ) + .await?; + + nfts_producer + .send( + Some(&NftEvents { + event: Some(NftEvent::DropCreated(DropCreation { + status: NftCreationStatus::InProgress as i32, + })), + }), + Some(&NftEventKey { + id: collection.id.to_string(), + project_id: input.project.to_string(), + user_id: user_id.to_string(), + }), + ) + .await?; + + Ok(CreateCollectionPayload { + collection: collection.into(), + }) + } + + /// This mutation retries an existing drop. + /// The drop returns immediately with a creation status of CREATING. + /// You can [set up a webhook](https://docs.holaplex.dev/hub/For%20Developers/webhooks-overview) to receive a notification when the drop is ready to be minted. + /// Errors + /// The mutation will fail if the drop and its related collection cannot be located, + /// if the transaction response cannot be built, + /// or if the transaction event cannot be emitted. + pub async fn retry_collection( + &self, + ctx: &Context<'_>, + input: RetryCollectionInput, + ) -> Result { + let AppContext { + db, + user_id, + organization_id, + balance, + .. + } = ctx.data::()?; + let UserID(id) = user_id; + let OrganizationId(org) = organization_id; + let conn = db.get(); + let solana = ctx.data::()?; + let polygon = ctx.data::()?; + let credits = ctx.data::>()?; + let user_id = id.ok_or(Error::new("X-USER-ID header not found"))?; + let org_id = org.ok_or(Error::new("X-ORGANIZATION-ID header not found"))?; + let balance = balance + .0 + .ok_or(Error::new("X-ORGANIZATION-BALANCE header not found"))?; + let collection = Collections::find() + .filter(collections::Column::Id.eq(input.id)) + .one(db.get()) + .await? + .ok_or(Error::new("collection not found"))?; + + if collection.creation_status == CreationStatus::Created { + return Err(Error::new("collection already created")); + } + + let metadata_json = MetadataJsons::find_by_id(collection.id) + .one(conn) + .await? + .ok_or(Error::new("metadata json not found"))?; + let creators = CollectionCreators::find() + .filter(collection_creators::Column::CollectionId.eq(collection.id)) + .all(conn) + .await?; + + let owner_address = fetch_owner(conn, collection.project_id, collection.blockchain).await?; + + let event_key = NftEventKey { + id: collection.id.to_string(), + user_id: user_id.to_string(), + project_id: collection.project_id.to_string(), + }; + + match collection.blockchain { + BlockchainEnum::Solana => { + solana + .event() + .retry_create_drop( + event_key, + MetaplexMasterEditionTransaction { + master_edition: Some(MasterEdition { + owner_address, + supply: collection.supply.map(TryInto::try_into).transpose()?, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: collection.seller_fee_basis_points.into(), + creators: creators + .into_iter() + .map(|c| ProtoCreator { + address: c.address, + verified: c.verified, + share: c.share, + }) + .collect(), + }), + }, + ) + .await?; + }, + BlockchainEnum::Polygon => { + let amount = collection + .supply + .ok_or(Error::new("Supply is null for polygon edition in db"))?; + + polygon + .event() + .retry_create_drop( + event_key, + CreateEditionTransaction { + edition_info: None, + amount, + fee_receiver: owner_address, + fee_numerator: collection.seller_fee_basis_points.into(), + }, + ) + .await?; + }, + BlockchainEnum::Ethereum => { + return Err(Error::new("blockchain not supported as this time")); + }, + }; + + submit_pending_deduction( + credits, + db, + DeductionParams { + balance, + user_id, + org_id, + collection: collection.id, + blockchain: collection.blockchain, + action: Actions::RetryDrop, + }, + ) + .await?; + + Ok(CreateCollectionPayload { + collection: collection.into(), + }) + } + + /// This mutation allows updating a drop and it's associated collection by ID. + /// It returns an error if it fails to reach the database, emit update events or assemble the on-chain transaction. + /// Returns the `PatchDropPayload` object on success. + pub async fn patch_collection( + &self, + ctx: &Context<'_>, + input: PatchCollectionInput, + ) -> Result { + let PatchCollectionInput { + id, + seller_fee_basis_points, + metadata_json, + creators, + } = input; + + let AppContext { db, user_id, .. } = ctx.data::()?; + let conn = db.get(); + let nft_storage = ctx.data::()?; + let solana = ctx.data::()?; + let polygon = ctx.data::()?; + + let user_id = user_id.0.ok_or(Error::new("X-USER-ID header not found"))?; + + let collection = Collections::find() + .filter(collections::Column::Id.eq(input.id)) + .one(db.get()) + .await? + .ok_or(Error::new("collection not found"))?; + + let owner_address = fetch_owner(conn, collection.project_id, collection.blockchain).await?; + + if let Some(creators) = &creators { + validate_creators(collection.blockchain, creators)?; + + if collection.blockchain == BlockchainEnum::Solana { + validate_solana_creator_verification(&owner_address, creators)?; + } + } + if let Some(metadata_json) = &metadata_json { + validate_json(collection.blockchain, metadata_json)?; + } + + let mut collection_am: collections::ActiveModel = collection.into(); + if let Some(seller_fee_basis_points) = seller_fee_basis_points { + collection_am.seller_fee_basis_points = Set(seller_fee_basis_points.try_into()?); + } + + let collection = collection_am.update(conn).await?; + + let current_creators = collection_creators::Entity::find() + .filter(collection_creators::Column::CollectionId.eq(collection.id)) + .all(conn) + .await?; + + if let Some(creators) = creators.clone() { + let creators = creators + .clone() + .into_iter() + .map(|creator| { + Ok(collection_creators::ActiveModel { + collection_id: Set(collection.id), + address: Set(creator.address), + verified: Set(creator.verified.unwrap_or_default()), + share: Set(creator.share.try_into()?), + }) + }) + .collect::>>()?; + + conn.transaction::<_, (), DbErr>(|txn| { + Box::pin(async move { + collection_creators::Entity::delete_many() + .filter(collection_creators::Column::CollectionId.eq(collection.id)) + .exec(txn) + .await?; + + collection_creators::Entity::insert_many(creators) + .exec(txn) + .await?; + + Ok(()) + }) + }) + .await?; + } + + let metadata_json_model = metadata_jsons::Entity::find() + .filter(metadata_jsons::Column::Id.eq(collection.id)) + .one(conn) + .await? + .ok_or(Error::new("metadata json not found"))?; + + let metadata_json_model = if let Some(metadata_json) = metadata_json { + metadata_json_model.clone().delete(conn).await?; + + MetadataJson::new(metadata_json.clone()) + .upload(nft_storage) + .await? + .save(collection.id, db) + .await? + } else { + metadata_json_model + }; + + let event_key = NftEventKey { + id: collection.id.to_string(), + user_id: user_id.to_string(), + project_id: collection.project_id.to_string(), + }; + + match collection.blockchain { + BlockchainEnum::Solana => { + let creators = if let Some(creators) = creators.clone() { + creators + .into_iter() + .map(TryFrom::try_from) + .collect::>()? + } else { + current_creators.into_iter().map(Into::into).collect() + }; + + solana + .event() + .update_drop( + event_key, + MetaplexMasterEditionTransaction { + master_edition: Some(MasterEdition { + owner_address, + supply: collection.supply.map(TryInto::try_into).transpose()?, + name: metadata_json_model.name, + symbol: metadata_json_model.symbol, + metadata_uri: metadata_json_model.uri, + seller_fee_basis_points: collection.seller_fee_basis_points.into(), + creators, + }), + }, + ) + .await?; + }, + BlockchainEnum::Polygon => { + let creator = if let Some(creators) = creators { + creators[0].address.clone() + } else { + current_creators + .get(0) + .ok_or(Error::new("No current creator found in db"))? + .address + .clone() + }; + + polygon + .event() + .update_drop( + event_key, + UpdateEdtionTransaction { + edition_info: Some(EditionInfo { + description: metadata_json_model.description, + image_uri: metadata_json_model.image, + collection: metadata_json_model.name, + uri: metadata_json_model.uri, + creator, + }), + }, + ) + .await?; + }, + BlockchainEnum::Ethereum => { + return Err(Error::new("blockchain not supported yet")); + }, + }; + + Ok(PatchCollectionPayload { + collection: collection.into(), + }) + } +} + +struct DeductionParams { + balance: u64, + user_id: Uuid, + org_id: Uuid, + collection: Uuid, + blockchain: BlockchainEnum, + action: Actions, +} + +async fn submit_pending_deduction( + credits: &CreditsClient, + db: &Connection, + params: DeductionParams, +) -> Result<()> { + let DeductionParams { + balance, + user_id, + org_id, + collection, + blockchain, + action, + } = params; + + let collection_model = Collections::find() + .filter(collections::Column::Id.eq(collection)) + .one(db.get()) + .await? + .ok_or(Error::new("drop not found"))?; + + if collection_model.credits_deduction_id.is_some() { + return Ok(()); + } + + let id = match blockchain { + BlockchainEnum::Solana | BlockchainEnum::Polygon => { + credits + .submit_pending_deduction(org_id, user_id, action, blockchain.into(), balance) + .await? + }, + BlockchainEnum::Ethereum => { + return Err(Error::new("blockchain not supported yet")); + }, + }; + + let deduction_id = id.ok_or(Error::new("Organization does not have enough credits"))?; + + let mut collection_am: collections::ActiveModel = collection_model.into(); + collection_am.credits_deduction_id = Set(Some(deduction_id.0)); + collection_am.update(db.get()).await?; + + Ok(()) +} + +async fn fetch_owner( + conn: &DatabaseConnection, + project: Uuid, + blockchain: BlockchainEnum, +) -> Result { + let wallet = project_wallets::Entity::find() + .filter( + project_wallets::Column::ProjectId + .eq(project) + .and(project_wallets::Column::Blockchain.eq(blockchain)), + ) + .one(conn) + .await?; + + let owner = wallet + .ok_or(Error::new(format!( + "no project wallet found for {blockchain} blockchain" + )))? + .wallet_address; + Ok(owner) +} + +#[derive(Debug, Clone, SimpleObject)] +pub struct CreateCollectionPayload { + collection: CollectionObject, +} + +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct CreateCollectionInput { + pub project: Uuid, + pub seller_fee_basis_points: Option, + pub supply: Option, + pub blockchain: BlockchainEnum, + pub creators: Vec, + pub metadata_json: MetadataJsonInput, +} + +impl CreateCollectionInput { + /// This function is used to validate the data of a new NFT drop before it is saved or submitted to the blockchain. + /// Validation Steps: + /// Validate the addresses of the creators. Each creator's address should be a valid address. + /// Ensure that the supply is greater than 0 or undefined. + /// Check if the end time (if provided) is in the future. + /// Validates the metadata JSON. + /// + /// # Returns: + /// - Ok(()) if all validations pass successfully. + /// # Errors + /// - Err with an appropriate error message if any validation fails. + pub fn validate(&self) -> Result<()> { + if self.supply == Some(0) { + return Err(Error::new("Supply must be greater than 0 or undefined")); + }; + + validate_creators(self.blockchain, &self.creators)?; + validate_json(self.blockchain, &self.metadata_json)?; + + Ok(()) + } +} + +fn validate_solana_creator_verification( + project_treasury_wallet_address: &str, + creators: &Vec, +) -> Result<()> { + for creator in creators { + if creator.verified.unwrap_or_default() + && creator.address != project_treasury_wallet_address + { + return Err(Error::new(format!( + "Only the project treasury wallet of {project_treasury_wallet_address} can be verified in the mutation. Other creators must be verified independently. See the Metaplex documentation for more details." + ))); + } + } + + Ok(()) +} + +/// Validates the addresses of the creators for a given blockchain. +/// # Returns +/// - Ok(()) if all creator addresses are valid blockchain addresses. +/// +/// # Errors +/// - Err with an appropriate error message if any creator address is not a valid address. +/// - Err if the blockchain is not supported. +fn validate_creators(blockchain: BlockchainEnum, creators: &Vec) -> Result<()> { + let royalty_share = creators.iter().map(|c| c.share).sum::(); + + if royalty_share != 100 { + return Err(Error::new( + "The sum of all creator shares must be equal to 100", + )); + } + + match blockchain { + BlockchainEnum::Solana => { + if creators.len() > 5 { + return Err(Error::new( + "Maximum number of creators is 5 for Solana Blockchain", + )); + } + + for creator in creators { + validate_solana_address(&creator.address)?; + } + }, + BlockchainEnum::Polygon => { + if creators.len() != 1 { + return Err(Error::new( + "Only one creator is allowed for Polygon Blockchain", + )); + } + + let address = &creators[0].clone().address; + validate_evm_address(address)?; + }, + BlockchainEnum::Ethereum => return Err(Error::new("Blockchain not supported yet")), + } + + Ok(()) +} + +pub fn validate_solana_address(address: &str) -> Result<()> { + if Pubkey::from_str(address).is_err() { + return Err(Error::new(format!( + "{address} is not a valid Solana address" + ))); + } + + Ok(()) +} + +pub fn validate_evm_address(address: &str) -> Result<()> { + let err = Err(Error::new(format!("{address} is not a valid EVM address"))); + + // Ethereum address must start with '0x' + if !address.starts_with("0x") { + return err; + } + + // Ethereum address must be exactly 40 characters long after removing '0x' + if address.len() != 42 { + return err; + } + + // Check that the address contains only hexadecimal characters + if !address[2..].chars().all(|c| c.is_ascii_hexdigit()) { + return err; + } + + Ok(()) +} + +/// Validates the JSON metadata input for the NFT drop. +/// # Returns +/// - Ok(()) if all JSON fields are valid. +/// +/// # Errors +/// - Err with an appropriate error message if any JSON field is invalid. +fn validate_json(blockchain: BlockchainEnum, json: &MetadataJsonInput) -> Result<()> { + json.animation_url + .as_ref() + .map(|animation_url| Url::from_str(animation_url)) + .transpose() + .map_err(|_| Error::new("Invalid animation url"))?; + + json.external_url + .as_ref() + .map(|external_url| Url::from_str(external_url)) + .transpose() + .map_err(|_| Error::new("Invalid external url"))?; + + Url::from_str(&json.image).map_err(|_| Error::new("Invalid image url"))?; + + if blockchain != BlockchainEnum::Solana { + return Ok(()); + } + + if json.name.chars().count() > 32 { + return Err(Error::new("Name must be less than 32 characters")); + } + + if json.symbol.chars().count() > 10 { + return Err(Error::new("Symbol must be less than 10 characters")); + } + + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct RetryCollectionInput { + pub collection: Uuid, +} + +#[derive(Debug, Clone, SimpleObject)] +pub struct RetryCollectionPayload { + collection: CollectionObject, +} + +/// Input object for patching a drop and associated collection by ID +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct PatchCollectionInput { + /// The unique identifier of the drop + pub id: Uuid, + /// The new seller fee basis points for the drop + pub seller_fee_basis_points: Option, + /// The new metadata JSON for the drop + pub metadata_json: Option, + /// The creators of the drop + pub creators: Option>, +} + +/// Represents the result of a successful patch drop mutation. +#[derive(Debug, Clone, SimpleObject)] +pub struct PatchCollectionPayload { + /// The drop that has been patched. + collection: CollectionObject, +} + +impl From for Blockchain { + fn from(v: BlockchainEnum) -> Self { + match v { + BlockchainEnum::Ethereum => Self::Ethereum, + BlockchainEnum::Polygon => Self::Polygon, + BlockchainEnum::Solana => Self::Solana, + } + } +} diff --git a/api/src/mutations/drop.rs b/api/src/mutations/drop.rs index 8157924..02ff5a6 100644 --- a/api/src/mutations/drop.rs +++ b/api/src/mutations/drop.rs @@ -938,13 +938,3 @@ pub struct ShutdownDropPayload { /// Drop that has been shutdown drop: Drop, } - -impl From for proto::Blockchain { - fn from(v: BlockchainEnum) -> Self { - match v { - BlockchainEnum::Ethereum => Self::Ethereum, - BlockchainEnum::Polygon => Self::Polygon, - BlockchainEnum::Solana => Self::Solana, - } - } -} diff --git a/api/src/mutations/mod.rs b/api/src/mutations/mod.rs index bbf350c..43452bc 100644 --- a/api/src/mutations/mod.rs +++ b/api/src/mutations/mod.rs @@ -1,10 +1,11 @@ #![allow(clippy::too_many_lines)] #![allow(clippy::unused_async)] -pub mod drop; +pub mod collection; pub mod mint; pub mod transfer; +pub mod collection; // // Add your other ones here to create a unified Mutation object // // e.x. Mutation(OrganizationMutation, OtherMutation, OtherOtherMutation) #[derive(async_graphql::MergedObject, Default)] -pub struct Mutation(drop::Mutation, mint::Mutation, transfer::Mutation); +pub struct Mutation(collection::Mutation, mint::Mutation, transfer::Mutation); From 6f3a929613034ab2b5843e18e9a8477e600d73a8 Mon Sep 17 00:00:00 2001 From: Kyle Espinola Date: Wed, 12 Jul 2023 08:33:50 +0200 Subject: [PATCH 2/9] feat: restore drops and add collection and mint to collection mutations. adjust data model for 1:1 mints. --- api/proto.lock | 14 +- api/proto.toml | 23 +- api/src/blockchains/mod.rs | 51 +-- api/src/blockchains/polygon.rs | 14 +- api/src/blockchains/solana.rs | 105 ++++- api/src/mutations/collection.rs | 165 ++----- api/src/mutations/drop.rs | 222 +++++---- api/src/mutations/mint.rs | 426 ++++++++++++++++-- api/src/mutations/mod.rs | 4 +- api/src/mutations/transfer.rs | 2 +- api/src/objects/collection.rs | 14 + migration/src/lib.rs | 4 + ...and_credits_deduction_id_to_collections.rs | 82 ++++ ..._rename_collection_creators_to_creators.rs | 88 ++++ 14 files changed, 877 insertions(+), 337 deletions(-) create mode 100644 migration/src/m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections.rs create mode 100644 migration/src/m20230711_150256_rename_collection_creators_to_creators.rs diff --git a/api/proto.lock b/api/proto.lock index f61c4b2..48b8453 100644 --- a/api/proto.lock +++ b/api/proto.lock @@ -1,26 +1,26 @@ [[schemas]] subject = "customer" -version = 2 +version = 1 sha512 = "d75800df0d4744c6b0f4d9a9952d3bfd0bb6b24a8babd19104cc11b54a525f85551b3c7375d69aeabbcf629cd826aa0bc6b0c0467add20716c504f5e856ce1c5" [[schemas]] subject = "nfts" -version = 19 -sha512 = "94be29cc87e02f9622ba880302349b275262bc30546e6a6daacea541a6c1c740df9a185d0e18de782eda77ebf9c51c0e46c295d89abb9f7fb725b0ce9cfaf6f1" +version = 1 +sha512 = "e1e47f595a3709db361e38d5995116a6fc03960a7e5f3c64e293454b062e72a95aa279d36161e7579209710d3f2e382efa559f8f380cc5a173ddc1b6ac44c259" [[schemas]] subject = "organization" -version = 5 +version = 1 sha512 = "9fb28ac73d9712292297394a5fa53a7dae9deba6847353582987ba749859301c23c05fd49d2ce84a1640f8864c5c04d59fa38907700b280000e5c4afc96654bf" [[schemas]] subject = "polygon_nfts" -version = 6 +version = 1 sha512 = "c5ddf43d2958ec690ee2261d0ff9808b67ce810d2fc4b6077f96f561929a920f03509fc8bd7adbda219250eb019f5f7be8a3f51c554f665ea1881f7a973ef2a6" [[schemas]] subject = "solana_nfts" -version = 4 +version = 1 sha512 = "272f1aed7d792a5fe5750cdca659091ca1a4a8dd4f36b35f5066ea6bb09cf4f1e905e7e5817dfa6c68d7ea3a8644192b4ff82e7ffcd85b2d9a58e48112a4a8bc" [[schemas]] @@ -30,5 +30,5 @@ sha512 = "d167e0a143c813073eef8597f0b237e5a8eaf32abbf709724e8071b2dd73ce0438b82f [[schemas]] subject = "treasury" -version = 17 +version = 1 sha512 = "c4caa4f7d032686dff56840909254f5bbb21c12b5e01cf890443dbe4d9806be637e5bbd09a66176152ecd42687fd7bc7008b50f00aec46ff941f4eb04bee25f2" diff --git a/api/proto.toml b/api/proto.toml index 0344df3..b59c3be 100644 --- a/api/proto.toml +++ b/api/proto.toml @@ -1,11 +1,20 @@ [registry] -endpoint = "https://schemas.holaplex.tools" +# endpoint = "https://schemas.holaplex.tools" +endpoint = "http://localhost:8081" [schemas] -organization = 5 -nfts = 19 -customer = 2 -treasury = 17 -solana_nfts = 4 -polygon_nfts = 6 +organization = 1 +nfts = 1 +customer = 1 +treasury = 1 +solana_nfts = 1 +polygon_nfts = 1 timestamp = 1 + +# organization = 5 +# nfts = 19 +# customer = 2 +# treasury = 17 +# solana_nfts = 4 +# polygon_nfts = 6 +# timestamp = 1 diff --git a/api/src/blockchains/mod.rs b/api/src/blockchains/mod.rs index 60a24c1..13690d5 100644 --- a/api/src/blockchains/mod.rs +++ b/api/src/blockchains/mod.rs @@ -1,7 +1,7 @@ pub mod polygon; pub mod solana; -use hub_core::{anyhow::Result, uuid::Uuid}; +use hub_core::anyhow::Result; use crate::proto::NftEventKey; @@ -15,42 +15,25 @@ pub struct TransactionResponse { pub signed_message_signatures: Vec, } -/// A trait that defines the fundamental operations that can be performed -/// on a given blockchain for a specific edition of an NFT. #[async_trait::async_trait] -pub trait Edition { - /// Creates a new NFT on the blockchain. The specifics of the creation - /// process, such as the parameters it takes and the values it returns, - /// are dependent on the implementation of this method for the specific blockchain. - async fn create(&self, payload: A) -> Result<(M, TransactionResponse)>; - - /// Mints a new instance of the NFT on the blockchain. The specifics of the minting - /// process, such as the parameters it takes and the values it returns, - /// are dependent on the implementation of this method for the specific blockchain. - async fn mint(&self, payload: B) -> Result<(M, TransactionResponse)>; - - /// Updates an existing collection on the blockchain. The specifics of the update - /// process, such as the parameters it takes and the values it returns, - /// are dependent on the implementation of this method for the specific blockchain. - async fn update(&self, payload: C) -> Result<(M, TransactionResponse)>; - - /// Transfers an NFT from one account to another on the blockchain. The specifics of the transfer - /// process, such as the parameters it takes and the values it returns, - /// are dependent on the implementation of this method for the specific blockchain. - async fn transfer(&self, payload: D) -> Result<(Uuid, TransactionResponse)>; - - /// Retries a failed drop of an NFT on the blockchain. The specifics of the retry drop - /// process, such as the parameters it takes and the values it returns, - /// are dependent on the implementation of this method for the specific blockchain. - async fn retry_drop(&self, payload: E) -> Result<(M, TransactionResponse)>; -} - -#[async_trait::async_trait] -pub trait Event { +pub trait DropEvent { async fn create_drop(&self, key: NftEventKey, payload: A) -> Result<()>; async fn retry_create_drop(&self, key: NftEventKey, payload: A) -> Result<()>; - async fn update_drop(&self, key: NftEventKey, payload: D) -> Result<()>; + async fn update_drop(&self, key: NftEventKey, payload: C) -> Result<()>; async fn mint_drop(&self, key: NftEventKey, payload: B) -> Result<()>; async fn retry_mint_drop(&self, key: NftEventKey, payload: B) -> Result<()>; - async fn transfer_asset(&self, key: NftEventKey, payload: C) -> Result<()>; +} + +#[async_trait::async_trait] +pub trait CollectionEvent { + async fn create_collection(&self, key: NftEventKey, payload: A) -> Result<()>; + async fn retry_create_collection(&self, key: NftEventKey, payload: A) -> Result<()>; + async fn update_collection(&self, key: NftEventKey, payload: B) -> Result<()>; + async fn mint_to_collection(&self, key: NftEventKey, payload: C) -> Result<()>; + async fn retry_mint_to_collection(&self, key: NftEventKey, payload: C) -> Result<()>; +} + +#[async_trait::async_trait] +pub trait TransferEvent { + async fn transfer_asset(&self, key: NftEventKey, payload: A) -> Result<()>; } diff --git a/api/src/blockchains/polygon.rs b/api/src/blockchains/polygon.rs index 1df5a6b..b8f9456 100644 --- a/api/src/blockchains/polygon.rs +++ b/api/src/blockchains/polygon.rs @@ -1,6 +1,6 @@ use hub_core::{anyhow::Result, producer::Producer}; -use super::Event; +use super::{DropEvent, TransferEvent}; use crate::proto::{ nft_events::Event::{ PolygonCreateDrop, PolygonMintDrop, PolygonRetryDrop, PolygonRetryMintDrop, @@ -24,22 +24,20 @@ impl Polygon { #[must_use] pub fn event( &self, - ) -> impl Event< + ) -> impl DropEvent< CreateEditionTransaction, MintEditionTransaction, - TransferPolygonAsset, UpdateEdtionTransaction, - > { + > + TransferEvent { self.clone() } } #[async_trait::async_trait] impl - Event< + DropEvent< CreateEditionTransaction, MintEditionTransaction, - TransferPolygonAsset, UpdateEdtionTransaction, > for Polygon { @@ -100,13 +98,17 @@ impl Ok(()) } +} +#[async_trait::async_trait] +impl TransferEvent for Polygon { async fn transfer_asset(&self, key: NftEventKey, payload: TransferPolygonAsset) -> Result<()> { let event = NftEvents { event: Some(PolygonTransferAsset(payload)), }; self.producer.send(Some(&event), Some(&key)).await?; + Ok(()) } } diff --git a/api/src/blockchains/solana.rs b/api/src/blockchains/solana.rs index 9cdd0ba..2e89b5d 100644 --- a/api/src/blockchains/solana.rs +++ b/api/src/blockchains/solana.rs @@ -1,12 +1,14 @@ use hub_core::{anyhow::Result, producer::Producer}; -use super::Event; +use super::{CollectionEvent, DropEvent, TransferEvent}; use crate::proto::{ nft_events::Event::{ - SolanaCreateDrop, SolanaMintDrop, SolanaRetryDrop, SolanaRetryMintDrop, - SolanaTransferAsset, SolanaUpdateDrop, + SolanaCreateCollection, SolanaCreateDrop, SolanaMintDrop, SolanaMintToCollection, + SolanaRetryCreateCollection, SolanaRetryDrop, SolanaRetryMintDrop, + SolanaRetryMintToCollection, SolanaTransferAsset, SolanaUpdateCollection, SolanaUpdateDrop, }, - MetaplexMasterEditionTransaction, MintMetaplexEditionTransaction, NftEventKey, NftEvents, + MetaplexCertifiedCollectionTransaction, MetaplexMasterEditionTransaction, + MintMetaplexEditionTransaction, MintMetaplexMetadataTransaction, NftEventKey, NftEvents, TransferMetaplexAssetTransaction, }; @@ -24,11 +26,15 @@ impl Solana { #[must_use] pub fn event( &self, - ) -> impl Event< + ) -> impl DropEvent< MetaplexMasterEditionTransaction, MintMetaplexEditionTransaction, - TransferMetaplexAssetTransaction, MetaplexMasterEditionTransaction, + > + TransferEvent + + CollectionEvent< + MetaplexCertifiedCollectionTransaction, + MetaplexCertifiedCollectionTransaction, + MintMetaplexMetadataTransaction, > { self.clone() } @@ -36,10 +42,9 @@ impl Solana { #[async_trait::async_trait] impl - Event< + DropEvent< MetaplexMasterEditionTransaction, MintMetaplexEditionTransaction, - TransferMetaplexAssetTransaction, MetaplexMasterEditionTransaction, > for Solana { @@ -112,13 +117,16 @@ impl Ok(()) } +} +#[async_trait::async_trait] +impl TransferEvent for Solana { async fn transfer_asset( &self, key: NftEventKey, payload: TransferMetaplexAssetTransaction, ) -> Result<()> { - let event: NftEvents = NftEvents { + let event = NftEvents { event: Some(SolanaTransferAsset(payload)), }; @@ -127,3 +135,82 @@ impl Ok(()) } } + +#[async_trait::async_trait] +impl + CollectionEvent< + MetaplexCertifiedCollectionTransaction, + MetaplexCertifiedCollectionTransaction, + MintMetaplexMetadataTransaction, + > for Solana +{ + async fn create_collection( + &self, + key: NftEventKey, + payload: MetaplexCertifiedCollectionTransaction, + ) -> Result<()> { + let event = NftEvents { + event: Some(SolanaCreateCollection(payload)), + }; + + self.producer.send(Some(&event), Some(&key)).await?; + + Ok(()) + } + + async fn retry_create_collection( + &self, + key: NftEventKey, + payload: MetaplexCertifiedCollectionTransaction, + ) -> Result<()> { + let event = NftEvents { + event: Some(SolanaRetryCreateCollection(payload)), + }; + + self.producer.send(Some(&event), Some(&key)).await?; + + Ok(()) + } + + async fn update_collection( + &self, + key: NftEventKey, + payload: MetaplexCertifiedCollectionTransaction, + ) -> Result<()> { + let event = NftEvents { + event: Some(SolanaUpdateCollection(payload)), + }; + + self.producer.send(Some(&event), Some(&key)).await?; + + Ok(()) + } + + async fn mint_to_collection( + &self, + key: NftEventKey, + payload: MintMetaplexMetadataTransaction, + ) -> Result<()> { + let event = NftEvents { + event: Some(SolanaMintToCollection(payload)), + }; + + self.producer.send(Some(&event), Some(&key)).await?; + + Ok(()) + } + + async fn retry_mint_to_collection( + &self, + key: NftEventKey, + payload: MintMetaplexMetadataTransaction, + ) -> Result<()> { + let event = NftEvents { + event: Some(SolanaRetryMintToCollection(payload)), + }; + + self.producer.send(Some(&event), Some(&key)).await?; + + Ok(()) + } +} diff --git a/api/src/mutations/collection.rs b/api/src/mutations/collection.rs index 63bf3b0..b35204e 100644 --- a/api/src/mutations/collection.rs +++ b/api/src/mutations/collection.rs @@ -1,31 +1,30 @@ use std::str::FromStr; -use async_graphql::{Context, Error, InputObject, Object, Result, SimpleObject}; -use hub_core::{chrono::Utc, credits::CreditsClient, producer::Producer}; -use reqwest::Url; -use sea_orm::{prelude::*, JoinType, ModelTrait, QuerySelect, Set, TransactionTrait}; -use serde::{Deserialize, Serialize}; -use solana_program::pubkey::Pubkey; - use crate::{ - blockchains::{polygon::Polygon, solana::Solana, Event}, + blockchains::{polygon::Polygon, solana::Solana, CollectionEvent}, collection::Collection, db::Connection, entities::{ - collection_creators, collections, drops, metadata_jsons, - prelude::{CollectionCreators, Collections, Drops, MetadataJsons}, + collection_creators, collections, metadata_jsons, + prelude::{CollectionCreators, Collections, MetadataJsons}, project_wallets, sea_orm_active_enums::{Blockchain as BlockchainEnum, CreationStatus}, }, metadata_json::MetadataJson, objects::{Collection as CollectionObject, CollectionCreator, MetadataJsonInput}, proto::{ - nft_events::Event as NftEvent, CreateEditionTransaction, - CreationStatus as NftCreationStatus, Creator as ProtoCreator, DropCreation, EditionInfo, - MasterEdition, MetaplexMasterEditionTransaction, NftEventKey, NftEvents, + nft_events::Event as NftEvent, CreationStatus as NftCreationStatus, + Creator as ProtoCreator, DropCreation, MetaplexCertifiedCollectionTransaction, + MetaplexMetadata, NftEventKey, NftEvents, }, - Actions, AppContext, NftStorageClient, OrganizationId, UpdateEdtionTransaction, UserID, + Actions, AppContext, NftStorageClient, OrganizationId, UserID, }; +use async_graphql::{Context, Error, InputObject, Object, Result, SimpleObject}; +use hub_core::{credits::CreditsClient, producer::Producer}; +use reqwest::Url; +use sea_orm::{prelude::*, ModelTrait, Set, TransactionTrait}; +use serde::{Deserialize, Serialize}; +use solana_program::pubkey::Pubkey; #[derive(Default)] pub struct Mutation; @@ -71,13 +70,11 @@ impl Mutation { validate_solana_creator_verification(&owner_address, &input.creators)?; } - let seller_fee_basis_points = input.seller_fee_basis_points.unwrap_or_default(); - let collection_am = collections::ActiveModel { blockchain: Set(input.blockchain), - supply: Set(input.supply.map(TryFrom::try_from).transpose()?), + supply: Set(Some(0)), creation_status: Set(CreationStatus::Pending), - seller_fee_basis_points: Set(seller_fee_basis_points.try_into()?), + project_id: Set(input.project), ..Default::default() }; @@ -102,16 +99,15 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .create_drop( + .create_collection( event_key, - MetaplexMasterEditionTransaction { - master_edition: Some(MasterEdition { + MetaplexCertifiedCollectionTransaction { + metadata: Some(MetaplexMetadata { owner_address, - supply: input.supply.map(TryInto::try_into).transpose()?, name: metadata_json.name, symbol: metadata_json.symbol, metadata_uri: metadata_json.uri, - seller_fee_basis_points: seller_fee_basis_points.into(), + seller_fee_basis_points: 0, creators: input .creators .into_iter() @@ -122,32 +118,7 @@ impl Mutation { ) .await?; }, - BlockchainEnum::Polygon => { - let amount = input.supply.ok_or(Error::new("supply is required"))?; - polygon - .create_drop( - event_key, - CreateEditionTransaction { - amount: amount.try_into()?, - edition_info: Some(EditionInfo { - creator: input - .creators - .get(0) - .ok_or(Error::new("creator is required"))? - .clone() - .address, - collection: metadata_json.name, - uri: metadata_json.uri, - description: metadata_json.description, - image_uri: metadata_json.image, - }), - fee_receiver: owner_address.clone(), - fee_numerator: seller_fee_basis_points.into(), - }, - ) - .await?; - }, - BlockchainEnum::Ethereum => { + BlockchainEnum::Ethereum | BlockchainEnum::Polygon => { return Err(Error::new("blockchain not supported as this time")); }, }; @@ -166,6 +137,7 @@ impl Mutation { ) .await?; + // TODO: separate event for collection creation nfts_producer .send( Some(&NftEvents { @@ -247,16 +219,15 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_create_drop( + .retry_create_collection( event_key, - MetaplexMasterEditionTransaction { - master_edition: Some(MasterEdition { + MetaplexCertifiedCollectionTransaction { + metadata: Some(MetaplexMetadata { owner_address, - supply: collection.supply.map(TryInto::try_into).transpose()?, name: metadata_json.name, symbol: metadata_json.symbol, metadata_uri: metadata_json.uri, - seller_fee_basis_points: collection.seller_fee_basis_points.into(), + seller_fee_basis_points: 0, creators: creators .into_iter() .map(|c| ProtoCreator { @@ -270,25 +241,7 @@ impl Mutation { ) .await?; }, - BlockchainEnum::Polygon => { - let amount = collection - .supply - .ok_or(Error::new("Supply is null for polygon edition in db"))?; - - polygon - .event() - .retry_create_drop( - event_key, - CreateEditionTransaction { - edition_info: None, - amount, - fee_receiver: owner_address, - fee_numerator: collection.seller_fee_basis_points.into(), - }, - ) - .await?; - }, - BlockchainEnum::Ethereum => { + BlockchainEnum::Polygon | BlockchainEnum::Ethereum => { return Err(Error::new("blockchain not supported as this time")); }, }; @@ -322,7 +275,6 @@ impl Mutation { ) -> Result { let PatchCollectionInput { id, - seller_fee_basis_points, metadata_json, creators, } = input; @@ -355,9 +307,6 @@ impl Mutation { } let mut collection_am: collections::ActiveModel = collection.into(); - if let Some(seller_fee_basis_points) = seller_fee_basis_points { - collection_am.seller_fee_basis_points = Set(seller_fee_basis_points.try_into()?); - } let collection = collection_am.update(conn).await?; @@ -434,50 +383,22 @@ impl Mutation { solana .event() - .update_drop( + .update_collection( event_key, - MetaplexMasterEditionTransaction { - master_edition: Some(MasterEdition { + MetaplexCertifiedCollectionTransaction { + metadata: Some(MetaplexMetadata { owner_address, - supply: collection.supply.map(TryInto::try_into).transpose()?, name: metadata_json_model.name, symbol: metadata_json_model.symbol, metadata_uri: metadata_json_model.uri, - seller_fee_basis_points: collection.seller_fee_basis_points.into(), + seller_fee_basis_points: 0, creators, }), }, ) .await?; }, - BlockchainEnum::Polygon => { - let creator = if let Some(creators) = creators { - creators[0].address.clone() - } else { - current_creators - .get(0) - .ok_or(Error::new("No current creator found in db"))? - .address - .clone() - }; - - polygon - .event() - .update_drop( - event_key, - UpdateEdtionTransaction { - edition_info: Some(EditionInfo { - description: metadata_json_model.description, - image_uri: metadata_json_model.image, - collection: metadata_json_model.name, - uri: metadata_json_model.uri, - creator, - }), - }, - ) - .await?; - }, - BlockchainEnum::Ethereum => { + BlockchainEnum::Polygon | BlockchainEnum::Ethereum => { return Err(Error::new("blockchain not supported yet")); }, }; @@ -522,12 +443,12 @@ async fn submit_pending_deduction( } let id = match blockchain { - BlockchainEnum::Solana | BlockchainEnum::Polygon => { + BlockchainEnum::Solana => { credits .submit_pending_deduction(org_id, user_id, action, blockchain.into(), balance) .await? }, - BlockchainEnum::Ethereum => { + BlockchainEnum::Ethereum | BlockchainEnum::Polygon => { return Err(Error::new("blockchain not supported yet")); }, }; @@ -571,8 +492,6 @@ pub struct CreateCollectionPayload { #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] pub struct CreateCollectionInput { pub project: Uuid, - pub seller_fee_basis_points: Option, - pub supply: Option, pub blockchain: BlockchainEnum, pub creators: Vec, pub metadata_json: MetadataJsonInput, @@ -591,10 +510,6 @@ impl CreateCollectionInput { /// # Errors /// - Err with an appropriate error message if any validation fails. pub fn validate(&self) -> Result<()> { - if self.supply == Some(0) { - return Err(Error::new("Supply must be greater than 0 or undefined")); - }; - validate_creators(self.blockchain, &self.creators)?; validate_json(self.blockchain, &self.metadata_json)?; @@ -732,7 +647,7 @@ fn validate_json(blockchain: BlockchainEnum, json: &MetadataJsonInput) -> Result #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] pub struct RetryCollectionInput { - pub collection: Uuid, + pub id: Uuid, } #[derive(Debug, Clone, SimpleObject)] @@ -745,8 +660,6 @@ pub struct RetryCollectionPayload { pub struct PatchCollectionInput { /// The unique identifier of the drop pub id: Uuid, - /// The new seller fee basis points for the drop - pub seller_fee_basis_points: Option, /// The new metadata JSON for the drop pub metadata_json: Option, /// The creators of the drop @@ -759,13 +672,3 @@ pub struct PatchCollectionPayload { /// The drop that has been patched. collection: CollectionObject, } - -impl From for Blockchain { - fn from(v: BlockchainEnum) -> Self { - match v { - BlockchainEnum::Ethereum => Self::Ethereum, - BlockchainEnum::Polygon => Self::Polygon, - BlockchainEnum::Solana => Self::Solana, - } - } -} diff --git a/api/src/mutations/drop.rs b/api/src/mutations/drop.rs index 02ff5a6..d617ac7 100644 --- a/api/src/mutations/drop.rs +++ b/api/src/mutations/drop.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use solana_program::pubkey::Pubkey; use crate::{ - blockchains::{polygon::Polygon, solana::Solana, Event}, + blockchains::{polygon::Polygon, solana::Solana, DropEvent}, collection::Collection, db::Connection, entities::{ @@ -114,43 +114,49 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .create_drop(event_key, proto::MetaplexMasterEditionTransaction { - master_edition: Some(proto::MasterEdition { - owner_address, - supply: input.supply.map(TryInto::try_into).transpose()?, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: seller_fee_basis_points.into(), - creators: input - .creators - .into_iter() - .map(TryFrom::try_from) - .collect::>()?, - }), - }) + .create_drop( + event_key, + proto::MetaplexMasterEditionTransaction { + master_edition: Some(proto::MasterEdition { + owner_address, + supply: input.supply.map(TryInto::try_into).transpose()?, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: seller_fee_basis_points.into(), + creators: input + .creators + .into_iter() + .map(TryFrom::try_from) + .collect::>()?, + }), + }, + ) .await?; }, BlockchainEnum::Polygon => { let amount = input.supply.ok_or(Error::new("supply is required"))?; polygon - .create_drop(event_key, proto::CreateEditionTransaction { - amount: amount.try_into()?, - edition_info: Some(proto::EditionInfo { - creator: input - .creators - .get(0) - .ok_or(Error::new("creator is required"))? - .clone() - .address, - collection: metadata_json.name, - uri: metadata_json.uri, - description: metadata_json.description, - image_uri: metadata_json.image, - }), - fee_receiver: owner_address.clone(), - fee_numerator: seller_fee_basis_points.into(), - }) + .create_drop( + event_key, + proto::CreateEditionTransaction { + amount: amount.try_into()?, + edition_info: Some(proto::EditionInfo { + creator: input + .creators + .get(0) + .ok_or(Error::new("creator is required"))? + .clone() + .address, + collection: metadata_json.name, + uri: metadata_json.uri, + description: metadata_json.description, + image_uri: metadata_json.image, + }), + fee_receiver: owner_address.clone(), + fee_numerator: seller_fee_basis_points.into(), + }, + ) .await?; }, BlockchainEnum::Ethereum => { @@ -158,14 +164,18 @@ impl Mutation { }, }; - submit_pending_deduction(credits, db, DeductionParams { - user_id, - org_id, - balance, - drop: drop_model.id, - blockchain: input.blockchain, - action: Actions::CreateDrop, - }) + submit_pending_deduction( + credits, + db, + DeductionParams { + user_id, + org_id, + balance, + drop: drop_model.id, + blockchain: input.blockchain, + action: Actions::CreateDrop, + }, + ) .await?; nfts_producer @@ -251,24 +261,27 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_create_drop(event_key, proto::MetaplexMasterEditionTransaction { - master_edition: Some(proto::MasterEdition { - owner_address, - supply: collection.supply.map(TryInto::try_into).transpose()?, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: collection.seller_fee_basis_points.into(), - creators: creators - .into_iter() - .map(|c| proto::Creator { - address: c.address, - verified: c.verified, - share: c.share, - }) - .collect(), - }), - }) + .retry_create_drop( + event_key, + proto::MetaplexMasterEditionTransaction { + master_edition: Some(proto::MasterEdition { + owner_address, + supply: collection.supply.map(TryInto::try_into).transpose()?, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: collection.seller_fee_basis_points.into(), + creators: creators + .into_iter() + .map(|c| proto::Creator { + address: c.address, + verified: c.verified, + share: c.share, + }) + .collect(), + }), + }, + ) .await?; }, BlockchainEnum::Polygon => { @@ -278,12 +291,15 @@ impl Mutation { polygon .event() - .retry_create_drop(event_key, proto::CreateEditionTransaction { - edition_info: None, - amount, - fee_receiver: owner_address, - fee_numerator: collection.seller_fee_basis_points.into(), - }) + .retry_create_drop( + event_key, + proto::CreateEditionTransaction { + edition_info: None, + amount, + fee_receiver: owner_address, + fee_numerator: collection.seller_fee_basis_points.into(), + }, + ) .await?; }, BlockchainEnum::Ethereum => { @@ -291,14 +307,18 @@ impl Mutation { }, }; - submit_pending_deduction(credits, db, DeductionParams { - balance, - user_id, - org_id, - drop: drop.id, - blockchain: collection.blockchain, - action: Actions::RetryDrop, - }) + submit_pending_deduction( + credits, + db, + DeductionParams { + balance, + user_id, + org_id, + drop: drop.id, + blockchain: collection.blockchain, + action: Actions::RetryDrop, + }, + ) .await?; Ok(CreateDropPayload { @@ -557,17 +577,20 @@ impl Mutation { solana .event() - .update_drop(event_key, proto::MetaplexMasterEditionTransaction { - master_edition: Some(proto::MasterEdition { - owner_address, - supply: collection.supply.map(TryInto::try_into).transpose()?, - name: metadata_json_model.name, - symbol: metadata_json_model.symbol, - metadata_uri: metadata_json_model.uri, - seller_fee_basis_points: collection.seller_fee_basis_points.into(), - creators, - }), - }) + .update_drop( + event_key, + proto::MetaplexMasterEditionTransaction { + master_edition: Some(proto::MasterEdition { + owner_address, + supply: collection.supply.map(TryInto::try_into).transpose()?, + name: metadata_json_model.name, + symbol: metadata_json_model.symbol, + metadata_uri: metadata_json_model.uri, + seller_fee_basis_points: collection.seller_fee_basis_points.into(), + creators, + }), + }, + ) .await?; }, BlockchainEnum::Polygon => { @@ -583,15 +606,18 @@ impl Mutation { polygon .event() - .update_drop(event_key, proto::UpdateEdtionTransaction { - edition_info: Some(EditionInfo { - description: metadata_json_model.description, - image_uri: metadata_json_model.image, - collection: metadata_json_model.name, - uri: metadata_json_model.uri, - creator, - }), - }) + .update_drop( + event_key, + proto::UpdateEdtionTransaction { + edition_info: Some(EditionInfo { + description: metadata_json_model.description, + image_uri: metadata_json_model.image, + collection: metadata_json_model.name, + uri: metadata_json_model.uri, + creator, + }), + }, + ) .await?; }, BlockchainEnum::Ethereum => { @@ -938,3 +964,13 @@ pub struct ShutdownDropPayload { /// Drop that has been shutdown drop: Drop, } + +impl From for proto::Blockchain { + fn from(v: BlockchainEnum) -> Self { + match v { + BlockchainEnum::Ethereum => Self::Ethereum, + BlockchainEnum::Polygon => Self::Polygon, + BlockchainEnum::Solana => Self::Solana, + } + } +} diff --git a/api/src/mutations/mint.rs b/api/src/mutations/mint.rs index 1d23eee..eb9f01b 100644 --- a/api/src/mutations/mint.rs +++ b/api/src/mutations/mint.rs @@ -5,7 +5,7 @@ use hub_core::{chrono::Utc, credits::CreditsClient, producer::Producer}; use sea_orm::{prelude::*, JoinType, QuerySelect, Set}; use crate::{ - blockchains::{polygon::Polygon, solana::Solana, Event}, + blockchains::{polygon::Polygon, solana::Solana, DropEvent, CollectionEvent}, db::Connection, entities::{ collection_mints, collections, drops, @@ -14,11 +14,12 @@ use crate::{ sea_orm_active_enums::{Blockchain as BlockchainEnum, CreationStatus}, }, metadata_json::MetadataJson, + objects::{CollectionCreator, MetadataJsonInput}, proto::{ self, nft_events::Event as NftEvent, CreationStatus as NftCreationStatus, MintCreation, - NftEventKey, NftEvents, + NftEventKey, NftEvents, MetaplexMetadata, }, - Actions, AppContext, OrganizationId, UserID, + Actions, AppContext, NftStorageClient, OrganizationId, UserID, }; #[derive(Default)] @@ -66,7 +67,7 @@ impl Mutation { let (drop_model, collection_model) = drop_model.ok_or(Error::new("drop not found"))?; // Call check_drop_status to check that drop is currently running - check_drop_status(&drop_model).await?; + check_drop_status(&drop_model)?; let collection = collection_model.ok_or(Error::new("collection not found"))?; @@ -120,22 +121,28 @@ impl Mutation { solana .event() - .mint_drop(event_key, proto::MintMetaplexEditionTransaction { - recipient_address: input.recipient.to_string(), - owner_address: owner_address.to_string(), - edition, - collection_id: collection.id.to_string(), - }) + .mint_drop( + event_key, + proto::MintMetaplexEditionTransaction { + recipient_address: input.recipient.to_string(), + owner_address: owner_address.to_string(), + edition, + collection_id: collection.id.to_string(), + }, + ) .await?; }, BlockchainEnum::Polygon => { polygon .event() - .mint_drop(event_key, proto::MintEditionTransaction { - receiver: input.recipient.to_string(), - amount: 1, - collection_id: collection.id.to_string(), - }) + .mint_drop( + event_key, + proto::MintEditionTransaction { + receiver: input.recipient.to_string(), + amount: 1, + collection_id: collection.id.to_string(), + }, + ) .await?; }, BlockchainEnum::Ethereum => { @@ -161,14 +168,18 @@ impl Mutation { purchase_am.insert(conn).await?; - submit_pending_deduction(credits, db, DeductionParams { - balance, - user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::MintEdition, - }) + submit_pending_deduction( + credits, + db, + DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::MintEdition, + }, + ) .await?; nfts_producer @@ -195,11 +206,11 @@ impl Mutation { /// This mutation retries a mint which failed or is in pending state. The mint returns immediately with a creation status of CREATING. You can [set up a webhook](https://docs.holaplex.dev/hub/For%20Developers/webhooks-overview) to receive a notification when the mint is accepted by the blockchain. /// # Errors /// If the mint cannot be saved to the database or fails to be emitted for submission to the desired blockchain, the mutation will result in an error. - pub async fn retry_mint( + pub async fn retry_mint_edition( &self, ctx: &Context<'_>, - input: RetryMintInput, - ) -> Result { + input: RetryMintEditionInput, + ) -> Result { let AppContext { db, user_id, @@ -276,22 +287,28 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_mint_drop(event_key, proto::MintMetaplexEditionTransaction { - recipient_address: recipient.to_string(), - owner_address: owner_address.to_string(), - edition, - collection_id: collection.id.to_string(), - }) + .retry_mint_drop( + event_key, + proto::MintMetaplexEditionTransaction { + recipient_address: recipient.to_string(), + owner_address: owner_address.to_string(), + edition, + collection_id: collection.id.to_string(), + }, + ) .await?; }, BlockchainEnum::Polygon => { polygon .event() - .retry_mint_drop(event_key, proto::MintEditionTransaction { - receiver: recipient.to_string(), - amount: 1, - collection_id: collection.id.to_string(), - }) + .retry_mint_drop( + event_key, + proto::MintEditionTransaction { + receiver: recipient.to_string(), + amount: 1, + collection_id: collection.id.to_string(), + }, + ) .await?; }, BlockchainEnum::Ethereum => { @@ -299,17 +316,299 @@ impl Mutation { }, }; - submit_pending_deduction(credits, db, DeductionParams { - balance, + submit_pending_deduction( + credits, + db, + DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::RetryMint, + }, + ) + .await?; + + Ok(RetryMintEditionPayload { + collection_mint: collection_mint_model.into(), + }) + } + + pub async fn mint_to_collection( + &self, + ctx: &Context<'_>, + input: MintToCollectionInput, + ) -> Result { + let AppContext { + db, user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::RetryMint, + organization_id, + balance, + .. + } = ctx.data::()?; + let credits = ctx.data::>()?; + let conn = db.get(); + let solana = ctx.data::()?; + let nfts_producer = ctx.data::>()?; + let nft_storage = ctx.data::()?; + + let UserID(id) = user_id; + let OrganizationId(org) = organization_id; + + let user_id = id.ok_or(Error::new("X-USER-ID header not found"))?; + let org_id = org.ok_or(Error::new("X-ORGANIZATION-ID header not found"))?; + let balance = balance + .0 + .ok_or(Error::new("X-CREDIT-BALANCE header not found"))?; + + let collection = Collections::find() + .filter(drops::Column::Id.eq(input.collection)) + .one(conn) + .await?; + + let collection = collection.ok_or(Error::new("collection not found"))?; + + check_collection_status(&collection)?; + + let seller_fee_basis_points = input.seller_fee_basis_points.unwrap_or_default(); + + // Fetch the project wallet address which will sign the transaction by hub-treasuries + let wallet = project_wallets::Entity::find() + .filter( + project_wallets::Column::ProjectId + .eq(collection.project_id) + .and(project_wallets::Column::Blockchain.eq(collection.blockchain)), + ) + .one(conn) + .await?; + + let owner_address = wallet + .ok_or(Error::new(format!( + "no project wallet found for {} blockchain", + collection.blockchain + )))? + .wallet_address; + + // insert a collection mint record into database + let collection_mint_active_model = collection_mints::ActiveModel { + collection_id: Set(collection.id), + owner: Set(input.recipient.clone()), + creation_status: Set(CreationStatus::Pending), + seller_fee_basis_points: Set(collection.seller_fee_basis_points), + created_by: Set(user_id), + ..Default::default() + }; + + let metadata_json = MetadataJson::new(input.metadata_json) + .upload(nft_storage) + .await? + .save(collection.id, db) + .await?; + + let collection_mint_model = collection_mint_active_model.insert(conn).await?; + let event_key = NftEventKey { + id: collection_mint_model.id.to_string(), + user_id: user_id.to_string(), + project_id: collection.project_id.to_string(), + }; + + match collection.blockchain { + BlockchainEnum::Solana => { + solana + .event() + .mint_to_collection( + event_key, + proto::MintMetaplexMetadataTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: seller_fee_basis_points.into(), + creators: input + .creators + .into_iter() + .map(TryFrom::try_from) + .collect::>()?, + }), + recipient_address: input.recipient.to_string(), + compressed: input.compressed, + collection_id: collection.id.to_string(), + }, + ) + .await?; + }, + BlockchainEnum::Ethereum | BlockchainEnum::Polygon => { + return Err(Error::new("blockchain not supported as this time")); + }, + }; + + let mut collection_am = collections::ActiveModel::from(collection.clone()); + collection_am.total_mints = Set(collection.total_mints + 1); + collection_am.update(conn).await?; + + // TODO: Switch to purchase history + // inserts a purchase record in the database + // let purchase_am = purchases::ActiveModel { + // mint_id: Set(collection_mint_model.id), + // wallet: Set(input.recipient), + // spent: Set(drop_model.price), + // drop_id: Set(Some(drop_model.id)), + // tx_signature: Set(None), + // status: Set(CreationStatus::Pending), + // created_at: Set(Utc::now().into()), + // ..Default::default() + // }; + + // purchase_am.insert(conn).await?; + + submit_pending_deduction( + credits, + db, + DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::MintEdition, + }, + ) + .await?; + + // nfts_producer + // .send( + // Some(&NftEvents { + // event: Some(NftEvent::DropMinted(MintCreation { + // drop_id: drop_model.id.to_string(), + // status: NftCreationStatus::InProgress as i32, + // })), + // }), + // Some(&NftEventKey { + // id: collection_mint_model.id.to_string(), + // project_id: collection.project_id.to_string(), + // user_id: user_id.to_string(), + // }), + // ) + // .await?; + + Ok(MintToCollectionPayload { + collection_mint: collection_mint_model.into(), }) + } + + pub async fn retry_mint_to_collection( + &self, + ctx: &Context<'_>, + input: RetryMintEditionInput, + ) -> Result { + let AppContext { + db, + user_id, + organization_id, + balance, + .. + } = ctx.data::()?; + let credits = ctx.data::>()?; + let conn = db.get(); + let solana = ctx.data::()?; + + let UserID(id) = user_id; + let OrganizationId(org) = organization_id; + + let user_id = id.ok_or(Error::new("X-USER-ID header not found"))?; + let org_id = org.ok_or(Error::new("X-ORGANIZATION-ID header not found"))?; + let balance = balance + .0 + .ok_or(Error::new("X-ORGANIZATION-BALANCE header not found"))?; + + let (collection_mint_model, drop) = collection_mints::Entity::find() + .join( + JoinType::InnerJoin, + collection_mints::Relation::Collections.def(), + ) + .join(JoinType::InnerJoin, collections::Relation::Drop.def()) + .select_also(drops::Entity) + .filter(collection_mints::Column::Id.eq(input.id)) + .one(conn) + .await? + .ok_or(Error::new("collection mint not found"))?; + + if collection_mint_model.creation_status == CreationStatus::Created { + return Err(Error::new("mint is already created")); + } + + let collection = collections::Entity::find() + .filter(collections::Column::Id.eq(collection_mint_model.collection_id)) + .one(conn) + .await? + .ok_or(Error::new("collection not found"))?; + + let drop_model = drop.ok_or(Error::new("drop not found"))?; + + let recipient = collection_mint_model.owner.clone(); + let edition = collection_mint_model.edition; + let project_id = drop_model.project_id; + + // Fetch the project wallet address which will sign the transaction by hub-treasuries + let wallet = project_wallets::Entity::find() + .filter( + project_wallets::Column::ProjectId + .eq(project_id) + .and(project_wallets::Column::Blockchain.eq(collection.blockchain)), + ) + .one(conn) + .await?; + + let owner_address = wallet + .ok_or(Error::new(format!( + "no project wallet found for {} blockchain", + collection.blockchain + )))? + .wallet_address; + + let event_key = NftEventKey { + id: collection_mint_model.id.to_string(), + user_id: user_id.to_string(), + project_id: project_id.to_string(), + }; + + match collection.blockchain { + BlockchainEnum::Solana => { + solana + .event() + .retry_mint_drop( + event_key, + proto::MintMetaplexEditionTransaction { + recipient_address: recipient.to_string(), + owner_address: owner_address.to_string(), + edition, + collection_id: collection.id.to_string(), + }, + ) + .await?; + }, + BlockchainEnum::Ethereum | BlockchainEnum::Polygon => { + return Err(Error::new("blockchain not supported as this time")); + }, + }; + + submit_pending_deduction( + credits, + db, + DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::RetryMint, + }, + ) .await?; - Ok(RetryMintPayload { + Ok(RetryMintEditionPayload { collection_mint: collection_mint_model.into(), }) } @@ -371,7 +670,7 @@ async fn submit_pending_deduction( /// /// This function returns an error if the drop is not yet created, paused, /// shutdown, has not yet started, or has already ended based -async fn check_drop_status(drop_model: &drops::Model) -> Result<(), Error> { +fn check_drop_status(drop_model: &drops::Model) -> Result<(), Error> { if drop_model.creation_status != CreationStatus::Created { return Err(Error::new("Drop has not been created")); } @@ -403,6 +702,14 @@ async fn check_drop_status(drop_model: &drops::Model) -> Result<(), Error> { Ok(()) } +fn check_collection_status(collection_model: &collections::Model) -> Result<(), Error> { + if collection_model.creation_status != CreationStatus::Created { + return Err(Error::new("Collection has not been created")); + } + + Ok(()) +} + /// Represents input data for `mint_edition` mutation with a UUID and recipient as fields #[derive(Debug, Clone, InputObject)] pub struct MintDropInput { @@ -418,12 +725,37 @@ pub struct MintEditionPayload { /// Represents input data for `retry_mint` mutation with an ID as a field of type UUID #[derive(Debug, Clone, InputObject)] -pub struct RetryMintInput { +pub struct RetryMintEditionInput { id: Uuid, } /// Represents payload data for `retry_mint` mutation #[derive(Debug, Clone, SimpleObject)] -pub struct RetryMintPayload { +pub struct RetryMintEditionPayload { + collection_mint: collection_mints::CollectionMint, +} + +#[derive(Debug, Clone, InputObject)] +pub struct MintToCollectionInput { + collection: Uuid, + recipient: String, + metadata_json: MetadataJsonInput, + seller_fee_basis_points: Option, + creators: Vec, + compressed: bool, +} + +#[derive(Debug, Clone, SimpleObject)] +pub struct MintToCollectionPayload { + collection_mint: collection_mints::CollectionMint, +} + +#[derive(Debug, Clone, InputObject)] +pub struct RetryMintToCollectionInput { + id: Uuid, +} + +#[derive(Debug, Clone, SimpleObject)] +pub struct RetryMintToCollectionPayload { collection_mint: collection_mints::CollectionMint, } diff --git a/api/src/mutations/mod.rs b/api/src/mutations/mod.rs index 43452bc..51c017a 100644 --- a/api/src/mutations/mod.rs +++ b/api/src/mutations/mod.rs @@ -2,10 +2,10 @@ #![allow(clippy::unused_async)] pub mod collection; pub mod mint; +pub mod drop; pub mod transfer; -pub mod collection; // // Add your other ones here to create a unified Mutation object // // e.x. Mutation(OrganizationMutation, OtherMutation, OtherOtherMutation) #[derive(async_graphql::MergedObject, Default)] -pub struct Mutation(collection::Mutation, mint::Mutation, transfer::Mutation); +pub struct Mutation(collection::Mutation, mint::Mutation, transfer::Mutation, drop::Mutation); diff --git a/api/src/mutations/transfer.rs b/api/src/mutations/transfer.rs index 96a3d5b..81c82be 100644 --- a/api/src/mutations/transfer.rs +++ b/api/src/mutations/transfer.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use super::drop::{validate_evm_address, validate_solana_address}; use crate::{ - blockchains::{polygon::Polygon, solana::Solana, Event}, + blockchains::{polygon::Polygon, solana::Solana, TransferEvent}, db::Connection, entities::{ collection_mints::{self, CollectionMint}, diff --git a/api/src/objects/collection.rs b/api/src/objects/collection.rs index 8d46dd9..9b4d1ed 100644 --- a/api/src/objects/collection.rs +++ b/api/src/objects/collection.rs @@ -33,6 +33,8 @@ pub struct Collection { pub signature: Option, /// The royalties assigned to mints belonging to the collection expressed in basis points. pub seller_fee_basis_points: i16, + pub project_id: Uuid, + pub credits_deduction_id: Option, } #[Object] @@ -56,6 +58,14 @@ impl Collection { self.creation_status } + async fn project_id(&self) -> Uuid { + self.project_id + } + + async fn credits_deduction_id(&self) -> Option { + self.credits_deduction_id.clone() + } + /// 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}`. @@ -141,6 +151,8 @@ impl From for Collection { signature, seller_fee_basis_points, address, + project_id, + credits_deduction_id, }: Model, ) -> Self { Self { @@ -152,6 +164,8 @@ impl From for Collection { total_mints, signature, seller_fee_basis_points, + project_id, + credits_deduction_id, } } } diff --git a/migration/src/lib.rs b/migration/src/lib.rs index daa2236..3b25fe4 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -44,6 +44,8 @@ mod m20230626_111748_customer_wallets_table; mod m20230706_130934_create_transfer_charges_table; mod m20230706_133356_backfill_transfer_charges; mod m20230706_134402_drop_column_credits_deduction_id_from_nft_transfers; +mod m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections; +mod m20230711_150256_rename_collection_creators_to_creators; pub struct Migrator; @@ -95,6 +97,8 @@ impl MigratorTrait for Migrator { Box::new(m20230706_130934_create_transfer_charges_table::Migration), Box::new(m20230706_133356_backfill_transfer_charges::Migration), Box::new(m20230706_134402_drop_column_credits_deduction_id_from_nft_transfers::Migration), + Box::new(m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections::Migration), + Box::new(m20230711_150256_rename_collection_creators_to_creators::Migration), ] } } diff --git a/migration/src/m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections.rs b/migration/src/m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections.rs new file mode 100644 index 0000000..7c5daf2 --- /dev/null +++ b/migration/src/m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections.rs @@ -0,0 +1,82 @@ +use sea_orm::{ConnectionTrait, Statement}; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Collections::Table) + .add_column_if_not_exists( + ColumnDef::new(Collections::CreditsDeductionId).uuid(), + ) + .add_column_if_not_exists(ColumnDef::new(Collections::ProjectId).uuid()) + .to_owned(), + ) + .await?; + + manager + .create_index( + IndexCreateStatement::new() + .name("collections-project_id_index") + .table(Collections::Table) + .col(Collections::ProjectId) + .index_type(IndexType::Hash) + .to_owned(), + ) + .await?; + + manager + .create_index( + IndexCreateStatement::new() + .name("collections-credits_deduction_id_index") + .table(Collections::Table) + .col(Collections::CreditsDeductionId) + .index_type(IndexType::Hash) + .to_owned(), + ) + .await?; + + let db = manager.get_connection(); + + let stmt = Statement::from_string( + manager.get_database_backend(), + r#"UPDATE collections SET credits_deduction_id = drops.credits_deduction_id, project_id = drops.project_id FROM collections c INNER JOIN drops ON c.id = drops.collection_id;"#.to_string(), + ); + + db.execute(stmt).await?; + + manager + .alter_table( + Table::alter() + .table(Collections::Table) + .modify_column(ColumnDef::new(Collections::ProjectId).not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Collections::Table) + .drop_column(Collections::CreditsDeductionId) + .drop_column(Collections::ProjectId) + .to_owned(), + ) + .await + } +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum Collections { + Table, + CreditsDeductionId, + ProjectId, +} diff --git a/migration/src/m20230711_150256_rename_collection_creators_to_creators.rs b/migration/src/m20230711_150256_rename_collection_creators_to_creators.rs new file mode 100644 index 0000000..25ab8f9 --- /dev/null +++ b/migration/src/m20230711_150256_rename_collection_creators_to_creators.rs @@ -0,0 +1,88 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // manager + // .alter_table( + // Table::alter() + // .table(CollectionCreators::Table) + // .rename_column( + // CollectionCreators::CollectionId, + // CollectionCreators::ParentId, + // ) + // .to_owned(), + // ) + // .await?; + + // manager + // .rename_table( + // Table::rename() + // .table(CollectionCreators::Table, Creators::Table) + // .to_owned(), + // ) + // .await?; + + manager + .alter_table( + Table::alter() + .table(Creators::Table) + .add_foreign_key( + TableForeignKey::new() + .name("fk-creators-collection_mint-parent_id") + .from_tbl(Creators::Table) + .from_col(Creators::ParentId) + .to_tbl(CollectionMints::Table) + .to_col(CollectionMints::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .rename_table( + Table::rename() + .table(Creators::Table, CollectionCreators::Table) + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(CollectionCreators::Table) + .rename_column( + CollectionCreators::ParentId, + CollectionCreators::CollectionId, + ) + .to_owned(), + ) + .await + } +} + +#[derive(Iden)] +enum CollectionMints { + Table, + Id, +} + +#[derive(Iden)] +enum CollectionCreators { + Table, + CollectionId, + ParentId, +} + +#[derive(Iden)] +enum Creators { + Table, + ParentId, +} From d892ca6c54847f234a321b50e60becb90cc4147c Mon Sep 17 00:00:00 2001 From: Kyle Espinola Date: Thu, 13 Jul 2023 16:58:22 +0200 Subject: [PATCH 3/9] refactor: add creators and compressed to collection mints. finish mint to collection and retry mint minus creating a purchase record and emitting mint webhook events. --- api/proto.lock | 12 +- api/proto.toml | 23 +- api/src/blockchains/polygon.rs | 15 +- api/src/blockchains/solana.rs | 2 +- api/src/collection.rs | 6 +- api/src/entities/collection_mints.rs | 21 +- api/src/entities/collections.rs | 1 + api/src/entities/mint_creators.rs | 60 ++++ api/src/entities/mod.rs | 1 + api/src/entities/prelude.rs | 5 +- api/src/metadata_json.rs | 8 +- api/src/mutations/collection.rs | 172 +++++----- api/src/mutations/drop.rs | 220 ++++++------- api/src/mutations/mint.rs | 307 ++++++++---------- api/src/mutations/mod.rs | 9 +- api/src/objects/collection.rs | 2 +- .../{collection_creator.rs => creator.rs} | 14 +- api/src/objects/mod.rs | 4 +- migration/src/lib.rs | 6 +- ..._rename_collection_creators_to_creators.rs | 88 ----- ...30713_151414_create_mint_creators_table.rs | 67 ++++ ...d_column_compressed_to_collection_mints.rs | 40 +++ 22 files changed, 546 insertions(+), 537 deletions(-) create mode 100644 api/src/entities/mint_creators.rs rename api/src/objects/{collection_creator.rs => creator.rs} (68%) delete mode 100644 migration/src/m20230711_150256_rename_collection_creators_to_creators.rs create mode 100644 migration/src/m20230713_151414_create_mint_creators_table.rs create mode 100644 migration/src/m20230713_163043_add_column_compressed_to_collection_mints.rs diff --git a/api/proto.lock b/api/proto.lock index 48b8453..6aa0047 100644 --- a/api/proto.lock +++ b/api/proto.lock @@ -1,26 +1,26 @@ [[schemas]] subject = "customer" -version = 1 +version = 2 sha512 = "d75800df0d4744c6b0f4d9a9952d3bfd0bb6b24a8babd19104cc11b54a525f85551b3c7375d69aeabbcf629cd826aa0bc6b0c0467add20716c504f5e856ce1c5" [[schemas]] subject = "nfts" -version = 1 +version = 20 sha512 = "e1e47f595a3709db361e38d5995116a6fc03960a7e5f3c64e293454b062e72a95aa279d36161e7579209710d3f2e382efa559f8f380cc5a173ddc1b6ac44c259" [[schemas]] subject = "organization" -version = 1 +version = 5 sha512 = "9fb28ac73d9712292297394a5fa53a7dae9deba6847353582987ba749859301c23c05fd49d2ce84a1640f8864c5c04d59fa38907700b280000e5c4afc96654bf" [[schemas]] subject = "polygon_nfts" -version = 1 +version = 6 sha512 = "c5ddf43d2958ec690ee2261d0ff9808b67ce810d2fc4b6077f96f561929a920f03509fc8bd7adbda219250eb019f5f7be8a3f51c554f665ea1881f7a973ef2a6" [[schemas]] subject = "solana_nfts" -version = 1 +version = 4 sha512 = "272f1aed7d792a5fe5750cdca659091ca1a4a8dd4f36b35f5066ea6bb09cf4f1e905e7e5817dfa6c68d7ea3a8644192b4ff82e7ffcd85b2d9a58e48112a4a8bc" [[schemas]] @@ -30,5 +30,5 @@ sha512 = "d167e0a143c813073eef8597f0b237e5a8eaf32abbf709724e8071b2dd73ce0438b82f [[schemas]] subject = "treasury" -version = 1 +version = 17 sha512 = "c4caa4f7d032686dff56840909254f5bbb21c12b5e01cf890443dbe4d9806be637e5bbd09a66176152ecd42687fd7bc7008b50f00aec46ff941f4eb04bee25f2" diff --git a/api/proto.toml b/api/proto.toml index b59c3be..41fa49b 100644 --- a/api/proto.toml +++ b/api/proto.toml @@ -1,20 +1,11 @@ [registry] -# endpoint = "https://schemas.holaplex.tools" -endpoint = "http://localhost:8081" +endpoint = "https://schemas.holaplex.tools" [schemas] -organization = 1 -nfts = 1 -customer = 1 -treasury = 1 -solana_nfts = 1 -polygon_nfts = 1 +organization = 5 +nfts = 20 +customer = 2 +treasury = 17 +solana_nfts = 4 +polygon_nfts = 6 timestamp = 1 - -# organization = 5 -# nfts = 19 -# customer = 2 -# treasury = 17 -# solana_nfts = 4 -# polygon_nfts = 6 -# timestamp = 1 diff --git a/api/src/blockchains/polygon.rs b/api/src/blockchains/polygon.rs index b8f9456..b2853ff 100644 --- a/api/src/blockchains/polygon.rs +++ b/api/src/blockchains/polygon.rs @@ -24,22 +24,15 @@ impl Polygon { #[must_use] pub fn event( &self, - ) -> impl DropEvent< - CreateEditionTransaction, - MintEditionTransaction, - UpdateEdtionTransaction, - > + TransferEvent { + ) -> impl DropEvent + + TransferEvent { self.clone() } } #[async_trait::async_trait] -impl - DropEvent< - CreateEditionTransaction, - MintEditionTransaction, - UpdateEdtionTransaction, - > for Polygon +impl DropEvent + for Polygon { async fn create_drop(&self, key: NftEventKey, payload: CreateEditionTransaction) -> Result<()> { let event = NftEvents { diff --git a/api/src/blockchains/solana.rs b/api/src/blockchains/solana.rs index 2e89b5d..adcf20f 100644 --- a/api/src/blockchains/solana.rs +++ b/api/src/blockchains/solana.rs @@ -31,7 +31,7 @@ impl Solana { MintMetaplexEditionTransaction, MetaplexMasterEditionTransaction, > + TransferEvent - + CollectionEvent< + + CollectionEvent< MetaplexCertifiedCollectionTransaction, MetaplexCertifiedCollectionTransaction, MintMetaplexMetadataTransaction, diff --git a/api/src/collection.rs b/api/src/collection.rs index 226c1b8..0bb22d6 100644 --- a/api/src/collection.rs +++ b/api/src/collection.rs @@ -7,13 +7,13 @@ use crate::{ collection_creators::ActiveModel as CollectionCreatorActiveModel, collections::{ActiveModel, Model}, }, - objects::CollectionCreator, + objects::Creator, }; #[derive(Debug, Clone)] pub struct Collection { collection: ActiveModel, - creators: Option>, + creators: Option>, } impl Collection { @@ -25,7 +25,7 @@ impl Collection { } } - pub fn creators(&mut self, creators: Vec) -> &Collection { + pub fn creators(&mut self, creators: Vec) -> &Collection { self.creators = Some(creators); self diff --git a/api/src/entities/collection_mints.rs b/api/src/entities/collection_mints.rs index 11b423a..206cff4 100644 --- a/api/src/entities/collection_mints.rs +++ b/api/src/entities/collection_mints.rs @@ -1,9 +1,12 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.0 use async_graphql::{ComplexObject, Context, Error, Result, SimpleObject}; -use sea_orm::entity::prelude::*; +use sea_orm::{entity::prelude::*, JoinType, QuerySelect, SelectTwo}; -use super::sea_orm_active_enums::{Blockchain, CreationStatus}; +use super::{ + collections, + sea_orm_active_enums::{Blockchain, CreationStatus}, +}; use crate::{ objects::{Collection, MetadataJson}, AppContext, @@ -27,6 +30,7 @@ pub struct Model { pub edition: i64, pub seller_fee_basis_points: i16, pub credits_deduction_id: Option, + pub compressed: bool, } /// Represents a single NFT minted from a collection. @@ -57,6 +61,8 @@ pub struct CollectionMint { pub seller_fee_basis_points: i16, /// credits deduction id pub credits_deduction_id: Option, + /// Indicates if the NFT is compressed. Compression is only supported on Solana. + pub compressed: bool, } #[ComplexObject] @@ -104,6 +110,7 @@ impl From for CollectionMint { edition, seller_fee_basis_points, credits_deduction_id, + compressed, }: Model, ) -> Self { Self { @@ -118,6 +125,7 @@ impl From for CollectionMint { edition, seller_fee_basis_points, credits_deduction_id, + compressed, } } } @@ -149,3 +157,12 @@ impl Related for Entity { } impl ActiveModelBehavior for ActiveModel {} + +impl Entity { + pub fn find_by_id_with_collection(id: Uuid) -> SelectTwo { + Self::find() + .join(JoinType::InnerJoin, Relation::Collections.def()) + .select_also(collections::Entity) + .filter(Column::Id.eq(id)) + } +} diff --git a/api/src/entities/collections.rs b/api/src/entities/collections.rs index 713678b..a044f0e 100644 --- a/api/src/entities/collections.rs +++ b/api/src/entities/collections.rs @@ -21,6 +21,7 @@ pub struct Model { #[sea_orm(nullable)] pub signature: Option, pub seller_fee_basis_points: i16, + // TODO: add to collections and backfill from the drops } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/api/src/entities/mint_creators.rs b/api/src/entities/mint_creators.rs new file mode 100644 index 0000000..d04d56d --- /dev/null +++ b/api/src/entities/mint_creators.rs @@ -0,0 +1,60 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.5 + +use async_graphql::SimpleObject; +use sea_orm::entity::prelude::*; + +use crate::proto; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, SimpleObject)] +#[sea_orm(table_name = "mint_creators")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub collection_mint_id: Uuid, + #[sea_orm(primary_key, auto_increment = false)] + pub address: String, + pub verified: bool, + pub share: i32, +} + +impl From for proto::Creator { + fn from( + Model { + address, + verified, + share, + .. + }: Model, + ) -> Self { + Self { + address, + verified, + share, + } + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::collection_mints::Entity", + from = "Column::CollectionMintId", + to = "super::collection_mints::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + CollectionMints, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CollectionMints.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +impl Entity { + pub fn find_by_collection_mint_id(collection_mint_id: Uuid) -> Select { + Self::find().filter(Column::CollectionMintId.eq(collection_mint_id)) + } +} diff --git a/api/src/entities/mod.rs b/api/src/entities/mod.rs index 481b844..9fb468e 100644 --- a/api/src/entities/mod.rs +++ b/api/src/entities/mod.rs @@ -11,6 +11,7 @@ pub mod drops; pub mod metadata_json_attributes; pub mod metadata_json_files; pub mod metadata_jsons; +pub mod mint_creators; pub mod nft_transfers; pub mod project_wallets; pub mod purchases; diff --git a/api/src/entities/prelude.rs b/api/src/entities/prelude.rs index bb51a6b..81ee759 100644 --- a/api/src/entities/prelude.rs +++ b/api/src/entities/prelude.rs @@ -5,6 +5,7 @@ pub use super::{ collections::Entity as Collections, customer_wallets::Entity as CustomerWallets, drops::Entity as Drops, metadata_json_attributes::Entity as MetadataJsonAttributes, metadata_json_files::Entity as MetadataJsonFiles, metadata_jsons::Entity as MetadataJsons, - nft_transfers::Entity as NftTransfers, project_wallets::Entity as ProjectWallets, - purchases::Entity as Purchases, transfer_charges::Entity as TransferCharges, + mint_creators::Entity as MintCreators, nft_transfers::Entity as NftTransfers, + project_wallets::Entity as ProjectWallets, purchases::Entity as Purchases, + transfer_charges::Entity as TransferCharges, }; diff --git a/api/src/metadata_json.rs b/api/src/metadata_json.rs index dce7e7d..9c95854 100644 --- a/api/src/metadata_json.rs +++ b/api/src/metadata_json.rs @@ -13,9 +13,9 @@ use crate::{ }; #[derive(Clone, Debug)] pub struct MetadataJson { - metadata_json: MetadataJsonInput, - uri: Option, - identifier: Option, + pub metadata_json: MetadataJsonInput, + pub uri: Option, + pub identifier: Option, } impl MetadataJson { @@ -46,7 +46,7 @@ impl MetadataJson { .await? .first() .map(ToOwned::to_owned) - .ok_or_else(|| anyhow!("no metadata_json entry found in db"))?; + .ok_or(anyhow!("no metadata_json entry found in db"))?; let files = metadata_json_files::Entity::find() .filter(metadata_json_files::Column::MetadataJsonId.eq(id)) diff --git a/api/src/mutations/collection.rs b/api/src/mutations/collection.rs index b35204e..e997431 100644 --- a/api/src/mutations/collection.rs +++ b/api/src/mutations/collection.rs @@ -1,5 +1,12 @@ use std::str::FromStr; +use async_graphql::{Context, Error, InputObject, Object, Result, SimpleObject}; +use hub_core::{credits::CreditsClient, producer::Producer}; +use reqwest::Url; +use sea_orm::{prelude::*, ModelTrait, Set, TransactionTrait}; +use serde::{Deserialize, Serialize}; +use solana_program::pubkey::Pubkey; + use crate::{ blockchains::{polygon::Polygon, solana::Solana, CollectionEvent}, collection::Collection, @@ -11,7 +18,7 @@ use crate::{ sea_orm_active_enums::{Blockchain as BlockchainEnum, CreationStatus}, }, metadata_json::MetadataJson, - objects::{Collection as CollectionObject, CollectionCreator, MetadataJsonInput}, + objects::{Collection as CollectionObject, Creator, MetadataJsonInput}, proto::{ nft_events::Event as NftEvent, CreationStatus as NftCreationStatus, Creator as ProtoCreator, DropCreation, MetaplexCertifiedCollectionTransaction, @@ -19,12 +26,6 @@ use crate::{ }, Actions, AppContext, NftStorageClient, OrganizationId, UserID, }; -use async_graphql::{Context, Error, InputObject, Object, Result, SimpleObject}; -use hub_core::{credits::CreditsClient, producer::Producer}; -use reqwest::Url; -use sea_orm::{prelude::*, ModelTrait, Set, TransactionTrait}; -use serde::{Deserialize, Serialize}; -use solana_program::pubkey::Pubkey; #[derive(Default)] pub struct Mutation; @@ -58,7 +59,7 @@ impl Mutation { let conn = db.get(); let credits = ctx.data::>()?; let solana = ctx.data::()?; - let polygon = ctx.data::()?; + let _polygon = ctx.data::()?; let nft_storage = ctx.data::()?; let nfts_producer = ctx.data::>()?; @@ -99,23 +100,20 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .create_collection( - event_key, - MetaplexCertifiedCollectionTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: 0, - creators: input - .creators - .into_iter() - .map(TryFrom::try_from) - .collect::>()?, - }), - }, - ) + .create_collection(event_key, MetaplexCertifiedCollectionTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: 0, + creators: input + .creators + .into_iter() + .map(TryFrom::try_from) + .collect::>()?, + }), + }) .await?; }, BlockchainEnum::Ethereum | BlockchainEnum::Polygon => { @@ -123,18 +121,14 @@ impl Mutation { }, }; - submit_pending_deduction( - credits, - db, - DeductionParams { - user_id, - org_id, - balance, - collection: collection.id, - blockchain: input.blockchain, - action: Actions::CreateDrop, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + user_id, + org_id, + balance, + collection: collection.id, + blockchain: input.blockchain, + action: Actions::CreateDrop, + }) .await?; // TODO: separate event for collection creation @@ -181,7 +175,7 @@ impl Mutation { let OrganizationId(org) = organization_id; let conn = db.get(); let solana = ctx.data::()?; - let polygon = ctx.data::()?; + let _polygon = ctx.data::()?; let credits = ctx.data::>()?; let user_id = id.ok_or(Error::new("X-USER-ID header not found"))?; let org_id = org.ok_or(Error::new("X-ORGANIZATION-ID header not found"))?; @@ -219,26 +213,23 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_create_collection( - event_key, - MetaplexCertifiedCollectionTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: 0, - creators: creators - .into_iter() - .map(|c| ProtoCreator { - address: c.address, - verified: c.verified, - share: c.share, - }) - .collect(), - }), - }, - ) + .retry_create_collection(event_key, MetaplexCertifiedCollectionTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: 0, + creators: creators + .into_iter() + .map(|c| ProtoCreator { + address: c.address, + verified: c.verified, + share: c.share, + }) + .collect(), + }), + }) .await?; }, BlockchainEnum::Polygon | BlockchainEnum::Ethereum => { @@ -246,18 +237,14 @@ impl Mutation { }, }; - submit_pending_deduction( - credits, - db, - DeductionParams { - balance, - user_id, - org_id, - collection: collection.id, - blockchain: collection.blockchain, - action: Actions::RetryDrop, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + balance, + user_id, + org_id, + collection: collection.id, + blockchain: collection.blockchain, + action: Actions::RetryDrop, + }) .await?; Ok(CreateCollectionPayload { @@ -274,7 +261,7 @@ impl Mutation { input: PatchCollectionInput, ) -> Result { let PatchCollectionInput { - id, + id: _, metadata_json, creators, } = input; @@ -283,7 +270,7 @@ impl Mutation { let conn = db.get(); let nft_storage = ctx.data::()?; let solana = ctx.data::()?; - let polygon = ctx.data::()?; + let _polygon = ctx.data::()?; let user_id = user_id.0.ok_or(Error::new("X-USER-ID header not found"))?; @@ -306,10 +293,6 @@ impl Mutation { validate_json(collection.blockchain, metadata_json)?; } - let mut collection_am: collections::ActiveModel = collection.into(); - - let collection = collection_am.update(conn).await?; - let current_creators = collection_creators::Entity::find() .filter(collection_creators::Column::CollectionId.eq(collection.id)) .all(conn) @@ -383,19 +366,16 @@ impl Mutation { solana .event() - .update_collection( - event_key, - MetaplexCertifiedCollectionTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json_model.name, - symbol: metadata_json_model.symbol, - metadata_uri: metadata_json_model.uri, - seller_fee_basis_points: 0, - creators, - }), - }, - ) + .update_collection(event_key, MetaplexCertifiedCollectionTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json_model.name, + symbol: metadata_json_model.symbol, + metadata_uri: metadata_json_model.uri, + seller_fee_basis_points: 0, + creators, + }), + }) .await?; }, BlockchainEnum::Polygon | BlockchainEnum::Ethereum => { @@ -462,7 +442,7 @@ async fn submit_pending_deduction( Ok(()) } -async fn fetch_owner( +pub async fn fetch_owner( conn: &DatabaseConnection, project: Uuid, blockchain: BlockchainEnum, @@ -493,7 +473,7 @@ pub struct CreateCollectionPayload { pub struct CreateCollectionInput { pub project: Uuid, pub blockchain: BlockchainEnum, - pub creators: Vec, + pub creators: Vec, pub metadata_json: MetadataJsonInput, } @@ -517,9 +497,9 @@ impl CreateCollectionInput { } } -fn validate_solana_creator_verification( +pub fn validate_solana_creator_verification( project_treasury_wallet_address: &str, - creators: &Vec, + creators: &Vec, ) -> Result<()> { for creator in creators { if creator.verified.unwrap_or_default() @@ -541,7 +521,7 @@ fn validate_solana_creator_verification( /// # Errors /// - Err with an appropriate error message if any creator address is not a valid address. /// - Err if the blockchain is not supported. -fn validate_creators(blockchain: BlockchainEnum, creators: &Vec) -> Result<()> { +pub fn validate_creators(blockchain: BlockchainEnum, creators: &Vec) -> Result<()> { let royalty_share = creators.iter().map(|c| c.share).sum::(); if royalty_share != 100 { @@ -615,7 +595,7 @@ pub fn validate_evm_address(address: &str) -> Result<()> { /// /// # Errors /// - Err with an appropriate error message if any JSON field is invalid. -fn validate_json(blockchain: BlockchainEnum, json: &MetadataJsonInput) -> Result<()> { +pub fn validate_json(blockchain: BlockchainEnum, json: &MetadataJsonInput) -> Result<()> { json.animation_url .as_ref() .map(|animation_url| Url::from_str(animation_url)) @@ -663,7 +643,7 @@ pub struct PatchCollectionInput { /// The new metadata JSON for the drop pub metadata_json: Option, /// The creators of the drop - pub creators: Option>, + pub creators: Option>, } /// Represents the result of a successful patch drop mutation. diff --git a/api/src/mutations/drop.rs b/api/src/mutations/drop.rs index d617ac7..bbe62e7 100644 --- a/api/src/mutations/drop.rs +++ b/api/src/mutations/drop.rs @@ -18,7 +18,7 @@ use crate::{ sea_orm_active_enums::{Blockchain as BlockchainEnum, CreationStatus}, }, metadata_json::MetadataJson, - objects::{CollectionCreator, Drop, MetadataJsonInput}, + objects::{Creator, Drop, MetadataJsonInput}, proto::{ self, nft_events::Event as NftEvent, CreationStatus as NftCreationStatus, EditionInfo, NftEventKey, NftEvents, @@ -114,49 +114,43 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .create_drop( - event_key, - proto::MetaplexMasterEditionTransaction { - master_edition: Some(proto::MasterEdition { - owner_address, - supply: input.supply.map(TryInto::try_into).transpose()?, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: seller_fee_basis_points.into(), - creators: input - .creators - .into_iter() - .map(TryFrom::try_from) - .collect::>()?, - }), - }, - ) + .create_drop(event_key, proto::MetaplexMasterEditionTransaction { + master_edition: Some(proto::MasterEdition { + owner_address, + supply: input.supply.map(TryInto::try_into).transpose()?, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: seller_fee_basis_points.into(), + creators: input + .creators + .into_iter() + .map(TryFrom::try_from) + .collect::>()?, + }), + }) .await?; }, BlockchainEnum::Polygon => { let amount = input.supply.ok_or(Error::new("supply is required"))?; polygon - .create_drop( - event_key, - proto::CreateEditionTransaction { - amount: amount.try_into()?, - edition_info: Some(proto::EditionInfo { - creator: input - .creators - .get(0) - .ok_or(Error::new("creator is required"))? - .clone() - .address, - collection: metadata_json.name, - uri: metadata_json.uri, - description: metadata_json.description, - image_uri: metadata_json.image, - }), - fee_receiver: owner_address.clone(), - fee_numerator: seller_fee_basis_points.into(), - }, - ) + .create_drop(event_key, proto::CreateEditionTransaction { + amount: amount.try_into()?, + edition_info: Some(proto::EditionInfo { + creator: input + .creators + .get(0) + .ok_or(Error::new("creator is required"))? + .clone() + .address, + collection: metadata_json.name, + uri: metadata_json.uri, + description: metadata_json.description, + image_uri: metadata_json.image, + }), + fee_receiver: owner_address.clone(), + fee_numerator: seller_fee_basis_points.into(), + }) .await?; }, BlockchainEnum::Ethereum => { @@ -164,18 +158,14 @@ impl Mutation { }, }; - submit_pending_deduction( - credits, - db, - DeductionParams { - user_id, - org_id, - balance, - drop: drop_model.id, - blockchain: input.blockchain, - action: Actions::CreateDrop, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + user_id, + org_id, + balance, + drop: drop_model.id, + blockchain: input.blockchain, + action: Actions::CreateDrop, + }) .await?; nfts_producer @@ -261,27 +251,24 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_create_drop( - event_key, - proto::MetaplexMasterEditionTransaction { - master_edition: Some(proto::MasterEdition { - owner_address, - supply: collection.supply.map(TryInto::try_into).transpose()?, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: collection.seller_fee_basis_points.into(), - creators: creators - .into_iter() - .map(|c| proto::Creator { - address: c.address, - verified: c.verified, - share: c.share, - }) - .collect(), - }), - }, - ) + .retry_create_drop(event_key, proto::MetaplexMasterEditionTransaction { + master_edition: Some(proto::MasterEdition { + owner_address, + supply: collection.supply.map(TryInto::try_into).transpose()?, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: collection.seller_fee_basis_points.into(), + creators: creators + .into_iter() + .map(|c| proto::Creator { + address: c.address, + verified: c.verified, + share: c.share, + }) + .collect(), + }), + }) .await?; }, BlockchainEnum::Polygon => { @@ -291,15 +278,12 @@ impl Mutation { polygon .event() - .retry_create_drop( - event_key, - proto::CreateEditionTransaction { - edition_info: None, - amount, - fee_receiver: owner_address, - fee_numerator: collection.seller_fee_basis_points.into(), - }, - ) + .retry_create_drop(event_key, proto::CreateEditionTransaction { + edition_info: None, + amount, + fee_receiver: owner_address, + fee_numerator: collection.seller_fee_basis_points.into(), + }) .await?; }, BlockchainEnum::Ethereum => { @@ -307,18 +291,14 @@ impl Mutation { }, }; - submit_pending_deduction( - credits, - db, - DeductionParams { - balance, - user_id, - org_id, - drop: drop.id, - blockchain: collection.blockchain, - action: Actions::RetryDrop, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + balance, + user_id, + org_id, + drop: drop.id, + blockchain: collection.blockchain, + action: Actions::RetryDrop, + }) .await?; Ok(CreateDropPayload { @@ -577,20 +557,17 @@ impl Mutation { solana .event() - .update_drop( - event_key, - proto::MetaplexMasterEditionTransaction { - master_edition: Some(proto::MasterEdition { - owner_address, - supply: collection.supply.map(TryInto::try_into).transpose()?, - name: metadata_json_model.name, - symbol: metadata_json_model.symbol, - metadata_uri: metadata_json_model.uri, - seller_fee_basis_points: collection.seller_fee_basis_points.into(), - creators, - }), - }, - ) + .update_drop(event_key, proto::MetaplexMasterEditionTransaction { + master_edition: Some(proto::MasterEdition { + owner_address, + supply: collection.supply.map(TryInto::try_into).transpose()?, + name: metadata_json_model.name, + symbol: metadata_json_model.symbol, + metadata_uri: metadata_json_model.uri, + seller_fee_basis_points: collection.seller_fee_basis_points.into(), + creators, + }), + }) .await?; }, BlockchainEnum::Polygon => { @@ -606,18 +583,15 @@ impl Mutation { polygon .event() - .update_drop( - event_key, - proto::UpdateEdtionTransaction { - edition_info: Some(EditionInfo { - description: metadata_json_model.description, - image_uri: metadata_json_model.image, - collection: metadata_json_model.name, - uri: metadata_json_model.uri, - creator, - }), - }, - ) + .update_drop(event_key, proto::UpdateEdtionTransaction { + edition_info: Some(EditionInfo { + description: metadata_json_model.description, + image_uri: metadata_json_model.image, + collection: metadata_json_model.name, + uri: metadata_json_model.uri, + creator, + }), + }) .await?; }, BlockchainEnum::Ethereum => { @@ -719,7 +693,7 @@ pub struct CreateDropInput { pub start_time: Option, pub end_time: Option, pub blockchain: BlockchainEnum, - pub creators: Vec, + pub creators: Vec, pub metadata_json: MetadataJsonInput, } @@ -767,7 +741,7 @@ fn validate_end_time(end_time: &Option) -> Result<()> { fn validate_solana_creator_verification( project_treasury_wallet_address: &str, - creators: &Vec, + creators: &Vec, ) -> Result<()> { for creator in creators { if creator.verified.unwrap_or_default() @@ -789,7 +763,7 @@ fn validate_solana_creator_verification( /// # Errors /// - Err with an appropriate error message if any creator address is not a valid address. /// - Err if the blockchain is not supported. -fn validate_creators(blockchain: BlockchainEnum, creators: &Vec) -> Result<()> { +fn validate_creators(blockchain: BlockchainEnum, creators: &Vec) -> Result<()> { let royalty_share = creators.iter().map(|c| c.share).sum::(); if royalty_share != 100 { @@ -919,7 +893,7 @@ pub struct PatchDropInput { /// The new metadata JSON for the drop pub metadata_json: Option, /// The creators of the drop - pub creators: Option>, + pub creators: Option>, } /// Represents the result of a successful patch drop mutation. diff --git a/api/src/mutations/mint.rs b/api/src/mutations/mint.rs index eb9f01b..bdd2bb0 100644 --- a/api/src/mutations/mint.rs +++ b/api/src/mutations/mint.rs @@ -4,20 +4,23 @@ use async_graphql::{Context, Error, InputObject, Object, Result, SimpleObject}; use hub_core::{chrono::Utc, credits::CreditsClient, producer::Producer}; use sea_orm::{prelude::*, JoinType, QuerySelect, Set}; +use super::collection::{ + fetch_owner, validate_creators, validate_json, validate_solana_creator_verification, +}; use crate::{ - blockchains::{polygon::Polygon, solana::Solana, DropEvent, CollectionEvent}, + blockchains::{polygon::Polygon, solana::Solana, CollectionEvent, DropEvent}, db::Connection, entities::{ - collection_mints, collections, drops, + collection_mints, collections, drops, mint_creators, prelude::{Collections, Drops}, project_wallets, purchases, sea_orm_active_enums::{Blockchain as BlockchainEnum, CreationStatus}, }, metadata_json::MetadataJson, - objects::{CollectionCreator, MetadataJsonInput}, + objects::{Creator, MetadataJsonInput}, proto::{ - self, nft_events::Event as NftEvent, CreationStatus as NftCreationStatus, MintCreation, - NftEventKey, NftEvents, MetaplexMetadata, + self, nft_events::Event as NftEvent, CreationStatus as NftCreationStatus, MetaplexMetadata, + MintCreation, NftEventKey, NftEvents, }, Actions, AppContext, NftStorageClient, OrganizationId, UserID, }; @@ -121,28 +124,22 @@ impl Mutation { solana .event() - .mint_drop( - event_key, - proto::MintMetaplexEditionTransaction { - recipient_address: input.recipient.to_string(), - owner_address: owner_address.to_string(), - edition, - collection_id: collection.id.to_string(), - }, - ) + .mint_drop(event_key, proto::MintMetaplexEditionTransaction { + recipient_address: input.recipient.to_string(), + owner_address: owner_address.to_string(), + edition, + collection_id: collection.id.to_string(), + }) .await?; }, BlockchainEnum::Polygon => { polygon .event() - .mint_drop( - event_key, - proto::MintEditionTransaction { - receiver: input.recipient.to_string(), - amount: 1, - collection_id: collection.id.to_string(), - }, - ) + .mint_drop(event_key, proto::MintEditionTransaction { + receiver: input.recipient.to_string(), + amount: 1, + collection_id: collection.id.to_string(), + }) .await?; }, BlockchainEnum::Ethereum => { @@ -168,18 +165,14 @@ impl Mutation { purchase_am.insert(conn).await?; - submit_pending_deduction( - credits, - db, - DeductionParams { - balance, - user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::MintEdition, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::MintEdition, + }) .await?; nfts_producer @@ -287,28 +280,22 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_mint_drop( - event_key, - proto::MintMetaplexEditionTransaction { - recipient_address: recipient.to_string(), - owner_address: owner_address.to_string(), - edition, - collection_id: collection.id.to_string(), - }, - ) + .retry_mint_drop(event_key, proto::MintMetaplexEditionTransaction { + recipient_address: recipient.to_string(), + owner_address: owner_address.to_string(), + edition, + collection_id: collection.id.to_string(), + }) .await?; }, BlockchainEnum::Polygon => { polygon .event() - .retry_mint_drop( - event_key, - proto::MintEditionTransaction { - receiver: recipient.to_string(), - amount: 1, - collection_id: collection.id.to_string(), - }, - ) + .retry_mint_drop(event_key, proto::MintEditionTransaction { + receiver: recipient.to_string(), + amount: 1, + collection_id: collection.id.to_string(), + }) .await?; }, BlockchainEnum::Ethereum => { @@ -316,18 +303,14 @@ impl Mutation { }, }; - submit_pending_deduction( - credits, - db, - DeductionParams { - balance, - user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::RetryMint, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::RetryMint, + }) .await?; Ok(RetryMintEditionPayload { @@ -350,7 +333,7 @@ impl Mutation { let credits = ctx.data::>()?; let conn = db.get(); let solana = ctx.data::()?; - let nfts_producer = ctx.data::>()?; + let _nfts_producer = ctx.data::>()?; let nft_storage = ctx.data::()?; let UserID(id) = user_id; @@ -362,33 +345,27 @@ impl Mutation { .0 .ok_or(Error::new("X-CREDIT-BALANCE header not found"))?; + let creators = input.creators; + let collection = Collections::find() .filter(drops::Column::Id.eq(input.collection)) .one(conn) .await?; let collection = collection.ok_or(Error::new("collection not found"))?; + let blockchain = collection.blockchain; + validate_creators(blockchain, &creators)?; + validate_json(blockchain, &input.metadata_json)?; check_collection_status(&collection)?; let seller_fee_basis_points = input.seller_fee_basis_points.unwrap_or_default(); - // Fetch the project wallet address which will sign the transaction by hub-treasuries - let wallet = project_wallets::Entity::find() - .filter( - project_wallets::Column::ProjectId - .eq(collection.project_id) - .and(project_wallets::Column::Blockchain.eq(collection.blockchain)), - ) - .one(conn) - .await?; + let owner_address = fetch_owner(conn, collection.project_id, collection.blockchain).await?; - let owner_address = wallet - .ok_or(Error::new(format!( - "no project wallet found for {} blockchain", - collection.blockchain - )))? - .wallet_address; + if collection.blockchain == BlockchainEnum::Solana { + validate_solana_creator_verification(&owner_address, &creators)?; + } // insert a collection mint record into database let collection_mint_active_model = collection_mints::ActiveModel { @@ -400,13 +377,25 @@ impl Mutation { ..Default::default() }; + let collection_mint_model = collection_mint_active_model.insert(conn).await?; + let metadata_json = MetadataJson::new(input.metadata_json) .upload(nft_storage) .await? - .save(collection.id, db) + .save(collection_mint_model.id, db) .await?; - let collection_mint_model = collection_mint_active_model.insert(conn).await?; + for creator in creators.clone() { + let am = mint_creators::ActiveModel { + collection_mint_id: Set(collection_mint_model.id), + address: Set(creator.address), + verified: Set(creator.verified.unwrap_or_default()), + share: Set(creator.share.try_into()?), + }; + + am.insert(conn).await?; + } + let event_key = NftEventKey { id: collection_mint_model.id.to_string(), user_id: user_id.to_string(), @@ -417,26 +406,22 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .mint_to_collection( - event_key, - proto::MintMetaplexMetadataTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: seller_fee_basis_points.into(), - creators: input - .creators - .into_iter() - .map(TryFrom::try_from) - .collect::>()?, - }), - recipient_address: input.recipient.to_string(), - compressed: input.compressed, - collection_id: collection.id.to_string(), - }, - ) + .mint_to_collection(event_key, proto::MintMetaplexMetadataTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: seller_fee_basis_points.into(), + creators: creators + .into_iter() + .map(TryFrom::try_from) + .collect::>()?, + }), + recipient_address: input.recipient.to_string(), + compressed: input.compressed, + collection_id: collection.id.to_string(), + }) .await?; }, BlockchainEnum::Ethereum | BlockchainEnum::Polygon => { @@ -445,7 +430,7 @@ impl Mutation { }; let mut collection_am = collections::ActiveModel::from(collection.clone()); - collection_am.total_mints = Set(collection.total_mints + 1); + collection_am.total_mints = Set(collection.total_mints.add(1)); collection_am.update(conn).await?; // TODO: Switch to purchase history @@ -463,18 +448,14 @@ impl Mutation { // purchase_am.insert(conn).await?; - submit_pending_deduction( - credits, - db, - DeductionParams { - balance, - user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::MintEdition, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::MintEdition, + }) .await?; // nfts_producer @@ -523,50 +504,28 @@ impl Mutation { .0 .ok_or(Error::new("X-ORGANIZATION-BALANCE header not found"))?; - let (collection_mint_model, drop) = collection_mints::Entity::find() - .join( - JoinType::InnerJoin, - collection_mints::Relation::Collections.def(), - ) - .join(JoinType::InnerJoin, collections::Relation::Drop.def()) - .select_also(drops::Entity) - .filter(collection_mints::Column::Id.eq(input.id)) - .one(conn) - .await? - .ok_or(Error::new("collection mint not found"))?; + let (collection_mint_model, collection) = + collection_mints::Entity::find_by_id_with_collection(input.id) + .one(conn) + .await? + .ok_or(Error::new("collection mint not found"))?; if collection_mint_model.creation_status == CreationStatus::Created { return Err(Error::new("mint is already created")); } - let collection = collections::Entity::find() - .filter(collections::Column::Id.eq(collection_mint_model.collection_id)) - .one(conn) - .await? - .ok_or(Error::new("collection not found"))?; - - let drop_model = drop.ok_or(Error::new("drop not found"))?; + let collection = collection.ok_or(Error::new("collection not found"))?; let recipient = collection_mint_model.owner.clone(); - let edition = collection_mint_model.edition; - let project_id = drop_model.project_id; + let _edition = collection_mint_model.edition; + let project_id = collection.project_id; + let blockchain = collection.blockchain; - // Fetch the project wallet address which will sign the transaction by hub-treasuries - let wallet = project_wallets::Entity::find() - .filter( - project_wallets::Column::ProjectId - .eq(project_id) - .and(project_wallets::Column::Blockchain.eq(collection.blockchain)), - ) - .one(conn) - .await?; + let owner_address = fetch_owner(conn, project_id, blockchain).await?; - let owner_address = wallet - .ok_or(Error::new(format!( - "no project wallet found for {} blockchain", - collection.blockchain - )))? - .wallet_address; + let MetadataJson { + metadata_json, uri, .. + } = MetadataJson::fetch(collection_mint_model.id, db).await?; let event_key = NftEventKey { id: collection_mint_model.id.to_string(), @@ -574,19 +533,29 @@ impl Mutation { project_id: project_id.to_string(), }; + let creators = mint_creators::Entity::find_by_collection_mint_id(collection_mint_model.id) + .all(conn) + .await?; + match collection.blockchain { BlockchainEnum::Solana => { solana .event() - .retry_mint_drop( - event_key, - proto::MintMetaplexEditionTransaction { - recipient_address: recipient.to_string(), - owner_address: owner_address.to_string(), - edition, - collection_id: collection.id.to_string(), - }, - ) + .retry_mint_to_collection(event_key, proto::MintMetaplexMetadataTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: uri.ok_or(Error::new("metadata uri not found"))?, + seller_fee_basis_points: collection_mint_model + .seller_fee_basis_points + .into(), + creators: creators.into_iter().map(Into::into).collect(), + }), + recipient_address: recipient.to_string(), + compressed: collection_mint_model.compressed, + collection_id: collection_mint_model.collection_id.to_string(), + }) .await?; }, BlockchainEnum::Ethereum | BlockchainEnum::Polygon => { @@ -594,18 +563,14 @@ impl Mutation { }, }; - submit_pending_deduction( - credits, - db, - DeductionParams { - balance, - user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::RetryMint, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::RetryMint, + }) .await?; Ok(RetryMintEditionPayload { @@ -741,7 +706,7 @@ pub struct MintToCollectionInput { recipient: String, metadata_json: MetadataJsonInput, seller_fee_basis_points: Option, - creators: Vec, + creators: Vec, compressed: bool, } diff --git a/api/src/mutations/mod.rs b/api/src/mutations/mod.rs index 51c017a..926ad74 100644 --- a/api/src/mutations/mod.rs +++ b/api/src/mutations/mod.rs @@ -1,11 +1,16 @@ #![allow(clippy::too_many_lines)] #![allow(clippy::unused_async)] pub mod collection; -pub mod mint; pub mod drop; +pub mod mint; pub mod transfer; // // Add your other ones here to create a unified Mutation object // // e.x. Mutation(OrganizationMutation, OtherMutation, OtherOtherMutation) #[derive(async_graphql::MergedObject, Default)] -pub struct Mutation(collection::Mutation, mint::Mutation, transfer::Mutation, drop::Mutation); +pub struct Mutation( + collection::Mutation, + mint::Mutation, + transfer::Mutation, + drop::Mutation, +); diff --git a/api/src/objects/collection.rs b/api/src/objects/collection.rs index 9b4d1ed..0d1fc58 100644 --- a/api/src/objects/collection.rs +++ b/api/src/objects/collection.rs @@ -63,7 +63,7 @@ impl Collection { } async fn credits_deduction_id(&self) -> Option { - self.credits_deduction_id.clone() + self.credits_deduction_id } /// The blockchain address of the collection used to view it in blockchain explorers. diff --git a/api/src/objects/collection_creator.rs b/api/src/objects/creator.rs similarity index 68% rename from api/src/objects/collection_creator.rs rename to api/src/objects/creator.rs index 86266b9..537c1c8 100644 --- a/api/src/objects/collection_creator.rs +++ b/api/src/objects/creator.rs @@ -3,13 +3,13 @@ use serde::{Deserialize, Serialize}; use crate::proto; -/// An attributed creator for a colleciton. +/// An attributed creator for a collection or mint. #[derive(Clone, Debug, Serialize, Deserialize, InputObject)] -#[graphql(name = "CollectionCreatorInput")] -pub struct CollectionCreator { +#[graphql(name = "CreatorInput")] +pub struct Creator { /// The wallet address of the creator. pub address: String, - /// This field indicates whether the collection's creator has been verified. This feature is only supported on the Solana blockchain. + /// This field indicates whether the creator has been verified. This feature is only supported on the Solana blockchain. /// ## References /// [Metaplex Token Metadata - Verify creator instruction](https://docs.metaplex.com/programs/token-metadata/instructions#verify-a-creator) pub verified: Option, @@ -17,15 +17,15 @@ pub struct CollectionCreator { pub share: u8, } -impl TryFrom for proto::Creator { +impl TryFrom for proto::Creator { type Error = Error; fn try_from( - CollectionCreator { + Creator { address, verified, share, - }: CollectionCreator, + }: Creator, ) -> Result { Ok(Self { address: address.parse()?, diff --git a/api/src/objects/mod.rs b/api/src/objects/mod.rs index 336adc5..242986e 100644 --- a/api/src/objects/mod.rs +++ b/api/src/objects/mod.rs @@ -1,7 +1,7 @@ #![allow(clippy::unused_async)] mod collection; -mod collection_creator; +mod creator; mod customer; mod drop; mod holder; @@ -10,7 +10,7 @@ mod project; mod wallet; pub use collection::Collection; -pub use collection_creator::CollectionCreator; +pub use creator::Creator; pub use customer::Customer; pub use drop::Drop; pub use holder::Holder; diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 3b25fe4..e940736 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -45,7 +45,8 @@ mod m20230706_130934_create_transfer_charges_table; mod m20230706_133356_backfill_transfer_charges; mod m20230706_134402_drop_column_credits_deduction_id_from_nft_transfers; mod m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections; -mod m20230711_150256_rename_collection_creators_to_creators; +mod m20230713_151414_create_mint_creators_table; +mod m20230713_163043_add_column_compressed_to_collection_mints; pub struct Migrator; @@ -98,7 +99,8 @@ impl MigratorTrait for Migrator { Box::new(m20230706_133356_backfill_transfer_charges::Migration), Box::new(m20230706_134402_drop_column_credits_deduction_id_from_nft_transfers::Migration), Box::new(m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections::Migration), - Box::new(m20230711_150256_rename_collection_creators_to_creators::Migration), + Box::new(m20230713_151414_create_mint_creators_table::Migration), + Box::new(m20230713_163043_add_column_compressed_to_collection_mints::Migration), ] } } diff --git a/migration/src/m20230711_150256_rename_collection_creators_to_creators.rs b/migration/src/m20230711_150256_rename_collection_creators_to_creators.rs deleted file mode 100644 index 25ab8f9..0000000 --- a/migration/src/m20230711_150256_rename_collection_creators_to_creators.rs +++ /dev/null @@ -1,88 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - // manager - // .alter_table( - // Table::alter() - // .table(CollectionCreators::Table) - // .rename_column( - // CollectionCreators::CollectionId, - // CollectionCreators::ParentId, - // ) - // .to_owned(), - // ) - // .await?; - - // manager - // .rename_table( - // Table::rename() - // .table(CollectionCreators::Table, Creators::Table) - // .to_owned(), - // ) - // .await?; - - manager - .alter_table( - Table::alter() - .table(Creators::Table) - .add_foreign_key( - TableForeignKey::new() - .name("fk-creators-collection_mint-parent_id") - .from_tbl(Creators::Table) - .from_col(Creators::ParentId) - .to_tbl(CollectionMints::Table) - .to_col(CollectionMints::Id) - .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Cascade), - ) - .to_owned(), - ) - .await - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .rename_table( - Table::rename() - .table(Creators::Table, CollectionCreators::Table) - .to_owned(), - ) - .await?; - - manager - .alter_table( - Table::alter() - .table(CollectionCreators::Table) - .rename_column( - CollectionCreators::ParentId, - CollectionCreators::CollectionId, - ) - .to_owned(), - ) - .await - } -} - -#[derive(Iden)] -enum CollectionMints { - Table, - Id, -} - -#[derive(Iden)] -enum CollectionCreators { - Table, - CollectionId, - ParentId, -} - -#[derive(Iden)] -enum Creators { - Table, - ParentId, -} diff --git a/migration/src/m20230713_151414_create_mint_creators_table.rs b/migration/src/m20230713_151414_create_mint_creators_table.rs new file mode 100644 index 0000000..3357396 --- /dev/null +++ b/migration/src/m20230713_151414_create_mint_creators_table.rs @@ -0,0 +1,67 @@ +use sea_orm_migration::prelude::*; + +use crate::m20230220_223223_create_collection_mints_table::CollectionMints; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(MintCreators::Table) + .if_not_exists() + .col( + ColumnDef::new(MintCreators::CollectionMintId) + .uuid() + .not_null(), + ) + .primary_key( + Index::create() + .col(MintCreators::CollectionMintId) + .col(MintCreators::Address), + ) + .col(ColumnDef::new(MintCreators::Address).string().not_null()) + .col(ColumnDef::new(MintCreators::Verified).boolean().not_null()) + .col(ColumnDef::new(MintCreators::Share).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-mints_creators-collection_mint_id") + .from(MintCreators::Table, MintCreators::CollectionMintId) + .to(CollectionMints::Table, CollectionMints::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + IndexCreateStatement::new() + .name("mint_creators-address-idx") + .table(MintCreators::Table) + .col(MintCreators::Address) + .index_type(IndexType::Hash) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(MintCreators::Table).to_owned()) + .await + } +} + +#[derive(Iden)] +enum MintCreators { + Table, + CollectionMintId, + Address, + Verified, + Share, +} diff --git a/migration/src/m20230713_163043_add_column_compressed_to_collection_mints.rs b/migration/src/m20230713_163043_add_column_compressed_to_collection_mints.rs new file mode 100644 index 0000000..39250e6 --- /dev/null +++ b/migration/src/m20230713_163043_add_column_compressed_to_collection_mints.rs @@ -0,0 +1,40 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(CollectionMints::Table) + .add_column_if_not_exists( + ColumnDef::new(CollectionMints::Compressed) + .boolean() + .default(false), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(CollectionMints::Table) + .drop_column(CollectionMints::Compressed) + .to_owned(), + ) + .await + } +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum CollectionMints { + Table, + Compressed, +} From db573a8d3ea8b0eadf8fc6fdbcbc4c64f35006fd Mon Sep 17 00:00:00 2001 From: Anshul Goel Date: Mon, 17 Jul 2023 15:15:53 +0530 Subject: [PATCH 4/9] add dataloaders for collection drop and project collections --- api/src/dataloaders/collection_drop.rs | 47 ++++++++++++++++++++++ api/src/dataloaders/mod.rs | 2 + api/src/dataloaders/project_collections.rs | 43 ++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 api/src/dataloaders/collection_drop.rs create mode 100644 api/src/dataloaders/project_collections.rs diff --git a/api/src/dataloaders/collection_drop.rs b/api/src/dataloaders/collection_drop.rs new file mode 100644 index 0000000..f3abe6d --- /dev/null +++ b/api/src/dataloaders/collection_drop.rs @@ -0,0 +1,47 @@ +use std::collections::HashMap; + +use async_graphql::{dataloader::Loader as DataLoader, FieldError, Result}; +use poem::async_trait; +use sea_orm::{prelude::*, JoinType, QuerySelect}; + +use crate::{ + db::Connection, + entities::{collections, drops}, + objects::Drop, +}; + +#[derive(Debug, Clone)] +pub struct Loader { + pub db: Connection, +} + +impl Loader { + #[must_use] + pub fn new(db: Connection) -> Self { + Self { db } + } +} + +#[async_trait] +impl DataLoader for Loader { + 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::CollectionId.is_in(keys.iter().map(ToOwned::to_owned))) + .all(self.db.get()) + .await?; + + drops + .into_iter() + .map(|(drop, collection)| { + Ok(( + drop.id, + )) + }) + .collect::>>() + } +} \ No newline at end of file diff --git a/api/src/dataloaders/mod.rs b/api/src/dataloaders/mod.rs index 1dcc925..7e0f9f7 100644 --- a/api/src/dataloaders/mod.rs +++ b/api/src/dataloaders/mod.rs @@ -6,6 +6,8 @@ mod drops; mod holders; mod metadata_json; mod purchases; +mod project_collections; +mod collection_drop; pub use collection::Loader as CollectionLoader; pub use collection_mints::{ diff --git a/api/src/dataloaders/project_collections.rs b/api/src/dataloaders/project_collections.rs new file mode 100644 index 0000000..41f7f56 --- /dev/null +++ b/api/src/dataloaders/project_collections.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; + +use async_graphql::{dataloader::Loader as DataLoader, FieldError, Result}; +use poem::async_trait; +use sea_orm::prelude::*; + +use crate::{db::Connection, entities::collections, objects::Collection}; + +#[derive(Debug, Clone)] +pub struct ProjectLoader { + pub db: Connection, +} + +impl ProjectCollectionsLoader { + #[must_use] + pub fn new(db: Connection) -> Self { + Self { db } + } +} + +#[async_trait] +impl DataLoader for ProjectLoader { + type Error = FieldError; + type Value = Vec; + + async fn load(&self, keys: &[Uuid]) -> Result, Self::Error> { + let collections = collections::Entity::find() + .filter(collections::Column::ProjectId.is_in(keys.iter().map(ToOwned::to_owned))) + .all(self.db.get()) + .await?; + + Ok(collections + .into_iter() + .fold(HashMap::new(), |mut acc, (project, collection)| { + acc.entry(project).or_insert_with(Vec::new); + + acc.entry(project) + .and_modify(|collections| collections.push(collection)); + + acc + })) + } +} From cb7f1c78a98744b2ecebe378fa09fd2c10ed3a66 Mon Sep 17 00:00:00 2001 From: Anshul Goel Date: Mon, 17 Jul 2023 18:13:25 +0530 Subject: [PATCH 5/9] update loaders, objects, fix bug --- api/src/blockchains/polygon.rs | 2 +- api/src/blockchains/solana.rs | 2 +- api/src/dataloaders/collection_drop.rs | 10 +- api/src/dataloaders/mod.rs | 8 +- api/src/dataloaders/project_collection.rs | 37 +++ api/src/dataloaders/project_collections.rs | 12 +- api/src/lib.rs | 19 +- api/src/mutations/collection.rs | 131 ++++++----- api/src/mutations/drop.rs | 210 ++++++++++-------- api/src/mutations/mint.rs | 204 ++++++++++------- api/src/mutations/transfer.rs | 28 ++- api/src/objects/collection.rs | 11 +- api/src/objects/project.rs | 21 +- ...0230214_212301_create_collections_table.rs | 28 ++- 14 files changed, 451 insertions(+), 272 deletions(-) create mode 100644 api/src/dataloaders/project_collection.rs diff --git a/api/src/blockchains/polygon.rs b/api/src/blockchains/polygon.rs index b2853ff..214b02c 100644 --- a/api/src/blockchains/polygon.rs +++ b/api/src/blockchains/polygon.rs @@ -25,7 +25,7 @@ impl Polygon { pub fn event( &self, ) -> impl DropEvent - + TransferEvent { + + TransferEvent { self.clone() } } diff --git a/api/src/blockchains/solana.rs b/api/src/blockchains/solana.rs index adcf20f..2e89b5d 100644 --- a/api/src/blockchains/solana.rs +++ b/api/src/blockchains/solana.rs @@ -31,7 +31,7 @@ impl Solana { MintMetaplexEditionTransaction, MetaplexMasterEditionTransaction, > + TransferEvent - + CollectionEvent< + + CollectionEvent< MetaplexCertifiedCollectionTransaction, MetaplexCertifiedCollectionTransaction, MintMetaplexMetadataTransaction, diff --git a/api/src/dataloaders/collection_drop.rs b/api/src/dataloaders/collection_drop.rs index f3abe6d..7855a06 100644 --- a/api/src/dataloaders/collection_drop.rs +++ b/api/src/dataloaders/collection_drop.rs @@ -39,9 +39,15 @@ impl DataLoader for Loader { .into_iter() .map(|(drop, collection)| { Ok(( - drop.id, + drop.collection_id, + Drop::new( + drop.clone(), + collection.ok_or_else(|| { + FieldError::new(format!("no collection for the drop {}", drop.id)) + })?, + ), )) }) .collect::>>() } -} \ No newline at end of file +} diff --git a/api/src/dataloaders/mod.rs b/api/src/dataloaders/mod.rs index 7e0f9f7..429b748 100644 --- a/api/src/dataloaders/mod.rs +++ b/api/src/dataloaders/mod.rs @@ -1,15 +1,17 @@ mod collection; +mod collection_drop; mod collection_mints; mod creators; mod drop; mod drops; mod holders; mod metadata_json; -mod purchases; +mod project_collection; mod project_collections; -mod collection_drop; +mod purchases; pub use collection::Loader as CollectionLoader; +pub use collection_drop::Loader as CollectionDropLoader; pub use collection_mints::{ Loader as CollectionMintsLoader, OwnerLoader as CollectionMintsOwnerLoader, }; @@ -20,6 +22,8 @@ pub use holders::Loader as HoldersLoader; pub use metadata_json::{ AttributesLoader as MetadataJsonAttributesLoader, Loader as MetadataJsonLoader, }; +pub use project_collection::ProjectCollectionLoader; +pub use project_collections::ProjectCollectionsLoader; pub use purchases::{ CollectionLoader as CollectionPurchasesLoader, DropLoader as DropPurchasesLoader, }; diff --git a/api/src/dataloaders/project_collection.rs b/api/src/dataloaders/project_collection.rs new file mode 100644 index 0000000..ec37cea --- /dev/null +++ b/api/src/dataloaders/project_collection.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; + +use async_graphql::{dataloader::Loader as DataLoader, FieldError, Result}; +use poem::async_trait; +use sea_orm::prelude::*; + +use crate::{db::Connection, entities::collections, objects::Collection}; + +#[derive(Debug, Clone)] +pub struct ProjectCollectionLoader { + pub db: Connection, +} + +impl ProjectCollectionLoader { + #[must_use] + pub fn new(db: Connection) -> Self { + Self { db } + } +} + +#[async_trait] +impl DataLoader for ProjectCollectionLoader { + type Error = FieldError; + type Value = Collection; + + async fn load(&self, keys: &[Uuid]) -> Result, Self::Error> { + let collections = collections::Entity::find() + .filter(collections::Column::ProjectId.is_in(keys.iter().map(ToOwned::to_owned))) + .all(self.db.get()) + .await?; + + collections + .into_iter() + .map(|collection| Ok((collection.id, collection.into()))) + .collect() + } +} diff --git a/api/src/dataloaders/project_collections.rs b/api/src/dataloaders/project_collections.rs index 41f7f56..80c3006 100644 --- a/api/src/dataloaders/project_collections.rs +++ b/api/src/dataloaders/project_collections.rs @@ -7,7 +7,7 @@ use sea_orm::prelude::*; use crate::{db::Connection, entities::collections, objects::Collection}; #[derive(Debug, Clone)] -pub struct ProjectLoader { +pub struct ProjectCollectionsLoader { pub db: Connection, } @@ -19,7 +19,7 @@ impl ProjectCollectionsLoader { } #[async_trait] -impl DataLoader for ProjectLoader { +impl DataLoader for ProjectCollectionsLoader { type Error = FieldError; type Value = Vec; @@ -31,11 +31,11 @@ impl DataLoader for ProjectLoader { Ok(collections .into_iter() - .fold(HashMap::new(), |mut acc, (project, collection)| { - acc.entry(project).or_insert_with(Vec::new); + .fold(HashMap::new(), |mut acc, collection| { + acc.entry(collection.project_id).or_insert_with(Vec::new); - acc.entry(project) - .and_modify(|collections| collections.push(collection)); + acc.entry(collection.project_id) + .and_modify(|collections| collections.push(collection.into())); acc })) diff --git a/api/src/lib.rs b/api/src/lib.rs index e99944a..1b22a64 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -22,9 +22,10 @@ use async_graphql::{ }; use blockchains::{polygon::Polygon, solana::Solana}; use dataloaders::{ - CollectionLoader, CollectionMintsLoader, CollectionMintsOwnerLoader, CollectionPurchasesLoader, - CreatorsLoader, DropLoader, DropPurchasesLoader, HoldersLoader, MetadataJsonAttributesLoader, - MetadataJsonLoader, ProjectDropsLoader, + CollectionDropLoader, CollectionLoader, CollectionMintsLoader, CollectionMintsOwnerLoader, + CollectionPurchasesLoader, CreatorsLoader, DropLoader, DropPurchasesLoader, HoldersLoader, + MetadataJsonAttributesLoader, MetadataJsonLoader, ProjectCollectionLoader, + ProjectCollectionsLoader, ProjectDropsLoader, }; use db::Connection; use hub_core::{ @@ -255,11 +256,14 @@ pub struct AppContext { organization_id: OrganizationId, balance: Balance, project_drops_loader: DataLoader, + project_collections_loader: DataLoader, + project_collection_loader: DataLoader, collection_loader: DataLoader, metadata_json_loader: DataLoader, metadata_json_attributes_loader: DataLoader, collection_mints_loader: DataLoader, collection_mints_owner_loader: DataLoader, + collection_drop_loader: DataLoader, drop_loader: DataLoader, creators_loader: DataLoader, holders_loader: DataLoader, @@ -278,6 +282,10 @@ impl AppContext { let project_drops_loader = DataLoader::new(ProjectDropsLoader::new(db.clone()), tokio::spawn); let collection_loader = DataLoader::new(CollectionLoader::new(db.clone()), tokio::spawn); + let project_collections_loader = + DataLoader::new(ProjectCollectionsLoader::new(db.clone()), tokio::spawn); + let project_collection_loader = + DataLoader::new(ProjectCollectionLoader::new(db.clone()), tokio::spawn); let metadata_json_loader = DataLoader::new(MetadataJsonLoader::new(db.clone()), tokio::spawn); let metadata_json_attributes_loader = @@ -286,6 +294,8 @@ impl AppContext { DataLoader::new(CollectionMintsLoader::new(db.clone()), tokio::spawn); let collection_mints_owner_loader = DataLoader::new(CollectionMintsOwnerLoader::new(db.clone()), tokio::spawn); + let collection_drop_loader: DataLoader<_> = + DataLoader::new(CollectionDropLoader::new(db.clone()), tokio::spawn); let drop_loader = DataLoader::new(DropLoader::new(db.clone()), tokio::spawn); let creators_loader = DataLoader::new(CreatorsLoader::new(db.clone()), tokio::spawn); let holders_loader = DataLoader::new(HoldersLoader::new(db.clone()), tokio::spawn); @@ -300,11 +310,14 @@ impl AppContext { organization_id, balance, project_drops_loader, + project_collections_loader, + project_collection_loader, collection_loader, metadata_json_loader, metadata_json_attributes_loader, collection_mints_loader, collection_mints_owner_loader, + collection_drop_loader, drop_loader, creators_loader, holders_loader, diff --git a/api/src/mutations/collection.rs b/api/src/mutations/collection.rs index e997431..e045fa4 100644 --- a/api/src/mutations/collection.rs +++ b/api/src/mutations/collection.rs @@ -100,20 +100,23 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .create_collection(event_key, MetaplexCertifiedCollectionTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: 0, - creators: input - .creators - .into_iter() - .map(TryFrom::try_from) - .collect::>()?, - }), - }) + .create_collection( + event_key, + MetaplexCertifiedCollectionTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: 0, + creators: input + .creators + .into_iter() + .map(TryFrom::try_from) + .collect::>()?, + }), + }, + ) .await?; }, BlockchainEnum::Ethereum | BlockchainEnum::Polygon => { @@ -121,14 +124,18 @@ impl Mutation { }, }; - submit_pending_deduction(credits, db, DeductionParams { - user_id, - org_id, - balance, - collection: collection.id, - blockchain: input.blockchain, - action: Actions::CreateDrop, - }) + submit_pending_deduction( + credits, + db, + DeductionParams { + user_id, + org_id, + balance, + collection: collection.id, + blockchain: input.blockchain, + action: Actions::CreateDrop, + }, + ) .await?; // TODO: separate event for collection creation @@ -213,23 +220,26 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_create_collection(event_key, MetaplexCertifiedCollectionTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: 0, - creators: creators - .into_iter() - .map(|c| ProtoCreator { - address: c.address, - verified: c.verified, - share: c.share, - }) - .collect(), - }), - }) + .retry_create_collection( + event_key, + MetaplexCertifiedCollectionTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: 0, + creators: creators + .into_iter() + .map(|c| ProtoCreator { + address: c.address, + verified: c.verified, + share: c.share, + }) + .collect(), + }), + }, + ) .await?; }, BlockchainEnum::Polygon | BlockchainEnum::Ethereum => { @@ -237,14 +247,18 @@ impl Mutation { }, }; - submit_pending_deduction(credits, db, DeductionParams { - balance, - user_id, - org_id, - collection: collection.id, - blockchain: collection.blockchain, - action: Actions::RetryDrop, - }) + submit_pending_deduction( + credits, + db, + DeductionParams { + balance, + user_id, + org_id, + collection: collection.id, + blockchain: collection.blockchain, + action: Actions::RetryDrop, + }, + ) .await?; Ok(CreateCollectionPayload { @@ -366,16 +380,19 @@ impl Mutation { solana .event() - .update_collection(event_key, MetaplexCertifiedCollectionTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json_model.name, - symbol: metadata_json_model.symbol, - metadata_uri: metadata_json_model.uri, - seller_fee_basis_points: 0, - creators, - }), - }) + .update_collection( + event_key, + MetaplexCertifiedCollectionTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json_model.name, + symbol: metadata_json_model.symbol, + metadata_uri: metadata_json_model.uri, + seller_fee_basis_points: 0, + creators, + }), + }, + ) .await?; }, BlockchainEnum::Polygon | BlockchainEnum::Ethereum => { diff --git a/api/src/mutations/drop.rs b/api/src/mutations/drop.rs index bbe62e7..81c4f20 100644 --- a/api/src/mutations/drop.rs +++ b/api/src/mutations/drop.rs @@ -114,43 +114,49 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .create_drop(event_key, proto::MetaplexMasterEditionTransaction { - master_edition: Some(proto::MasterEdition { - owner_address, - supply: input.supply.map(TryInto::try_into).transpose()?, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: seller_fee_basis_points.into(), - creators: input - .creators - .into_iter() - .map(TryFrom::try_from) - .collect::>()?, - }), - }) + .create_drop( + event_key, + proto::MetaplexMasterEditionTransaction { + master_edition: Some(proto::MasterEdition { + owner_address, + supply: input.supply.map(TryInto::try_into).transpose()?, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: seller_fee_basis_points.into(), + creators: input + .creators + .into_iter() + .map(TryFrom::try_from) + .collect::>()?, + }), + }, + ) .await?; }, BlockchainEnum::Polygon => { let amount = input.supply.ok_or(Error::new("supply is required"))?; polygon - .create_drop(event_key, proto::CreateEditionTransaction { - amount: amount.try_into()?, - edition_info: Some(proto::EditionInfo { - creator: input - .creators - .get(0) - .ok_or(Error::new("creator is required"))? - .clone() - .address, - collection: metadata_json.name, - uri: metadata_json.uri, - description: metadata_json.description, - image_uri: metadata_json.image, - }), - fee_receiver: owner_address.clone(), - fee_numerator: seller_fee_basis_points.into(), - }) + .create_drop( + event_key, + proto::CreateEditionTransaction { + amount: amount.try_into()?, + edition_info: Some(proto::EditionInfo { + creator: input + .creators + .get(0) + .ok_or(Error::new("creator is required"))? + .clone() + .address, + collection: metadata_json.name, + uri: metadata_json.uri, + description: metadata_json.description, + image_uri: metadata_json.image, + }), + fee_receiver: owner_address.clone(), + fee_numerator: seller_fee_basis_points.into(), + }, + ) .await?; }, BlockchainEnum::Ethereum => { @@ -158,14 +164,18 @@ impl Mutation { }, }; - submit_pending_deduction(credits, db, DeductionParams { - user_id, - org_id, - balance, - drop: drop_model.id, - blockchain: input.blockchain, - action: Actions::CreateDrop, - }) + submit_pending_deduction( + credits, + db, + DeductionParams { + user_id, + org_id, + balance, + drop: drop_model.id, + blockchain: input.blockchain, + action: Actions::CreateDrop, + }, + ) .await?; nfts_producer @@ -251,24 +261,27 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_create_drop(event_key, proto::MetaplexMasterEditionTransaction { - master_edition: Some(proto::MasterEdition { - owner_address, - supply: collection.supply.map(TryInto::try_into).transpose()?, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: collection.seller_fee_basis_points.into(), - creators: creators - .into_iter() - .map(|c| proto::Creator { - address: c.address, - verified: c.verified, - share: c.share, - }) - .collect(), - }), - }) + .retry_create_drop( + event_key, + proto::MetaplexMasterEditionTransaction { + master_edition: Some(proto::MasterEdition { + owner_address, + supply: collection.supply.map(TryInto::try_into).transpose()?, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: collection.seller_fee_basis_points.into(), + creators: creators + .into_iter() + .map(|c| proto::Creator { + address: c.address, + verified: c.verified, + share: c.share, + }) + .collect(), + }), + }, + ) .await?; }, BlockchainEnum::Polygon => { @@ -278,12 +291,15 @@ impl Mutation { polygon .event() - .retry_create_drop(event_key, proto::CreateEditionTransaction { - edition_info: None, - amount, - fee_receiver: owner_address, - fee_numerator: collection.seller_fee_basis_points.into(), - }) + .retry_create_drop( + event_key, + proto::CreateEditionTransaction { + edition_info: None, + amount, + fee_receiver: owner_address, + fee_numerator: collection.seller_fee_basis_points.into(), + }, + ) .await?; }, BlockchainEnum::Ethereum => { @@ -291,14 +307,18 @@ impl Mutation { }, }; - submit_pending_deduction(credits, db, DeductionParams { - balance, - user_id, - org_id, - drop: drop.id, - blockchain: collection.blockchain, - action: Actions::RetryDrop, - }) + submit_pending_deduction( + credits, + db, + DeductionParams { + balance, + user_id, + org_id, + drop: drop.id, + blockchain: collection.blockchain, + action: Actions::RetryDrop, + }, + ) .await?; Ok(CreateDropPayload { @@ -557,17 +577,20 @@ impl Mutation { solana .event() - .update_drop(event_key, proto::MetaplexMasterEditionTransaction { - master_edition: Some(proto::MasterEdition { - owner_address, - supply: collection.supply.map(TryInto::try_into).transpose()?, - name: metadata_json_model.name, - symbol: metadata_json_model.symbol, - metadata_uri: metadata_json_model.uri, - seller_fee_basis_points: collection.seller_fee_basis_points.into(), - creators, - }), - }) + .update_drop( + event_key, + proto::MetaplexMasterEditionTransaction { + master_edition: Some(proto::MasterEdition { + owner_address, + supply: collection.supply.map(TryInto::try_into).transpose()?, + name: metadata_json_model.name, + symbol: metadata_json_model.symbol, + metadata_uri: metadata_json_model.uri, + seller_fee_basis_points: collection.seller_fee_basis_points.into(), + creators, + }), + }, + ) .await?; }, BlockchainEnum::Polygon => { @@ -583,15 +606,18 @@ impl Mutation { polygon .event() - .update_drop(event_key, proto::UpdateEdtionTransaction { - edition_info: Some(EditionInfo { - description: metadata_json_model.description, - image_uri: metadata_json_model.image, - collection: metadata_json_model.name, - uri: metadata_json_model.uri, - creator, - }), - }) + .update_drop( + event_key, + proto::UpdateEdtionTransaction { + edition_info: Some(EditionInfo { + description: metadata_json_model.description, + image_uri: metadata_json_model.image, + collection: metadata_json_model.name, + uri: metadata_json_model.uri, + creator, + }), + }, + ) .await?; }, BlockchainEnum::Ethereum => { diff --git a/api/src/mutations/mint.rs b/api/src/mutations/mint.rs index bdd2bb0..fd3e0fc 100644 --- a/api/src/mutations/mint.rs +++ b/api/src/mutations/mint.rs @@ -124,22 +124,28 @@ impl Mutation { solana .event() - .mint_drop(event_key, proto::MintMetaplexEditionTransaction { - recipient_address: input.recipient.to_string(), - owner_address: owner_address.to_string(), - edition, - collection_id: collection.id.to_string(), - }) + .mint_drop( + event_key, + proto::MintMetaplexEditionTransaction { + recipient_address: input.recipient.to_string(), + owner_address: owner_address.to_string(), + edition, + collection_id: collection.id.to_string(), + }, + ) .await?; }, BlockchainEnum::Polygon => { polygon .event() - .mint_drop(event_key, proto::MintEditionTransaction { - receiver: input.recipient.to_string(), - amount: 1, - collection_id: collection.id.to_string(), - }) + .mint_drop( + event_key, + proto::MintEditionTransaction { + receiver: input.recipient.to_string(), + amount: 1, + collection_id: collection.id.to_string(), + }, + ) .await?; }, BlockchainEnum::Ethereum => { @@ -165,14 +171,18 @@ impl Mutation { purchase_am.insert(conn).await?; - submit_pending_deduction(credits, db, DeductionParams { - balance, - user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::MintEdition, - }) + submit_pending_deduction( + credits, + db, + DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::MintEdition, + }, + ) .await?; nfts_producer @@ -280,22 +290,28 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_mint_drop(event_key, proto::MintMetaplexEditionTransaction { - recipient_address: recipient.to_string(), - owner_address: owner_address.to_string(), - edition, - collection_id: collection.id.to_string(), - }) + .retry_mint_drop( + event_key, + proto::MintMetaplexEditionTransaction { + recipient_address: recipient.to_string(), + owner_address: owner_address.to_string(), + edition, + collection_id: collection.id.to_string(), + }, + ) .await?; }, BlockchainEnum::Polygon => { polygon .event() - .retry_mint_drop(event_key, proto::MintEditionTransaction { - receiver: recipient.to_string(), - amount: 1, - collection_id: collection.id.to_string(), - }) + .retry_mint_drop( + event_key, + proto::MintEditionTransaction { + receiver: recipient.to_string(), + amount: 1, + collection_id: collection.id.to_string(), + }, + ) .await?; }, BlockchainEnum::Ethereum => { @@ -303,14 +319,18 @@ impl Mutation { }, }; - submit_pending_deduction(credits, db, DeductionParams { - balance, - user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::RetryMint, - }) + submit_pending_deduction( + credits, + db, + DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::RetryMint, + }, + ) .await?; Ok(RetryMintEditionPayload { @@ -406,22 +426,25 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .mint_to_collection(event_key, proto::MintMetaplexMetadataTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: seller_fee_basis_points.into(), - creators: creators - .into_iter() - .map(TryFrom::try_from) - .collect::>()?, - }), - recipient_address: input.recipient.to_string(), - compressed: input.compressed, - collection_id: collection.id.to_string(), - }) + .mint_to_collection( + event_key, + proto::MintMetaplexMetadataTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: seller_fee_basis_points.into(), + creators: creators + .into_iter() + .map(TryFrom::try_from) + .collect::>()?, + }), + recipient_address: input.recipient.to_string(), + compressed: input.compressed, + collection_id: collection.id.to_string(), + }, + ) .await?; }, BlockchainEnum::Ethereum | BlockchainEnum::Polygon => { @@ -448,14 +471,18 @@ impl Mutation { // purchase_am.insert(conn).await?; - submit_pending_deduction(credits, db, DeductionParams { - balance, - user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::MintEdition, - }) + submit_pending_deduction( + credits, + db, + DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::MintEdition, + }, + ) .await?; // nfts_producer @@ -541,21 +568,24 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_mint_to_collection(event_key, proto::MintMetaplexMetadataTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: uri.ok_or(Error::new("metadata uri not found"))?, - seller_fee_basis_points: collection_mint_model - .seller_fee_basis_points - .into(), - creators: creators.into_iter().map(Into::into).collect(), - }), - recipient_address: recipient.to_string(), - compressed: collection_mint_model.compressed, - collection_id: collection_mint_model.collection_id.to_string(), - }) + .retry_mint_to_collection( + event_key, + proto::MintMetaplexMetadataTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: uri.ok_or(Error::new("metadata uri not found"))?, + seller_fee_basis_points: collection_mint_model + .seller_fee_basis_points + .into(), + creators: creators.into_iter().map(Into::into).collect(), + }), + recipient_address: recipient.to_string(), + compressed: collection_mint_model.compressed, + collection_id: collection_mint_model.collection_id.to_string(), + }, + ) .await?; }, BlockchainEnum::Ethereum | BlockchainEnum::Polygon => { @@ -563,14 +593,18 @@ impl Mutation { }, }; - submit_pending_deduction(credits, db, DeductionParams { - balance, - user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::RetryMint, - }) + submit_pending_deduction( + credits, + db, + DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::RetryMint, + }, + ) .await?; Ok(RetryMintEditionPayload { diff --git a/api/src/mutations/transfer.rs b/api/src/mutations/transfer.rs index 81c82be..13f723d 100644 --- a/api/src/mutations/transfer.rs +++ b/api/src/mutations/transfer.rs @@ -116,23 +116,29 @@ impl Mutation { solana .event() - .transfer_asset(event_key, proto::TransferMetaplexAssetTransaction { - recipient_address, - owner_address, - collection_mint_id, - }) + .transfer_asset( + event_key, + proto::TransferMetaplexAssetTransaction { + recipient_address, + owner_address, + collection_mint_id, + }, + ) .await?; }, Blockchain::Polygon => { let polygon = ctx.data::()?; polygon .event() - .transfer_asset(event_key, TransferPolygonAsset { - collection_mint_id, - owner_address, - recipient_address, - amount: 1, - }) + .transfer_asset( + event_key, + TransferPolygonAsset { + collection_mint_id, + owner_address, + recipient_address, + amount: 1, + }, + ) .await?; }, Blockchain::Ethereum => { diff --git a/api/src/objects/collection.rs b/api/src/objects/collection.rs index 0d1fc58..39a1600 100644 --- a/api/src/objects/collection.rs +++ b/api/src/objects/collection.rs @@ -1,7 +1,7 @@ use async_graphql::{Context, Object, Result}; use sea_orm::entity::prelude::*; -use super::{metadata_json::MetadataJson, Holder}; +use super::{metadata_json::MetadataJson, Drop, Holder}; use crate::{ entities::{ collection_creators, collection_mints, @@ -138,6 +138,15 @@ impl Collection { collection_purchases_loader.load_one(self.id).await } + + async fn drop(&self, ctx: &Context<'_>) -> Result> { + let AppContext { + collection_drop_loader, + .. + } = ctx.data::()?; + + collection_drop_loader.load_one(self.id).await + } } impl From for Collection { diff --git a/api/src/objects/project.rs b/api/src/objects/project.rs index eeff992..892aa6c 100644 --- a/api/src/objects/project.rs +++ b/api/src/objects/project.rs @@ -1,7 +1,7 @@ use async_graphql::{ComplexObject, Context, Result, SimpleObject}; use hub_core::uuid::Uuid; -use crate::{objects::Drop, AppContext}; +use crate::{objects::Collection, objects::Drop, AppContext}; #[derive(SimpleObject, Debug, Clone)] #[graphql(complex)] @@ -27,4 +27,23 @@ impl Project { drop_loader.load_one(id).await } + + /// The collections associated with the project. + async fn collections(&self, ctx: &Context<'_>) -> Result>> { + let AppContext { + project_collections_loader, + .. + } = ctx.data::()?; + + project_collections_loader.load_one(self.id).await + } + + async fn collection(&self, id: Uuid, ctx: &Context<'_>) -> Result> { + let AppContext { + project_collection_loader, + .. + } = ctx.data::()?; + + project_collection_loader.load_one(id).await + } } diff --git a/migration/src/m20230214_212301_create_collections_table.rs b/migration/src/m20230214_212301_create_collections_table.rs index ac9f469..52c1fd1 100644 --- a/migration/src/m20230214_212301_create_collections_table.rs +++ b/migration/src/m20230214_212301_create_collections_table.rs @@ -111,11 +111,15 @@ pub enum Blockchain { impl Iden for Blockchain { fn unquoted(&self, s: &mut dyn std::fmt::Write) { - write!(s, "{}", match self { - Self::Type => "blockchain", - Self::Solana => "solana", - Self::Polygon => "polygon", - }) + write!( + s, + "{}", + match self { + Self::Type => "blockchain", + Self::Solana => "solana", + Self::Polygon => "polygon", + } + ) .unwrap(); } } @@ -128,11 +132,15 @@ pub enum CreationStatus { impl Iden for CreationStatus { fn unquoted(&self, s: &mut dyn std::fmt::Write) { - write!(s, "{}", match self { - Self::Type => "creation_status", - Self::Pending => "pending", - Self::Created => "created", - }) + write!( + s, + "{}", + match self { + Self::Type => "creation_status", + Self::Pending => "pending", + Self::Created => "created", + } + ) .unwrap(); } } From 128257689f45afda4af016d8f07d2bc0d6d978e0 Mon Sep 17 00:00:00 2001 From: Anshul Goel Date: Mon, 17 Jul 2023 20:02:55 +0530 Subject: [PATCH 6/9] Inputtype fix --- api/src/dataloaders/project_collection.rs | 6 +++--- api/src/objects/project.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/dataloaders/project_collection.rs b/api/src/dataloaders/project_collection.rs index ec37cea..0c505d9 100644 --- a/api/src/dataloaders/project_collection.rs +++ b/api/src/dataloaders/project_collection.rs @@ -29,9 +29,9 @@ impl DataLoader for ProjectCollectionLoader { .all(self.db.get()) .await?; - collections + Ok(collections .into_iter() - .map(|collection| Ok((collection.id, collection.into()))) - .collect() + .map(|collection| (collection.id, collection.into())) + .collect()) } } diff --git a/api/src/objects/project.rs b/api/src/objects/project.rs index 892aa6c..3953ed9 100644 --- a/api/src/objects/project.rs +++ b/api/src/objects/project.rs @@ -38,7 +38,7 @@ impl Project { project_collections_loader.load_one(self.id).await } - async fn collection(&self, id: Uuid, ctx: &Context<'_>) -> Result> { + async fn collection(&self, ctx: &Context<'_>, id: Uuid) -> Result> { let AppContext { project_collection_loader, .. From b56c8330d9a758b62262f98f831387c6aa4298cc Mon Sep 17 00:00:00 2001 From: Anshul Goel Date: Tue, 18 Jul 2023 13:52:41 +0530 Subject: [PATCH 7/9] minor update in collection drop loader, cargo fmt --- api/src/dataloaders/collection_drop.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/src/dataloaders/collection_drop.rs b/api/src/dataloaders/collection_drop.rs index 7855a06..49642a7 100644 --- a/api/src/dataloaders/collection_drop.rs +++ b/api/src/dataloaders/collection_drop.rs @@ -42,9 +42,10 @@ impl DataLoader for Loader { drop.collection_id, Drop::new( drop.clone(), - collection.ok_or_else(|| { - FieldError::new(format!("no collection for the drop {}", drop.id)) - })?, + collection.ok_or(FieldError::new(format!( + "no collection for the drop {}", + drop.id + )))?, ), )) }) From 558ca67fdad18de5f0452bfd1d9c912fad6b5058 Mon Sep 17 00:00:00 2001 From: Kyle Espinola Date: Tue, 18 Jul 2023 10:27:52 +0200 Subject: [PATCH 8/9] fix: run cargo fmt --- api/src/blockchains/polygon.rs | 2 +- api/src/blockchains/solana.rs | 2 +- api/src/mutations/collection.rs | 131 +++++------ api/src/mutations/drop.rs | 210 ++++++++---------- api/src/mutations/mint.rs | 204 +++++++---------- api/src/mutations/transfer.rs | 28 +-- api/src/objects/project.rs | 5 +- ...0230214_212301_create_collections_table.rs | 28 +-- 8 files changed, 261 insertions(+), 349 deletions(-) diff --git a/api/src/blockchains/polygon.rs b/api/src/blockchains/polygon.rs index 214b02c..b2853ff 100644 --- a/api/src/blockchains/polygon.rs +++ b/api/src/blockchains/polygon.rs @@ -25,7 +25,7 @@ impl Polygon { pub fn event( &self, ) -> impl DropEvent - + TransferEvent { + + TransferEvent { self.clone() } } diff --git a/api/src/blockchains/solana.rs b/api/src/blockchains/solana.rs index 2e89b5d..adcf20f 100644 --- a/api/src/blockchains/solana.rs +++ b/api/src/blockchains/solana.rs @@ -31,7 +31,7 @@ impl Solana { MintMetaplexEditionTransaction, MetaplexMasterEditionTransaction, > + TransferEvent - + CollectionEvent< + + CollectionEvent< MetaplexCertifiedCollectionTransaction, MetaplexCertifiedCollectionTransaction, MintMetaplexMetadataTransaction, diff --git a/api/src/mutations/collection.rs b/api/src/mutations/collection.rs index e045fa4..e997431 100644 --- a/api/src/mutations/collection.rs +++ b/api/src/mutations/collection.rs @@ -100,23 +100,20 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .create_collection( - event_key, - MetaplexCertifiedCollectionTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: 0, - creators: input - .creators - .into_iter() - .map(TryFrom::try_from) - .collect::>()?, - }), - }, - ) + .create_collection(event_key, MetaplexCertifiedCollectionTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: 0, + creators: input + .creators + .into_iter() + .map(TryFrom::try_from) + .collect::>()?, + }), + }) .await?; }, BlockchainEnum::Ethereum | BlockchainEnum::Polygon => { @@ -124,18 +121,14 @@ impl Mutation { }, }; - submit_pending_deduction( - credits, - db, - DeductionParams { - user_id, - org_id, - balance, - collection: collection.id, - blockchain: input.blockchain, - action: Actions::CreateDrop, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + user_id, + org_id, + balance, + collection: collection.id, + blockchain: input.blockchain, + action: Actions::CreateDrop, + }) .await?; // TODO: separate event for collection creation @@ -220,26 +213,23 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_create_collection( - event_key, - MetaplexCertifiedCollectionTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: 0, - creators: creators - .into_iter() - .map(|c| ProtoCreator { - address: c.address, - verified: c.verified, - share: c.share, - }) - .collect(), - }), - }, - ) + .retry_create_collection(event_key, MetaplexCertifiedCollectionTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: 0, + creators: creators + .into_iter() + .map(|c| ProtoCreator { + address: c.address, + verified: c.verified, + share: c.share, + }) + .collect(), + }), + }) .await?; }, BlockchainEnum::Polygon | BlockchainEnum::Ethereum => { @@ -247,18 +237,14 @@ impl Mutation { }, }; - submit_pending_deduction( - credits, - db, - DeductionParams { - balance, - user_id, - org_id, - collection: collection.id, - blockchain: collection.blockchain, - action: Actions::RetryDrop, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + balance, + user_id, + org_id, + collection: collection.id, + blockchain: collection.blockchain, + action: Actions::RetryDrop, + }) .await?; Ok(CreateCollectionPayload { @@ -380,19 +366,16 @@ impl Mutation { solana .event() - .update_collection( - event_key, - MetaplexCertifiedCollectionTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json_model.name, - symbol: metadata_json_model.symbol, - metadata_uri: metadata_json_model.uri, - seller_fee_basis_points: 0, - creators, - }), - }, - ) + .update_collection(event_key, MetaplexCertifiedCollectionTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json_model.name, + symbol: metadata_json_model.symbol, + metadata_uri: metadata_json_model.uri, + seller_fee_basis_points: 0, + creators, + }), + }) .await?; }, BlockchainEnum::Polygon | BlockchainEnum::Ethereum => { diff --git a/api/src/mutations/drop.rs b/api/src/mutations/drop.rs index 81c4f20..bbe62e7 100644 --- a/api/src/mutations/drop.rs +++ b/api/src/mutations/drop.rs @@ -114,49 +114,43 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .create_drop( - event_key, - proto::MetaplexMasterEditionTransaction { - master_edition: Some(proto::MasterEdition { - owner_address, - supply: input.supply.map(TryInto::try_into).transpose()?, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: seller_fee_basis_points.into(), - creators: input - .creators - .into_iter() - .map(TryFrom::try_from) - .collect::>()?, - }), - }, - ) + .create_drop(event_key, proto::MetaplexMasterEditionTransaction { + master_edition: Some(proto::MasterEdition { + owner_address, + supply: input.supply.map(TryInto::try_into).transpose()?, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: seller_fee_basis_points.into(), + creators: input + .creators + .into_iter() + .map(TryFrom::try_from) + .collect::>()?, + }), + }) .await?; }, BlockchainEnum::Polygon => { let amount = input.supply.ok_or(Error::new("supply is required"))?; polygon - .create_drop( - event_key, - proto::CreateEditionTransaction { - amount: amount.try_into()?, - edition_info: Some(proto::EditionInfo { - creator: input - .creators - .get(0) - .ok_or(Error::new("creator is required"))? - .clone() - .address, - collection: metadata_json.name, - uri: metadata_json.uri, - description: metadata_json.description, - image_uri: metadata_json.image, - }), - fee_receiver: owner_address.clone(), - fee_numerator: seller_fee_basis_points.into(), - }, - ) + .create_drop(event_key, proto::CreateEditionTransaction { + amount: amount.try_into()?, + edition_info: Some(proto::EditionInfo { + creator: input + .creators + .get(0) + .ok_or(Error::new("creator is required"))? + .clone() + .address, + collection: metadata_json.name, + uri: metadata_json.uri, + description: metadata_json.description, + image_uri: metadata_json.image, + }), + fee_receiver: owner_address.clone(), + fee_numerator: seller_fee_basis_points.into(), + }) .await?; }, BlockchainEnum::Ethereum => { @@ -164,18 +158,14 @@ impl Mutation { }, }; - submit_pending_deduction( - credits, - db, - DeductionParams { - user_id, - org_id, - balance, - drop: drop_model.id, - blockchain: input.blockchain, - action: Actions::CreateDrop, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + user_id, + org_id, + balance, + drop: drop_model.id, + blockchain: input.blockchain, + action: Actions::CreateDrop, + }) .await?; nfts_producer @@ -261,27 +251,24 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_create_drop( - event_key, - proto::MetaplexMasterEditionTransaction { - master_edition: Some(proto::MasterEdition { - owner_address, - supply: collection.supply.map(TryInto::try_into).transpose()?, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: collection.seller_fee_basis_points.into(), - creators: creators - .into_iter() - .map(|c| proto::Creator { - address: c.address, - verified: c.verified, - share: c.share, - }) - .collect(), - }), - }, - ) + .retry_create_drop(event_key, proto::MetaplexMasterEditionTransaction { + master_edition: Some(proto::MasterEdition { + owner_address, + supply: collection.supply.map(TryInto::try_into).transpose()?, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: collection.seller_fee_basis_points.into(), + creators: creators + .into_iter() + .map(|c| proto::Creator { + address: c.address, + verified: c.verified, + share: c.share, + }) + .collect(), + }), + }) .await?; }, BlockchainEnum::Polygon => { @@ -291,15 +278,12 @@ impl Mutation { polygon .event() - .retry_create_drop( - event_key, - proto::CreateEditionTransaction { - edition_info: None, - amount, - fee_receiver: owner_address, - fee_numerator: collection.seller_fee_basis_points.into(), - }, - ) + .retry_create_drop(event_key, proto::CreateEditionTransaction { + edition_info: None, + amount, + fee_receiver: owner_address, + fee_numerator: collection.seller_fee_basis_points.into(), + }) .await?; }, BlockchainEnum::Ethereum => { @@ -307,18 +291,14 @@ impl Mutation { }, }; - submit_pending_deduction( - credits, - db, - DeductionParams { - balance, - user_id, - org_id, - drop: drop.id, - blockchain: collection.blockchain, - action: Actions::RetryDrop, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + balance, + user_id, + org_id, + drop: drop.id, + blockchain: collection.blockchain, + action: Actions::RetryDrop, + }) .await?; Ok(CreateDropPayload { @@ -577,20 +557,17 @@ impl Mutation { solana .event() - .update_drop( - event_key, - proto::MetaplexMasterEditionTransaction { - master_edition: Some(proto::MasterEdition { - owner_address, - supply: collection.supply.map(TryInto::try_into).transpose()?, - name: metadata_json_model.name, - symbol: metadata_json_model.symbol, - metadata_uri: metadata_json_model.uri, - seller_fee_basis_points: collection.seller_fee_basis_points.into(), - creators, - }), - }, - ) + .update_drop(event_key, proto::MetaplexMasterEditionTransaction { + master_edition: Some(proto::MasterEdition { + owner_address, + supply: collection.supply.map(TryInto::try_into).transpose()?, + name: metadata_json_model.name, + symbol: metadata_json_model.symbol, + metadata_uri: metadata_json_model.uri, + seller_fee_basis_points: collection.seller_fee_basis_points.into(), + creators, + }), + }) .await?; }, BlockchainEnum::Polygon => { @@ -606,18 +583,15 @@ impl Mutation { polygon .event() - .update_drop( - event_key, - proto::UpdateEdtionTransaction { - edition_info: Some(EditionInfo { - description: metadata_json_model.description, - image_uri: metadata_json_model.image, - collection: metadata_json_model.name, - uri: metadata_json_model.uri, - creator, - }), - }, - ) + .update_drop(event_key, proto::UpdateEdtionTransaction { + edition_info: Some(EditionInfo { + description: metadata_json_model.description, + image_uri: metadata_json_model.image, + collection: metadata_json_model.name, + uri: metadata_json_model.uri, + creator, + }), + }) .await?; }, BlockchainEnum::Ethereum => { diff --git a/api/src/mutations/mint.rs b/api/src/mutations/mint.rs index fd3e0fc..bdd2bb0 100644 --- a/api/src/mutations/mint.rs +++ b/api/src/mutations/mint.rs @@ -124,28 +124,22 @@ impl Mutation { solana .event() - .mint_drop( - event_key, - proto::MintMetaplexEditionTransaction { - recipient_address: input.recipient.to_string(), - owner_address: owner_address.to_string(), - edition, - collection_id: collection.id.to_string(), - }, - ) + .mint_drop(event_key, proto::MintMetaplexEditionTransaction { + recipient_address: input.recipient.to_string(), + owner_address: owner_address.to_string(), + edition, + collection_id: collection.id.to_string(), + }) .await?; }, BlockchainEnum::Polygon => { polygon .event() - .mint_drop( - event_key, - proto::MintEditionTransaction { - receiver: input.recipient.to_string(), - amount: 1, - collection_id: collection.id.to_string(), - }, - ) + .mint_drop(event_key, proto::MintEditionTransaction { + receiver: input.recipient.to_string(), + amount: 1, + collection_id: collection.id.to_string(), + }) .await?; }, BlockchainEnum::Ethereum => { @@ -171,18 +165,14 @@ impl Mutation { purchase_am.insert(conn).await?; - submit_pending_deduction( - credits, - db, - DeductionParams { - balance, - user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::MintEdition, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::MintEdition, + }) .await?; nfts_producer @@ -290,28 +280,22 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_mint_drop( - event_key, - proto::MintMetaplexEditionTransaction { - recipient_address: recipient.to_string(), - owner_address: owner_address.to_string(), - edition, - collection_id: collection.id.to_string(), - }, - ) + .retry_mint_drop(event_key, proto::MintMetaplexEditionTransaction { + recipient_address: recipient.to_string(), + owner_address: owner_address.to_string(), + edition, + collection_id: collection.id.to_string(), + }) .await?; }, BlockchainEnum::Polygon => { polygon .event() - .retry_mint_drop( - event_key, - proto::MintEditionTransaction { - receiver: recipient.to_string(), - amount: 1, - collection_id: collection.id.to_string(), - }, - ) + .retry_mint_drop(event_key, proto::MintEditionTransaction { + receiver: recipient.to_string(), + amount: 1, + collection_id: collection.id.to_string(), + }) .await?; }, BlockchainEnum::Ethereum => { @@ -319,18 +303,14 @@ impl Mutation { }, }; - submit_pending_deduction( - credits, - db, - DeductionParams { - balance, - user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::RetryMint, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::RetryMint, + }) .await?; Ok(RetryMintEditionPayload { @@ -426,25 +406,22 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .mint_to_collection( - event_key, - proto::MintMetaplexMetadataTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: metadata_json.uri, - seller_fee_basis_points: seller_fee_basis_points.into(), - creators: creators - .into_iter() - .map(TryFrom::try_from) - .collect::>()?, - }), - recipient_address: input.recipient.to_string(), - compressed: input.compressed, - collection_id: collection.id.to_string(), - }, - ) + .mint_to_collection(event_key, proto::MintMetaplexMetadataTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: metadata_json.uri, + seller_fee_basis_points: seller_fee_basis_points.into(), + creators: creators + .into_iter() + .map(TryFrom::try_from) + .collect::>()?, + }), + recipient_address: input.recipient.to_string(), + compressed: input.compressed, + collection_id: collection.id.to_string(), + }) .await?; }, BlockchainEnum::Ethereum | BlockchainEnum::Polygon => { @@ -471,18 +448,14 @@ impl Mutation { // purchase_am.insert(conn).await?; - submit_pending_deduction( - credits, - db, - DeductionParams { - balance, - user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::MintEdition, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::MintEdition, + }) .await?; // nfts_producer @@ -568,24 +541,21 @@ impl Mutation { BlockchainEnum::Solana => { solana .event() - .retry_mint_to_collection( - event_key, - proto::MintMetaplexMetadataTransaction { - metadata: Some(MetaplexMetadata { - owner_address, - name: metadata_json.name, - symbol: metadata_json.symbol, - metadata_uri: uri.ok_or(Error::new("metadata uri not found"))?, - seller_fee_basis_points: collection_mint_model - .seller_fee_basis_points - .into(), - creators: creators.into_iter().map(Into::into).collect(), - }), - recipient_address: recipient.to_string(), - compressed: collection_mint_model.compressed, - collection_id: collection_mint_model.collection_id.to_string(), - }, - ) + .retry_mint_to_collection(event_key, proto::MintMetaplexMetadataTransaction { + metadata: Some(MetaplexMetadata { + owner_address, + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri: uri.ok_or(Error::new("metadata uri not found"))?, + seller_fee_basis_points: collection_mint_model + .seller_fee_basis_points + .into(), + creators: creators.into_iter().map(Into::into).collect(), + }), + recipient_address: recipient.to_string(), + compressed: collection_mint_model.compressed, + collection_id: collection_mint_model.collection_id.to_string(), + }) .await?; }, BlockchainEnum::Ethereum | BlockchainEnum::Polygon => { @@ -593,18 +563,14 @@ impl Mutation { }, }; - submit_pending_deduction( - credits, - db, - DeductionParams { - balance, - user_id, - org_id, - mint: collection_mint_model.id, - blockchain: collection.blockchain, - action: Actions::RetryMint, - }, - ) + submit_pending_deduction(credits, db, DeductionParams { + balance, + user_id, + org_id, + mint: collection_mint_model.id, + blockchain: collection.blockchain, + action: Actions::RetryMint, + }) .await?; Ok(RetryMintEditionPayload { diff --git a/api/src/mutations/transfer.rs b/api/src/mutations/transfer.rs index 13f723d..81c82be 100644 --- a/api/src/mutations/transfer.rs +++ b/api/src/mutations/transfer.rs @@ -116,29 +116,23 @@ impl Mutation { solana .event() - .transfer_asset( - event_key, - proto::TransferMetaplexAssetTransaction { - recipient_address, - owner_address, - collection_mint_id, - }, - ) + .transfer_asset(event_key, proto::TransferMetaplexAssetTransaction { + recipient_address, + owner_address, + collection_mint_id, + }) .await?; }, Blockchain::Polygon => { let polygon = ctx.data::()?; polygon .event() - .transfer_asset( - event_key, - TransferPolygonAsset { - collection_mint_id, - owner_address, - recipient_address, - amount: 1, - }, - ) + .transfer_asset(event_key, TransferPolygonAsset { + collection_mint_id, + owner_address, + recipient_address, + amount: 1, + }) .await?; }, Blockchain::Ethereum => { diff --git a/api/src/objects/project.rs b/api/src/objects/project.rs index 3953ed9..4515b77 100644 --- a/api/src/objects/project.rs +++ b/api/src/objects/project.rs @@ -1,7 +1,10 @@ use async_graphql::{ComplexObject, Context, Result, SimpleObject}; use hub_core::uuid::Uuid; -use crate::{objects::Collection, objects::Drop, AppContext}; +use crate::{ + objects::{Collection, Drop}, + AppContext, +}; #[derive(SimpleObject, Debug, Clone)] #[graphql(complex)] diff --git a/migration/src/m20230214_212301_create_collections_table.rs b/migration/src/m20230214_212301_create_collections_table.rs index 52c1fd1..ac9f469 100644 --- a/migration/src/m20230214_212301_create_collections_table.rs +++ b/migration/src/m20230214_212301_create_collections_table.rs @@ -111,15 +111,11 @@ pub enum Blockchain { impl Iden for Blockchain { fn unquoted(&self, s: &mut dyn std::fmt::Write) { - write!( - s, - "{}", - match self { - Self::Type => "blockchain", - Self::Solana => "solana", - Self::Polygon => "polygon", - } - ) + write!(s, "{}", match self { + Self::Type => "blockchain", + Self::Solana => "solana", + Self::Polygon => "polygon", + }) .unwrap(); } } @@ -132,15 +128,11 @@ pub enum CreationStatus { impl Iden for CreationStatus { fn unquoted(&self, s: &mut dyn std::fmt::Write) { - write!( - s, - "{}", - match self { - Self::Type => "creation_status", - Self::Pending => "pending", - Self::Created => "created", - } - ) + write!(s, "{}", match self { + Self::Type => "creation_status", + Self::Pending => "pending", + Self::Created => "created", + }) .unwrap(); } } From fe25471a8716acc7c4c47a9fefccb7b983d73980 Mon Sep 17 00:00:00 2001 From: Kyle Espinola Date: Tue, 18 Jul 2023 11:55:55 +0200 Subject: [PATCH 9/9] feat: add created_at and created_by to collections backfill based on drop. add new fields to collection when drop created. --- api/src/entities/collections.rs | 3 +- api/src/mutations/collection.rs | 1 + api/src/mutations/drop.rs | 151 ++---------------- api/src/mutations/mint.rs | 1 - api/src/mutations/transfer.rs | 2 +- api/src/objects/collection.rs | 16 ++ migration/src/lib.rs | 2 + ...and_credits_deduction_id_to_collections.rs | 2 +- ...t_and_created_by_columns_to_collections.rs | 63 ++++++++ 9 files changed, 102 insertions(+), 139 deletions(-) create mode 100644 migration/src/m20230718_111347_add_created_at_and_created_by_columns_to_collections.rs diff --git a/api/src/entities/collections.rs b/api/src/entities/collections.rs index a044f0e..1525478 100644 --- a/api/src/entities/collections.rs +++ b/api/src/entities/collections.rs @@ -21,7 +21,8 @@ pub struct Model { #[sea_orm(nullable)] pub signature: Option, pub seller_fee_basis_points: i16, - // TODO: add to collections and backfill from the drops + pub created_by: Uuid, + pub created_at: DateTimeWithTimeZone, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/api/src/mutations/collection.rs b/api/src/mutations/collection.rs index e997431..6bfffdd 100644 --- a/api/src/mutations/collection.rs +++ b/api/src/mutations/collection.rs @@ -76,6 +76,7 @@ impl Mutation { supply: Set(Some(0)), creation_status: Set(CreationStatus::Pending), project_id: Set(input.project), + created_by: Set(user_id), ..Default::default() }; diff --git a/api/src/mutations/drop.rs b/api/src/mutations/drop.rs index bbe62e7..1c94d9d 100644 --- a/api/src/mutations/drop.rs +++ b/api/src/mutations/drop.rs @@ -1,12 +1,9 @@ -use std::str::FromStr; - use async_graphql::{Context, Error, InputObject, Object, Result, SimpleObject}; use hub_core::{chrono::Utc, credits::CreditsClient, producer::Producer}; -use reqwest::Url; use sea_orm::{prelude::*, JoinType, ModelTrait, QuerySelect, Set, TransactionTrait}; use serde::{Deserialize, Serialize}; -use solana_program::pubkey::Pubkey; +use super::collection::{validate_creators, validate_json, validate_solana_creator_verification}; use crate::{ blockchains::{polygon::Polygon, solana::Solana, DropEvent}, collection::Collection, @@ -77,6 +74,8 @@ impl Mutation { supply: Set(input.supply.map(TryFrom::try_from).transpose()?), creation_status: Set(CreationStatus::Pending), seller_fee_basis_points: Set(seller_fee_basis_points.try_into()?), + created_by: Set(user_id), + project_id: Set(input.project), ..Default::default() }; @@ -627,12 +626,18 @@ async fn submit_pending_deduction( blockchain, action, } = params; + let conn = db.get(); - let drop_model = drops::Entity::find_by_id(drop) - .one(db.get()) + let (drop_model, collection) = Drops::find() + .join(JoinType::InnerJoin, drops::Relation::Collections.def()) + .select_also(Collections) + .filter(drops::Column::Id.eq(drop)) + .one(conn) .await? .ok_or(Error::new("drop not found"))?; + let collection = collection.ok_or(Error::new("collection not found"))?; + if drop_model.credits_deduction_id.is_some() { return Ok(()); } @@ -651,8 +656,12 @@ async fn submit_pending_deduction( let deduction_id = id.ok_or(Error::new("Organization does not have enough credits"))?; let mut drop: drops::ActiveModel = drop_model.into(); + let mut collection: collections::ActiveModel = collection.into(); drop.credits_deduction_id = Set(Some(deduction_id.0)); - drop.update(db.get()).await?; + collection.credits_deduction_id = Set(Some(deduction_id.0)); + + collection.update(conn).await?; + drop.update(conn).await?; Ok(()) } @@ -739,134 +748,6 @@ fn validate_end_time(end_time: &Option) -> Result<()> { Ok(()) } -fn validate_solana_creator_verification( - project_treasury_wallet_address: &str, - creators: &Vec, -) -> Result<()> { - for creator in creators { - if creator.verified.unwrap_or_default() - && creator.address != project_treasury_wallet_address - { - return Err(Error::new(format!( - "Only the project treasury wallet of {project_treasury_wallet_address} can be verified in the mutation. Other creators must be verified independently. See the Metaplex documentation for more details." - ))); - } - } - - Ok(()) -} - -/// Validates the addresses of the creators for a given blockchain. -/// # Returns -/// - Ok(()) if all creator addresses are valid blockchain addresses. -/// -/// # Errors -/// - Err with an appropriate error message if any creator address is not a valid address. -/// - Err if the blockchain is not supported. -fn validate_creators(blockchain: BlockchainEnum, creators: &Vec) -> Result<()> { - let royalty_share = creators.iter().map(|c| c.share).sum::(); - - if royalty_share != 100 { - return Err(Error::new( - "The sum of all creator shares must be equal to 100", - )); - } - - match blockchain { - BlockchainEnum::Solana => { - if creators.len() > 5 { - return Err(Error::new( - "Maximum number of creators is 5 for Solana Blockchain", - )); - } - - for creator in creators { - validate_solana_address(&creator.address)?; - } - }, - BlockchainEnum::Polygon => { - if creators.len() != 1 { - return Err(Error::new( - "Only one creator is allowed for Polygon Blockchain", - )); - } - - let address = &creators[0].clone().address; - validate_evm_address(address)?; - }, - BlockchainEnum::Ethereum => return Err(Error::new("Blockchain not supported yet")), - } - - Ok(()) -} - -pub fn validate_solana_address(address: &str) -> Result<()> { - if Pubkey::from_str(address).is_err() { - return Err(Error::new(format!( - "{address} is not a valid Solana address" - ))); - } - - Ok(()) -} - -pub fn validate_evm_address(address: &str) -> Result<()> { - let err = Err(Error::new(format!("{address} is not a valid EVM address"))); - - // Ethereum address must start with '0x' - if !address.starts_with("0x") { - return err; - } - - // Ethereum address must be exactly 40 characters long after removing '0x' - if address.len() != 42 { - return err; - } - - // Check that the address contains only hexadecimal characters - if !address[2..].chars().all(|c| c.is_ascii_hexdigit()) { - return err; - } - - Ok(()) -} - -/// Validates the JSON metadata input for the NFT drop. -/// # Returns -/// - Ok(()) if all JSON fields are valid. -/// -/// # Errors -/// - Err with an appropriate error message if any JSON field is invalid. -fn validate_json(blockchain: BlockchainEnum, json: &MetadataJsonInput) -> Result<()> { - json.animation_url - .as_ref() - .map(|animation_url| Url::from_str(animation_url)) - .transpose() - .map_err(|_| Error::new("Invalid animation url"))?; - - json.external_url - .as_ref() - .map(|external_url| Url::from_str(external_url)) - .transpose() - .map_err(|_| Error::new("Invalid external url"))?; - - Url::from_str(&json.image).map_err(|_| Error::new("Invalid image url"))?; - - if blockchain != BlockchainEnum::Solana { - return Ok(()); - } - - if json.name.chars().count() > 32 { - return Err(Error::new("Name must be less than 32 characters")); - } - - if json.symbol.chars().count() > 10 { - return Err(Error::new("Symbol must be less than 10 characters")); - } - - Ok(()) -} - #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] pub struct RetryDropInput { pub drop: Uuid, diff --git a/api/src/mutations/mint.rs b/api/src/mutations/mint.rs index bdd2bb0..d81af5d 100644 --- a/api/src/mutations/mint.rs +++ b/api/src/mutations/mint.rs @@ -517,7 +517,6 @@ impl Mutation { let collection = collection.ok_or(Error::new("collection not found"))?; let recipient = collection_mint_model.owner.clone(); - let _edition = collection_mint_model.edition; let project_id = collection.project_id; let blockchain = collection.blockchain; diff --git a/api/src/mutations/transfer.rs b/api/src/mutations/transfer.rs index 81c82be..51d6f42 100644 --- a/api/src/mutations/transfer.rs +++ b/api/src/mutations/transfer.rs @@ -3,7 +3,7 @@ use hub_core::credits::CreditsClient; use sea_orm::{prelude::*, JoinType, QuerySelect, Set}; use serde::{Deserialize, Serialize}; -use super::drop::{validate_evm_address, validate_solana_address}; +use super::collection::{validate_evm_address, validate_solana_address}; use crate::{ blockchains::{polygon::Polygon, solana::Solana, TransferEvent}, db::Connection, diff --git a/api/src/objects/collection.rs b/api/src/objects/collection.rs index 39a1600..cdaeae3 100644 --- a/api/src/objects/collection.rs +++ b/api/src/objects/collection.rs @@ -35,6 +35,8 @@ pub struct Collection { pub seller_fee_basis_points: i16, pub project_id: Uuid, pub credits_deduction_id: Option, + pub created_at: DateTimeWithTimeZone, + pub created_by: Uuid, } #[Object] @@ -62,6 +64,16 @@ impl Collection { self.project_id } + /// The date and time in UTC when the collection was created. + async fn created_at(&self) -> DateTimeWithTimeZone { + self.created_at + } + + /// The user id of the person who created the collection. + async fn created_by_id(&self) -> Uuid { + self.created_by + } + async fn credits_deduction_id(&self) -> Option { self.credits_deduction_id } @@ -162,6 +174,8 @@ impl From for Collection { address, project_id, credits_deduction_id, + created_at, + created_by, }: Model, ) -> Self { Self { @@ -175,6 +189,8 @@ impl From for Collection { seller_fee_basis_points, project_id, credits_deduction_id, + created_at, + created_by, } } } diff --git a/migration/src/lib.rs b/migration/src/lib.rs index e940736..a808128 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -47,6 +47,7 @@ mod m20230706_134402_drop_column_credits_deduction_id_from_nft_transfers; mod m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections; mod m20230713_151414_create_mint_creators_table; mod m20230713_163043_add_column_compressed_to_collection_mints; +mod m20230718_111347_add_created_at_and_created_by_columns_to_collections; pub struct Migrator; @@ -101,6 +102,7 @@ impl MigratorTrait for Migrator { Box::new(m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections::Migration), Box::new(m20230713_151414_create_mint_creators_table::Migration), Box::new(m20230713_163043_add_column_compressed_to_collection_mints::Migration), + Box::new(m20230718_111347_add_created_at_and_created_by_columns_to_collections::Migration), ] } } diff --git a/migration/src/m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections.rs b/migration/src/m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections.rs index 7c5daf2..6801923 100644 --- a/migration/src/m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections.rs +++ b/migration/src/m20230706_142939_add_columns_project_id_and_credits_deduction_id_to_collections.rs @@ -45,7 +45,7 @@ impl MigrationTrait for Migration { let stmt = Statement::from_string( manager.get_database_backend(), - r#"UPDATE collections SET credits_deduction_id = drops.credits_deduction_id, project_id = drops.project_id FROM collections c INNER JOIN drops ON c.id = drops.collection_id;"#.to_string(), + r#"UPDATE collections SET credits_deduction_id = drops.credits_deduction_id, project_id = drops.project_id FROM drops WHERE drops.collection_id = collections.id;"#.to_string(), ); db.execute(stmt).await?; diff --git a/migration/src/m20230718_111347_add_created_at_and_created_by_columns_to_collections.rs b/migration/src/m20230718_111347_add_created_at_and_created_by_columns_to_collections.rs new file mode 100644 index 0000000..5259069 --- /dev/null +++ b/migration/src/m20230718_111347_add_created_at_and_created_by_columns_to_collections.rs @@ -0,0 +1,63 @@ +use sea_orm::{ConnectionTrait, Statement}; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Collections::Table) + .add_column_if_not_exists(ColumnDef::new(Collections::CreatedBy).uuid()) + .add_column_if_not_exists( + ColumnDef::new(Collections::CreatedAt) + .timestamp_with_time_zone() + .extra("default now()".to_string()), + ) + .to_owned(), + ) + .await?; + + let db = manager.get_connection(); + + let stmt = Statement::from_string( + manager.get_database_backend(), + r#"UPDATE collections SET created_by = drops.created_by, created_at = drops.created_at FROM drops WHERE drops.collection_id = collections.id;"#.to_string(), + ); + + db.execute(stmt).await?; + + manager + .alter_table( + Table::alter() + .table(Collections::Table) + .modify_column(ColumnDef::new(Collections::CreatedBy).not_null()) + .modify_column(ColumnDef::new(Collections::CreatedAt).not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(Collections::Table) + .drop_column(Collections::CreatedBy) + .drop_column(Collections::CreatedAt) + .to_owned(), + ) + .await + } +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum Collections { + Table, + CreatedBy, + CreatedAt, +}