diff --git a/consumer/src/backend.rs b/consumer/src/backend.rs index cbd656d..3515f4e 100644 --- a/consumer/src/backend.rs +++ b/consumer/src/backend.rs @@ -1,7 +1,8 @@ use holaplex_hub_nfts_solana_core::proto::{ MetaplexMasterEditionTransaction, SolanaPendingTransaction, TransferMetaplexAssetTransaction, + UpdateSolanaMintPayload, }; -use holaplex_hub_nfts_solana_entity::collections; +use holaplex_hub_nfts_solana_entity::{collection_mints, collections, update_revisions}; use hub_core::prelude::*; use solana_program::pubkey::Pubkey; @@ -54,6 +55,13 @@ pub struct UpdateMasterEditionAddresses { pub update_authority: Pubkey, } +#[derive(Clone)] +pub struct UpdateCollectionMintAddresses { + pub payer: Pubkey, + pub metadata: Pubkey, + pub update_authority: Pubkey, +} + #[derive(Clone)] pub struct TransferAssetAddresses { pub owner: Pubkey, @@ -101,6 +109,18 @@ pub trait CollectionBackend { collection: &collections::Model, txn: MetaplexMasterEditionTransaction, ) -> Result>; + + fn update_mint( + &self, + collection: &collections::Model, + mint: &collection_mints::Model, + txn: UpdateSolanaMintPayload, + ) -> Result>; + + fn retry_update_mint( + &self, + revision: &update_revisions::Model, + ) -> Result>; } pub trait MintBackend { diff --git a/consumer/src/events.rs b/consumer/src/events.rs index 8034c39..3598f8e 100644 --- a/consumer/src/events.rs +++ b/consumer/src/events.rs @@ -8,12 +8,14 @@ use holaplex_hub_nfts_solana_core::{ MintMetaplexMetadataTransaction, SolanaCompletedMintTransaction, SolanaCompletedTransferTransaction, SolanaCompletedUpdateTransaction, SolanaFailedTransaction, SolanaNftEventKey, SolanaNftEvents, SolanaPendingTransaction, - SolanaTransactionFailureReason, TransferMetaplexAssetTransaction, + SolanaTransactionFailureReason, TransferMetaplexAssetTransaction, UpdateSolanaMintPayload, }, - sea_orm::{DatabaseConnection, DbErr, Set}, + sea_orm::{ActiveModelTrait, DatabaseConnection, DbErr, EntityTrait, Set}, Collection, CollectionMint, CompressionLeaf, Services, }; -use holaplex_hub_nfts_solana_entity::{collection_mints, collections, compression_leafs}; +use holaplex_hub_nfts_solana_entity::{ + collection_mints, collections, compression_leafs, update_revisions, +}; use hub_core::{ chrono::Utc, prelude::*, @@ -29,7 +31,7 @@ use solana_sdk::signature::Signature; use crate::{ backend::{ CollectionBackend, MasterEditionAddresses, MintBackend, MintEditionAddresses, - MintMetaplexAddresses, TransferBackend, + MintMetaplexAddresses, TransferBackend, UpdateCollectionMintAddresses, }, solana::{CompressedRef, EditionRef, Solana, SolanaAssetIdError, UncompressedRef}, }; @@ -109,6 +111,8 @@ pub enum EventKind { UpdateCollection, MintToCollection, RetryMintToCollection, + UpdateCollectionMint, + RetryUpdateCollectionMint, } impl EventKind { @@ -125,6 +129,8 @@ impl EventKind { Self::UpdateCollection => "collection update", Self::MintToCollection => "mint to collection", Self::RetryMintToCollection => "mint to collection retry", + Self::UpdateCollectionMint => "collection mint update", + Self::RetryUpdateCollectionMint => "collection mint update retry", } } @@ -145,6 +151,12 @@ impl EventKind { EventKind::RetryMintToCollection => { SolanaNftEvent::RetryMintToCollectionSigningRequested(tx) }, + EventKind::UpdateCollectionMint => { + SolanaNftEvent::UpdateCollectionMintSigningRequested(tx) + }, + EventKind::RetryUpdateCollectionMint => { + SolanaNftEvent::RetryUpdateMintSigningRequested(tx) + }, } } @@ -286,6 +298,16 @@ impl EventKind { address: collection_mint.mint, }) }, + Self::UpdateCollectionMint => { + SolanaNftEvent::UpdateCollectionMintSubmitted(SolanaCompletedUpdateTransaction { + signature, + }) + }, + Self::RetryUpdateCollectionMint => { + SolanaNftEvent::RetryUpdateMintSubmitted(SolanaCompletedUpdateTransaction { + signature, + }) + }, }) } @@ -302,6 +324,8 @@ impl EventKind { Self::UpdateCollection => SolanaNftEvent::UpdateCollectionFailed(tx), Self::MintToCollection => SolanaNftEvent::MintToCollectionFailed(tx), Self::RetryMintToCollection => SolanaNftEvent::RetryMintToCollectionFailed(tx), + Self::UpdateCollectionMint => SolanaNftEvent::UpdateCollectionMintFailed(tx), + Self::RetryUpdateCollectionMint => SolanaNftEvent::RetryUpdateMintFailed(tx), } } } @@ -435,6 +459,29 @@ impl Processor { ) .await }, + Some(NftEvent::SolanaUpdatedCollectionMint(payload)) => { + self.process_nft( + EventKind::UpdateCollectionMint, + &key, + self.update_collection_mint( + &UncompressedRef(self.solana()), + &key, + payload, + ), + ) + .await + }, + Some(NftEvent::SolanaRetryUpdatedCollectionMint(_)) => { + self.process_nft( + EventKind::RetryUpdateCollectionMint, + &key, + self.retry_update_collection_mint( + &UncompressedRef(self.solana()), + &key, + ), + ) + .await + }, _ => Ok(()), } }, @@ -483,6 +530,14 @@ impl Processor { self.process_treasury(EventKind::RetryCreateCollection, key, res) .await }, + Some(TreasuryEvent::SolanaUpdateCollectionMintSigned(res)) => { + self.process_treasury(EventKind::UpdateCollectionMint, key, res) + .await + }, + Some(TreasuryEvent::SolanaRetryUpdateCollectionMintSigned(res)) => { + self.process_treasury(EventKind::RetryUpdateCollectionMint, key, res) + .await + }, _ => Ok(()), } }, @@ -735,6 +790,62 @@ impl Processor { Ok(tx.into()) } + async fn update_collection_mint( + &self, + backend: &B, + key: &SolanaNftEventKey, + payload: UpdateSolanaMintPayload, + ) -> ProcessResult { + let collection_id = Uuid::parse_str(&payload.collection_id)?; + let collection = Collection::find_by_id(self.db.get(), collection_id) + .await? + .ok_or(ProcessorErrorKind::RecordNotFound)?; + let mint = CollectionMint::find_by_id(self.db.get(), key.id.parse()?) + .await? + .ok_or(ProcessorErrorKind::RecordNotFound)?; + + let tx = backend + .update_mint(&collection, &mint, payload) + .map_err(ProcessorErrorKind::Solana)?; + + let UpdateCollectionMintAddresses { + payer, + metadata, + update_authority, + } = tx.addresses.clone(); + let msg_bytes = tx.serialized_message.clone(); + + let revision = update_revisions::ActiveModel { + id: Set(key.id.parse()?), + mint_id: Set(mint.id), + serialized_message: Set(msg_bytes), + payer: Set(payer.to_string()), + metadata: Set(metadata.to_string()), + update_authority: Set(update_authority.to_string()), + }; + + revision.insert(self.db.get()).await?; + + Ok(tx.into()) + } + + async fn retry_update_collection_mint( + &self, + backend: &B, + key: &SolanaNftEventKey, + ) -> ProcessResult { + let revision = update_revisions::Entity::find_by_id(Uuid::from_str(&key.id)?) + .one(self.db.get()) + .await? + .ok_or(ProcessorErrorKind::RecordNotFound)?; + + let tx = backend + .retry_update_mint(&revision) + .map_err(ProcessorErrorKind::Solana)?; + + Ok(tx.into()) + } + async fn transfer_asset( &self, _key: &SolanaNftEventKey, diff --git a/consumer/src/solana.rs b/consumer/src/solana.rs index d15231e..80f1216 100644 --- a/consumer/src/solana.rs +++ b/consumer/src/solana.rs @@ -2,9 +2,11 @@ use anchor_lang::{prelude::AccountMeta, AnchorDeserialize, InstructionData}; use holaplex_hub_nfts_solana_core::proto::{ treasury_events::SolanaTransactionResult, MasterEdition, MetaplexMasterEditionTransaction, MetaplexMetadata, MintMetaplexEditionTransaction, MintMetaplexMetadataTransaction, - TransferMetaplexAssetTransaction, + TransferMetaplexAssetTransaction, UpdateSolanaMintPayload, +}; +use holaplex_hub_nfts_solana_entity::{ + collection_mints, collections, compression_leafs, update_revisions, }; -use holaplex_hub_nfts_solana_entity::{collection_mints, collections, compression_leafs}; use hub_core::{anyhow::Result, clap, prelude::*, thiserror, uuid::Uuid}; use mpl_bubblegum::state::metaplex_adapter::{ Collection, Creator as BubblegumCreator, TokenProgramVersion, @@ -41,7 +43,8 @@ use crate::{ backend::{ CollectionBackend, MasterEditionAddresses, MintBackend, MintCompressedMintV1Addresses, MintEditionAddresses, MintMetaplexAddresses, TransactionResponse, TransferAssetAddresses, - TransferBackend, TransferCompressedMintV1Addresses, UpdateMasterEditionAddresses, + TransferBackend, TransferCompressedMintV1Addresses, UpdateCollectionMintAddresses, + UpdateMasterEditionAddresses, }, }; @@ -468,6 +471,118 @@ impl<'a> CollectionBackend for UncompressedRef<'a> { }, }) } + + fn update_mint( + &self, + collection: &collections::Model, + collection_mint: &collection_mints::Model, + payload: UpdateSolanaMintPayload, + ) -> Result> { + let metadata = payload + .metadata + .ok_or(SolanaErrorNotFoundMessage::Metadata)?; + let payer: Pubkey = self.0.treasury_wallet_address; + let rpc = &self.0.rpc_client; + + let MetaplexMetadata { + name, + symbol, + seller_fee_basis_points, + metadata_uri, + creators, + owner_address, + } = metadata; + let update_authority: Pubkey = owner_address.parse()?; + let mint_pubkey: Pubkey = collection_mint.mint.parse()?; + + let (metadata, _) = Pubkey::find_program_address( + &[ + b"metadata", + mpl_token_metadata::ID.as_ref(), + mint_pubkey.as_ref(), + ], + &mpl_token_metadata::ID, + ); + + let blockhash = rpc.get_latest_blockhash()?; + + let update_ins: Instruction = mpl_token_metadata::instruction::update_metadata_accounts_v2( + mpl_token_metadata::ID, + 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: Some(mpl_token_metadata::state::Collection { + verified: true, + key: collection.mint.parse()?, + }), + uses: None, + }), + None, + Some(true), + ); + + let message = solana_program::message::Message::new_with_blockhash( + &[update_ins], + Some(&payer), + &blockhash, + ); + + let serialized_message = message.serialize(); + + Ok(TransactionResponse { + serialized_message, + signatures_or_signers_public_keys: vec![ + payer.to_string(), + update_authority.to_string(), + ], + addresses: UpdateCollectionMintAddresses { + payer, + metadata, + update_authority, + }, + }) + } + + fn retry_update_mint( + &self, + revision: &update_revisions::Model, + ) -> Result> { + let rpc = &self.0.rpc_client; + + let update_authority: Pubkey = revision.update_authority.parse()?; + let metadata = revision.metadata.parse()?; + let payer = Pubkey::from_str(&revision.payer)?; + + let mut message: solana_program::message::Message = + bincode::deserialize(&revision.serialized_message)?; + + let blockhash = rpc.get_latest_blockhash()?; + message.recent_blockhash = blockhash; + + Ok(TransactionResponse { + serialized_message: message.serialize(), + signatures_or_signers_public_keys: vec![ + payer.to_string(), + update_authority.to_string(), + ], + addresses: UpdateCollectionMintAddresses { + payer, + metadata, + update_authority, + }, + }) + } } impl<'a> MintBackend for EditionRef<'a> { diff --git a/core/proto.lock b/core/proto.lock index 2e734d8..3812abf 100644 --- a/core/proto.lock +++ b/core/proto.lock @@ -1,14 +1,14 @@ [[schemas]] subject = "nfts" -version = 22 -sha512 = "c9920f6a5792b067396c88e40b9bd2adfcb55b582734aff924a67a9d5841a5e2839fc734c1bbff66f402f9a9d8852ca5fef1339aaaa3d5b05aa7868ddfa375c1" +version = 25 +sha512 = "90dadff6bc75b59bb79d9ed2a65d582923f9ce66b5915c020306571bb446d68ff6648543386838510a60081d7cbb14f43fa2ae22c4c8ecd85874bee4323dd26a" [[schemas]] subject = "solana_nfts" -version = 7 -sha512 = "73570b9e58f91a06901ba6455986ce1a0d3675e33860d2447160d711a8cebcfb78cfc714fb08644ad83495dc8612b0b123203561af6d93d29ffb0256725047ba" +version = 9 +sha512 = "312a84e8ae8b9222c7ec2b307d036dae0bd8dac4363e813c2fcffd5d7fba8741bd802953b1ec0a96baf57a7ce852debb724fcccf3b0bd8a27a9e4cc60344a56f" [[schemas]] subject = "treasury" -version = 19 -sha512 = "af9c5b6f8f6aef713a686b9253ef87a2332e8a69b4e0adbeab3a587ef2b74c09b510e3b017bf75bc2d1e083846073dcfd39cd104d95a4a76406a3a7fa0dbe2fb" +version = 21 +sha512 = "734cff313b8b4854b9a4c03cfd6f95f07b1fd86f8678393ab466443d9da4d6e7c9fc400bdbcc718d83e6c7711857941d4b6dc0ea5d1d926f05a7859a65a15509" diff --git a/core/proto.toml b/core/proto.toml index 1aef615..002d0f7 100644 --- a/core/proto.toml +++ b/core/proto.toml @@ -2,6 +2,6 @@ endpoint = "https://schemas.holaplex.tools" [schemas] -nfts = 22 -treasury = 19 -solana_nfts = 7 +nfts = 25 +treasury = 21 +solana_nfts = 9 \ No newline at end of file diff --git a/entity/src/lib.rs b/entity/src/lib.rs index 04ce5b9..1e19917 100644 --- a/entity/src/lib.rs +++ b/entity/src/lib.rs @@ -1,5 +1,6 @@ pub mod collection_mints; pub mod collections; pub mod compression_leafs; +pub mod update_revisions; pub mod prelude; diff --git a/entity/src/mod.rs b/entity/src/mod.rs index 6690f3a..4ff1864 100644 --- a/entity/src/mod.rs +++ b/entity/src/mod.rs @@ -5,4 +5,5 @@ pub mod prelude; pub mod collection_mints; pub mod editions; pub mod certified_collections; -pub mod compression_leafs; \ No newline at end of file +pub mod compression_leafs; +pub mod update_revisions; \ No newline at end of file diff --git a/entity/src/prelude.rs b/entity/src/prelude.rs index be23938..9c26550 100644 --- a/entity/src/prelude.rs +++ b/entity/src/prelude.rs @@ -2,5 +2,5 @@ pub use super::{ collection_mints::Entity as CollectionMints, collections::Entity as Collections, - compression_leafs::Entity as CompressionLeafs, + compression_leafs::Entity as CompressionLeafs, update_revisions::Entity as UpdateRevisions, }; diff --git a/entity/src/update_revisions.rs b/entity/src/update_revisions.rs new file mode 100644 index 0000000..640173a --- /dev/null +++ b/entity/src/update_revisions.rs @@ -0,0 +1,21 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "update_revisions")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub mint_id: Uuid, + #[sea_orm(column_type = "Binary(BlobSize::Blob(None))")] + pub serialized_message: Vec, + pub payer: String, + pub metadata: String, + pub update_authority: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/migration/src/lib.rs b/migration/src/lib.rs index b5c18d9..4efe28a 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -6,6 +6,7 @@ mod m20230614_132203_make_associated_token_account_nullable_on_collection_mints; mod m20230616_091724_backfill_associated_token_account_on_collection_mints; mod m20230721_135829_set_default_collection_and_mint_id; mod m20230725_143421_add_compression_leafs_table; +mod m20230807_135202_update_revisions; pub struct Migrator; @@ -17,8 +18,9 @@ impl MigratorTrait for Migrator { Box::new(m20230530_131917_create_collection_mints_table::Migration), Box::new(m20230614_132203_make_associated_token_account_nullable_on_collection_mints::Migration), Box::new(m20230616_091724_backfill_associated_token_account_on_collection_mints::Migration), - Box::new(m20230725_143421_add_compression_leafs_table::Migration), Box::new(m20230721_135829_set_default_collection_and_mint_id::Migration), + Box::new(m20230725_143421_add_compression_leafs_table::Migration), + Box::new(m20230807_135202_update_revisions::Migration), ] } } diff --git a/migration/src/m20230807_135202_update_revisions.rs b/migration/src/m20230807_135202_update_revisions.rs new file mode 100644 index 0000000..87a06f0 --- /dev/null +++ b/migration/src/m20230807_135202_update_revisions.rs @@ -0,0 +1,58 @@ +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 + .create_table( + Table::create() + .table(UpdateRevisions::Table) + .if_not_exists() + .col( + ColumnDef::new(UpdateRevisions::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(UpdateRevisions::MintId).uuid().not_null()) + .col( + ColumnDef::new(UpdateRevisions::SerializedMessage) + .binary() + .not_null(), + ) + .col(ColumnDef::new(UpdateRevisions::Payer).string().not_null()) + .col( + ColumnDef::new(UpdateRevisions::Metadata) + .string() + .not_null(), + ) + .col( + ColumnDef::new(UpdateRevisions::UpdateAuthority) + .string() + .not_null(), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(UpdateRevisions::Table).to_owned()) + .await + } +} + +#[derive(Iden)] +enum UpdateRevisions { + Table, + Id, + MintId, + SerializedMessage, + Payer, + Metadata, + UpdateAuthority, +}