diff --git a/Cargo.lock b/Cargo.lock index b830817..211149d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5043,6 +5043,7 @@ dependencies = [ name = "solana-indexer" version = "0.1.0" dependencies = [ + "anchor-lang", "backoff", "bs58 0.5.0", "dashmap", @@ -5051,6 +5052,7 @@ dependencies = [ "holaplex-hub-core", "holaplex-hub-nfts-solana-core", "holaplex-hub-nfts-solana-entity", + "mpl-bubblegum", "sea-orm 0.10.7", "solana-client", "solana-program", diff --git a/consumer/src/asset_api.rs b/consumer/src/asset_api.rs index 6e611aa..30f0f27 100644 --- a/consumer/src/asset_api.rs +++ b/consumer/src/asset_api.rs @@ -76,7 +76,7 @@ pub struct Asset { pub struct AssetSupply { pub print_max_supply: u32, pub print_current_supply: u32, - pub edition_nonce: u64, + pub edition_nonce: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/consumer/src/backend.rs b/consumer/src/backend.rs index 08d32a4..cbd656d 100644 --- a/consumer/src/backend.rs +++ b/consumer/src/backend.rs @@ -1,7 +1,7 @@ use holaplex_hub_nfts_solana_core::proto::{ MetaplexMasterEditionTransaction, SolanaPendingTransaction, TransferMetaplexAssetTransaction, }; -use holaplex_hub_nfts_solana_entity::{collection_mints, collections}; +use holaplex_hub_nfts_solana_entity::collections; use hub_core::prelude::*; use solana_program::pubkey::Pubkey; @@ -43,6 +43,11 @@ pub struct MintCompressedMintV1Addresses { pub leaf_owner: Pubkey, } +pub struct TransferCompressedMintV1Addresses { + pub owner: Pubkey, + pub recipient: Pubkey, +} + #[derive(Clone)] pub struct UpdateMasterEditionAddresses { pub metadata: Pubkey, @@ -103,10 +108,10 @@ pub trait MintBackend { } #[async_trait] -pub trait TransferBackend { +pub trait TransferBackend { async fn transfer( &self, - collection_mint: &collection_mints::Model, + collection_mint: &M, txn: TransferMetaplexAssetTransaction, - ) -> Result>; + ) -> Result>; } diff --git a/consumer/src/events.rs b/consumer/src/events.rs index 8d7fe61..3f09306 100644 --- a/consumer/src/events.rs +++ b/consumer/src/events.rs @@ -387,7 +387,7 @@ impl Processor { self.process_nft( EventKind::TransferAsset, &key, - self.transfer_asset(&UncompressedRef(self.solana()), &key, payload), + self.transfer_asset(&key, payload), ) .await }, @@ -730,19 +730,33 @@ impl Processor { Ok(tx.into()) } - async fn transfer_asset( + async fn transfer_asset( &self, - backend: &B, _key: &SolanaNftEventKey, payload: TransferMetaplexAssetTransaction, ) -> ProcessResult { let collection_mint_id = Uuid::parse_str(&payload.collection_mint_id.clone())?; - let collection_mint = CollectionMint::find_by_id(&self.db, collection_mint_id) + let collection_mint = CollectionMint::find_by_id(&self.db, collection_mint_id).await?; + + if let Some(collection_mint) = collection_mint { + let backend = &UncompressedRef(self.solana()); + + let tx = backend + .transfer(&collection_mint, payload) + .await + .map_err(ProcessorErrorKind::Solana)?; + + return Ok(tx.into()); + } + + let compression_leaf = CompressionLeaf::find_by_id(&self.db, collection_mint_id) .await? .ok_or(ProcessorErrorKind::RecordNotFound)?; + let backend = &CompressedRef(self.solana()); + let tx = backend - .transfer(&collection_mint, payload) + .transfer(&compression_leaf, payload) .await .map_err(ProcessorErrorKind::Solana)?; diff --git a/consumer/src/import.rs b/consumer/src/import.rs index c71fbd6..ecc0c9e 100644 --- a/consumer/src/import.rs +++ b/consumer/src/import.rs @@ -53,7 +53,7 @@ impl Processor { _ => Ok(None), } }, - _ => Ok(None), + Services::Treasury(..) => Ok(None), } } diff --git a/consumer/src/solana.rs b/consumer/src/solana.rs index 2b8654a..d15231e 100644 --- a/consumer/src/solana.rs +++ b/consumer/src/solana.rs @@ -4,7 +4,7 @@ use holaplex_hub_nfts_solana_core::proto::{ MetaplexMetadata, MintMetaplexEditionTransaction, MintMetaplexMetadataTransaction, TransferMetaplexAssetTransaction, }; -use holaplex_hub_nfts_solana_entity::{collection_mints, collections}; +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, @@ -13,7 +13,7 @@ use mpl_token_metadata::{ instruction::{mint_new_edition_from_master_edition_via_token, update_metadata_accounts_v2}, state::{Creator, DataV2, EDITION, PREFIX}, }; -use solana_client::rpc_client::RpcClient; +use solana_client::rpc_client::RpcClient as SolanaRpcClient; use solana_program::{ instruction::Instruction, program_pack::Pack, pubkey::Pubkey, system_instruction::create_account, system_program, @@ -37,11 +37,11 @@ use spl_token::{ }; use crate::{ - asset_api::RpcClient as _, + asset_api::RpcClient, backend::{ CollectionBackend, MasterEditionAddresses, MintBackend, MintCompressedMintV1Addresses, MintEditionAddresses, MintMetaplexAddresses, TransactionResponse, TransferAssetAddresses, - TransferBackend, UpdateMasterEditionAddresses, + TransferBackend, TransferCompressedMintV1Addresses, UpdateMasterEditionAddresses, }, }; @@ -114,11 +114,13 @@ pub enum SolanaAssetIdError { Base58(#[from] bs58::decode::Error), #[error("Borsh deserialization error")] BorshDeserialize(#[from] std::io::Error), + #[error("Asset id not found")] + NotFound, } #[derive(Clone)] pub struct Solana { - rpc_client: Arc, + rpc_client: Arc, treasury_wallet_address: Pubkey, bubblegum_tree_authority: Pubkey, bubblegum_merkle_tree: Pubkey, @@ -135,7 +137,7 @@ impl Solana { tree_authority, merkle_tree, } = args; - let rpc_client = Arc::new(RpcClient::new(solana_endpoint)); + let rpc_client = Arc::new(SolanaRpcClient::new(solana_endpoint)); let (bubblegum_cpi_address, _) = Pubkey::find_program_address( &[mpl_bubblegum::state::COLLECTION_CPI_PREFIX.as_bytes()], @@ -160,7 +162,7 @@ impl Solana { } #[must_use] - pub fn rpc(&self) -> Arc { + pub fn rpc(&self) -> Arc { self.rpc_client.clone() } @@ -581,7 +583,7 @@ impl<'a> MintBackend for E } #[async_trait] -impl<'a> TransferBackend for UncompressedRef<'a> { +impl<'a> TransferBackend for UncompressedRef<'a> { async fn transfer( &self, collection_mint: &collection_mints::Model, @@ -645,50 +647,81 @@ impl<'a> TransferBackend for UncompressedRef<'a> { } #[async_trait] -impl<'a> TransferBackend for CompressedRef<'a> { +impl<'a> TransferBackend + for CompressedRef<'a> +{ async fn transfer( &self, - collection_mint: &collection_mints::Model, + compression_leaf: &compression_leafs::Model, txn: TransferMetaplexAssetTransaction, - ) -> hub_core::prelude::Result> { + ) -> hub_core::prelude::Result> { let TransferMetaplexAssetTransaction { recipient_address, owner_address, - 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) + let asset_api = &self.0.asset_rpc(); + + let tree_authority_address = Pubkey::from_str(&compression_leaf.tree_authority)?; + let merkle_tree_address = Pubkey::from_str(&compression_leaf.merkle_tree)?; + + let asset_id = compression_leaf + .asset_id + .clone() + .ok_or(SolanaAssetIdError::NotFound)?; + let asset = asset_api + .get_asset(&asset_id) .await - .context("Error getting asset data")?; + .context("fetching asset from DAA")?; + let asset_proof = asset_api + .get_asset_proof(&asset_id) + .await + .context("fetching asset proof from DAA")?; + + let root: Vec = asset_proof.root.into(); + let data_hash: Vec = asset.compression.data_hash.context("no data hash")?.into(); + let creator_hash: Vec = asset + .compression + .creator_hash + .context("no creator hash")? + .into(); + let leaf_id = asset.compression.leaf_id; + let proofs = asset_proof + .proof + .into_iter() + .map(|proof| Ok(AccountMeta::new_readonly(proof.try_into()?, false))) + .collect::>>()?; + + let mut accounts = vec![ + AccountMeta::new(tree_authority_address, false), + AccountMeta::new_readonly(owner, true), + AccountMeta::new_readonly(owner, false), + AccountMeta::new_readonly(recipient, false), + AccountMeta::new(merkle_tree_address, false), + AccountMeta::new_readonly(spl_noop::ID, false), + AccountMeta::new_readonly(spl_account_compression::ID, false), + AccountMeta::new_readonly(system_program::ID, false), + ]; + + accounts.extend(proofs); 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(), + accounts, 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"), + root: root.try_into().map_err(|_| anyhow!("Invalid root hash"))?, + data_hash: data_hash + .try_into() + .map_err(|_| anyhow!("Invalid data hash"))?, + creator_hash: creator_hash + .try_into() + .map_err(|_| anyhow!("Invalid creator hash"))?, + nonce: leaf_id.into(), + index: leaf_id, } .data(), }]; @@ -703,12 +736,7 @@ impl<'a> TransferBackend 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: TransferCompressedMintV1Addresses { owner, recipient }, }) } } diff --git a/core/src/compression_leafs.rs b/core/src/compression_leafs.rs index 5637aff..bd225f3 100644 --- a/core/src/compression_leafs.rs +++ b/core/src/compression_leafs.rs @@ -20,6 +20,18 @@ impl CompressionLeaf { Entity::find().filter(Column::Id.eq(id)).one(conn).await } + pub async fn find_by_asset_id( + db: &Connection, + address: String, + ) -> Result, DbErr> { + let conn = db.get(); + + Entity::find() + .filter(Column::AssetId.eq(address)) + .one(conn) + .await + } + pub async fn update(db: &Connection, model: ActiveModel) -> Result { let conn = db.get(); diff --git a/indexer/Cargo.toml b/indexer/Cargo.toml index b53e132..f0727fe 100644 --- a/indexer/Cargo.toml +++ b/indexer/Cargo.toml @@ -21,7 +21,9 @@ bs58 = "0.5.0" futures = "0.3.24" hex = "0.4.3" solana-sdk = "1.14" +mpl-bubblegum = "0.7.0" solana-program = "1.14" +anchor-lang = "0.26.0" yellowstone-grpc-client = { git = "https://github.com/rpcpool/yellowstone-grpc", tag = "v1.0.0+solana.1.16.1" } yellowstone-grpc-proto = { git = "https://github.com/rpcpool/yellowstone-grpc", tag = "v1.0.0+solana.1.16.1" } dashmap = "5.4.0" diff --git a/indexer/src/connector.rs b/indexer/src/connector.rs index e6d2fb3..423cd95 100644 --- a/indexer/src/connector.rs +++ b/indexer/src/connector.rs @@ -38,7 +38,7 @@ impl GeyserGrpcConnector { vote: Some(false), failed: Some(false), signature: None, - account_include: vec![spl_token::ID.to_string()], + account_include: vec![spl_token::ID.to_string(), mpl_bubblegum::ID.to_string()], account_exclude: Vec::new(), account_required: Vec::new(), }); diff --git a/indexer/src/handler.rs b/indexer/src/handler.rs index fe0291a..2f13862 100644 --- a/indexer/src/handler.rs +++ b/indexer/src/handler.rs @@ -1,5 +1,6 @@ use std::{convert::TryInto, sync::Arc}; +use anchor_lang::AnchorDeserialize; use backoff::ExponentialBackoff; use dashmap::DashMap; use futures::{sink::SinkExt, stream::StreamExt}; @@ -9,16 +10,21 @@ use holaplex_hub_nfts_solana_core::{ solana_nft_events::Event::UpdateMintOwner, MintOwnershipUpdate, SolanaNftEventKey, SolanaNftEvents, }, - CollectionMint, + sea_orm::Set, + CollectionMint, CompressionLeaf, }; +use holaplex_hub_nfts_solana_entity::compression_leafs; use hub_core::{prelude::*, producer::Producer, tokio::task}; +use mpl_bubblegum::utils::get_asset_id; use solana_client::rpc_client::RpcClient; use solana_program::program_pack::Pack; use solana_sdk::{pubkey::Pubkey, signature::Signature}; use spl_token::{instruction::TokenInstruction, state::Account}; use yellowstone_grpc_client::GeyserGrpcClientError; use yellowstone_grpc_proto::{ - prelude::{subscribe_update::UpdateOneof, SubscribeUpdate, SubscribeUpdateTransaction}, + prelude::{ + subscribe_update::UpdateOneof, Message, SubscribeUpdate, SubscribeUpdateTransaction, + }, tonic::Status, }; @@ -106,23 +112,104 @@ impl MessageHandler { .context("Transaction not found")? .message .context("Message not found")?; + let sig = tx + .transaction + .as_ref() + .ok_or_else(|| anyhow!("failed to get transaction"))? + .signature + .clone(); - let mut i = 0; let keys = message.clone().account_keys; for (idx, key) in message.clone().account_keys.iter().enumerate() { - let k = Pubkey::new(key); + let key: &[u8] = key; + let k = Pubkey::try_from(key)?; if k == spl_token::ID { - i = idx; - break; + self.process_spl_token_transaction(idx, &keys, &sig, &message) + .await?; + } else if k == mpl_bubblegum::ID { + self.process_mpl_bubblegum_transaction(idx, &keys, &sig, &message) + .await?; } } + Ok(()) + } + + async fn process_mpl_bubblegum_transaction( + &self, + program_account_index: usize, + keys: &[Vec], + sig: &Vec, + message: &Message, + ) -> Result<()> { for ins in message.instructions.iter() { let account_indices = ins.accounts.clone(); let program_idx: usize = ins.program_id_index.try_into()?; - if program_idx == i { + if program_idx == program_account_index { + let conn = &self.db; + let data = ins.data.clone(); + let data = data.as_slice(); + + let tkn_instruction = + mpl_bubblegum::instruction::Transfer::try_from_slice(&data[8..])?; + let new_leaf_owner_account_index = account_indices[3]; + let merkle_tree_account_index = account_indices[4]; + let new_leaf_owner_bytes: &[u8] = &keys[new_leaf_owner_account_index as usize]; + let merkle_tree_bytes: &[u8] = &keys[merkle_tree_account_index as usize]; + let new_leaf_owner = Pubkey::try_from(new_leaf_owner_bytes)?; + let merkle_tree = Pubkey::try_from(merkle_tree_bytes)?; + + let asset_id = get_asset_id(&merkle_tree, tkn_instruction.nonce); + + let compression_leaf = + CompressionLeaf::find_by_asset_id(conn, asset_id.to_string()) + .await? + .context("compression leaf not found")?; + + let collection_mint_id = compression_leaf.id; + let leaf_owner = compression_leaf.leaf_owner.clone(); + let mut compression_leaf: compression_leafs::ActiveModel = compression_leaf.into(); + + compression_leaf.leaf_owner = Set(new_leaf_owner.to_string()); + + CompressionLeaf::update(conn, compression_leaf).await?; + + self.producer + .send( + Some(&SolanaNftEvents { + event: Some(UpdateMintOwner(MintOwnershipUpdate { + mint_address: asset_id.to_string(), + sender: leaf_owner, + recipient: new_leaf_owner.to_string(), + tx_signature: Signature::new(sig.as_slice()).to_string(), + })), + }), + Some(&SolanaNftEventKey { + id: collection_mint_id.to_string(), + ..Default::default() + }), + ) + .await?; + } + } + + Ok(()) + } + + async fn process_spl_token_transaction( + &self, + program_account_index: usize, + keys: &[Vec], + sig: &Vec, + message: &Message, + ) -> Result<()> { + for ins in message.instructions.iter() { + let account_indices = ins.accounts.clone(); + let program_idx: usize = ins.program_id_index.try_into()?; + + if program_idx == program_account_index { let data = ins.data.clone(); let data = data.as_slice(); let tkn_instruction = spl_token::instruction::TokenInstruction::unpack(data)?; @@ -138,16 +225,9 @@ impl MessageHandler { } if let Some((1, destination_ata_index)) = transfer_info { - let sig = tx - .transaction - .as_ref() - .ok_or_else(|| anyhow!("failed to get transaction"))? - .signature - .clone(); - let source_account_index = account_indices[0]; - let source_bytes = &keys[source_account_index as usize]; - let source = Pubkey::new(source_bytes); + let source_bytes: &[u8] = &keys[source_account_index as usize]; + let source = Pubkey::try_from(source_bytes)?; let collection_mint = CollectionMint::find_by_ata(&self.db, source.to_string()).await?; @@ -157,8 +237,8 @@ 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_bytes: &[u8] = &keys[destination_account_index as usize]; + let destination = Pubkey::try_from(destination_bytes)?; let acct = fetch_account(&self.rpc, &destination).await?; let destination_tkn_act = Account::unpack(&acct.data)?;