diff --git a/Cargo.lock b/Cargo.lock index e0d30e0..0bb1712 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2260,6 +2260,7 @@ dependencies = [ "holaplex-hub-core", "holaplex-hub-core-build", "holaplex-hub-nfts-solana-entity", + "mpl-bubblegum", "mpl-token-metadata", "prost", "sea-orm 0.11.3", diff --git a/consumer/src/backend.rs b/consumer/src/backend.rs index 16fe60b..8fcd149 100644 --- a/consumer/src/backend.rs +++ b/consumer/src/backend.rs @@ -1,9 +1,7 @@ -use holaplex_hub_nfts_solana_entity::{collection_mints, collections}; - use holaplex_hub_nfts_solana_core::proto::{ - MetaplexMasterEditionTransaction, MintMetaplexEditionTransaction, SolanaPendingTransaction, - TransferMetaplexAssetTransaction, + MetaplexMasterEditionTransaction, SolanaPendingTransaction, TransferMetaplexAssetTransaction, }; +use holaplex_hub_nfts_solana_entity::{collection_mints, collections}; use hub_core::prelude::*; use solana_program::pubkey::Pubkey; @@ -27,6 +25,12 @@ pub struct MintEditionAddresses { pub recipient: Pubkey, } +#[derive(Clone)] +pub struct MintCompressedMintV1Addresses { + pub owner: Pubkey, + pub recipient: Pubkey, +} + #[derive(Clone)] pub struct UpdateMasterEditionAddresses { pub metadata: Pubkey, @@ -69,44 +73,27 @@ impl From> for SolanaPendingTransaction { } } -// TODO: include this in collections::Model -pub enum CollectionType { - Legacy, - Verified, -} - -// Legacy, Verified -#[async_trait] pub trait CollectionBackend { fn create( &self, txn: MetaplexMasterEditionTransaction, ) -> Result>; -} - -// Uncompressed, Compressed -#[async_trait] -pub trait MintBackend { - fn mint( - &self, - collection_ty: CollectionType, - collection: &collections::Model, - txn: MintMetaplexEditionTransaction, - ) -> Result>; - // TODO: probably better to replace all errors here with an Error enum - fn try_update( + fn update( &self, - collection_ty: CollectionType, collection: &collections::Model, txn: MetaplexMasterEditionTransaction, - ) -> Result>>; + ) -> Result>; +} - // Right now only this one needs to be async to support hitting the asset - // API for transfer data +pub trait MintBackend { + fn mint(&self, collection: &collections::Model, txn: T) -> Result>; +} + +#[async_trait] +pub trait TransferBackend { async fn transfer( &self, - collection_ty: CollectionType, collection_mint: &collection_mints::Model, txn: TransferMetaplexAssetTransaction, ) -> Result>; diff --git a/consumer/src/events.rs b/consumer/src/events.rs index fa4632c..8510059 100644 --- a/consumer/src/events.rs +++ b/consumer/src/events.rs @@ -25,8 +25,11 @@ use hub_core::{ }; use crate::{ - backend::{self, MasterEditionAddresses}, - solana::{Solana, UncompressedRef}, + backend::{ + CollectionBackend, MasterEditionAddresses, MintBackend, MintEditionAddresses, + TransferBackend, + }, + solana::{EditionRef, Solana, UncompressedRef}, }; #[derive(Debug, thiserror::Error)] @@ -93,6 +96,9 @@ pub enum EventKind { TransferAsset, RetryCreateDrop, RetryMintDrop, + CreateCollection, + RetryCreateCollection, + UpdateCollection, } impl EventKind { @@ -104,6 +110,9 @@ impl EventKind { Self::TransferAsset => "drop asset transfer", Self::RetryCreateDrop => "drop creation retry", Self::RetryMintDrop => "drop mint retry", + Self::CreateCollection => "collection creation", + Self::RetryCreateCollection => "collection creation retry", + Self::UpdateCollection => "collection update", } } @@ -115,6 +124,11 @@ impl EventKind { EventKind::TransferAsset => SolanaNftEvent::TransferAssetSigningRequested(tx), EventKind::RetryCreateDrop => SolanaNftEvent::RetryCreateDropSigningRequested(tx), EventKind::RetryMintDrop => SolanaNftEvent::RetryMintDropSigningRequested(tx), + EventKind::CreateCollection => SolanaNftEvent::CreateCollectionSigningRequested(tx), + EventKind::UpdateCollection => SolanaNftEvent::UpdateCollectionSigningRequested(tx), + EventKind::RetryCreateCollection => { + SolanaNftEvent::RetryCreateCollectionSigningRequested(tx) + }, } } @@ -138,6 +152,35 @@ impl EventKind { address: collection.mint, }) }, + Self::CreateCollection => { + let id = id()?; + + let collection = Collection::find_by_id(db, id) + .await? + .ok_or(ProcessorErrorKind::RecordNotFound)?; + + SolanaNftEvent::CreateCollectionSubmitted(SolanaCompletedMintTransaction { + signature, + address: collection.mint, + }) + }, + Self::RetryCreateCollection => { + let id = id()?; + + let collection = Collection::find_by_id(db, id) + .await? + .ok_or(ProcessorErrorKind::RecordNotFound)?; + + SolanaNftEvent::RetryCreateCollectionSubmitted(SolanaCompletedMintTransaction { + signature, + address: collection.mint, + }) + }, + Self::UpdateCollection => { + SolanaNftEvent::UpdateCollectionSubmitted(SolanaCompletedUpdateTransaction { + signature, + }) + }, Self::MintDrop => { let id = id()?; let collection_mint = CollectionMint::find_by_id(db, id) @@ -190,6 +233,9 @@ impl EventKind { Self::TransferAsset => SolanaNftEvent::TransferAssetFailed(tx), Self::RetryCreateDrop => SolanaNftEvent::RetryCreateDropFailed(tx), Self::RetryMintDrop => SolanaNftEvent::RetryMintDropFailed(tx), + Self::CreateCollection => SolanaNftEvent::CreateCollectionFailed(tx), + Self::RetryCreateCollection => SolanaNftEvent::RetryCreateCollectionFailed(tx), + Self::UpdateCollection => SolanaNftEvent::UpdateCollectionFailed(tx), } } } @@ -227,7 +273,15 @@ impl Processor { self.process_nft( EventKind::CreateDrop, &key, - self.create_drop(&UncompressedRef(self.solana()), &key, payload), + self.create_collection(&UncompressedRef(self.solana()), &key, payload), + ) + .await + }, + Some(NftEvent::SolanaCreateCollection(payload)) => { + self.process_nft( + EventKind::CreateCollection, + &key, + self.create_collection(&UncompressedRef(self.solana()), &key, payload), ) .await }, @@ -235,7 +289,7 @@ impl Processor { self.process_nft( EventKind::MintDrop, &key, - self.mint_drop(&UncompressedRef(self.solana()), &key, payload), + self.mint_drop(&EditionRef(self.solana()), &key, payload), ) .await }, @@ -243,7 +297,15 @@ impl Processor { self.process_nft( EventKind::UpdateDrop, &key, - self.update_drop(&UncompressedRef(self.solana()), &key, payload), + self.update_collection(&UncompressedRef(self.solana()), &key, payload), + ) + .await + }, + Some(NftEvent::SolanaUpdateCollection(payload)) => { + self.process_nft( + EventKind::UpdateCollection, + &key, + self.update_collection(&UncompressedRef(self.solana()), &key, payload), ) .await }, @@ -259,7 +321,23 @@ impl Processor { self.process_nft( EventKind::RetryCreateDrop, &key, - self.retry_create_drop(&UncompressedRef(self.solana()), &key, payload), + self.retry_create_collection( + &UncompressedRef(self.solana()), + &key, + payload, + ), + ) + .await + }, + Some(NftEvent::SolanaRetryCreateCollection(payload)) => { + self.process_nft( + EventKind::RetryCreateCollection, + &key, + self.retry_create_collection( + &UncompressedRef(self.solana()), + &key, + payload, + ), ) .await }, @@ -267,7 +345,7 @@ impl Processor { self.process_nft( EventKind::RetryMintDrop, &key, - self.retry_mint_drop(&UncompressedRef(self.solana()), &key, payload), + self.retry_mint_drop(&EditionRef(self.solana()), &key, payload), ) .await }, @@ -357,7 +435,7 @@ impl Processor { match self.solana().submit_transaction(&res) { Ok(sig) => self - .event_submitted(kind, key, sig) + .event_submitted(kind, &key, sig) .await .map_err(|k| ProcessorError::new(k, kind, ErrorSource::TreasurySuccess)), Err(e) => { @@ -375,15 +453,15 @@ impl Processor { async fn event_submitted( &self, kind: EventKind, - key: SolanaNftEventKey, + key: &SolanaNftEventKey, sig: String, ) -> ProcessResult<()> { self.producer .send( Some(&SolanaNftEvents { - event: Some(kind.into_success(&self.db, &key, sig).await?), + event: Some(kind.into_success(&self.db, key, sig).await?), }), - Some(&key), + Some(key), ) .await .map_err(Into::into) @@ -408,7 +486,7 @@ impl Processor { .map_err(Into::into) } - async fn create_drop( + async fn create_collection( &self, backend: &B, key: &SolanaNftEventKey, @@ -444,7 +522,7 @@ impl Processor { Ok(tx.into()) } - async fn mint_drop( + async fn mint_drop>( &self, backend: &B, key: &SolanaNftEventKey, @@ -456,10 +534,8 @@ impl Processor { .await? .ok_or(ProcessorErrorKind::RecordNotFound)?; - // TODO: the collection mint record may fail to be created if this fails. Need to handle upserting the record in retry mint. - let collection_ty = todo!("determine collection type"); let tx = backend - .mint(collection_ty, &collection, payload) + .mint(&collection, payload) .map_err(ProcessorErrorKind::Solana)?; let collection_mint = collection_mints::Model { @@ -476,7 +552,7 @@ impl Processor { Ok(tx.into()) } - async fn update_drop( + async fn update_collection( &self, backend: &B, key: &SolanaNftEventKey, @@ -487,19 +563,17 @@ impl Processor { .await? .ok_or(ProcessorErrorKind::RecordNotFound)?; - let collection_ty = todo!("determine collection type"); let tx = backend - .try_update(collection_ty, &collection, payload) + .update(&collection, payload) .map_err(ProcessorErrorKind::Solana)?; - let Some(tx) = tx else { todo!("handle un-updateable assets") }; Ok(tx.into()) } - async fn transfer_asset( + async fn transfer_asset( &self, backend: &B, - key: &SolanaNftEventKey, + _key: &SolanaNftEventKey, payload: TransferMetaplexAssetTransaction, ) -> ProcessResult { let collection_mint_id = Uuid::parse_str(&payload.collection_mint_id.clone())?; @@ -507,16 +581,15 @@ impl Processor { .await? .ok_or(ProcessorErrorKind::RecordNotFound)?; - let collection_ty = todo!("determine collection type"); let tx = backend - .transfer(collection_ty, &collection_mint, payload) + .transfer(&collection_mint, payload) .await .map_err(ProcessorErrorKind::Solana)?; Ok(tx.into()) } - async fn retry_create_drop( + async fn retry_create_collection( &self, backend: &B, key: &SolanaNftEventKey, @@ -542,7 +615,7 @@ impl Processor { let mut collection: collections::ActiveModel = collection.into(); - collection.master_edition = Set(metadata.to_string()); + collection.metadata = Set(metadata.to_string()); collection.associated_token_account = Set(associated_token_account.to_string()); collection.mint = Set(mint.to_string()); collection.master_edition = Set(master_edition.to_string()); @@ -554,7 +627,9 @@ impl Processor { Ok(tx.into()) } - async fn retry_mint_drop( + async fn retry_mint_drop< + B: MintBackend, + >( &self, backend: &B, key: &SolanaNftEventKey, @@ -569,17 +644,22 @@ impl Processor { let collection = collection.ok_or(ProcessorErrorKind::RecordNotFound)?; - let collection_ty = todo!("determine collection type"); let tx = backend - .mint(collection_ty, &collection, payload) + .mint(&collection, payload) .map_err(ProcessorErrorKind::Solana)?; + let MintEditionAddresses { + mint, + recipient, + associated_token_account, + .. + } = tx.addresses; + let mut collection_mint: collection_mints::ActiveModel = collection_mint.into(); - collection_mint.mint = Set(tx.addresses.mint.to_string()); - collection_mint.owner = Set(tx.addresses.recipient.to_string()); - collection_mint.associated_token_account = - Set(Some(tx.addresses.associated_token_account.to_string())); + collection_mint.mint = Set(mint.to_string()); + collection_mint.owner = Set(recipient.to_string()); + collection_mint.associated_token_account = Set(Some(associated_token_account.to_string())); CollectionMint::update(&self.db, collection_mint).await?; diff --git a/consumer/src/solana.rs b/consumer/src/solana.rs index 31409f8..1f3f70f 100644 --- a/consumer/src/solana.rs +++ b/consumer/src/solana.rs @@ -1,10 +1,14 @@ use anchor_lang::{prelude::AccountMeta, InstructionData}; use holaplex_hub_nfts_solana_core::proto::{ treasury_events::SolanaTransactionResult, MasterEdition, MetaplexMasterEditionTransaction, - MintMetaplexEditionTransaction, TransferMetaplexAssetTransaction, + MetaplexMetadata, MintMetaplexEditionTransaction, MintMetaplexMetadataTransaction, + TransferMetaplexAssetTransaction, }; use holaplex_hub_nfts_solana_entity::{collection_mints, collections}; use hub_core::{anyhow::Result, clap, prelude::*, thiserror::Error, uuid::Uuid}; +use mpl_bubblegum::state::metaplex_adapter::{ + Collection, Creator as BubblegumCreator, TokenProgramVersion, +}; use mpl_token_metadata::{ instruction::{mint_new_edition_from_master_edition_via_token, update_metadata_accounts_v2}, state::{Creator, DataV2, EDITION, PREFIX}, @@ -30,8 +34,8 @@ use spl_token::{ use crate::{ asset_api::RpcClient as _, backend::{ - CollectionBackend, CollectionType, MasterEditionAddresses, MintBackend, - MintEditionAddresses, TransactionResponse, TransferAssetAddresses, + CollectionBackend, MasterEditionAddresses, MintBackend, MintCompressedMintV1Addresses, + MintEditionAddresses, TransactionResponse, TransferAssetAddresses, TransferBackend, UpdateMasterEditionAddresses, }, }; @@ -83,11 +87,13 @@ pub struct TransferAssetRequest { } #[derive(Debug, Error)] -enum SolanaError { +enum SolanaErrorNotFoundMessage { #[error("master edition message not found")] - MasterEditionMessageNotFound, + MasterEdition, #[error("serialized message message not found")] - SerializedMessageNotFound, + Serialized, + #[error("metadata message not found")] + Metadata, } #[derive(Clone)] @@ -96,6 +102,7 @@ pub struct Solana { treasury_wallet_address: Pubkey, bubblegum_tree_authority: Pubkey, bubblegum_merkle_tree: Pubkey, + bubblegum_cpi_address: Pubkey, asset_rpc_client: jsonrpsee::http_client::HttpClient, } @@ -110,14 +117,19 @@ impl Solana { } = args; let rpc_client = Arc::new(RpcClient::new(solana_endpoint)); + let (bubblegum_cpi_address, _) = Pubkey::find_program_address( + &[mpl_bubblegum::state::COLLECTION_CPI_PREFIX.as_bytes()], + &mpl_bubblegum::ID, + ); + Ok(Self { rpc_client, treasury_wallet_address: solana_treasury_wallet_address, bubblegum_tree_authority: tree_authority, bubblegum_merkle_tree: merkle_tree, + bubblegum_cpi_address, asset_rpc_client: jsonrpsee::http_client::HttpClientBuilder::default() .request_timeout(std::time::Duration::from_secs(5)) - // .set_headers(...) // TODO: add auth here? .build(digital_asset_api_endpoint) .context("Failed to initialize asset API client")?, }) @@ -146,7 +158,7 @@ impl Solana { &transaction .serialized_message .clone() - .ok_or(SolanaError::SerializedMessageNotFound)?, + .ok_or(SolanaErrorNotFoundMessage::Serialized)?, )?; let transaction = Transaction { @@ -158,14 +170,24 @@ impl Solana { Ok(signature.to_string()) } +} + +#[repr(transparent)] +pub struct UncompressedRef<'a>(pub &'a Solana); +#[repr(transparent)] +pub struct CompressedRef<'a>(pub &'a Solana); +#[repr(transparent)] +pub struct EditionRef<'a>(pub &'a Solana); - #[allow(clippy::too_many_lines)] - fn master_edition_transaction( +impl<'a> CollectionBackend for UncompressedRef<'a> { + fn create( &self, - payload: MasterEdition, - ) -> Result> { - let payer: Pubkey = self.treasury_wallet_address; - let rpc = &self.rpc_client; + txn: MetaplexMasterEditionTransaction, + ) -> hub_core::prelude::Result> { + let MetaplexMasterEditionTransaction { master_edition, .. } = txn; + let master_edition = master_edition.ok_or(SolanaErrorNotFoundMessage::MasterEdition)?; + let payer: Pubkey = self.0.treasury_wallet_address; + let rpc = &self.0.rpc_client; let mint = Keypair::new(); let MasterEdition { name, @@ -175,7 +197,7 @@ impl Solana { creators, supply, owner_address, - } = payload; + } = master_edition; let owner: Pubkey = owner_address.parse()?; let (metadata, _) = Pubkey::find_program_address( @@ -297,43 +319,80 @@ impl Solana { }, }) } -} - -#[repr(transparent)] -pub struct UncompressedRef<'a>(pub &'a Solana); -#[repr(transparent)] -pub struct CompressedRef<'a>(pub &'a Solana); -#[repr(transparent)] -pub struct LegacyCollectionRef<'a>(pub &'a Solana); -impl<'a> CollectionBackend for UncompressedRef<'a> { - fn create( + fn update( &self, + collection: &collections::Model, txn: MetaplexMasterEditionTransaction, - ) -> hub_core::prelude::Result> { - todo!() - } -} + ) -> hub_core::prelude::Result> { + let rpc = &self.0.rpc_client; -impl<'a> CollectionBackend for LegacyCollectionRef<'a> { - fn create( - &self, - txn: MetaplexMasterEditionTransaction, - ) -> hub_core::prelude::Result> { let MetaplexMasterEditionTransaction { master_edition, .. } = txn; - let master_edition = master_edition.ok_or(SolanaError::MasterEditionMessageNotFound)?; - let tx = self.0.master_edition_transaction(master_edition)?; + let master_edition = master_edition.ok_or(SolanaErrorNotFoundMessage::MasterEdition)?; + + let MasterEdition { + name, + seller_fee_basis_points, + symbol, + creators, + metadata_uri, + .. + } = master_edition; + + let payer: Pubkey = self.0.treasury_wallet_address; + + let program_pubkey = mpl_token_metadata::id(); + let update_authority: Pubkey = master_edition.owner_address.parse()?; + let metadata: Pubkey = collection.metadata.parse()?; + + let ins = update_metadata_accounts_v2( + program_pubkey, + metadata, + update_authority, + None, + Some(DataV2 { + name, + symbol, + uri: metadata_uri, + seller_fee_basis_points: seller_fee_basis_points.try_into()?, + creators: Some( + creators + .into_iter() + .map(TryInto::try_into) + .collect::>>()?, + ), + collection: None, + uses: None, + }), + None, + None, + ); + + let blockhash = rpc.get_latest_blockhash()?; + + let message = + solana_program::message::Message::new_with_blockhash(&[ins], Some(&payer), &blockhash); + + let serialized_message = message.serialize(); - Ok(tx) + Ok(TransactionResponse { + serialized_message, + signatures_or_signers_public_keys: vec![ + payer.to_string(), + update_authority.to_string(), + ], + addresses: UpdateMasterEditionAddresses { + metadata, + update_authority, + }, + }) } } -#[async_trait] -impl<'a> MintBackend for UncompressedRef<'a> { +impl<'a> MintBackend for EditionRef<'a> { fn mint( &self, - collection_ty: CollectionType, collection: &collections::Model, txn: MintMetaplexEditionTransaction, ) -> hub_core::prelude::Result> { @@ -441,80 +500,12 @@ impl<'a> MintBackend for UncompressedRef<'a> { }, }) } +} - fn try_update( - &self, - collection_ty: CollectionType, - collection: &collections::Model, - txn: MetaplexMasterEditionTransaction, - ) -> hub_core::prelude::Result>> { - let rpc = &self.0.rpc_client; - - let MetaplexMasterEditionTransaction { master_edition, .. } = txn; - - let master_edition = master_edition.ok_or(SolanaError::MasterEditionMessageNotFound)?; - - let MasterEdition { - name, - seller_fee_basis_points, - symbol, - creators, - metadata_uri, - .. - } = master_edition; - - let payer: Pubkey = self.0.treasury_wallet_address; - - let program_pubkey = mpl_token_metadata::id(); - let update_authority: Pubkey = master_edition.owner_address.parse()?; - let metadata: Pubkey = collection.metadata.parse()?; - - let ins = update_metadata_accounts_v2( - program_pubkey, - metadata, - update_authority, - None, - Some(DataV2 { - name, - symbol, - uri: metadata_uri, - seller_fee_basis_points: seller_fee_basis_points.try_into()?, - creators: Some( - creators - .into_iter() - .map(TryInto::try_into) - .collect::>>()?, - ), - collection: None, - uses: None, - }), - None, - None, - ); - - let blockhash = rpc.get_latest_blockhash()?; - - let message = - solana_program::message::Message::new_with_blockhash(&[ins], Some(&payer), &blockhash); - - let serialized_message = message.serialize(); - - Ok(Some(TransactionResponse { - serialized_message, - signatures_or_signers_public_keys: vec![ - payer.to_string(), - update_authority.to_string(), - ], - addresses: UpdateMasterEditionAddresses { - metadata, - update_authority, - }, - })) - } - +#[async_trait] +impl<'a> TransferBackend for UncompressedRef<'a> { async fn transfer( &self, - collection_ty: CollectionType, collection_mint: &collection_mints::Model, txn: TransferMetaplexAssetTransaction, ) -> hub_core::prelude::Result> { @@ -576,171 +567,50 @@ impl<'a> MintBackend for UncompressedRef<'a> { } #[async_trait] -impl<'a> MintBackend for CompressedRef<'a> { - // fn create( - // &self, - // evt: MetaplexMasterEditionTransaction, - // ) -> Result> { - // let MetaplexMasterEditionTransaction { master_edition, .. } = evt; - // let MasterEdition { - // name, - // symbol, - // metadata_uri, - // creators, - // seller_fee_basis_points, - // supply, // TODO: ? - // owner_address, - // } = master_edition.context("Missing master edition message")?; - // let payer = self.treasury; - // let owner = owner_address.parse()?; - // let mint = Keypair::new(); - - // let (metadata, _) = Pubkey::find_program_address( - // &[ - // b"metadata", - // mpl_token_metadata::ID.as_ref(), - // mint.pubkey().as_ref(), - // ], - // &mpl_token_metadata::ID, - // ); - // let associated_token_account = get_associated_token_address(&owner, &mint.pubkey()); - - // let mint_len = spl_token::state::Mint::LEN; - - // // TODO: this is the collection NFT, right? - // let instructions = [ - // solana_program::system_instruction::create_account( - // &payer, - // &mint.pubkey(), - // self.rpc.get_minimum_balance_for_rent_exemption(mint_len)?, - // mint_len.try_into()?, - // &spl_token::ID, - // ), - // spl_token::instruction::initialize_mint( - // &spl_token::ID, - // &mint.pubkey(), - // &owner, - // Some(&owner), - // 0, - // )?, - // spl_associated_token_account::instruction::create_associated_token_account( - // &payer, - // &owner, - // &mint.pubkey(), - // &spl_token::ID, - // ), - // spl_token::instruction::mint_to( - // &spl_token::ID, - // &mint.pubkey(), - // &associated_token_account, - // &owner, - // &[], - // 1, - // )?, - // mpl_token_metadata::instruction::create_metadata_accounts_v3( - // mpl_token_metadata::ID, - // metadata, - // mint.pubkey(), - // owner, - // payer, - // owner, - // name, - // symbol, - // metadata_uri, - // Some( - // creators - // .into_iter() - // .map(TryInto::try_into) - // .collect::, _>>()?, - // ), - // seller_fee_basis_points.try_into()?, - // true, - // true, - // None, - // None, - // None, - // ), - // ]; - - // let serialized_message = solana_program::message::Message::new_with_blockhash( - // &instructions, - // Some(&payer), - // &self.rpc.get_latest_blockhash()?, - // ) - // .serialize(); - // let mint_signature = mint.try_sign_message(&serialized_message)?; - - // Ok(TransactionResponse { - // serialized_message, - // signatures_or_signers_public_keys: vec![ - // payer.to_string(), - // mint_signature.to_string(), - // owner.to_string(), - // ], - // addresses: MasterEditionAddresses { - // metadata, - // associated_token_account, - // owner, - // master_edition: todo!("what"), - // mint: mint.pubkey(), - // update_authority: owner, - // }, - // }) - // } - - fn mint( +impl<'a> TransferBackend for CompressedRef<'a> { + async fn transfer( &self, - collection_ty: CollectionType, - collection: &collections::Model, - txn: MintMetaplexEditionTransaction, - ) -> hub_core::prelude::Result> { - match collection_ty { - CollectionType::Legacy => { - bail!("Legacy collections are not supported with compressed NFTs.") - }, - CollectionType::Verified => (), - } - - let MintMetaplexEditionTransaction { + collection_mint: &collection_mints::Model, + txn: TransferMetaplexAssetTransaction, + ) -> hub_core::prelude::Result> { + let TransferMetaplexAssetTransaction { recipient_address, owner_address, - edition, + collection_mint_id, .. } = txn; let payer = self.0.treasury_wallet_address; let recipient = recipient_address.parse()?; let owner = owner_address.parse()?; + let asset_id = todo!("wait where's the asset address"); + let asset = self + .0 + .asset_rpc_client + .get_asset(asset_id) + .await + .context("Error getting asset data")?; + let instructions = [Instruction { program_id: mpl_bubblegum::ID, accounts: [ AccountMeta::new(self.0.bubblegum_tree_authority, false), - AccountMeta::new_readonly(recipient, false), + AccountMeta::new_readonly(owner, true), + AccountMeta::new_readonly(owner, false), AccountMeta::new_readonly(recipient, false), AccountMeta::new(self.0.bubblegum_merkle_tree, false), - AccountMeta::new_readonly(payer, true), - AccountMeta::new_readonly(owner, true), // TODO: who will own the trees?? AccountMeta::new_readonly(spl_noop::ID, false), AccountMeta::new_readonly(spl_account_compression::ID, false), AccountMeta::new_readonly(system_program::ID, false), ] .into_iter() .collect(), - data: mpl_bubblegum::instruction::MintV1 { - message: mpl_bubblegum::state::metaplex_adapter::MetadataArgs { - name: todo!(), - symbol: todo!(), - uri: todo!(), - seller_fee_basis_points: todo!(), - primary_sale_happened: todo!(), - is_mutable: todo!(), - edition_nonce: todo!(), - token_standard: todo!(), - collection: todo!(), - uses: todo!(), - token_program_version: todo!(), - creators: todo!(), - }, + data: mpl_bubblegum::instruction::Transfer { + root: todo!("how does DAA work"), + data_hash: todo!("how does DAA work"), + creator_hash: todo!("how does DAA work"), + nonce: todo!("how does DAA work"), + index: todo!("how does DAA work"), } .data(), }]; @@ -755,70 +625,107 @@ impl<'a> MintBackend for CompressedRef<'a> { Ok(TransactionResponse { serialized_message, signatures_or_signers_public_keys: vec![payer.to_string(), owner.to_string()], - addresses: MintEditionAddresses { - edition: todo!("what"), - mint: todo!("what"), - metadata: todo!("what"), + addresses: TransferAssetAddresses { owner, - associated_token_account: todo!("what"), recipient, + recipient_associated_token_account: todo!("what"), + owner_associated_token_account: todo!("what"), }, }) } +} - fn try_update( +impl<'a> MintBackend + for CompressedRef<'a> +{ + fn mint( &self, - collection_ty: CollectionType, collection: &collections::Model, - txn: MetaplexMasterEditionTransaction, - ) -> hub_core::prelude::Result>> { - Ok(None) - } - - async fn transfer( - &self, - collection_ty: CollectionType, - collection_mint: &collection_mints::Model, - txn: TransferMetaplexAssetTransaction, - ) -> hub_core::prelude::Result> { - let TransferMetaplexAssetTransaction { + txn: MintMetaplexMetadataTransaction, + ) -> hub_core::prelude::Result> { + let MintMetaplexMetadataTransaction { recipient_address, - owner_address, - collection_mint_id, + metadata, .. } = txn; + + let MetaplexMetadata { + name, + seller_fee_basis_points, + symbol, + creators, + metadata_uri, + owner_address, + } = metadata.ok_or(SolanaErrorNotFoundMessage::Metadata)?; let payer = self.0.treasury_wallet_address; let recipient = recipient_address.parse()?; let owner = owner_address.parse()?; + let treasury_wallet_address = self.0.treasury_wallet_address; + + let mut accounts = vec![ + // Tree authority + AccountMeta::new(self.0.bubblegum_tree_authority, false), + // TODO: can we make the project treasury the leaf owner while keeping the tree authority the holaplex treasury wallet + // Leaf owner + AccountMeta::new_readonly(recipient, false), + // Leaf delegate + AccountMeta::new_readonly(recipient, false), + // Merkle tree + AccountMeta::new(self.0.bubblegum_merkle_tree, false), + // Payer [signer] + AccountMeta::new_readonly(payer, true), + // Tree delegate [signer] + AccountMeta::new_readonly(treasury_wallet_address, true), + // Collection authority [signer] + AccountMeta::new_readonly(owner, true), + // Collection authority pda + AccountMeta::new_readonly(mpl_bubblegum::ID, false), + // Collection mint + AccountMeta::new_readonly(collection.mint.parse()?, false), + // collection metadata [mutable] + AccountMeta::new(collection.metadata.parse()?, false), + // Edition account + AccountMeta::new_readonly(collection.master_edition.parse()?, false), + // Bubblegum Signer + AccountMeta::new_readonly(self.0.bubblegum_cpi_address, false), + AccountMeta::new_readonly(spl_noop::ID, false), + AccountMeta::new_readonly(spl_account_compression::ID, false), + AccountMeta::new_readonly(mpl_token_metadata::ID, false), + AccountMeta::new_readonly(system_program::ID, false), + ]; - let asset_id = todo!("wait where's the asset address"); - let asset = self - .0 - .asset_rpc_client - .get_asset(asset_id) - .await - .context("Error getting asset data")?; + if creators + .iter() + .find(|&creator| creator.verified && creator.address == owner.to_string()) + .is_some() + { + accounts.push(AccountMeta::new_readonly(owner, true)); + } let instructions = [Instruction { program_id: mpl_bubblegum::ID, - accounts: [ - AccountMeta::new(self.0.bubblegum_tree_authority, false), - AccountMeta::new_readonly(owner, true), - AccountMeta::new_readonly(owner, false), - AccountMeta::new_readonly(recipient, false), - AccountMeta::new(self.0.bubblegum_merkle_tree, false), - AccountMeta::new_readonly(spl_noop::ID, false), - AccountMeta::new_readonly(spl_account_compression::ID, false), - AccountMeta::new_readonly(system_program::ID, false), - ] - .into_iter() - .collect(), - data: mpl_bubblegum::instruction::Transfer { - root: todo!("how does DAA work"), - data_hash: todo!("how does DAA work"), - creator_hash: todo!("how does DAA work"), - nonce: todo!("how does DAA work"), - index: todo!("how does DAA work"), + accounts: accounts.into_iter().collect(), + data: mpl_bubblegum::instruction::MintToCollectionV1 { + metadata_args: mpl_bubblegum::state::metaplex_adapter::MetadataArgs { + name, + symbol, + uri: metadata_uri, + seller_fee_basis_points: seller_fee_basis_points.try_into()?, + primary_sale_happened: false, + is_mutable: true, + edition_nonce: None, + token_standard: None, + collection: Some(Collection { + verified: true, + key: collection.mint.parse()?, + }), + uses: None, + token_program_version: TokenProgramVersion::Original, + creators: creators + .into_iter() + .map(TryInto::try_into) + .collect::>>()?, + }, } .data(), }]; @@ -833,12 +740,7 @@ impl<'a> MintBackend for CompressedRef<'a> { Ok(TransactionResponse { serialized_message, signatures_or_signers_public_keys: vec![payer.to_string(), owner.to_string()], - addresses: TransferAssetAddresses { - owner, - recipient, - recipient_associated_token_account: todo!("what"), - owner_associated_token_account: todo!("what"), - }, + addresses: MintCompressedMintV1Addresses { owner, recipient }, }) } } diff --git a/core/Cargo.toml b/core/Cargo.toml index 1598c15..cafb570 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -24,6 +24,7 @@ sea-orm = { version = "0.11.3", features = [ ] } prost = "0.11.9" mpl-token-metadata = "1.8.3" +mpl-bubblegum = "0.8.0" [dependencies.hub-core] package = "holaplex-hub-core" diff --git a/core/proto.lock b/core/proto.lock index 7233579..ee59848 100644 --- a/core/proto.lock +++ b/core/proto.lock @@ -5,10 +5,10 @@ sha512 = "893f6ec5b59d8af3c0db48b3cdf461104447bf818114d4f379a5aca55235da6a4b3238 [[schemas]] subject = "solana_nfts" -version = 4 -sha512 = "272f1aed7d792a5fe5750cdca659091ca1a4a8dd4f36b35f5066ea6bb09cf4f1e905e7e5817dfa6c68d7ea3a8644192b4ff82e7ffcd85b2d9a58e48112a4a8bc" +version = 5 +sha512 = "bdc812d4bdbb0d8ac22e6458b53fbd0420daeeb5bb2c4305639d3890741752507ce67bcb7fa40d119806eb578401bcbe1af90c4ef418539d46adba0645746507" [[schemas]] subject = "treasury" -version = 16 -sha512 = "bf8ad07bb11acefeaced6e5da417a9b49bad3770e4dd7f3d29b743fb943da973ab8587f7fef10a40a76b2e477d1c3956410de8403b7fb6efa80b95f2c3b8e8cf" +version = 18 +sha512 = "6653f41a2cb5b9ead358011f9273ae03791d2f36ecc905c1dedc56f88ef268771dbb4f35f58185c24538849c39590147e9ae6ce4b8e22e099df8d63bd7e89413" diff --git a/core/proto.toml b/core/proto.toml index 4771cf2..c00ad2f 100644 --- a/core/proto.toml +++ b/core/proto.toml @@ -3,5 +3,5 @@ endpoint = "https://schemas.holaplex.tools" [schemas] nfts = 20 -treasury = 16 -solana_nfts = 4 +treasury = 18 +solana_nfts = 5 diff --git a/core/src/collection_mints.rs b/core/src/collection_mints.rs index aaf105b..f80e40d 100644 --- a/core/src/collection_mints.rs +++ b/core/src/collection_mints.rs @@ -53,8 +53,8 @@ impl CollectionMint { let conn = db.get(); Entity::find() - .find_also_related(collections::Entity) .join(JoinType::InnerJoin, Relation::Collections.def()) + .find_also_related(collections::Entity) .filter(Column::Id.eq(id)) .one(conn) .await diff --git a/core/src/lib.rs b/core/src/lib.rs index b5eac87..60868de 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -109,3 +109,23 @@ impl TryFrom for Creator { }) } } + +use mpl_bubblegum::state::metaplex_adapter::Creator as BubblegumCreator; + +impl TryFrom for BubblegumCreator { + type Error = Error; + + fn try_from( + ProtoCreator { + address, + verified, + share, + }: ProtoCreator, + ) -> Result { + Ok(Self { + address: address.parse()?, + verified, + share: share.try_into()?, + }) + } +} diff --git a/entity/src/collections.rs b/entity/src/collections.rs index f7ec25c..9ffb151 100644 --- a/entity/src/collections.rs +++ b/entity/src/collections.rs @@ -14,6 +14,7 @@ pub struct Model { pub mint: String, pub metadata: String, pub created_at: DateTime, + // TODO: add supply column to help denote mcc from editions } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/entity/src/mod.rs b/entity/src/mod.rs index 50effb6..da3ad48 100644 --- a/entity/src/mod.rs +++ b/entity/src/mod.rs @@ -3,4 +3,5 @@ pub mod prelude; pub mod collection_mints; -pub mod collections; +pub mod editions; +pub mod certified_collections; \ No newline at end of file diff --git a/indexer/src/connector.rs b/indexer/src/connector.rs index a2379c6..e6d2fb3 100644 --- a/indexer/src/connector.rs +++ b/indexer/src/connector.rs @@ -34,17 +34,14 @@ impl GeyserGrpcConnector { slots.insert("client".to_owned(), SubscribeRequestFilterSlots {}); let mut transactions = HashMap::new(); - transactions.insert( - "client".to_string(), - SubscribeRequestFilterTransactions { - vote: Some(false), - failed: Some(false), - signature: None, - account_include: vec![spl_token::ID.to_string()], - account_exclude: Vec::new(), - account_required: Vec::new(), - }, - ); + transactions.insert("client".to_string(), SubscribeRequestFilterTransactions { + vote: Some(false), + failed: Some(false), + signature: None, + account_include: vec![spl_token::ID.to_string()], + account_exclude: Vec::new(), + account_required: Vec::new(), + }); SubscribeRequest { accounts: HashMap::new(), diff --git a/indexer/src/handler.rs b/indexer/src/handler.rs index 40641f7..fe0291a 100644 --- a/indexer/src/handler.rs +++ b/indexer/src/handler.rs @@ -111,7 +111,7 @@ impl MessageHandler { let keys = message.clone().account_keys; for (idx, key) in message.clone().account_keys.iter().enumerate() { - let k = Pubkey::new(&key); + let k = Pubkey::new(key); if k == spl_token::ID { i = idx; break; @@ -147,7 +147,7 @@ impl MessageHandler { let source_account_index = account_indices[0]; let source_bytes = &keys[source_account_index as usize]; - let source = Pubkey::new(&source_bytes); + let source = Pubkey::new(source_bytes); let collection_mint = CollectionMint::find_by_ata(&self.db, source.to_string()).await?; @@ -158,7 +158,7 @@ impl MessageHandler { let destination_account_index = account_indices[destination_ata_index]; let destination_bytes = &keys[destination_account_index as usize]; - let destination = Pubkey::new(&destination_bytes); + let destination = Pubkey::new(destination_bytes); let acct = fetch_account(&self.rpc, &destination).await?; let destination_tkn_act = Account::unpack(&acct.data)?;