diff --git a/api/proto.lock b/api/proto.lock index 489bf67..b8d9326 100644 --- a/api/proto.lock +++ b/api/proto.lock @@ -5,8 +5,8 @@ sha512 = "d75800df0d4744c6b0f4d9a9952d3bfd0bb6b24a8babd19104cc11b54a525f85551b3c [[schemas]] subject = "nfts" -version = 21 -sha512 = "7859cdec771803b1f115375833215b89fafd553cc769a824376032f2d44494b8aee5d82f868dab8189bfd10fb09b68668ed74de916294aacf721448184d381f1" +version = 22 +sha512 = "c9920f6a5792b067396c88e40b9bd2adfcb55b582734aff924a67a9d5841a5e2839fc734c1bbff66f402f9a9d8852ca5fef1339aaaa3d5b05aa7868ddfa375c1" [[schemas]] subject = "organization" @@ -20,8 +20,8 @@ sha512 = "c5ddf43d2958ec690ee2261d0ff9808b67ce810d2fc4b6077f96f561929a920f03509f [[schemas]] subject = "solana_nfts" -version = 6 -sha512 = "106f013837b6efc1449778e7beb825d501214af42bd60153c8f09dfd25e3084b46782093ab6f1e5f194b99d9a5cb5eb9b72fec2ba953ae92c1e8ce9724264b40" +version = 7 +sha512 = "73570b9e58f91a06901ba6455986ce1a0d3675e33860d2447160d711a8cebcfb78cfc714fb08644ad83495dc8612b0b123203561af6d93d29ffb0256725047ba" [[schemas]] subject = "timestamp" diff --git a/api/proto.toml b/api/proto.toml index aac0c27..ee4b436 100644 --- a/api/proto.toml +++ b/api/proto.toml @@ -3,9 +3,9 @@ endpoint = "https://schemas.holaplex.tools" [schemas] organization = 5 -nfts = 21 +nfts = 22 customer = 2 treasury = 17 -solana_nfts = 6 +solana_nfts = 7 polygon_nfts = 6 timestamp = 1 diff --git a/api/src/events.rs b/api/src/events.rs index 0dfe667..0d374dd 100644 --- a/api/src/events.rs +++ b/api/src/events.rs @@ -13,7 +13,9 @@ use sea_orm::{ use crate::{ db::Connection, entities::{ - collection_mints, collections, customer_wallets, drops, nft_transfers, + collection_creators, collection_mints, collections, customer_wallets, drops, + metadata_json_attributes, metadata_json_files, metadata_jsons, mint_creators, + nft_transfers, prelude::{CollectionMints, Purchases}, project_wallets, purchases, sea_orm_active_enums::{Blockchain, CreationStatus}, @@ -27,10 +29,10 @@ use crate::{ Blockchain as ProtoBlockchainEnum, CustomerWallet, Event as TreasuryEvent, PolygonTransactionResult, ProjectWallet, TransactionStatus, }, - CreationStatus as NftCreationStatus, DropCreation, MintCollectionCreation, MintCreation, - MintOwnershipUpdate, MintedTokensOwnershipUpdate, NftEventKey, NftEvents, - SolanaCompletedMintTransaction, SolanaCompletedTransferTransaction, SolanaNftEventKey, - TreasuryEventKey, + Attribute, CreationStatus as NftCreationStatus, DropCreation, File, Metadata, + MintCollectionCreation, MintCreation, MintOwnershipUpdate, MintedTokensOwnershipUpdate, + NftEventKey, NftEvents, SolanaCollectionPayload, SolanaCompletedMintTransaction, + SolanaCompletedTransferTransaction, SolanaMintPayload, SolanaNftEventKey, TreasuryEventKey, }, Actions, Services, }; @@ -96,7 +98,14 @@ impl Processor { }, None | Some(_) => Ok(()), }, - Services::Solana(SolanaNftEventKey { id, .. }, e) => match e.event { + Services::Solana( + SolanaNftEventKey { + id, + project_id, + user_id, + }, + e, + ) => match e.event { Some( SolanaNftsEvent::CreateDropSubmitted(payload) | SolanaNftsEvent::RetryCreateDropSubmitted(payload), @@ -153,6 +162,12 @@ impl Processor { self.drop_minted(id, MintResult::Failure).await }, Some(SolanaNftsEvent::UpdateMintOwner(e)) => self.update_mint_owner(id, e).await, + Some(SolanaNftsEvent::ImportedExternalCollection(e)) => { + self.index_collection(id, project_id, user_id, e).await + }, + Some(SolanaNftsEvent::ImportedExternalMint(e)) => { + self.index_mint(id, user_id, e).await + }, None | Some(_) => Ok(()), }, Services::Polygon(_, e) => match e.event { @@ -164,6 +179,154 @@ impl Processor { } } + async fn index_collection( + &self, + id: String, + project_id: String, + created_by: String, + payload: SolanaCollectionPayload, + ) -> Result<()> { + let SolanaCollectionPayload { + supply, + mint_address, + seller_fee_basis_points, + creators, + metadata, + files, + .. + } = payload; + + let metadata = metadata.context("no collection metadata found")?; + + let Metadata { + name, + description, + symbol, + attributes, + uri, + image, + } = metadata; + + let collection_am = collections::ActiveModel { + id: Set(id.parse()?), + blockchain: Set(Blockchain::Solana), + supply: Set(supply.map(Into::into)), + project_id: Set(project_id.parse()?), + credits_deduction_id: Set(None), + creation_status: Set(CreationStatus::Created), + total_mints: Set(-1), + address: Set(Some(mint_address)), + signature: Set(None), + seller_fee_basis_points: Set(seller_fee_basis_points.try_into()?), + created_by: Set(created_by.parse()?), + created_at: Set(Utc::now().into()), + }; + + collection_am.insert(self.db.get()).await?; + + let metadata_json = metadata_jsons::ActiveModel { + id: Set(id.parse()?), + name: Set(name), + uri: Set(uri), + symbol: Set(symbol), + description: Set(description.unwrap_or_default()), + image: Set(image), + animation_url: Set(None), + external_url: Set(None), + ..Default::default() + }; + + let json_model = metadata_json.insert(self.db.get()).await?; + for creator in creators { + let collection_creator = collection_creators::ActiveModel { + collection_id: Set(id.parse()?), + address: Set(creator.address), + verified: Set(creator.verified), + share: Set(creator.share.try_into()?), + }; + collection_creator.insert(self.db.get()).await?; + } + index_attributes(&self.db, json_model.id, attributes).await?; + index_files(&self.db, json_model.id, files).await?; + + Ok(()) + } + + async fn index_mint( + &self, + id: String, + created_by: String, + payload: SolanaMintPayload, + ) -> Result<()> { + let SolanaMintPayload { + collection_id, + mint_address, + owner, + seller_fee_basis_points, + compressed, + creators, + files, + metadata, + .. + } = payload; + + let metadata = metadata.context("no collection metadata found")?; + + let Metadata { + name, + description, + symbol, + attributes, + uri, + image, + } = metadata; + + let mint_am = collection_mints::ActiveModel { + id: Set(id.parse()?), + collection_id: Set(collection_id.parse()?), + address: Set(Some(mint_address)), + owner: Set(owner), + creation_status: Set(CreationStatus::Created), + created_by: Set(created_by.parse()?), + created_at: Set(Utc::now().into()), + signature: Set(None), + edition: Set(-1), + seller_fee_basis_points: Set(seller_fee_basis_points.try_into()?), + credits_deduction_id: Set(None), + compressed: Set(compressed), + }; + + let mint_model = mint_am.insert(self.db.get()).await?; + + let metadata_json = metadata_jsons::ActiveModel { + id: Set(id.parse()?), + name: Set(name), + uri: Set(uri), + symbol: Set(symbol), + description: Set(description.unwrap_or_default()), + image: Set(image), + animation_url: Set(None), + external_url: Set(None), + ..Default::default() + }; + + let json_model = metadata_json.insert(self.db.get()).await?; + + for creator in creators { + let mint_creator_am = mint_creators::ActiveModel { + collection_mint_id: Set(mint_model.id), + address: Set(creator.address), + verified: Set(creator.verified), + share: Set(creator.share.try_into()?), + }; + mint_creator_am.insert(self.db.get()).await?; + } + index_attributes(&self.db, json_model.id, attributes).await?; + index_files(&self.db, json_model.id, files).await?; + + Ok(()) + } + async fn update_mint_owner(&self, id: String, payload: MintOwnershipUpdate) -> Result<()> { let id = Uuid::from_str(&id)?; let db = self.db.get(); @@ -607,3 +770,37 @@ impl From for TransferResult { } } } + +async fn index_attributes( + db: &Connection, + json_id: Uuid, + attributes: Vec, +) -> Result<()> { + for attr in attributes { + let attribute = metadata_json_attributes::ActiveModel { + metadata_json_id: Set(json_id), + trait_type: Set(attr.trait_type), + value: Set(attr.value), + ..Default::default() + }; + + attribute.insert(db.get()).await?; + } + + Ok(()) +} + +async fn index_files(db: &Connection, json_id: Uuid, files: Vec) -> Result<()> { + for file in files { + let file_am = metadata_json_files::ActiveModel { + metadata_json_id: Set(json_id), + uri: Set(Some(file.uri)), + file_type: Set(file.mime), + ..Default::default() + }; + + file_am.insert(db.get()).await?; + } + + Ok(()) +} diff --git a/api/src/mutations/collection.rs b/api/src/mutations/collection.rs index 0323a3a..462d21b 100644 --- a/api/src/mutations/collection.rs +++ b/api/src/mutations/collection.rs @@ -12,17 +12,17 @@ use crate::{ collection::Collection, db::Connection, entities::{ - collection_creators, collections, metadata_jsons, - prelude::{CollectionCreators, Collections, MetadataJsons}, + collection_creators, collection_mints, collections, metadata_jsons, + prelude::{CollectionCreators, CollectionMints, Collections, MetadataJsons}, project_wallets, sea_orm_active_enums::{Blockchain as BlockchainEnum, CreationStatus}, }, metadata_json::MetadataJson, objects::{Collection as CollectionObject, Creator, MetadataJsonInput}, proto::{ - nft_events::Event as NftEvent, CollectionCreation, CreationStatus as NftCreationStatus, - Creator as ProtoCreator, MasterEdition, MetaplexMasterEditionTransaction, NftEventKey, - NftEvents, + nft_events::Event as NftEvent, CollectionCreation, CollectionImport, + CreationStatus as NftCreationStatus, Creator as ProtoCreator, MasterEdition, + MetaplexMasterEditionTransaction, NftEventKey, NftEvents, }, Actions, AppContext, NftStorageClient, OrganizationId, UserID, }; @@ -255,6 +255,74 @@ impl Mutation { }) } + pub async fn import_solana_collection( + &self, + ctx: &Context<'_>, + input: ImportCollectionInput, + ) -> Result { + let nfts_producer = ctx.data::>()?; + let AppContext { db, user_id, .. } = ctx.data::()?; + let user_id = user_id.0.ok_or(Error::new("X-USER-ID header not found"))?; + + let txn = db.get().begin().await?; + + validate_solana_address(&input.collection)?; + + let collection = Collections::find() + .filter(collections::Column::Address.eq(input.collection.clone())) + .one(db.get()) + .await?; + + if let Some(collection) = collection { + let mints = CollectionMints::find() + .filter(collection_mints::Column::CollectionId.eq(collection.id)) + .all(&txn) + .await?; + + if let Some(collection_json) = + MetadataJsons::find_by_id(collection.id).one(&txn).await? + { + collection_json.delete(&txn).await?; + } + + let mint_ids = mints.iter().map(|m| m.id).collect::>(); + + let mint_jsons = MetadataJsons::find() + .filter(metadata_jsons::Column::Id.is_in(mint_ids)) + .all(&txn) + .await?; + + for json in mint_jsons { + json.delete(&txn).await?; + } + + collection.delete(&txn).await?; + + txn.commit().await?; + } + + nfts_producer + .send( + Some(&NftEvents { + event: Some(NftEvent::StartedImportingSolanaCollection( + CollectionImport { + mint_address: input.collection, + }, + )), + }), + Some(&NftEventKey { + id: String::new(), + project_id: input.project.to_string(), + user_id: user_id.to_string(), + }), + ) + .await?; + + Ok(ImportCollectionPayload { + status: CreationStatus::Pending, + }) + } + /// 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. @@ -656,3 +724,15 @@ pub struct PatchCollectionPayload { /// The drop that has been patched. collection: CollectionObject, } + +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct ImportCollectionInput { + project: Uuid, + // Mint address of Metaplex Certified Collection NFT + collection: String, +} + +#[derive(Debug, Clone, SimpleObject)] +pub struct ImportCollectionPayload { + status: CreationStatus, +}