diff --git a/api/proto.lock b/api/proto.lock index 2fbdf20..22051c0 100644 --- a/api/proto.lock +++ b/api/proto.lock @@ -5,8 +5,8 @@ sha512 = "d75800df0d4744c6b0f4d9a9952d3bfd0bb6b24a8babd19104cc11b54a525f85551b3c [[schemas]] subject = "nfts" -version = 30 -sha512 = "bee70bd6f0f18a8049f93bceb9c4b500b49352f9d19d55d5a411da92cbd786c88bec47f73e1ef6946ceefc7de8e558f704bf8187be9d9f4e49bd102baec29327" +version = 31 +sha512 = "449574f8551ab8c17824af9e08b1658ad1b26ac80340230ddf02e7a1e0979d8a47025913a6598799cf83dd1a9cda87697ee87a13f404ebb52c95ea0084205767" [[schemas]] subject = "organization" @@ -20,8 +20,8 @@ sha512 = "c5ddf43d2958ec690ee2261d0ff9808b67ce810d2fc4b6077f96f561929a920f03509f [[schemas]] subject = "solana_nfts" -version = 11 -sha512 = "967fefde938a0f6ce05194e4fca15673e681caac54d8aeec114c5d38418632b9696dbaf5362345a15114e5abb49de55d0af8b9edcc0f2c91f9ef1ccc4ff55d68" +version = 12 +sha512 = "4f85496c50a82cb40faa097cf6d0cb23275b3b90cb561d01388f3e5a71282a8b8e1eea617b7d712b0e415d65af483209fac2db1591456fa814a1f41a1c457433" [[schemas]] subject = "timestamp" diff --git a/api/proto.toml b/api/proto.toml index 44a45d7..5bb1a54 100644 --- a/api/proto.toml +++ b/api/proto.toml @@ -3,9 +3,9 @@ endpoint = "https://schemas.holaplex.tools" [schemas] organization = 5 -nfts = 30 +nfts = 31 customer = 2 treasury = 23 -solana_nfts = 11 +solana_nfts = 12 polygon_nfts = 6 timestamp = 1 \ No newline at end of file diff --git a/api/src/mutations/mint.rs b/api/src/mutations/mint.rs index 7af2012..da3de74 100644 --- a/api/src/mutations/mint.rs +++ b/api/src/mutations/mint.rs @@ -39,7 +39,8 @@ use crate::{ objects::{CollectionMint, Creator, MetadataJsonInput}, proto::{ self, nft_events::Event as NftEvent, CreationStatus as NftCreationStatus, MetaplexMetadata, - MintCollectionCreation, MintCreation, NftEventKey, NftEvents, RetryUpdateSolanaMintPayload, + MintCollectionCreation, MintCreation, MintOpenDropTransaction, NftEventKey, NftEvents, + RetryUpdateSolanaMintPayload, SolanaMintOpenDropBatchedPayload, }, Actions, AppContext, OrganizationId, UserID, }; @@ -1291,6 +1292,194 @@ impl Mutation { collection_mint: mint.into(), }) } + + async fn mint_random_queued_to_drop_batched( + &self, + ctx: &Context<'_>, + input: MintRandomQueuedBatchedInput, + ) -> Result { + let AppContext { + db, + user_id, + organization_id, + balance, + .. + } = ctx.data::()?; + let credits = ctx.data::>()?; + let conn = db.get(); + let nfts_producer = 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 batch_size = input.recipients.len(); + + if batch_size == 0 { + return Err(Error::new("No recipients provided")); + } + + if batch_size > 250 { + return Err(Error::new("Batch size cannot be greater than 250")); + } + + let drop = drops::Entity::find_by_id(input.drop) + .one(conn) + .await? + .ok_or(Error::new("drop not found"))?; + + let result = CollectionMints::find() + .select_also(metadata_jsons::Entity) + .join( + JoinType::InnerJoin, + metadata_jsons::Entity::belongs_to(CollectionMints) + .from(metadata_jsons::Column::Id) + .to(collection_mints::Column::Id) + .into(), + ) + .filter(collection_mints::Column::CollectionId.eq(drop.collection_id)) + .filter(collection_mints::Column::CreationStatus.eq(CreationStatus::Queued)) + .order_by(SimpleExpr::FunctionCall(Func::random()), Order::Asc) + .limit(Some(batch_size.try_into()?)) + .all(conn) + .await?; + + let (mints, _): (Vec<_>, Vec<_>) = result.iter().cloned().unzip(); + + let creators = mints.load_many(mint_creators::Entity, conn).await?; + + if mints.len() != batch_size { + return Err(Error::new("Not enough mints found for the drop")); + } + + let collection = collections::Entity::find_by_id(drop.collection_id) + .one(conn) + .await? + .ok_or(Error::new("collection not found"))?; + + let project_id = collection.project_id; + let blockchain = collection.blockchain; + + if blockchain != BlockchainEnum::Solana { + return Err(Error::new("Only Solana is supported at this time")); + } + + let owner_address = fetch_owner(conn, project_id, blockchain).await?; + + let action = if input.compressed { + Actions::MintCompressed + } else { + Actions::Mint + }; + + let event_key = NftEventKey { + id: collection.id.to_string(), + user_id: user_id.to_string(), + project_id: project_id.to_string(), + }; + + let mut transactions = Vec::new(); + + for (((mint, metadata_json), creators), recipient) in result + .into_iter() + .zip(creators.into_iter()) + .zip(input.recipients.into_iter()) + { + let metadata_json = metadata_json.ok_or(Error::new("No metadata json found"))?; + let metadata_uri = metadata_json + .uri + .ok_or(Error::new("No metadata json uri found"))?; + + let TransactionId(deduction_id) = credits + .submit_pending_deduction( + org_id, + user_id, + action, + collection.blockchain.into(), + balance, + ) + .await?; + + let tx = conn.begin().await?; + + let mut mint_am: collection_mints::ActiveModel = mint.into(); + + mint_am.creation_status = Set(CreationStatus::Pending); + mint_am.credits_deduction_id = Set(Some(deduction_id)); + mint_am.compressed = Set(Some(input.compressed)); + mint_am.owner = Set(Some(recipient.clone())); + mint_am.seller_fee_basis_points = Set(collection.seller_fee_basis_points); + + let mint = mint_am.update(&tx).await?; + + let mint_history_am = mint_histories::ActiveModel { + mint_id: Set(mint.id), + wallet: Set(recipient.clone()), + collection_id: Set(collection.id), + tx_signature: Set(None), + status: Set(CreationStatus::Pending), + created_at: Set(Utc::now().into()), + ..Default::default() + }; + + mint_history_am.insert(&tx).await?; + + tx.commit().await?; + + nfts_producer + .send( + Some(&NftEvents { + event: Some(NftEvent::DropMinted(MintCreation { + drop_id: drop.id.to_string(), + status: NftCreationStatus::InProgress as i32, + })), + }), + Some(&NftEventKey { + id: mint.id.to_string(), + project_id: drop.project_id.to_string(), + user_id: user_id.to_string(), + }), + ) + .await?; + + transactions.push(MintOpenDropTransaction { + recipient_address: recipient, + metadata: Some(MetaplexMetadata { + owner_address: owner_address.clone(), + name: metadata_json.name, + symbol: metadata_json.symbol, + metadata_uri, + seller_fee_basis_points: mint.seller_fee_basis_points.into(), + creators: creators.into_iter().map(Into::into).collect(), + }), + mint_id: mint.id.to_string(), + }); + } + + nfts_producer + .send( + Some(&NftEvents { + event: Some(NftEvent::SolanaMintOpenDropBatched( + SolanaMintOpenDropBatchedPayload { + collection_id: collection.id.to_string(), + compressed: input.compressed, + mint_open_drop_transactions: transactions, + }, + )), + }), + Some(&event_key), + ) + .await?; + + Ok(MintRandomQueuedBatchedPayload { + collection_mints: mints.into_iter().map(Into::into).collect(), + }) + } } fn validate_compress(blockchain: BlockchainEnum, compressed: bool) -> Result<(), Error> { @@ -1475,3 +1664,17 @@ pub struct MintRandomQueuedInput { recipient: String, compressed: bool, } + +/// Represents input data for `mint_random_queued_batched` mutation +#[derive(Debug, Clone, InputObject)] +pub struct MintRandomQueuedBatchedInput { + drop: Uuid, + recipients: Vec, + compressed: bool, +} + +/// Represents payload data for `mint_random_queued_batched` mutation +#[derive(Debug, Clone, SimpleObject)] +pub struct MintRandomQueuedBatchedPayload { + collection_mints: Vec, +}