From 70715a2ece15e89442ebb088b9b71335fa4e013e Mon Sep 17 00:00:00 2001 From: pmantica11 <151664502+pmantica11@users.noreply.github.com> Date: Thu, 2 May 2024 09:59:38 -0500 Subject: [PATCH] Merge blockbuster into DAS (#189) * Add blockbuster * Fix gitignore * Auto fix clippy * Fix clippy * Fix cargo fmt --- .gitignore | 1 - Cargo.lock | 21 +- Cargo.toml | 25 +- blockbuster/.gitignore | 11 + blockbuster/Cargo.toml | 42 ++ blockbuster/README.md | 22 + blockbuster/src/error.rs | 34 ++ blockbuster/src/instruction.rs | 89 +++ blockbuster/src/lib.rs | 7 + blockbuster/src/parsed_programs.rs | 9 + blockbuster/src/program_handler.rs | 52 ++ .../src/programs/account_closure/mod.rs | 73 +++ blockbuster/src/programs/bubblegum/mod.rs | 301 ++++++++++ blockbuster/src/programs/mod.rs | 34 ++ .../src/programs/mpl_core_program/mod.rs | 92 +++ blockbuster/src/programs/token_account/mod.rs | 77 +++ .../programs/token_extensions/extension.rs | 542 ++++++++++++++++++ .../src/programs/token_extensions/mod.rs | 210 +++++++ .../src/programs/token_metadata/mod.rs | 147 +++++ blockbuster/tests/bubblegum_parser_test.rs | 222 +++++++ .../tests/fixtures/double_bubblegum_mint.json | 162 ++++++ .../fixtures/helium_mint_double_tree.json | 393 +++++++++++++ blockbuster/tests/fixtures/helium_nested.json | 257 +++++++++ blockbuster/tests/helpers.rs | 396 +++++++++++++ blockbuster/tests/instructions_test.rs | 227 ++++++++ 25 files changed, 3435 insertions(+), 11 deletions(-) create mode 100644 blockbuster/.gitignore create mode 100644 blockbuster/Cargo.toml create mode 100644 blockbuster/README.md create mode 100644 blockbuster/src/error.rs create mode 100644 blockbuster/src/instruction.rs create mode 100644 blockbuster/src/lib.rs create mode 100644 blockbuster/src/parsed_programs.rs create mode 100644 blockbuster/src/program_handler.rs create mode 100644 blockbuster/src/programs/account_closure/mod.rs create mode 100644 blockbuster/src/programs/bubblegum/mod.rs create mode 100644 blockbuster/src/programs/mod.rs create mode 100644 blockbuster/src/programs/mpl_core_program/mod.rs create mode 100644 blockbuster/src/programs/token_account/mod.rs create mode 100644 blockbuster/src/programs/token_extensions/extension.rs create mode 100644 blockbuster/src/programs/token_extensions/mod.rs create mode 100644 blockbuster/src/programs/token_metadata/mod.rs create mode 100644 blockbuster/tests/bubblegum_parser_test.rs create mode 100644 blockbuster/tests/fixtures/double_bubblegum_mint.json create mode 100644 blockbuster/tests/fixtures/helium_mint_double_tree.json create mode 100644 blockbuster/tests/fixtures/helium_nested.json create mode 100644 blockbuster/tests/helpers.rs create mode 100644 blockbuster/tests/instructions_test.rs diff --git a/.gitignore b/.gitignore index c396adfb8..8089db9f9 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,6 @@ dist test-ledger *.swp *error.log -programs *.iml skaffold-state.json test-programs diff --git a/Cargo.lock b/Cargo.lock index 8c2e4dd33..3a82309d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1008,24 +1008,29 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "blockbuster" version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa134b9b69102afd22e6895035eb891ecffe372b006c43cde006e7211b824c" dependencies = [ "anchor-lang", "async-trait", "borsh 0.10.3", "bs58 0.4.0", "bytemuck", + "flatbuffers", "lazy_static", "log", "mpl-bubblegum", "mpl-core", "mpl-token-metadata", + "plerkle_serialization", + "rand 0.8.5", "serde", + "serde_json", + "solana-client", + "solana-geyser-plugin-interface", "solana-sdk", "solana-transaction-status", "solana-zk-token-sdk", "spl-account-compression", + "spl-concurrent-merkle-tree", "spl-noop", "spl-pod", "spl-token", @@ -5676,6 +5681,18 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "solana-geyser-plugin-interface" +version = "1.17.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f633425dc9409c6d3f019658b90bb1ad53747ed55acd45e0a18f58b95a0b89e5" +dependencies = [ + "log", + "solana-sdk", + "solana-transaction-status", + "thiserror", +] + [[package]] name = "solana-logger" version = "1.17.28" diff --git a/Cargo.toml b/Cargo.toml index ccc5f8048..fa58a9303 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "blockbuster", "core", "das_api", "digital_asset_types", @@ -31,17 +32,18 @@ anyhow = "1.0.75" async-std = "1.0.0" async-trait = "0.1.60" backon = "0.4.1" -blockbuster = "2.3.0" +blockbuster = {path = "blockbuster"} borsh = "~0.10.3" borsh-derive = "~0.10.3" bs58 = "0.4.0" +bytemuck = {version = "1.14.0", features = ["derive"]} cadence = "0.29.0" cadence-macros = "0.29.0" chrono = "0.4.19" clap = "4.2.2" -das_api = { path = "das_api" } -das-core = { path = "core" } -digital_asset_types = { path = "digital_asset_types" } +das-core = {path = "core"} +das_api = {path = "das_api"} +digital_asset_types = {path = "digital_asset_types"} enum-iterator = "1.2.0" enum-iterator-derive = "1.1.0" env_logger = "0.10.0" @@ -63,11 +65,12 @@ jsonrpsee-core = "0.16.2" lazy_static = "1.4.0" log = "0.4.17" metrics = "0.20.1" -migration = { path = "migration" } +migration = {path = "migration"} mime_guess = "2.0.4" mpl-bubblegum = "1.2.0" +mpl-core = {version = "0.5.0", features = ["serde"]} mpl-token-metadata = "4.1.1" -nft_ingester = { path = "nft_ingester" } +nft_ingester = {path = "nft_ingester"} num-derive = "0.3.3" num-traits = "0.2.15" once_cell = "1.19.0" @@ -75,7 +78,7 @@ open-rpc-derive = "0.0.4" open-rpc-schema = "0.0.4" plerkle_messenger = "1.6.0" plerkle_serialization = "1.8.0" -program_transformers = { path = "program_transformers" } +program_transformers = {path = "program_transformers"} prometheus = "0.13.3" proxy-wasm = "0.2.0" rand = "0.8.5" @@ -92,14 +95,20 @@ serde_json = "1.0.81" serial_test = "2.0.0" solana-account-decoder = "~1.17" solana-client = "~1.17" +solana-geyser-plugin-interface = "~1.17" solana-program = "~1.17" solana-sdk = "~1.17" solana-transaction-status = "~1.17" +solana-zk-token-sdk = "1.17.16" spl-account-compression = "0.3.0" spl-associated-token-account = ">= 1.1.3, < 3.0" spl-concurrent-merkle-tree = "0.2.0" spl-noop = "0.2.0" +spl-pod = {version = "0.1.0", features = ["serde-traits"]} spl-token = ">= 3.5.0, < 5.0" +spl-token-2022 = {version = "1.0", features = ["no-entrypoint"]} +spl-token-group-interface = "0.1.0" +spl-token-metadata-interface = "0.2.0" sqlx = "0.6.2" stretto = "0.7.2" thiserror = "1.0.31" @@ -109,7 +118,7 @@ tower = "0.4.13" tower-http = "0.3.5" tracing = "0.1.35" tracing-subscriber = "0.3.16" -txn_forwarder = { path = "tools/txn_forwarder" } +txn_forwarder = {path = "tools/txn_forwarder"} url = "2.3.1" wasi = "0.7.0" wasm-bindgen = "0.2.83" diff --git a/blockbuster/.gitignore b/blockbuster/.gitignore new file mode 100644 index 000000000..28aaccd5c --- /dev/null +++ b/blockbuster/.gitignore @@ -0,0 +1,11 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + + +# These are backup files generated by rustfmt +**/*.rs.bk + +*.iml +blockbuster/target +target/ diff --git a/blockbuster/Cargo.toml b/blockbuster/Cargo.toml new file mode 100644 index 000000000..e110009ea --- /dev/null +++ b/blockbuster/Cargo.toml @@ -0,0 +1,42 @@ +[package] +authors = ["Metaplex Developers "] +description = "Metaplex canonical program parsers, for indexing, analytics etc...." +edition = "2021" +license = "AGPL-3.0" +name = "blockbuster" +readme = "../README.md" +repository = "https://github.com/metaplex-foundation/blockbuster" +version = "2.3.0" + +[dependencies] +anchor-lang = {workspace = true} +async-trait = {workspace = true} +borsh = {workspace = true} +bs58 = {workspace = true} +bytemuck = {workspace = true} +lazy_static = {workspace = true} +log = {workspace = true} +mpl-bubblegum = {workspace = true} +mpl-core = {workspace = true, features = ["serde"]} +mpl-token-metadata = {workspace = true, features = ["serde"]} +serde = {workspace = true} +solana-sdk = {workspace = true} +solana-transaction-status = {workspace = true} +solana-zk-token-sdk = {workspace = true} +spl-account-compression = {workspace = true, features = ["no-entrypoint"]} +spl-noop = {workspace = true, features = ["no-entrypoint"]} +spl-pod = {workspace = true, features = ["serde-traits"]} +spl-token = {workspace = true, features = ["no-entrypoint"]} +spl-token-2022 = {workspace = true, features = ["no-entrypoint"]} +spl-token-group-interface = {workspace = true} +spl-token-metadata-interface = {workspace = true} +thiserror = {workspace = true} + +[dev-dependencies] +flatbuffers = {workspace = true} +plerkle_serialization = {workspace = true} +rand = {workspace = true} +serde_json = {workspace = true} +solana-client = {workspace = true} +solana-geyser-plugin-interface = {workspace = true} +spl-concurrent-merkle-tree = {workspace = true} diff --git a/blockbuster/README.md b/blockbuster/README.md new file mode 100644 index 000000000..896d8bbc5 --- /dev/null +++ b/blockbuster/README.md @@ -0,0 +1,22 @@ +# BlockBuster + +BlockBuster -> "Busting Solana blocks into little pieces to index and operate on the programs therein" - Noone - 1995 + +This repository is the home for Metaplex Program Parsers. Program parsers are canonical libraries that take a transaction or account update from a geyser plugin and parse them correctly according to Metaplex smart contracts. This sort of parsing is hard to automate as it must contain some knowledge of the API structure of the contract which is not fully describable yet via IDLs. Things like remaining accounts, optional accounts and complex instruction data are not always 100% clear what they mean without knowledge of the contract. + +## Mode of Operation +This library works best as a consumer of messages sent via a geyser plugin using the [Plerkle Serialization](https://github.com/metaplex-foundation/digital-asset-validator-plugin) library by metaplex. The types from that library are FlatBuffer based currently, and are the wire format of messages coming out of Plerkle into the rest of the infrastructure. +For more information about Plerkle and the [Digital Asset RPC infrastructure](https://github.com/metaplex-foundation/digital-asset-validator-plugin) It can however be used in any general programs provided you can create the data in the FlatBuffer types. + +## Scope + +This library contains parsers for the following programs and the parsers are specific to how these contracts relate to metaplex assets. + +* Gummyroll (Solana) +* Bubblegum (Metaplex) +* Spl Token (Solana) +* Token Metadata (Metaplex) +* Auction House (Metaplex) +* Candy Machine (Metaplex) +* Hydra (Metaplex) + diff --git a/blockbuster/src/error.rs b/blockbuster/src/error.rs new file mode 100644 index 000000000..199eb3bb2 --- /dev/null +++ b/blockbuster/src/error.rs @@ -0,0 +1,34 @@ +use std::io::Error; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum BlockbusterError { + #[error("Instruction Data Parsing Error")] + InstructionParsingError, + #[error("IO Error {0}")] + IOError(String), + #[error("Could not deserialize data")] + DeserializationError, + #[error("Missing Bubblegum event data")] + MissingBubblegumEventData, + #[error("Data length is invalid.")] + InvalidDataLength, + #[error("Unknown anchor account discriminator.")] + UnknownAccountDiscriminator, + #[error("Account type is not valid")] + InvalidAccountType, + #[error("Master edition version is invalid")] + FailedToDeserializeToMasterEdition, + #[error("Uninitialized account type")] + UninitializedAccount, + #[error("Account Type Not implemented")] + AccountTypeNotImplemented, + #[error("Could not deserialize data: {0}")] + CustomDeserializationError(String), +} + +impl From for BlockbusterError { + fn from(err: Error) -> Self { + BlockbusterError::IOError(err.to_string()) + } +} diff --git a/blockbuster/src/instruction.rs b/blockbuster/src/instruction.rs new file mode 100644 index 000000000..4345e29ed --- /dev/null +++ b/blockbuster/src/instruction.rs @@ -0,0 +1,89 @@ +use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; +use solana_transaction_status::InnerInstructions; +use std::collections::{HashSet, VecDeque}; + +pub type IxPair<'a> = (Pubkey, &'a CompiledInstruction); + +#[derive(Debug, Clone, Copy)] +pub struct InstructionBundle<'a> { + pub txn_id: &'a str, + pub program: Pubkey, + pub instruction: Option<&'a CompiledInstruction>, + pub inner_ix: Option<&'a [IxPair<'a>]>, + pub keys: &'a [Pubkey], + pub slot: u64, +} + +impl<'a> Default for InstructionBundle<'a> { + fn default() -> Self { + InstructionBundle { + txn_id: "", + program: Pubkey::new_from_array([0; 32]), + instruction: None, + inner_ix: None, + keys: &[], + slot: 0, + } + } +} + +pub fn order_instructions<'a>( + programs: &HashSet, + account_keys: &[Pubkey], + message_instructions: &'a [CompiledInstruction], + meta_inner_instructions: &'a [InnerInstructions], +) -> VecDeque<(IxPair<'a>, Option>>)> { + let mut ordered_ixs: VecDeque<(IxPair, Option>)> = VecDeque::new(); + + // Get inner instructions. + for (outer_instruction_index, message_instruction) in message_instructions.iter().enumerate() { + let non_hoisted_inner_instruction = meta_inner_instructions + .iter() + .filter_map(|ix| { + (ix.index == outer_instruction_index as u8).then_some(&ix.instructions) + }) + .flatten() + .map(|inner_ix| { + let cix = &inner_ix.instruction; + (account_keys[cix.program_id_index as usize], cix) + }) + .collect::>(); + + let hoisted = hoist_known_programs(programs, &non_hoisted_inner_instruction); + ordered_ixs.extend(hoisted); + + if let Some(outer_program_id) = + account_keys.get(message_instruction.program_id_index as usize) + { + if programs.contains(outer_program_id) { + ordered_ixs.push_back(( + (*outer_program_id, message_instruction), + Some(non_hoisted_inner_instruction), + )); + } + } else { + eprintln!("outer program id deserialization error"); + } + } + ordered_ixs +} + +fn hoist_known_programs<'a>( + programs: &HashSet, + ix_pairs: &[IxPair<'a>], +) -> Vec<(IxPair<'a>, Option>>)> { + ix_pairs + .iter() + .enumerate() + .filter(|&(_index, &(pid, _ci))| programs.contains(&pid)) + .map(|(index, &(pid, ci))| { + let inner_copy = ix_pairs + .iter() + .skip(index + 1) + .take_while(|&&(inner_pid, _)| inner_pid != pid) + .cloned() + .collect::>>(); + ((pid, ci), Some(inner_copy)) + }) + .collect() +} diff --git a/blockbuster/src/lib.rs b/blockbuster/src/lib.rs new file mode 100644 index 000000000..983c72897 --- /dev/null +++ b/blockbuster/src/lib.rs @@ -0,0 +1,7 @@ +pub mod error; +pub mod instruction; +pub mod program_handler; +pub mod programs; + +pub use mpl_core; +pub use mpl_token_metadata as token_metadata; diff --git a/blockbuster/src/parsed_programs.rs b/blockbuster/src/parsed_programs.rs new file mode 100644 index 000000000..c14db38a1 --- /dev/null +++ b/blockbuster/src/parsed_programs.rs @@ -0,0 +1,9 @@ +pub enum Program { + Bubblegum { + parser: bubblegum::BubblegumParser, + instruction_result: BubblegumInstruction, + account_result: (), + }, +} + +impl ProgramParser for Program {} diff --git a/blockbuster/src/program_handler.rs b/blockbuster/src/program_handler.rs new file mode 100644 index 000000000..ac7bb3a49 --- /dev/null +++ b/blockbuster/src/program_handler.rs @@ -0,0 +1,52 @@ +use crate::{ + error::BlockbusterError, instruction::InstructionBundle, programs::ProgramParseResult, +}; +use solana_sdk::pubkey::Pubkey; + +pub trait ParseResult: Sync + Send { + fn result_type(&self) -> ProgramParseResult; + + fn result(&self) -> &Self + where + Self: Sized, + { + self + } +} + +pub struct NotUsed(()); + +impl NotUsed { + pub fn new() -> Self { + NotUsed(()) + } +} + +impl Default for NotUsed { + fn default() -> Self { + Self::new() + } +} + +impl ParseResult for NotUsed { + fn result_type(&self) -> ProgramParseResult { + ProgramParseResult::Unknown + } +} + +pub trait ProgramParser: Sync + Send { + fn key(&self) -> Pubkey; + fn key_match(&self, key: &Pubkey) -> bool; + fn handles_instructions(&self) -> bool; + fn handles_account_updates(&self) -> bool; + fn handle_account( + &self, + _account_data: &[u8], + ) -> Result, BlockbusterError>; + fn handle_instruction( + &self, + _bundle: &InstructionBundle, + ) -> Result, BlockbusterError> { + Ok(Box::new(NotUsed::new())) + } +} diff --git a/blockbuster/src/programs/account_closure/mod.rs b/blockbuster/src/programs/account_closure/mod.rs new file mode 100644 index 000000000..7586b7d22 --- /dev/null +++ b/blockbuster/src/programs/account_closure/mod.rs @@ -0,0 +1,73 @@ +use crate::{ + error::BlockbusterError, + program_handler::{ParseResult, ProgramParser}, + programs::ProgramParseResult, +}; +use solana_sdk::{pubkey::Pubkey, pubkeys}; + +use plerkle_serialization::AccountInfo; + +pubkeys!(solana_program_id, "11111111111111111111111111111111"); + +pub struct ClosedAccountInfo { + pub pubkey: Vec, + pub owner: Vec, +} + +#[allow(clippy::large_enum_variant)] +pub enum AccountClosureData { + ClosedAccountInfo(ClosedAccountInfo), + EmptyAccount, +} + +impl ParseResult for AccountClosureData { + fn result(&self) -> &Self + where + Self: Sized, + { + self + } + fn result_type(&self) -> ProgramParseResult { + ProgramParseResult::AccountClosure(self) + } +} + +pub struct AccountClosureParser; + +impl ProgramParser for AccountClosureParser { + fn key(&self) -> Pubkey { + solana_program_id() + } + fn key_match(&self, key: &Pubkey) -> bool { + key == &solana_program_id() + } + + fn handles_account_updates(&self) -> bool { + true + } + + fn handles_instructions(&self) -> bool { + false + } + + fn handle_account( + &self, + account_info: &AccountInfo, + ) -> Result, BlockbusterError> { + let account_data: ClosedAccountInfo = match (account_info.pubkey(), account_info.owner()) { + (Some(pubkey), Some(owner)) => ClosedAccountInfo { + pubkey: pubkey.0.to_vec(), + owner: owner.0.to_vec(), + }, + _ => return Ok(Box::new(AccountClosureData::EmptyAccount)), + }; + + if account_info.lamports() == 0 { + Ok(Box::new(AccountClosureData::ClosedAccountInfo( + account_data, + ))) + } else { + Ok(Box::new(AccountClosureData::EmptyAccount)) + } + } +} diff --git a/blockbuster/src/programs/bubblegum/mod.rs b/blockbuster/src/programs/bubblegum/mod.rs new file mode 100644 index 000000000..faa1b8aa1 --- /dev/null +++ b/blockbuster/src/programs/bubblegum/mod.rs @@ -0,0 +1,301 @@ +use crate::{ + error::BlockbusterError, + instruction::InstructionBundle, + program_handler::{NotUsed, ParseResult, ProgramParser}, + programs::ProgramParseResult, +}; +use borsh::de::BorshDeserialize; +use log::warn; +use mpl_bubblegum::{ + get_instruction_type, + instructions::{ + UnverifyCreatorInstructionArgs, UpdateMetadataInstructionArgs, VerifyCreatorInstructionArgs, + }, + types::{BubblegumEventType, MetadataArgs, UpdateArgs}, +}; +pub use mpl_bubblegum::{ + types::{LeafSchema, UseMethod}, + InstructionName, LeafSchemaEvent, ID, +}; +use solana_sdk::pubkey::Pubkey; +pub use spl_account_compression::events::{ + AccountCompressionEvent::{self, ApplicationData, ChangeLog}, + ApplicationDataEvent, ChangeLogEvent, ChangeLogEventV1, +}; + +use spl_noop; + +#[derive(Eq, PartialEq)] +pub enum Payload { + Unknown, + MintV1 { + args: MetadataArgs, + authority: Pubkey, + tree_id: Pubkey, + }, + Decompress { + args: MetadataArgs, + }, + CancelRedeem { + root: Pubkey, + }, + CreatorVerification { + metadata: MetadataArgs, + creator: Pubkey, + verify: bool, + }, + CollectionVerification { + collection: Pubkey, + verify: bool, + }, + UpdateMetadata { + current_metadata: MetadataArgs, + update_args: UpdateArgs, + tree_id: Pubkey, + }, +} +//TODO add more of the parsing here to minimize program transformer code +pub struct BubblegumInstruction { + pub instruction: InstructionName, + pub tree_update: Option, + pub leaf_update: Option, + pub payload: Option, +} + +impl BubblegumInstruction { + pub fn new(ix: InstructionName) -> Self { + BubblegumInstruction { + instruction: ix, + tree_update: None, + leaf_update: None, + payload: None, + } + } +} + +impl ParseResult for BubblegumInstruction { + fn result_type(&self) -> ProgramParseResult { + ProgramParseResult::Bubblegum(self) + } + fn result(&self) -> &Self + where + Self: Sized, + { + self + } +} + +pub struct BubblegumParser; + +impl ProgramParser for BubblegumParser { + fn key(&self) -> Pubkey { + ID + } + + fn key_match(&self, key: &Pubkey) -> bool { + key == &ID + } + fn handles_account_updates(&self) -> bool { + false + } + + fn handles_instructions(&self) -> bool { + true + } + fn handle_account( + &self, + _account_data: &[u8], + ) -> Result, BlockbusterError> { + Ok(Box::new(NotUsed::new())) + } + + fn handle_instruction( + &self, + bundle: &InstructionBundle, + ) -> Result, BlockbusterError> { + let InstructionBundle { + txn_id, + instruction, + inner_ix, + keys, + .. + } = bundle; + let outer_ix_data = match instruction { + Some(cix) => cix.data.as_ref(), + _ => return Err(BlockbusterError::DeserializationError), + }; + let ix_type = get_instruction_type(outer_ix_data); + let mut b_inst = BubblegumInstruction::new(ix_type); + if let Some(ixs) = inner_ix { + for (pid, cix) in ixs.iter() { + if pid == &spl_noop::id() && !cix.data.is_empty() { + match AccountCompressionEvent::try_from_slice(&cix.data) { + Ok(result) => match result { + ChangeLog(changelog_event) => { + let ChangeLogEvent::V1(changelog_event) = changelog_event; + b_inst.tree_update = Some(changelog_event); + } + ApplicationData(app_data) => { + let ApplicationDataEvent::V1(app_data) = app_data; + let app_data = app_data.application_data; + + let event_type_byte = if !app_data.is_empty() { + &app_data[0..1] + } else { + return Err(BlockbusterError::DeserializationError); + }; + + match BubblegumEventType::try_from_slice(event_type_byte)? { + BubblegumEventType::Uninitialized => { + return Err(BlockbusterError::MissingBubblegumEventData); + } + BubblegumEventType::LeafSchemaEvent => { + b_inst.leaf_update = + Some(LeafSchemaEvent::try_from_slice(&app_data)?); + } + } + } + }, + Err(e) => { + warn!( + "Error while deserializing txn {:?} with noop data: {:?}", + txn_id, e + ); + } + } + } + } + } + + if outer_ix_data.len() >= 8 { + let ix_data = &outer_ix_data[8..]; + if !ix_data.is_empty() { + match b_inst.instruction { + InstructionName::MintV1 => { + b_inst.payload = Some(build_mint_v1_payload(keys, ix_data, false)?); + } + + InstructionName::MintToCollectionV1 => { + b_inst.payload = Some(build_mint_v1_payload(keys, ix_data, true)?); + } + InstructionName::DecompressV1 => { + let args: MetadataArgs = MetadataArgs::try_from_slice(ix_data)?; + b_inst.payload = Some(Payload::Decompress { args }); + } + InstructionName::CancelRedeem => { + let slice: [u8; 32] = ix_data + .try_into() + .map_err(|_e| BlockbusterError::InstructionParsingError)?; + let root = Pubkey::new_from_array(slice); + b_inst.payload = Some(Payload::CancelRedeem { root }); + } + InstructionName::VerifyCreator => { + b_inst.payload = + Some(build_creator_verification_payload(keys, ix_data, true)?); + } + InstructionName::UnverifyCreator => { + b_inst.payload = + Some(build_creator_verification_payload(keys, ix_data, false)?); + } + InstructionName::VerifyCollection | InstructionName::SetAndVerifyCollection => { + b_inst.payload = Some(build_collection_verification_payload(keys, true)?); + } + InstructionName::UnverifyCollection => { + b_inst.payload = Some(build_collection_verification_payload(keys, false)?); + } + InstructionName::UpdateMetadata => { + b_inst.payload = Some(build_update_metadata_payload(keys, ix_data)?); + } + _ => {} + }; + } + } + + Ok(Box::new(b_inst)) + } +} + +// See Bubblegum documentation for offsets and positions: +// https://github.com/metaplex-foundation/mpl-bubblegum/blob/main/programs/bubblegum/README.md#-verify_creator-and-unverify_creator +fn build_creator_verification_payload( + keys: &[Pubkey], + ix_data: &[u8], + verify: bool, +) -> Result { + let metadata = if verify { + VerifyCreatorInstructionArgs::try_from_slice(ix_data)?.metadata + } else { + UnverifyCreatorInstructionArgs::try_from_slice(ix_data)?.metadata + }; + + let creator = *keys + .get(5) + .ok_or(BlockbusterError::InstructionParsingError)?; + + Ok(Payload::CreatorVerification { + metadata, + creator, + verify, + }) +} + +// See Bubblegum for offsets and positions: +// https://github.com/metaplex-foundation/mpl-bubblegum/blob/main/programs/bubblegum/README.md#-verify_collection-unverify_collection-and-set_and_verify_collection +// This uses the account. The collection is only provided as an argument for `set_and_verify_collection`. +fn build_collection_verification_payload( + keys: &[Pubkey], + verify: bool, +) -> Result { + let collection = *keys + .get(8) + .ok_or(BlockbusterError::InstructionParsingError)?; + Ok(Payload::CollectionVerification { collection, verify }) +} + +// See Bubblegum for offsets and positions: +// https://github.com/metaplex-foundation/mpl-bubblegum/blob/main/programs/bubblegum/README.md +fn build_mint_v1_payload( + keys: &[Pubkey], + ix_data: &[u8], + set_verify: bool, +) -> Result { + let mut args: MetadataArgs = MetadataArgs::try_from_slice(ix_data)?; + if set_verify { + if let Some(ref mut col) = args.collection { + col.verified = true; + } + } + + let authority = *keys + .first() + .ok_or(BlockbusterError::InstructionParsingError)?; + + let tree_id = *keys + .get(3) + .ok_or(BlockbusterError::InstructionParsingError)?; + + Ok(Payload::MintV1 { + args, + authority, + tree_id, + }) +} + +// See Bubblegum for offsets and positions: +// https://github.com/metaplex-foundation/mpl-bubblegum/blob/main/programs/bubblegum/README.md +fn build_update_metadata_payload( + keys: &[Pubkey], + ix_data: &[u8], +) -> Result { + let args = UpdateMetadataInstructionArgs::try_from_slice(ix_data)?; + + let tree_id = *keys + .get(8) + .ok_or(BlockbusterError::InstructionParsingError)?; + + Ok(Payload::UpdateMetadata { + current_metadata: args.current_metadata, + update_args: args.update_args, + tree_id, + }) +} diff --git a/blockbuster/src/programs/mod.rs b/blockbuster/src/programs/mod.rs new file mode 100644 index 000000000..8da2feed0 --- /dev/null +++ b/blockbuster/src/programs/mod.rs @@ -0,0 +1,34 @@ +use bubblegum::BubblegumInstruction; +use mpl_core_program::MplCoreAccountState; +use token_account::TokenProgramAccount; +use token_extensions::TokenExtensionsProgramAccount; +use token_metadata::TokenMetadataAccountState; + +pub mod bubblegum; +pub mod mpl_core_program; +pub mod token_account; +pub mod token_extensions; +pub mod token_metadata; + +// Note: `ProgramParseResult` used to contain the following variants that have been deprecated and +// removed from blockbuster since the `version-1.16` tag: +// CandyGuard(&'a CandyGuardAccountData), +// CandyMachine(&'a CandyMachineAccountData), +// CandyMachineCore(&'a CandyMachineCoreAccountData), +// +// Candy Machine V3 parsing was removed because Candy Guard (`mpl-candy-guard`) and +// Candy Machine Core (`mpl-candy-machine-core`) were dependent upon a specific Solana +// version (1.16), there was no Candy Machine parsing in DAS (`digital-asset-rpc-infrastructure`), +// and we wanted to use the Rust clients for Bubblegum and Token Metadata so that going forward we +// could more easily update blockbuster to new Solana versions. +// +// Candy Machine V2 (`mpl-candy-machine`) parsing was removed at the same time as V3 because even +// though it did not depend on the `mpl-candy-machine` crate, it was also not being used by DAS. +pub enum ProgramParseResult<'a> { + Bubblegum(&'a BubblegumInstruction), + MplCore(&'a MplCoreAccountState), + TokenMetadata(&'a TokenMetadataAccountState), + TokenProgramAccount(&'a TokenProgramAccount), + TokenExtensionsProgramAccount(&'a TokenExtensionsProgramAccount), + Unknown, +} diff --git a/blockbuster/src/programs/mpl_core_program/mod.rs b/blockbuster/src/programs/mpl_core_program/mod.rs new file mode 100644 index 000000000..cafbd0a79 --- /dev/null +++ b/blockbuster/src/programs/mpl_core_program/mod.rs @@ -0,0 +1,92 @@ +use crate::{ + error::BlockbusterError, + program_handler::{ParseResult, ProgramParser}, + programs::ProgramParseResult, +}; +use borsh::BorshDeserialize; +use mpl_core::{types::Key, IndexableAsset}; +use solana_sdk::{pubkey::Pubkey, pubkeys}; + +pubkeys!(mpl_core_id, "CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d"); + +#[derive(Clone, Debug, PartialEq)] +pub enum MplCoreAccountData { + Asset(IndexableAsset), + Collection(IndexableAsset), + HashedAsset, + EmptyAccount, +} + +pub struct MplCoreAccountState { + pub key: Key, + pub data: MplCoreAccountData, +} + +impl ParseResult for MplCoreAccountState { + fn result(&self) -> &Self + where + Self: Sized, + { + self + } + fn result_type(&self) -> ProgramParseResult { + ProgramParseResult::MplCore(self) + } +} + +pub struct MplCoreParser; + +impl ProgramParser for MplCoreParser { + fn key(&self) -> Pubkey { + mpl_core_id() + } + fn key_match(&self, key: &Pubkey) -> bool { + key == &mpl_core_id() + } + + fn handles_account_updates(&self) -> bool { + true + } + + fn handles_instructions(&self) -> bool { + false + } + + fn handle_account( + &self, + account_data: &[u8], + ) -> Result, BlockbusterError> { + if account_data.is_empty() { + return Ok(Box::new(MplCoreAccountState { + key: Key::Uninitialized, + data: MplCoreAccountData::EmptyAccount, + })); + } + let key = Key::try_from_slice(&account_data[0..1])?; + let mpl_core_account_state = match key { + Key::AssetV1 => { + let indexable_asset = IndexableAsset::fetch(key, account_data)?; + MplCoreAccountState { + key, + data: MplCoreAccountData::Asset(indexable_asset), + } + } + Key::CollectionV1 => { + let indexable_asset = IndexableAsset::fetch(key, account_data)?; + MplCoreAccountState { + key, + data: MplCoreAccountData::Collection(indexable_asset), + } + } + Key::Uninitialized => MplCoreAccountState { + key: Key::Uninitialized, + data: MplCoreAccountData::EmptyAccount, + }, + _ => { + return Err(BlockbusterError::AccountTypeNotImplemented); + } + }; + + Ok(Box::new(mpl_core_account_state)) + } +} diff --git a/blockbuster/src/programs/token_account/mod.rs b/blockbuster/src/programs/token_account/mod.rs new file mode 100644 index 000000000..298fa3336 --- /dev/null +++ b/blockbuster/src/programs/token_account/mod.rs @@ -0,0 +1,77 @@ +use crate::{ + error::BlockbusterError, + program_handler::{ParseResult, ProgramParser}, + programs::ProgramParseResult, +}; +use solana_sdk::{program_pack::Pack, pubkey::Pubkey, pubkeys}; +use spl_token::state::{Account as TokenAccount, Mint}; + +pubkeys!( + token_program_id, + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" +); + +pub struct TokenAccountParser; + +pub enum TokenProgramAccount { + Mint(Mint), + TokenAccount(TokenAccount), +} + +impl ParseResult for TokenProgramAccount { + fn result(&self) -> &Self + where + Self: Sized, + { + self + } + fn result_type(&self) -> ProgramParseResult { + ProgramParseResult::TokenProgramAccount(self) + } +} + +impl ProgramParser for TokenAccountParser { + fn key(&self) -> Pubkey { + token_program_id() + } + fn key_match(&self, key: &Pubkey) -> bool { + key == &token_program_id() + } + fn handles_account_updates(&self) -> bool { + true + } + + fn handles_instructions(&self) -> bool { + false + } + fn handle_account( + &self, + account_data: &[u8], + ) -> Result, BlockbusterError> { + let account_type = match account_data.len() { + 165 => { + let token_account = TokenAccount::unpack(account_data).map_err(|_| { + BlockbusterError::CustomDeserializationError( + "Token Account Unpack Failed".to_string(), + ) + })?; + + TokenProgramAccount::TokenAccount(token_account) + } + 82 => { + let mint = Mint::unpack(account_data).map_err(|_| { + BlockbusterError::CustomDeserializationError( + "Token MINT Unpack Failed".to_string(), + ) + })?; + + TokenProgramAccount::Mint(mint) + } + _ => { + return Err(BlockbusterError::InvalidDataLength); + } + }; + + Ok(Box::new(account_type)) + } +} diff --git a/blockbuster/src/programs/token_extensions/extension.rs b/blockbuster/src/programs/token_extensions/extension.rs new file mode 100644 index 000000000..c9a0b9b4a --- /dev/null +++ b/blockbuster/src/programs/token_extensions/extension.rs @@ -0,0 +1,542 @@ +use bytemuck::Zeroable; +use serde::{Deserialize, Serialize}; +use solana_zk_token_sdk::zk_token_elgamal::pod::{AeCiphertext, ElGamalCiphertext, ElGamalPubkey}; +use spl_pod::{ + optional_keys::{OptionalNonZeroElGamalPubkey, OptionalNonZeroPubkey}, + primitives::{PodBool, PodI64, PodU16, PodU32, PodU64}, +}; + +use spl_token_2022::extension::{ + confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint}, + confidential_transfer_fee::{ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig}, + cpi_guard::CpiGuard, + default_account_state::DefaultAccountState, + group_member_pointer::GroupMemberPointer, + group_pointer::GroupPointer, + immutable_owner::ImmutableOwner, + interest_bearing_mint::{BasisPoints, InterestBearingConfig}, + memo_transfer::MemoTransfer, + metadata_pointer::MetadataPointer, + mint_close_authority::MintCloseAuthority, + permanent_delegate::PermanentDelegate, + transfer_fee::{TransferFee, TransferFeeAmount, TransferFeeConfig}, + transfer_hook::TransferHook, +}; +use std::fmt; + +use spl_token_group_interface::state::{TokenGroup, TokenGroupMember}; +use spl_token_metadata_interface::state::TokenMetadata; + +const AE_CIPHERTEXT_LEN: usize = 36; +const UNIT_LEN: usize = 32; +const RISTRETTO_POINT_LEN: usize = UNIT_LEN; +pub(crate) const DECRYPT_HANDLE_LEN: usize = RISTRETTO_POINT_LEN; +pub(crate) const PEDERSEN_COMMITMENT_LEN: usize = RISTRETTO_POINT_LEN; +const ELGAMAL_PUBKEY_LEN: usize = RISTRETTO_POINT_LEN; +const ELGAMAL_CIPHERTEXT_LEN: usize = PEDERSEN_COMMITMENT_LEN + DECRYPT_HANDLE_LEN; +type PodAccountState = u8; +pub type UnixTimestamp = PodI64; +pub type EncryptedBalance = ShadowElGamalCiphertext; +pub type DecryptableBalance = ShadowAeCiphertext; +pub type EncryptedWithheldAmount = ShadowElGamalCiphertext; + +use serde::{ + de::{self, SeqAccess, Visitor}, + Deserializer, Serializer, +}; + +/// Bs58 encoded public key string. Used for storing Pubkeys in a human readable format. +/// Ideally we'd store them as is in the DB and later convert them to bs58 for display on the API. +/// But, +/// - We currently store them in DB as JSONB. +/// - `Pubkey` serializes to an u8 vector, unlike sth like `OptionalNonZeroElGamalPubkey` which serializes to a string. +/// So `Pubkey` is stored as a u8 vector in the DB. +/// - `Pubkey` doesn't implement something like `schemars::JsonSchema` so we can't convert them back to the rust struct either. +type PublicKeyString = String; + +struct ShadowAeCiphertextVisitor; + +struct ShadowElGamalCiphertextVisitor; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ShadowAeCiphertext(pub [u8; AE_CIPHERTEXT_LEN]); + +#[derive(Clone, Copy, Debug, PartialEq, Zeroable)] +pub struct ShadowElGamalCiphertext(pub [u8; ELGAMAL_CIPHERTEXT_LEN]); + +#[derive(Clone, Copy, Debug, Default, Zeroable, PartialEq, Eq, Serialize, Deserialize)] +pub struct ShadowElGamalPubkey(pub [u8; ELGAMAL_PUBKEY_LEN]); + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowCpiGuard { + pub lock_cpi: PodBool, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowDefaultAccountState { + pub state: PodAccountState, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowImmutableOwner; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowInterestBearingConfig { + pub rate_authority: OptionalNonZeroPubkey, + pub initialization_timestamp: UnixTimestamp, + pub pre_update_average_rate: BasisPoints, + pub last_update_timestamp: UnixTimestamp, + pub current_rate: BasisPoints, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowMemoTransfer { + /// Require transfers into this account to be accompanied by a memo + pub require_incoming_transfer_memos: PodBool, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowMetadataPointer { + pub authority: OptionalNonZeroPubkey, + pub metadata_address: OptionalNonZeroPubkey, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowGroupMemberPointer { + pub authority: OptionalNonZeroPubkey, + pub member_address: OptionalNonZeroPubkey, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowGroupPointer { + /// Authority that can set the group address + pub authority: OptionalNonZeroPubkey, + /// Account address that holds the group + pub group_address: OptionalNonZeroPubkey, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct ShadowTokenGroup { + /// The authority that can sign to update the group + pub update_authority: OptionalNonZeroPubkey, + /// The associated mint, used to counter spoofing to be sure that group + /// belongs to a particular mint + pub mint: PublicKeyString, + /// The current number of group members + pub size: PodU32, + /// The maximum number of group members + pub max_size: PodU32, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct ShadowTokenGroupMember { + /// The associated mint, used to counter spoofing to be sure that member + /// belongs to a particular mint + pub mint: PublicKeyString, + /// The pubkey of the `TokenGroup` + pub group: PublicKeyString, + /// The member number + pub member_number: PodU32, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowMintCloseAuthority { + pub close_authority: OptionalNonZeroPubkey, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct NonTransferableAccount; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowPermanentDelegate { + pub delegate: OptionalNonZeroPubkey, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowTransferFee { + pub epoch: PodU64, + pub maximum_fee: PodU64, + pub transfer_fee_basis_points: PodU16, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowTransferHook { + pub authority: OptionalNonZeroPubkey, + pub program_id: OptionalNonZeroPubkey, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowConfidentialTransferMint { + pub authority: OptionalNonZeroPubkey, + pub auto_approve_new_accounts: PodBool, + pub auditor_elgamal_pubkey: OptionalNonZeroElGamalPubkey, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct ShadowConfidentialTransferAccount { + pub approved: PodBool, + pub elgamal_pubkey: ShadowElGamalPubkey, + pub pending_balance_lo: EncryptedBalance, + pub pending_balance_hi: EncryptedBalance, + pub available_balance: EncryptedBalance, + pub decryptable_available_balance: DecryptableBalance, + pub allow_confidential_credits: PodBool, + pub allow_non_confidential_credits: PodBool, + pub pending_balance_credit_counter: PodU64, + pub maximum_pending_balance_credit_counter: PodU64, + pub expected_pending_balance_credit_counter: PodU64, + pub actual_pending_balance_credit_counter: PodU64, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowConfidentialTransferFeeConfig { + pub authority: OptionalNonZeroPubkey, + pub withdraw_withheld_authority_elgamal_pubkey: ShadowElGamalPubkey, + pub harvest_to_mint_enabled: PodBool, + pub withheld_amount: EncryptedWithheldAmount, +} + +pub struct ShadowConfidentialTransferFeeAmount { + pub withheld_amount: EncryptedWithheldAmount, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowTransferFeeAmount { + pub withheld_amount: PodU64, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Zeroable, Serialize, Deserialize)] +pub struct ShadowTransferFeeConfig { + pub transfer_fee_config_authority: OptionalNonZeroPubkey, + pub withdraw_withheld_authority: OptionalNonZeroPubkey, + pub withheld_amount: PodU64, + pub older_transfer_fee: ShadowTransferFee, + pub newer_transfer_fee: ShadowTransferFee, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct ShadowMetadata { + pub update_authority: OptionalNonZeroPubkey, + pub mint: PublicKeyString, + pub name: String, + pub symbol: String, + pub uri: String, + pub additional_metadata: Vec<(String, String)>, +} + +impl Serialize for ShadowAeCiphertext { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bytes(&self.0) + } +} + +impl<'de> Visitor<'de> for ShadowElGamalCiphertextVisitor { + type Value = ShadowElGamalCiphertext; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte array of length ELGAMAL_CIPHERTEXT_LEN") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: de::Error, + { + if v.len() == ELGAMAL_CIPHERTEXT_LEN { + let mut arr = [0u8; ELGAMAL_CIPHERTEXT_LEN]; + arr.copy_from_slice(v); + Ok(ShadowElGamalCiphertext(arr)) + } else { + Err(E::invalid_length(v.len(), &self)) + } + } +} + +impl<'de> Deserialize<'de> for ShadowElGamalCiphertext { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_bytes(ShadowElGamalCiphertextVisitor) + } +} + +impl Default for ShadowElGamalCiphertext { + fn default() -> Self { + ShadowElGamalCiphertext([0u8; ELGAMAL_CIPHERTEXT_LEN]) + } +} + +impl Default for ShadowAeCiphertext { + fn default() -> Self { + ShadowAeCiphertext([0u8; AE_CIPHERTEXT_LEN]) + } +} + +impl<'de> Visitor<'de> for ShadowAeCiphertextVisitor { + type Value = ShadowAeCiphertext; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a byte array of length AE_CIPHERTEXT_LEN") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut arr = [0u8; AE_CIPHERTEXT_LEN]; + for (i, item) in arr.iter_mut().enumerate().take(AE_CIPHERTEXT_LEN) { + *item = seq + .next_element()? + .ok_or(de::Error::invalid_length(i, &self))?; + } + Ok(ShadowAeCiphertext(arr)) + } +} + +impl<'de> Deserialize<'de> for ShadowAeCiphertext { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_tuple(AE_CIPHERTEXT_LEN, ShadowAeCiphertextVisitor) + } +} + +impl Serialize for ShadowElGamalCiphertext { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bytes(&self.0) + } +} + +impl From for ShadowAeCiphertext { + fn from(original: AeCiphertext) -> Self { + ShadowAeCiphertext(original.0) + } +} + +impl From for ShadowElGamalCiphertext { + fn from(original: ElGamalCiphertext) -> Self { + ShadowElGamalCiphertext(original.0) + } +} + +impl From for ShadowElGamalPubkey { + fn from(original: ElGamalPubkey) -> Self { + ShadowElGamalPubkey(original.0) + } +} + +impl From for ShadowCpiGuard { + fn from(original: CpiGuard) -> Self { + ShadowCpiGuard { + lock_cpi: original.lock_cpi, + } + } +} + +impl From for ShadowDefaultAccountState { + fn from(original: DefaultAccountState) -> Self { + ShadowDefaultAccountState { + state: original.state, + } + } +} + +impl From for ShadowImmutableOwner { + fn from(_: ImmutableOwner) -> Self { + ShadowImmutableOwner + } +} + +impl From for ShadowConfidentialTransferFeeAmount { + fn from(original: ConfidentialTransferFeeAmount) -> Self { + ShadowConfidentialTransferFeeAmount { + withheld_amount: original.withheld_amount.into(), + } + } +} + +impl From for ShadowTransferFeeAmount { + fn from(original: TransferFeeAmount) -> Self { + ShadowTransferFeeAmount { + withheld_amount: original.withheld_amount, + } + } +} + +impl From for ShadowMemoTransfer { + fn from(original: MemoTransfer) -> Self { + ShadowMemoTransfer { + require_incoming_transfer_memos: original.require_incoming_transfer_memos, + } + } +} + +impl From for ShadowMetadataPointer { + fn from(original: MetadataPointer) -> Self { + ShadowMetadataPointer { + authority: original.authority, + metadata_address: original.metadata_address, + } + } +} + +impl From for ShadowGroupPointer { + fn from(original: GroupPointer) -> Self { + ShadowGroupPointer { + authority: original.authority, + group_address: original.group_address, + } + } +} + +impl From for ShadowTokenGroup { + fn from(original: TokenGroup) -> Self { + ShadowTokenGroup { + update_authority: original.update_authority, + mint: bs58::encode(original.mint).into_string(), + size: original.size, + max_size: original.max_size, + } + } +} + +impl From for ShadowTokenGroupMember { + fn from(original: TokenGroupMember) -> Self { + ShadowTokenGroupMember { + mint: bs58::encode(original.mint).into_string(), + group: bs58::encode(original.group).into_string(), + member_number: original.member_number, + } + } +} + +impl From for ShadowGroupMemberPointer { + fn from(original: GroupMemberPointer) -> Self { + ShadowGroupMemberPointer { + authority: original.authority, + member_address: original.member_address, + } + } +} + +impl From for ShadowTransferFee { + fn from(original: TransferFee) -> Self { + ShadowTransferFee { + epoch: original.epoch, + maximum_fee: original.maximum_fee, + transfer_fee_basis_points: original.transfer_fee_basis_points, + } + } +} + +impl From for ShadowTransferFeeConfig { + fn from(original: TransferFeeConfig) -> Self { + ShadowTransferFeeConfig { + transfer_fee_config_authority: original.transfer_fee_config_authority, + withdraw_withheld_authority: original.withdraw_withheld_authority, + withheld_amount: original.withheld_amount, + older_transfer_fee: ShadowTransferFee::from(original.older_transfer_fee), + newer_transfer_fee: ShadowTransferFee::from(original.newer_transfer_fee), + } + } +} + +impl From for ShadowInterestBearingConfig { + fn from(original: InterestBearingConfig) -> Self { + ShadowInterestBearingConfig { + rate_authority: original.rate_authority, + initialization_timestamp: original.initialization_timestamp, + pre_update_average_rate: original.pre_update_average_rate, + last_update_timestamp: original.last_update_timestamp, + current_rate: original.current_rate, + } + } +} + +impl From for ShadowMintCloseAuthority { + fn from(original: MintCloseAuthority) -> Self { + ShadowMintCloseAuthority { + close_authority: original.close_authority, + } + } +} + +impl From for ShadowPermanentDelegate { + fn from(original: PermanentDelegate) -> Self { + ShadowPermanentDelegate { + delegate: original.delegate, + } + } +} + +impl From for ShadowTransferHook { + fn from(original: TransferHook) -> Self { + ShadowTransferHook { + authority: original.authority, + program_id: original.program_id, + } + } +} + +impl From for ShadowConfidentialTransferMint { + fn from(original: ConfidentialTransferMint) -> Self { + ShadowConfidentialTransferMint { + authority: original.authority, + auto_approve_new_accounts: original.auto_approve_new_accounts, + auditor_elgamal_pubkey: original.auditor_elgamal_pubkey, + } + } +} + +impl From for ShadowConfidentialTransferAccount { + fn from(original: ConfidentialTransferAccount) -> Self { + ShadowConfidentialTransferAccount { + approved: original.approved, + elgamal_pubkey: original.elgamal_pubkey.into(), + pending_balance_lo: original.pending_balance_lo.into(), + pending_balance_hi: original.pending_balance_hi.into(), + available_balance: original.available_balance.into(), + decryptable_available_balance: original.decryptable_available_balance.into(), + allow_confidential_credits: original.allow_confidential_credits, + allow_non_confidential_credits: original.allow_non_confidential_credits, + pending_balance_credit_counter: original.pending_balance_credit_counter, + maximum_pending_balance_credit_counter: original.maximum_pending_balance_credit_counter, + expected_pending_balance_credit_counter: original + .expected_pending_balance_credit_counter, + actual_pending_balance_credit_counter: original.actual_pending_balance_credit_counter, + } + } +} + +impl From for ShadowConfidentialTransferFeeConfig { + fn from(original: ConfidentialTransferFeeConfig) -> Self { + ShadowConfidentialTransferFeeConfig { + authority: original.authority, + withdraw_withheld_authority_elgamal_pubkey: original + .withdraw_withheld_authority_elgamal_pubkey + .into(), + harvest_to_mint_enabled: original.harvest_to_mint_enabled, + withheld_amount: original.withheld_amount.into(), + } + } +} + +impl From for ShadowMetadata { + fn from(original: TokenMetadata) -> Self { + ShadowMetadata { + update_authority: original.update_authority, + mint: bs58::encode(original.mint).into_string(), + name: original.name, + symbol: original.symbol, + uri: original.uri, + additional_metadata: original.additional_metadata, + } + } +} diff --git a/blockbuster/src/programs/token_extensions/mod.rs b/blockbuster/src/programs/token_extensions/mod.rs new file mode 100644 index 000000000..ec1633c9c --- /dev/null +++ b/blockbuster/src/programs/token_extensions/mod.rs @@ -0,0 +1,210 @@ +pub mod extension; +use crate::{ + error::BlockbusterError, + program_handler::{ParseResult, ProgramParser}, + programs::ProgramParseResult, +}; +use serde::{Deserialize, Serialize}; +use solana_sdk::{pubkey::Pubkey, pubkeys}; +use spl_token_2022::{ + extension::{ + confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint}, + confidential_transfer_fee::ConfidentialTransferFeeConfig, + cpi_guard::CpiGuard, + default_account_state::DefaultAccountState, + group_member_pointer::GroupMemberPointer, + group_pointer::GroupPointer, + interest_bearing_mint::InterestBearingConfig, + memo_transfer::MemoTransfer, + metadata_pointer::MetadataPointer, + mint_close_authority::MintCloseAuthority, + permanent_delegate::PermanentDelegate, + transfer_fee::{TransferFeeAmount, TransferFeeConfig}, + transfer_hook::TransferHook, + BaseStateWithExtensions, StateWithExtensions, + }, + state::{Account, Mint}, +}; +use spl_token_group_interface::state::{TokenGroup, TokenGroupMember}; +use spl_token_metadata_interface::state::TokenMetadata; + +use self::extension::{ + ShadowConfidentialTransferAccount, ShadowConfidentialTransferFeeConfig, + ShadowConfidentialTransferMint, ShadowCpiGuard, ShadowDefaultAccountState, + ShadowGroupMemberPointer, ShadowGroupPointer, ShadowInterestBearingConfig, ShadowMemoTransfer, + ShadowMetadata, ShadowMetadataPointer, ShadowMintCloseAuthority, ShadowPermanentDelegate, + ShadowTokenGroup, ShadowTokenGroupMember, ShadowTransferFeeAmount, ShadowTransferFeeConfig, + ShadowTransferHook, +}; + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct MintAccountExtensions { + pub default_account_state: Option, + pub confidential_transfer_mint: Option, + pub confidential_transfer_account: Option, + pub confidential_transfer_fee_config: Option, + pub interest_bearing_config: Option, + pub transfer_fee_config: Option, + pub mint_close_authority: Option, + pub permanent_delegate: Option, + pub metadata_pointer: Option, + pub metadata: Option, + pub transfer_hook: Option, + pub group_pointer: Option, + pub token_group: Option, + pub group_member_pointer: Option, + pub token_group_member: Option, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct TokenAccountExtensions { + pub confidential_transfer: Option, + pub cpi_guard: Option, + pub memo_transfer: Option, + pub transfer_fee_amount: Option, +} +#[derive(Debug, PartialEq)] +pub struct TokenAccount { + pub account: Account, + pub extensions: TokenAccountExtensions, +} + +#[derive(Debug, PartialEq)] +pub struct MintAccount { + pub account: Mint, + pub extensions: MintAccountExtensions, +} + +pubkeys!( + token_program_id, + "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" +); + +pub struct Token2022AccountParser; + +#[allow(clippy::large_enum_variant)] +pub enum TokenExtensionsProgramAccount { + TokenAccount(TokenAccount), + MintAccount(MintAccount), + EmptyAccount, +} + +impl ParseResult for TokenExtensionsProgramAccount { + fn result(&self) -> &Self + where + Self: Sized, + { + self + } + fn result_type(&self) -> ProgramParseResult { + ProgramParseResult::TokenExtensionsProgramAccount(self) + } +} + +impl ProgramParser for Token2022AccountParser { + fn key(&self) -> Pubkey { + token_program_id() + } + fn key_match(&self, key: &Pubkey) -> bool { + key == &token_program_id() + } + fn handles_account_updates(&self) -> bool { + true + } + + fn handles_instructions(&self) -> bool { + false + } + + fn handle_account( + &self, + account_data: &[u8], + ) -> Result, BlockbusterError> { + if account_data.is_empty() { + return Ok(Box::new(TokenExtensionsProgramAccount::EmptyAccount)); + } + + let result: TokenExtensionsProgramAccount; + + if let Ok(account) = StateWithExtensions::::unpack(account_data) { + let confidential_transfer = account + .get_extension::() + .ok() + .copied(); + let cpi_guard = account.get_extension::().ok().copied(); + let memo_transfer = account.get_extension::().ok().copied(); + let transfer_fee_amount = account.get_extension::().ok().copied(); + + // Create a structured account with extensions + let structured_account = TokenAccount { + account: account.base, + extensions: TokenAccountExtensions { + confidential_transfer: confidential_transfer + .map(ShadowConfidentialTransferAccount::from), + cpi_guard: cpi_guard.map(ShadowCpiGuard::from), + memo_transfer: memo_transfer.map(ShadowMemoTransfer::from), + transfer_fee_amount: transfer_fee_amount.map(ShadowTransferFeeAmount::from), + }, + }; + + result = TokenExtensionsProgramAccount::TokenAccount(structured_account); + } else if let Ok(mint) = StateWithExtensions::::unpack(account_data) { + let confidential_transfer_mint = mint + .get_extension::() + .ok() + .copied(); + let confidential_transfer_account = mint + .get_extension::() + .ok() + .copied(); + let confidential_transfer_fee_config = mint + .get_extension::() + .ok() + .copied(); + let default_account_state = mint.get_extension::().ok().copied(); + let interest_bearing_config = + mint.get_extension::().ok().copied(); + let transfer_fee_config = mint.get_extension::().ok().copied(); + let mint_close_authority = mint.get_extension::().ok().copied(); + let permanent_delegate = mint.get_extension::().ok().copied(); + let metadata_pointer = mint.get_extension::().ok().copied(); + let metadata = mint.get_variable_len_extension::().ok(); + let group_pointer = mint.get_extension::().ok().copied(); + let token_group = mint.get_extension::().ok().copied(); + let group_member_pointer = mint.get_extension::().ok().copied(); + let token_group_member = mint.get_extension::().ok().copied(); + let transfer_hook = mint.get_extension::().ok().copied(); + + let structured_mint = MintAccount { + account: mint.base, + extensions: MintAccountExtensions { + confidential_transfer_mint: confidential_transfer_mint + .map(ShadowConfidentialTransferMint::from), + confidential_transfer_account: confidential_transfer_account + .map(ShadowConfidentialTransferAccount::from), + confidential_transfer_fee_config: confidential_transfer_fee_config + .map(ShadowConfidentialTransferFeeConfig::from), + default_account_state: default_account_state + .map(ShadowDefaultAccountState::from), + interest_bearing_config: interest_bearing_config + .map(ShadowInterestBearingConfig::from), + transfer_fee_config: transfer_fee_config.map(ShadowTransferFeeConfig::from), + mint_close_authority: mint_close_authority.map(ShadowMintCloseAuthority::from), + permanent_delegate: permanent_delegate.map(ShadowPermanentDelegate::from), + metadata_pointer: metadata_pointer.map(ShadowMetadataPointer::from), + metadata: metadata.map(ShadowMetadata::from), + transfer_hook: transfer_hook.map(ShadowTransferHook::from), + group_pointer: group_pointer.map(ShadowGroupPointer::from), + token_group: token_group.map(ShadowTokenGroup::from), + group_member_pointer: group_member_pointer.map(ShadowGroupMemberPointer::from), + token_group_member: token_group_member.map(ShadowTokenGroupMember::from), + }, + }; + result = TokenExtensionsProgramAccount::MintAccount(structured_mint); + } else { + return Err(BlockbusterError::InvalidDataLength); + }; + + Ok(Box::new(result)) + } +} diff --git a/blockbuster/src/programs/token_metadata/mod.rs b/blockbuster/src/programs/token_metadata/mod.rs new file mode 100644 index 000000000..e19ee210f --- /dev/null +++ b/blockbuster/src/programs/token_metadata/mod.rs @@ -0,0 +1,147 @@ +use crate::{ + error::BlockbusterError, + program_handler::{ParseResult, ProgramParser}, + programs::ProgramParseResult, +}; +use borsh::BorshDeserialize; +use solana_sdk::{borsh0_10::try_from_slice_unchecked, pubkey::Pubkey, pubkeys}; + +use mpl_token_metadata::{ + accounts::{ + CollectionAuthorityRecord, DeprecatedMasterEditionV1, Edition, EditionMarker, + MasterEdition, Metadata, UseAuthorityRecord, + }, + types::Key, +}; + +pubkeys!( + token_metadata_id, + "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" +); + +#[allow(clippy::large_enum_variant)] +pub enum TokenMetadataAccountData { + EditionV1(Edition), + MasterEditionV1(DeprecatedMasterEditionV1), + MetadataV1(Metadata), + MasterEditionV2(MasterEdition), + EditionMarker(EditionMarker), + UseAuthorityRecord(UseAuthorityRecord), + CollectionAuthorityRecord(CollectionAuthorityRecord), + EmptyAccount, +} + +pub struct TokenMetadataAccountState { + pub key: Key, + pub data: TokenMetadataAccountData, +} + +impl ParseResult for TokenMetadataAccountState { + fn result(&self) -> &Self + where + Self: Sized, + { + self + } + fn result_type(&self) -> ProgramParseResult { + ProgramParseResult::TokenMetadata(self) + } +} + +pub struct TokenMetadataParser; + +impl ProgramParser for TokenMetadataParser { + fn key(&self) -> Pubkey { + token_metadata_id() + } + fn key_match(&self, key: &Pubkey) -> bool { + key == &token_metadata_id() + } + + fn handles_account_updates(&self) -> bool { + true + } + + fn handles_instructions(&self) -> bool { + false + } + + fn handle_account( + &self, + account_data: &[u8], + ) -> Result, BlockbusterError> { + if account_data.is_empty() { + return Ok(Box::new(TokenMetadataAccountState { + key: Key::Uninitialized, + data: TokenMetadataAccountData::EmptyAccount, + })); + } + let key = Key::try_from_slice(&account_data[0..1])?; + let token_metadata_account_state = match key { + Key::EditionV1 => { + let account: Edition = try_from_slice_unchecked(account_data)?; + + TokenMetadataAccountState { + key: account.key, + data: TokenMetadataAccountData::EditionV1(account), + } + } + Key::MasterEditionV1 => { + let account: DeprecatedMasterEditionV1 = try_from_slice_unchecked(account_data)?; + + TokenMetadataAccountState { + key: account.key, + data: TokenMetadataAccountData::MasterEditionV1(account), + } + } + Key::MasterEditionV2 => { + let account: MasterEdition = try_from_slice_unchecked(account_data)?; + + TokenMetadataAccountState { + key: account.key, + data: TokenMetadataAccountData::MasterEditionV2(account), + } + } + Key::UseAuthorityRecord => { + let account: UseAuthorityRecord = try_from_slice_unchecked(account_data)?; + + TokenMetadataAccountState { + key: account.key, + data: TokenMetadataAccountData::UseAuthorityRecord(account), + } + } + Key::EditionMarker => { + let account: EditionMarker = try_from_slice_unchecked(account_data)?; + + TokenMetadataAccountState { + key: account.key, + data: TokenMetadataAccountData::EditionMarker(account), + } + } + Key::CollectionAuthorityRecord => { + let account: CollectionAuthorityRecord = try_from_slice_unchecked(account_data)?; + + TokenMetadataAccountState { + key: account.key, + data: TokenMetadataAccountData::CollectionAuthorityRecord(account), + } + } + Key::MetadataV1 => { + let account = Metadata::safe_deserialize(account_data)?; + + TokenMetadataAccountState { + key: account.key, + data: TokenMetadataAccountData::MetadataV1(account), + } + } + Key::Uninitialized => { + return Err(BlockbusterError::UninitializedAccount); + } + _ => { + return Err(BlockbusterError::AccountTypeNotImplemented); + } + }; + + Ok(Box::new(token_metadata_account_state)) + } +} diff --git a/blockbuster/tests/bubblegum_parser_test.rs b/blockbuster/tests/bubblegum_parser_test.rs new file mode 100644 index 000000000..5c2afdfc4 --- /dev/null +++ b/blockbuster/tests/bubblegum_parser_test.rs @@ -0,0 +1,222 @@ +#[cfg(test)] +use blockbuster::{ + program_handler::ProgramParser, + programs::{bubblegum::BubblegumParser, ProgramParseResult}, +}; +use flatbuffers::FlatBufferBuilder; +use helpers::*; +use mpl_bubblegum::{ + instructions::{MintV1InstructionArgs, TransferInstructionArgs}, + types::{BubblegumEventType, Creator, LeafSchema, MetadataArgs, TokenProgramVersion, Version}, + LeafSchemaEvent, +}; +use spl_account_compression::{ + events::{AccountCompressionEvent, ChangeLogEvent}, + state::PathNode, +}; + +mod helpers; + +#[test] +fn test_setup() { + let subject = BubblegumParser {}; + assert_eq!(subject.key(), mpl_bubblegum::ID); + assert!(subject.key_match(&mpl_bubblegum::ID)); +} + +#[test] +fn test_mint() { + let subject = BubblegumParser {}; + + let accounts = random_list_of(9, |_i| random_pubkey()); + let fb_accounts = accounts.clone(); + let fb_account_indexes: Vec = fb_accounts + .iter() + .enumerate() + .map(|(i, _)| i as u8) + .collect(); + + let metadata = MetadataArgs { + name: "test".to_string(), + symbol: "test".to_string(), + uri: "www.solana.pos".to_owned(), + seller_fee_basis_points: 0, + primary_sale_happened: false, + is_mutable: false, + edition_nonce: None, + token_standard: None, + token_program_version: TokenProgramVersion::Original, + collection: None, + uses: None, + creators: vec![Creator { + address: random_pubkey(), + verified: false, + share: 20, + }], + }; + + // We are only using this to get the instruction data, so the accounts don't actually matter + // here. + let mut accounts_iter = accounts.iter(); + let ix = mpl_bubblegum::instructions::MintV1 { + tree_config: *accounts_iter.next().unwrap(), + leaf_owner: *accounts_iter.next().unwrap(), + leaf_delegate: *accounts_iter.next().unwrap(), + merkle_tree: *accounts_iter.next().unwrap(), + payer: *accounts_iter.next().unwrap(), + tree_creator_or_delegate: *accounts_iter.next().unwrap(), + log_wrapper: *accounts_iter.next().unwrap(), + compression_program: *accounts_iter.next().unwrap(), + system_program: *accounts_iter.next().unwrap(), + }; + let ix_data = ix.instruction(MintV1InstructionArgs { metadata }).data; + + let lse = LeafSchemaEvent { + event_type: BubblegumEventType::LeafSchemaEvent, + version: Version::V1, + schema: LeafSchema::V1 { + id: random_pubkey(), + owner: random_pubkey(), + delegate: random_pubkey(), + nonce: 0, + data_hash: [0; 32], + creator_hash: [0; 32], + }, + leaf_hash: [0; 32], + }; + + let cs = ChangeLogEvent::new( + random_pubkey(), + vec![PathNode { + node: [0; 32], + index: 0, + }], + 0, + 0, + ); + let cs_event = AccountCompressionEvent::ChangeLog(cs); + + let mut fbb1 = FlatBufferBuilder::new(); + let mut fbb2 = FlatBufferBuilder::new(); + let mut fbb3 = FlatBufferBuilder::new(); + let mut fbb4 = FlatBufferBuilder::new(); + + let ix_b = build_bubblegum_bundle( + &mut fbb1, + &mut fbb2, + &mut fbb3, + &mut fbb4, + &fb_accounts, + &fb_account_indexes, + &ix_data, + lse, + cs_event, + ); + + let result = subject.handle_instruction(&ix_b); + + if let ProgramParseResult::Bubblegum(b) = result.unwrap().result_type() { + let matched = match b.instruction { + mpl_bubblegum::InstructionName::MintV1 => Ok(()), + _ => Err(()), + }; + assert!(matched.is_ok()); + assert!(b.payload.is_some()); + assert!(b.leaf_update.is_some()); + assert!(b.tree_update.is_some()); + } else { + panic!("Unexpected ProgramParseResult variant"); + } +} + +#[test] +fn test_basic_success_parsing() { + let subject = BubblegumParser {}; + + let accounts = random_list_of(8, |_i| random_pubkey()); + let fb_accounts = accounts.clone(); + let fb_account_indexes: Vec = fb_accounts + .iter() + .enumerate() + .map(|(i, _)| i as u8) + .collect(); + + // We are only using this to get the instruction data, so the accounts don't actually matter + // here. + let mut accounts_iter = accounts.iter(); + let ix = mpl_bubblegum::instructions::Transfer { + tree_config: *accounts_iter.next().unwrap(), + leaf_owner: (*accounts_iter.next().unwrap(), true), + leaf_delegate: (*accounts_iter.next().unwrap(), false), + merkle_tree: *accounts_iter.next().unwrap(), + log_wrapper: *accounts_iter.next().unwrap(), + compression_program: *accounts_iter.next().unwrap(), + system_program: *accounts_iter.next().unwrap(), + new_leaf_owner: *accounts_iter.next().unwrap(), + }; + let ix_data = ix + .instruction(TransferInstructionArgs { + root: [0; 32], + data_hash: [0; 32], + creator_hash: [0; 32], + nonce: 0, + index: 0, + }) + .data; + + let lse = LeafSchemaEvent { + event_type: BubblegumEventType::LeafSchemaEvent, + version: Version::V1, + schema: LeafSchema::V1 { + id: random_pubkey(), + owner: random_pubkey(), + delegate: random_pubkey(), + nonce: 0, + data_hash: [0; 32], + creator_hash: [0; 32], + }, + leaf_hash: [0; 32], + }; + + let cs = ChangeLogEvent::new( + random_pubkey(), + vec![PathNode { + node: [0; 32], + index: 0, + }], + 0, + 0, + ); + let cs_event = AccountCompressionEvent::ChangeLog(cs); + + let mut fbb1 = FlatBufferBuilder::new(); + let mut fbb2 = FlatBufferBuilder::new(); + let mut fbb3 = FlatBufferBuilder::new(); + let mut fbb4 = FlatBufferBuilder::new(); + + let ix_b = build_bubblegum_bundle( + &mut fbb1, + &mut fbb2, + &mut fbb3, + &mut fbb4, + &fb_accounts, + &fb_account_indexes, + &ix_data, + lse, + cs_event, + ); + let result = subject.handle_instruction(&ix_b); + + if let ProgramParseResult::Bubblegum(b) = result.unwrap().result_type() { + assert!(b.payload.is_none()); + let matched = match b.instruction { + mpl_bubblegum::InstructionName::Transfer => Ok(()), + _ => Err(()), + }; + assert!(matched.is_ok()); + assert!(b.leaf_update.is_some()); + assert!(b.tree_update.is_some()); + } else { + panic!("Unexpected ProgramParseResult variant"); + } +} diff --git a/blockbuster/tests/fixtures/double_bubblegum_mint.json b/blockbuster/tests/fixtures/double_bubblegum_mint.json new file mode 100644 index 000000000..70318af8d --- /dev/null +++ b/blockbuster/tests/fixtures/double_bubblegum_mint.json @@ -0,0 +1,162 @@ +{ + "blockTime": 1675092216, + "meta": { + "computeUnitsConsumed": 141280, + "err": null, + "fee": 5000, + "innerInstructions": [ + { + "index": 0, + "instructions": [ + { + "accounts": [ + 1, + 0, + 9, + 5 + ], + "data": "Tc14EPctAN3Z", + "programIdIndex": 11 + }, + { + "accounts": [], + "data": "2GJh7oUmkZKowPMPT4SvaD5QV55UkcqB2PpZ2BZ1W7sjJMC8uggZASX6NokKX4h5pmQnSa6YyoSR9jFVbmr8bb8yxP4MavzdWEt4X2RHiJCZa4dZe1hHo2DsMXPNDATWPJQDEQDGJ6pL7ejbVwScEoDLAzgYCPv7Tqi6vFcWRfHsy9Az6BFETLyFuszod6tyGrCXwoRXyNaVKGzggGToXK1sAdiCixsLKNhfNjYnKKb1kDTvCV7n9cNeug4PPvTmaEB3tUkgWEPpi14StUju4W4UUdrSk", + "programIdIndex": 12 + }, + { + "accounts": [ + 2, + 3, + 12 + ], + "data": "8RkZ9BWdS73MdDxbzibxQiF8EL66q8CbKTi4Hhn1kLVKZTDqNdyfqKe", + "programIdIndex": 8 + }, + { + "accounts": [], + "data": "11XERtpD5QHDbyeBPMqzJYcChfbt8smXYiKDE6UUcGzUrQnr4vJdDATGtCDGexvDG34Fyzp7xiBvzHxqaqKi6Pn7gQEL2psAjKSp6wHwP7Rn97VDq3u9eu6mdP7gQuUCoyJc5vS9t5TmCao8vmus5oisFiPNvkrKUoN7AXjc83TmeFAxqEJjuTTFcK4kupsBirUPscDPRurvtqf3wUWdj9G8CmWNZYoBwTCAHmJ6WFeAVJjEz3GToB4X9sRgbDASTqqYdzmKr6xHX7VrsZ1GDSwxtZK5VVKjZtBNKBS1D1obbXwt49cBVVoPQJzXBEWgCrvjsB3s8hBTaz7Eqt4QYVFznuT4uLbZf4C3MEQG4KeVKh86zsaK9EN4eutQE2VAbJgSCpV2hXGoxP9jAoPbu57TQekEAv23QtqzwrPW1iUQUrmoARro4tmqFegdBs8JCKbF5MAftjrii3HvN7z9z9ArihvBTYxz4eemmYdv8TbkkWkKmqaazMrRQK2Ad4UPhdvhmmnJ7Ap7UELPGjaSUEjUhzByrETwAsG3tSkvgsDadVKV39qRafWiABGR7zcR5am1MTGN5RFu3SbHEX7E7RLwFan2ax2wPu5mjE2BmFb42srnhjDxrAYpJ1KMxg6sz5QWv92VTT1CoheDqAyv29iGLzz1quAZvFAA2EQogGFWto4FuTqDRbXUk9uTcYh8b9T5ThDc89ZECfxPHBm5UNGzjTZYEYumBmPwZPQfA1DEpU5PYQBAaCtvJZKU3bgGa6bWLazRoftMuoh7e4q1RKETZQNznGVEowiNeE66byfpkjUu495ZrAZRCaxzHinFgck545HYycniFoGrNKxWxaS8YitAYitqvn4A5oRKE7ZSE3kA6VawMykq894pRF1UPD6NmjiDpYkbvc17D58uXMz3wZNPnRJiPHm9Y42nMfbQikuUuHhkzZrYQGeKpAXYQuqCukTjoxk8cnF74VrMbuRFmJ7LvzdDS4Yhjho5LdpYXjt783n256qv8Ca7qnEhAnF75KLLPWFGSSb3kXNaR3MrUHvK52Eq9vgHDwYa4AAF2Bp8To69nmP69M5DKJtm7d8gwVycBezb", + "programIdIndex": 12 + } + ] + }, + { + "index": 1, + "instructions": [ + { + "accounts": [ + 1, + 0, + 9, + 5 + ], + "data": "TcAkuQRheyK5", + "programIdIndex": 11 + }, + { + "accounts": [], + "data": "2GJh7oUmkZKoTNgp3ScqBk7HXTfuLs2mJqYQ5UEijKbh5nSzFkKz4hb3nBEFiUKNrZ7KSWVjQPAaAaSNpo2MDe3nXnCp2JWxsgwwxWEFMBLD2hHSDAJBp3q9NUPghUgedmiy2stwuswB62T9yzCZ52aNTWgGf5rnjw7cF7LgpKrmgSyLgYHcEV6GJqputph1toXRQ3Fwx3mxGaWP14vaQp8us3xUbKKNXTd7CMWumY21odXrQyvzBPXbegtYhRksx4gPw9NwK5SQ7ZWxkoFfGf9taHWQL", + "programIdIndex": 12 + }, + { + "accounts": [ + 2, + 3, + 12 + ], + "data": "8RkZ9BWdS73KRTUfXCGaVsDdpgjwac9RAFPUC3fXePXqB3vTrMy6gFW", + "programIdIndex": 8 + }, + { + "accounts": [], + "data": "11XERtpD5QHDbyeBPMqzJYcChfbt8smXYiKDE6UUcGzUrQnr4vJZZNofWv4Sg3EyxdT6FfPv68VapsveBkXgnLkS3KkaFixHgfzfGUSsX15rPZrREQbQ2mzhu3vt8GFTQbTR58ynr3GtiRqfh5WtCykDvbYYyLXwJmH3ccsuxTxYAbE9jvqSm2NATxnyYCGU1fc7cfKWw8rTePM1WmzFERveMNaAaj5uquNWLE85fMxFrgsR5Y7rVrrcpQC2kCdXcajrCsQHVZwUzoJRJxGs7smARkSNbuq4rgPNqasdqxWoc9wE8AC3sZdDSodARjHqKanvyyU9C134mdGZ4QSR5qsdPLmiQ6ZsVxnKpmLLm1oyJwGT8mjZn1XR9tWM6mE1dpKN1Abi3NxpodqMTq3NQ7YVfsk9KicnfhGvRRx1mY3uWgs6cLWETq18B5DTjrXqAC3EXhjGnUHpxHU3TB7Msb5SVrvMnBAgLseBBetmJxPGm3YZ2mnZcSfZB7wUuF8o8FPdsb5kZDQUmQL7ksxLz8dRZc2vfynUxr5qCEGnLLZvJofRXDPgre7gw4vzdubWZNkAtsvtPZDNR51Rw1gAwHAGduH72yLJmJFPvcJBB9v4642ccEsrsFnb7XMsnasM6w9w86qgv2AtgfvW52c7AH3ovHC2tXw5g5oU7jAn17tWGyjF6mCGJm7bPyGtp3M3zGszoiJarAzM9CNjKvYtvjz3R2s16GLVemozmsHqw7JRKhGZYbQpNEzU1fvKRbcW2KfmB2W7cLMU1BqMbfa3xK9HMeWh8XZM2bwMzXo5cgjGEHG88aZ4WLz3f5qyDrGT9aifGJKr4op6L2sfMrZStySxCySW2RSGWZb86hQ3ewbYWf7vZG1KRa6rb8wL4WgERfzPv8jZs2oRoYH1j4UDvNqVRdEezKxL54iufbYNKYZJRz8EqQdA2soPR5JhUwbDkEJ18aFJVn1AkQgKAwkxm98JwWJELMVanXofNM41YkB6KxHjaZiyv5d9b9fTDjy4spmQBztesUsV6eQ8xwpgo7Mxyf9Tfnhw66uNMJaDLfwRKiKEd5aERi8boHLnfsPWVcJ9ATzqUnuD", + "programIdIndex": 12 + } + ] + } + ], + "loadedAddresses": { + "readonly": [], + "writable": [] + }, + "logMessages": [ + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY invoke [1]", + "Program log: Instruction: MintToCollectionV1", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s invoke [2]", + "Program log: Instruction: Bubblegum Program Set Collection Size", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s consumed 18849 of 374956 compute units", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s success", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV invoke [2]", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV consumed 97 of 346956 compute units", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV success", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK invoke [2]", + "Program log: Instruction: Append", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV invoke [3]", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV consumed 97 of 332669 compute units", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV success", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK consumed 11188 of 343413 compute units", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK success", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY consumed 69776 of 400000 compute units", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY success", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY invoke [1]", + "Program log: Instruction: MintToCollectionV1", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s invoke [2]", + "Program log: Instruction: Bubblegum Program Set Collection Size", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s consumed 18849 of 305180 compute units", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s success", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV invoke [2]", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV consumed 97 of 274180 compute units", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV success", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK invoke [2]", + "Program log: Instruction: Append", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV invoke [3]", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV consumed 97 of 261165 compute units", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV success", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK consumed 9916 of 270637 compute units", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK success", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY consumed 71504 of 330224 compute units", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY success" + ], + "postBalances": [ + 22941440, + 5616720, + 309079680, + 1559040, + 1, + 0, + 4000000000, + 1141440, + 1141440, + 1461600, + 2853600, + 1141440, + 1141440 + ], + "postTokenBalances": [], + "preBalances": [ + 22946440, + 5616720, + 309079680, + 1559040, + 1, + 0, + 4000000000, + 1141440, + 1141440, + 1461600, + 2853600, + 1141440, + 1141440 + ], + "preTokenBalances": [], + "rewards": [], + "status": { + "Ok": null + } + }, + "slot": 192425259, + "transaction": [ + "AUiK0mY5PMz5PmL2AN54NLy3BrLi7aK9zD0WUgYHp5ulX04Xw6gCDSdQ/ftteGfMlESd0Ix3puDNA7FM89TzDAkBAAkNO4HP7Fnu2yFZWCS/3W3YbyPBkD2wPz5OJmjV5HUPoPAYHppoOOkD5TuhY/KCn2Irylt4d5Ji1O8v7odiypZRZYtnklxge77v9Uc2nmOA03u1hm7P+wBaMoy+kUpYenTTnFzQXHWdpvFM3DMM0zgOht3QLvpreuqP5+G/V8OKqP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADZLL1Rhodf4wAu7ZXc4ZiZjkwHTJUvMr2m/ME7O6J68Ab5le7POYPqScPWw2sE25de1jhCY6bqrk6CQQwAzLfqYi4DreTUoabIkdF9Z3b+KJljKE9xogSEmNRyuB8GlpQkqE+6VxBy6CKZ/WsZ+jffh2hFiXh1kE3+PTyODA38UzVj/xF4n8CMtj/RqlHJamnxyGTP4UMxLp40TamGf+5LnxMlEJ5+6+nPHSRw9nqfMjicZXux8QrnpSnlcGogghQtwZbHj0XxFOJ1Sf2sEw81YuGxzGqD9tUm20bwD+ClGC7wPwLtHyi90xBEulKsTz6PGNOXcF+rLA80aI81+eHxWuf+0epsjKc4q703M+16AHhFQNILcp6oXi1rUeaz3NwIHEAMGBgIAAAAHCQEKBQwICwThAZkSsi/FnlYPDwAAAFRlc3QgU3RpY2tlciAjMQMAAABUUDFpAAAAaHR0cHM6Ly9kaWFsZWN0LWZpbGUtc3RvcmFnZS5zMy51cy13ZXN0LTIuYW1hem9uYXdzLmNvbS9zdGlja2VyLXBhY2tzL3Rlc3QtcGFjay0xL3N0aWNrZXItMS1tZXRhZGF0YS5qc29uAAAAAQEAAQABAM1Y/8ReJ/AjLY/0apRyWpp8chkz+FDMS6eNE2phn/uSAAABAAAAO4HP7Fnu2yFZWCS/3W3YbyPBkD2wPz5OJmjV5HUPoPABZAcQAwYGAgAAAAcJAQoFDAgLBOEBmRKyL8WeVg8PAAAAVGVzdCBTdGlja2VyICMyAwAAAFRQMWkAAABodHRwczovL2RpYWxlY3QtZmlsZS1zdG9yYWdlLnMzLnVzLXdlc3QtMi5hbWF6b25hd3MuY29tL3N0aWNrZXItcGFja3MvdGVzdC1wYWNrLTEvc3RpY2tlci0yLW1ldGFkYXRhLmpzb24AAAABAQABAAEAzVj/xF4n8CMtj/RqlHJamnxyGTP4UMxLp40TamGf+5IAAAEAAAA7gc/sWe7bIVlYJL/dbdhvI8GQPbA/Pk4maNXkdQ+g8AFk", + "base64" + ], + "version": "legacy" +} \ No newline at end of file diff --git a/blockbuster/tests/fixtures/helium_mint_double_tree.json b/blockbuster/tests/fixtures/helium_mint_double_tree.json new file mode 100644 index 000000000..c7689ff1c --- /dev/null +++ b/blockbuster/tests/fixtures/helium_mint_double_tree.json @@ -0,0 +1,393 @@ + { + "blockTime": 1676150526, + "meta": { + "computeUnitsConsumed": 493369, + "err": null, + "fee": 5000, + "innerInstructions": [ + { + "index": 1, + "instructions": [ + { + "accounts": [ + 0, + 1 + ], + "data": "11115iGgYn1iB2MUEQn96GLGpRFmmMBVQo3myRzELdi4Q6GYCJcjUW74EXyBynLYEHdJeM", + "programIdIndex": 28 + }, + { + "accounts": [ + 18, + 21, + 14, + 30, + 22, + 8, + 31, + 32, + 2, + 3, + 19, + 9, + 20, + 33, + 34, + 35, + 36, + 37, + 28 + ], + "data": "P127ZWPqFCnngvAodxeqzoi83Vj9xLjpoLaAhi3YiHUUyNiieU4B3N77noSKC7fZvk5SME5cwirhUm6KQhRtFEcD2pHJQ3unDM", + "programIdIndex": 29 + }, + { + "accounts": [ + 18, + 2 + ], + "data": "11115jJ3wfqpy1rBHuw3dUPuJHNEtEGk3VTUqjUZn3SiFTZBpQtLCdH6DA8gjCfWnQ4tEG", + "programIdIndex": 28 + }, + { + "accounts": [ + 18, + 3 + ], + "data": "1111cSRr6MFDeX4jK6Ks3qj3Q3VevDadC5dJg6EF283WhGy7JgZaH3VycWXz2pvcrPY5J", + "programIdIndex": 28 + }, + { + "accounts": [ + 19, + 9, + 9, + 20, + 18, + 22, + 22, + 36, + 21, + 14, + 30, + 33, + 35, + 37, + 34, + 28, + 31 + ], + "data": "6PxggWMmLXh3GufgEQVnxYEDNEQo3S6eC4T6uXZuxhAa86fae5FG17KXd4iYSz2HFzv87hq2P4DgUxbgz2YERwAqCuZPx1WhCqpqsA8FnhSzN43WsT4mg9aQFVHC5RvWuX7xQQd9pK4id2T5jsmo5L4PJg72ib58jniEg4incgiaNKPpHvnkWhj4kAbC2RJCJAn9d6u3NrF634SVCPEQUBqDaiXPed5LEyUcsBKiuXkZg63b2ktfpyN4uxTBRtkDd6e2xtVkr3EGixQjtQHr1rdxYMfxaCi5B9x81VAobwjJF76yH", + "programIdIndex": 36 + }, + { + "accounts": [ + 14, + 22, + 21, + 33 + ], + "data": "Tp3YfPjqg9Jo", + "programIdIndex": 34 + }, + { + "accounts": [], + "data": "2GJh7oUmkZKnwfzmSY57AHYZ18ksm4SNYjoqv14aNYTBYSVqXqs3YsQedsfDtGwvVwB26FiHy9wuhQhwawV2VhcTJARicaKLNn3tCgo8y3SpCxQVAzhYAigwtxfRTttmfnrmkcrUbHE5Mo1RzAdxY9FxeBUMwaV5Bh5So4LzVLCj5ttPCewfnsA7JAQzGW5DEWB5iYEGrTvPZErU7R327ruapUripe9fPfUSHz4isbAZzDreZsXAfjGtFQzqFRfSm4AGMtcqhUD7aNGGWAxEaEeufdQJr", + "programIdIndex": 35 + }, + { + "accounts": [ + 20, + 19, + 35 + ], + "data": "8RkZ9BWdS73BAyXDMiAn6BNQ8sRJbDhx9LfKqDUzRwUwHZ3SdbYPpNA", + "programIdIndex": 37 + }, + { + "accounts": [], + "data": "11cvjWXwfL69CPWmmyhtDQXFCUVF38S8oJLjxNaFVtMn5YZjfuRtcy5MC63cjAmJYV4ywaHS665MC7G59aR3ixF6Thbqbexcs2zrL9Ac2HGy3Z412EYx2gPNaeJsTGS7RvFfwYugyz4B2Nt1yGEdiTnLV3EN6qPQZHmvrTkn6uNAoEZu4eZ1v1TvKhgQdk4mREBKfKCNkXZi7CcbrkYDVBMZsYk9ncH77JmxeT1U6JJtVrCeEyv5ypXAbmA5H1A8xjEHCk5WQLLLxYawkzqxHkG8ip4NXCahjEGj4H2WcNMPho4wWFL3itFxeK4P2yxtU67hf8uU5vaQb235p3TuWN3Xxjrr9YqEKCexE7cKF6p7CTg6AmvafRuWAhWs741dQSvsNZiEu27WdiEAccp4nLdgwg4cMJHuzVftfj1VKjZj4Qk8SJRNmQaXGzmF3ZkrEQcap81H5j7PdybSJALa3L7zZRs2v8VnypqbGuE9oW2GhiesY7rXXcToEK2LaT2ZtX6CHhYsHFZzdetd7r7z26eMGtTw1ej25NpW9DMG68fqhg7qPjS4QRVy77QRDfhJtdgXypwDPyG7zy5RMKwX3xm26C5cBZ85ToZ5J6nRAmyhL3LmYhzXWiHtSoiyy3FVLpoMSgZ7T4L4ehyvPK5Cdt55JtnG47Pwgzr9hpA3aNiC2sJYJEuVoZSNUDduCWoQUXJf4v41KMqv6DhfJ8SZREbF2JmFK97wLiC3LPFTt1bG6bLnp9M6ELuudJPGzL2diPd5PtdGS7xtDnYYRQLhHWnUSoSCkDjiVGuuv2yksdX8dBhTgNL22ktY9r2BnY1rnRxRMcooGgCKh8ZLG1tiyKSihLJNgZKX7G5nQGhcUtBisFig2NeCUCyFNLNvCmTs8Fnf3jZkRpBhVQUGq3eq3S6PCFU6PdXeRTzPRL1SotV9VepJ4nHQsTAz7Uj5JHdL5fBAszB6m8VUeE59sZACQ6kmVBCvw6wiNX1Cq332CFBZ3QU3DfSU6gJgUGgGtQDA9kYdUNKWGVkSiWSjn5nmVJkXz1ruGr8gawNktYszjaNrNL81M255uyLg3i2RpRm118hRjctSKffD", + "programIdIndex": 35 + }, + { + "accounts": [ + 18, + 23, + 15, + 24, + 25, + 8, + 31, + 32, + 4, + 5, + 16, + 10, + 17, + 33, + 34, + 35, + 36, + 37, + 28 + ], + "data": "P127ZWPqFCnngvAodxfgpna5Vcdv6bvghkUsaVmYB7Jd7tGzuc9PHodg2QpSQUgej93f7krLKik2in93UKsgCJf9TQoeozETWo", + "programIdIndex": 29 + }, + { + "accounts": [ + 18, + 4 + ], + "data": "11115jJ3wfqpy1rBHuw3dUPuJHNEtEGk3VTUqjUZn3SiFTZBpQtLCdH6DA8gjCfWnQ4tEG", + "programIdIndex": 28 + }, + { + "accounts": [ + 18, + 5 + ], + "data": "1111cSRr6MFDeX4jK6Ks3qj3Q3VevDadC5dJg6EF283WhGy7JgZaH3VycWXz2pvcrPY5J", + "programIdIndex": 28 + }, + { + "accounts": [ + 16, + 10, + 10, + 17, + 18, + 25, + 25, + 36, + 23, + 15, + 24, + 33, + 35, + 37, + 34, + 28, + 31 + ], + "data": "H4jhg7xGfzqFsRjjboyfADudDG2ghfjkbvNCdQse6B9y4LcZPXhpLXadsQ3gmwE5hvhvJmdo6h4C3wkqbfgsawY8GdrMDwSQmb6XEjEv3fam1LiQPrZqjL6GFSyvcUV8cGYS8UT98Q22BQHBzLTDbaBC9LqXcVPMFEEVmJiCotoNPP8pGYWNj4qkMSnGbDLDED74q3Y1ZR1Hk8ip5cZJqYz9fkYky3itVaFnXtj9UhAzCqY9d5WLCBsnPeeCq3GpAnZQCZwrtkqZ75VxiVTVnh9vvkqaBALDrqJKoUYG6Sr2Ry", + "programIdIndex": 36 + }, + { + "accounts": [ + 15, + 25, + 23, + 33 + ], + "data": "TbfetN1FB9WX", + "programIdIndex": 34 + }, + { + "accounts": [], + "data": "2GJh7oUmkZKnNeiK3mrfqNXJFiqQ5nvbxSPFeap5nNdWgiJfs6iHDVrHt32wjmdigfiPzZkFeuDoesWN2oG6AEpk4TJ1grroCfx4U1YkYEPxhRzmjQhh9iDwXfCLgeM9dqLE2YdaTBNLrS8vohgr6xyhcurxdCJjPC6J4hFeHrdG43jA3GipyrcrR2W28fkGsS2TxC46bx2zEckrY6H5S4mQaLnUCMDHtN8ywgpkLAELUzyYQzK1P9ewHPMEkoTUNYHtFYkR9K2Axx3drHqcrG1bEPqaL", + "programIdIndex": 35 + }, + { + "accounts": [ + 17, + 16, + 35 + ], + "data": "8RkZ9BWdS73A6y6am8ubeh1UKvLMH4HKQK53BqCRGRUtiaEPzDLrQip", + "programIdIndex": 37 + }, + { + "accounts": [], + "data": "112vtEkQ8GUhgay9HR98m8N4F35daB4XSC8F2AVLX2T7JU9HGHBeVGv2Bo3mpQ5REae9FwDiTCpzNE7GYN1Ub76dqvTRdTT66HjCGPMw2NwnQFfeGNeauZWCEMCcJumGLY9A5b3Lg9gsNVf8Zswy7Jst6HdJmTH2k671gPb5MeQD78oc3aheK9bLXPYR1JfUqA4xXF9S1oqktfG9Ftnae7kXy5mm81Kf9a5CDoR2PN8HYKZcQ3MfC9SgV92QfyzYPJUXmtHuoZoPQnEefHAwJkTPSTXw8dbLxbWS27JsbkZ6HdtFEKh1eeRXE4hxZdD4HcCnsdCX7UKbVhic791hNCorcZiDRFewVZxvNrY86xJC1Rejp1uehPpFxiu1X27SXJR5KPPiV3v3RZ5yiQzCWrM99nuY4Hnm55DSkLdp7oyeSxecLzAkmnnFYbRaTSX6ukLn9KxMWAv3gZvGDdcK1VE16kGvkrKrHZD4D4M2o8enzfz2BHpd1vyASuYJneXucCF8yco5WypeBPZhdHaRBcvrcv74UKLFXW2pn6pvSGcjZPwCdARja2FjYPBZs3zmUJrfSdDEhGmh3s8fZoXeDeyQRcyQAMVQSWjGrERdiRbunfkjCCDgxHPh5xmY9VYwHzB5ZbcvTFSKXNnnTFPPqSfdDvqNTNHMruAbso1vAvziH8jYCGhKK9jsqmidytBdDkwoNrE9NmH2M7m9bZv9Ubf9vfxDx2doDSCdCyAGhY1upTCESCFMdzLorkFHkDxuuMLEAE7kBE5n4qjNx6WBfKcKhkCQr9TLKQ617anr8p2sBE1Crf5Hso43rFFpT79QcD9kg4FDWkBtaVNJxUwPK61JDGrXj7VUpz3bPLEDdvduHVWsekDgTDGZeUPc3fwk79k3bCmm", + "programIdIndex": 35 + } + ] + } + ], + "loadedAddresses": { + "readonly": [ + "3hCnRKCDDwjrjT9xSKnUf6wQW1k5CwiHwVs57Syh4LPc", + "6dJKZKdrqJ6oPse63ADMSe1m9Bjemv6r5b5PZfaYm8ts", + "HXxZRpcwf5oHHcPQMLZ7KWPJJ3WZaNughAPqWv1LX3UR", + "HarYhsPxpM8Qu1F7nxNQaSZJZpZJ4odxYSopqykXDzSG", + "7MjNh26LUJgwfM4XgPyGCzZd8tbH3ZqYhsjpjDQ1m4ki", + "34rsLFmHgPwncdKKa55T5w9CKAvd3xMYz5eAMZnyjEHF", + "canSFmMSWjTnKn5yfMb6VQ83AbpSpmo9GW7ctwy5UL3", + "11111111111111111111111111111111", + "hemjuPXBpNvggtaUnN1MwT3wrdhttKEfosTcc2P9Pg8", + "2oQ6R7SW88dTFGZDkSCkjoK2evembZtA6tcxvC3ve3CT", + "Fv5hf1Fg58htfC7YEXKNEfkpuogUUQDDTLgjGWxxv48H", + "BQ3MCuTT5zVBhNfQ4SjMh3NPVhFy73MPV8rjfq5d1zie", + "4ewWZC5gT6TGpm5LZNDs9wVonfUT2q5PP5sc9kVbwMAK", + "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", + "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV", + "BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY", + "cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK" + ], + "writable": [ + "8n64SmPfKRf6ZR3AH3WtXBax6o9HN95B17sk8qbWu9us", + "BrX6opz1Qo2Yw86wVrnqyzpAfwoeT7mAQqrjV9crnE3y", + "g245XJ43RySEchJYnqkZAyPm5CCwdxRLKpn592uNLHh", + "9Yn32WbXK8JTVFpx5CGFrPKqbeQ7LE1UZi5bG7k9P4Rv", + "GLXbPYM459CUHX4h2uSBDR7ATv1UdCkje5Y7UaguBHhW", + "FPBicBBbcqtc9JK3xAv7j2hPmiPqLesXdeq3N6VyAbuT", + "C9gqeeZZFMUFQBZ2X6MoFVn1sUhphkwQ4BKwGqktu9RR" + ] + }, + "logMessages": [ + "Program ComputeBudget111111111111111111111111111111 invoke [1]", + "Program ComputeBudget111111111111111111111111111111 success", + "Program 1atrmQs3eq1N2FEYWu6tyTXbCjP4uQwExpjtnhXtS8h invoke [1]", + "Program log: Instruction: ExecuteTransactionV0", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program hemjuPXBpNvggtaUnN1MwT3wrdhttKEfosTcc2P9Pg8 invoke [2]", + "Program log: Instruction: GenesisIssueHotspotV0", + "Program 11111111111111111111111111111111 invoke [3]", + "Program 11111111111111111111111111111111 success", + "Program 11111111111111111111111111111111 invoke [3]", + "Program 11111111111111111111111111111111 success", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY invoke [3]", + "Program log: Instruction: MintToCollectionV1", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s invoke [4]", + "Program log: Instruction: Bubblegum Program Set Collection Size", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s consumed 17078 of 466049 compute units", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s success", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV invoke [4]", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV consumed 97 of 442836 compute units", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV success", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK invoke [4]", + "Program log: Instruction: Append", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV invoke [5]", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV consumed 97 of 393650 compute units", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV success", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK consumed 12533 of 405739 compute units", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK success", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY consumed 105173 of 496381 compute units", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY success", + "Program hemjuPXBpNvggtaUnN1MwT3wrdhttKEfosTcc2P9Pg8 consumed 213090 of 600524 compute units", + "Program hemjuPXBpNvggtaUnN1MwT3wrdhttKEfosTcc2P9Pg8 success", + "Program hemjuPXBpNvggtaUnN1MwT3wrdhttKEfosTcc2P9Pg8 invoke [2]", + "Program log: Instruction: GenesisIssueHotspotV0", + "Program 11111111111111111111111111111111 invoke [3]", + "Program 11111111111111111111111111111111 success", + "Program 11111111111111111111111111111111 invoke [3]", + "Program 11111111111111111111111111111111 success", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY invoke [3]", + "Program log: Instruction: MintToCollectionV1", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s invoke [4]", + "Program log: Instruction: Bubblegum Program Set Collection Size", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s consumed 17078 of 253432 compute units", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s success", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV invoke [4]", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV consumed 97 of 230236 compute units", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV success", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK invoke [4]", + "Program log: Instruction: Append", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV invoke [5]", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV consumed 97 of 215529 compute units", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV success", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK consumed 9545 of 224630 compute units", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK success", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY consumed 73573 of 286660 compute units", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY success", + "Program hemjuPXBpNvggtaUnN1MwT3wrdhttKEfosTcc2P9Pg8 consumed 150201 of 359514 compute units", + "Program hemjuPXBpNvggtaUnN1MwT3wrdhttKEfosTcc2P9Pg8 success", + "Program 1atrmQs3eq1N2FEYWu6tyTXbCjP4uQwExpjtnhXtS8h consumed 493369 of 700000 compute units", + "Program 1atrmQs3eq1N2FEYWu6tyTXbCjP4uQwExpjtnhXtS8h success" + ], + "postBalances": [ + 13615531680, + 946560, + 3730560, + 1746960, + 3730560, + 1746960, + 1, + 1141440, + 2199360, + 1001390880, + 0, + 0, + 0, + 0, + 5616720, + 5616720, + 1559040, + 3899771520, + 97236498080, + 1559040, + 58693345920, + 1461600, + 2644800, + 1461600, + 2853600, + 2644800, + 2199360, + 58385164080, + 1, + 1141440, + 2853600, + 0, + 3034560, + 0, + 1141440, + 1141440, + 1141440, + 1141440 + ], + "postTokenBalances": [], + "preBalances": [ + 13616483240, + 0, + 0, + 0, + 0, + 0, + 1, + 1141440, + 2199360, + 1001390880, + 0, + 0, + 0, + 0, + 5616720, + 5616720, + 1559040, + 3899771520, + 97247453120, + 1559040, + 58693345920, + 1461600, + 2644800, + 1461600, + 2853600, + 2644800, + 2199360, + 58385164080, + 1, + 1141440, + 2853600, + 0, + 3034560, + 0, + 1141440, + 1141440, + 1141440, + 1141440 + ], + "preTokenBalances": [], + "rewards": [], + "status": { + "Ok": null + } + }, + "slot": 195109311, + "transaction": [ + "AbLXN7V98aktKsi6aL91p3RmkyQFdyYGNHYMpJk5ySOLcdUclkjHIINJntucxWJ3XFW9yZP2aO4SS5esMpQqKQeAAQAIDg08T8z9CrdlPF7csXVhEB0ffCNBeZOhcp8Xz8p6pKrJanSfCQgbNLH2NDfLwRI4Q/Y597Jh2Eh02uRQrYUteJYm4QxTDNG1wGPkkJ4XVbMzGP49rIHwMQM2hLbmfsMd7XOYxZrmlbu/aBXUENICPfGpr5riRwOakVG7czObFPKdTP3ZK1eZVoJwYlzFhM2Oi3jJ4hwZIitCed2Jj5Q5Te7F5B+4N/guZt81iEQYRiVVIXYmt9WMCXFhYCe+a6VYxwMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAACZS/dKSS7FFnW51YwDm0qD9peZ2i0G+WoSTQ5C3zdoez/obcN1L9l3KoKOrQnrJfIERkbKkP0FX2DMFaVvq8JSIS6K3fj/wnJTXTIdiQe27s2rqOdDgATNm2IYs1IxxMGfh5ntG89DuUDImkHVWLGHLwtMtcpR48k63L2mQme9optTCnQ7oW8a1ujOu7Gj0aQkF3yx4XoGT7noKgXbzB09kLG0wiKRpl4FTjaATJXguia9lNqQm83VzRbBdrFV5JoajO4911f9lM3rS6Zt2IZeIw6HYE2UkExcmGWtv2qvtbTnrKopvMBp0R4vlKuMhKPkIEIa+BS5kFTQPSxP3JgIGAAUCYK4KAAcmABobEgEcHRIVDh4WCB8gAgMTCRQhIiMkJRwXDxgZBAUQChELDA3cAdldrmGCt34sAgAAAAATAAAAAQIDBAUGBwgJCgsMDQ4PEBESE0gAAAAZss4XKNQ5XyYAAAAAACaRcQ2qDEFGNYivXJfQ6B1rszzJBDfKaT+how6qQg0tKxJouAH/mwqL1PHBCAEKAAAAASgAAAABAQAAEwAAAAEUFRYXBgcIGBkaGxwODxAREhNIAAAAGbLOFyjUOV8mAAAAAACDHcRONA+8/xvGqrTrfWUSwrDoDw27TyRvh4CQ3N5CuRjPndAB/xsjFtxGxAgBEAAAAAEoAAAAAQEAAAAAANEjBQACe4SwDquYEP3ABgS/kuJV5iTb9hz1ZN27iyS/4fOcdRwE/woMDQX+/AkLBwiE3SwMB5CNMJr6sUgqvnym/0x5ZYFCo0ZsCFawzBZDAyIBAgwgERYjABcdGBITLBQ=", + "base64" + ], + "version": 0 + } \ No newline at end of file diff --git a/blockbuster/tests/fixtures/helium_nested.json b/blockbuster/tests/fixtures/helium_nested.json new file mode 100644 index 000000000..7ea115983 --- /dev/null +++ b/blockbuster/tests/fixtures/helium_nested.json @@ -0,0 +1,257 @@ +{ + "blockTime": 1670988259, + "meta": { + "computeUnitsConsumed": 192758, + "err": null, + "fee": 5000, + "innerInstructions": [ + { + "index": 1, + "instructions": [ + { + "accounts": [ + 0, + 1 + ], + "data": "111183WoV6ghGmq1bAjAaExRNVss7TWYU13qHQDCyEEG3XQCD2mmVTJb1Ktzgt9ZKiEqNX", + "programIdIndex": 8 + }, + { + "accounts": [ + 13, + 18, + 4, + 0, + 6, + 12, + 25, + 8, + 24 + ], + "data": "7DQYMN554iaKFme3Q3weXMABVTFgifLCWyshjXFPbPwbhTre4ocuBABc3jK3YXsmFjGgF9JFszj2oHcmT", + "programIdIndex": 17 + }, + { + "accounts": [ + 4, + 6, + 18 + ], + "data": "C", + "programIdIndex": 25 + }, + { + "accounts": [ + 4, + 6, + 0 + ], + "data": "6vPfxaE6dMFu", + "programIdIndex": 25 + }, + { + "accounts": [ + 4, + 6, + 18 + ], + "data": "B", + "programIdIndex": 25 + }, + { + "accounts": [ + 2, + 0, + 0, + 3, + 0, + 13, + 13, + 14, + 19, + 7, + 11, + 9, + 22, + 15, + 21, + 8 + ], + "data": "TvtTj5iYFZtqetS8Y3aVanTB4F5svNs5UMdAUzpp4ZN9ajuqKJZkLwkeUwqziPetCRtDGJLREBvbJSMexX8FC7tBYzswhAyUmzCki8AqRLqbPvFCx7kRJqFHXJE3xYhRmgjCwyqfUFZ5ugcc72B7VU7g1vddeoEe2tvfxjhYHob57dNdjMyvXNb7QJEHhPihgCMHE5WPUMAf92pLyHccqoCDrDRPjspDhNcyip7TBDbKC36iJeCLUwzQ8eqFfaD8uF7y53JCF", + "programIdIndex": 14 + }, + { + "accounts": [ + 7, + 13, + 19, + 9 + ], + "data": "TazrCJmyCiST", + "programIdIndex": 21 + }, + { + "accounts": [], + "data": "2GJh7oUmkZKoxWwpm7fkx3PY8ZyA9RWzNfYwWWNxQ9B5Y3fv8WZLz2hknnhgKTGvLmjNqeSmexVAkbhTuAoz7mYgQ7vD5nVZmVH4XEr2sRUzGkNdeaNReXKUtLZnrL7n1TCXHS347zDpdh7iXzkPZujWTU5jXLYq9RNVx7mgkuNxtKzWvEn2FZkhbaj1rQfEsCs3pznjb1CntWwheQVMxEi3UzkNKxmACV4umkANNmdgZt2Vhub7UPWHXfJKio8xNc1ne7YMbk1vQaxtPsTpSZWc9VsWR", + "programIdIndex": 22 + }, + { + "accounts": [ + 3, + 2, + 22 + ], + "data": "8RkZ9BWdS73BHnVYLoXxr4cHnXaYPYgiP1KumFycRWihSyJjYAcCuYM", + "programIdIndex": 15 + }, + { + "accounts": [], + "data": "11SNhoEi4fQkop41Rapc8vMYZBUQ7PGEH8E15mji6m3PSQYAT6paXf8o78TVXRHCGBKrT5ZQsBzaApavTsrgYjpeG9FJvQCP6p6tvuvP4dXK7RYguXkACiTWWKwqTFYcxDFdeX8Ucs5stwRgwKShFnZzSFPwpg9py4DM5qQgpESQ8xvQUXgfxD1VNA2wZJXqoEeTWEaCg4nqwboz8sh29gtgcZ29S2bej8CjFMBMvuNyPAdy4cyDKvEC16pfvNNbQLErdoLXe1HUhTD5mhMkNHbDpSDv1ewBpTmEJRxgJoqtV7TGGNYFciVKUfoQKoXPNaNg5E9ZV8h2UbHtgyLzTAG7yZvtXLLNBVh8DZn6GtvUSjEFGjkubh4MDjSxHNGFaTtVANJEvySkd6W8spmWCKAn4swCXZESY1KZfFrXqJq8gDtTwcESsMzoN1hDLVG6z8aTxntDmNkR2Kjv3oiLBFcVNyrCKHYM3MA6ucrtjxZUMc7fiAzPbVL53F3G1W6xiJqA1poEwyaDmjsRHpw3svbGTu9bqpev2hgo7h73dgZLQaGmNNj4fcrKQui75dGhpZzAgwk5qCPwouXRVYg8BPaPTrr1ViNB1rz85SKWyi1op81ZFaEQjAzeRnvnwK2kub5tfA7MH9qJHiSh2X17B4Mo1LGFASZKbGQHMgsqfTkJ36X3sSH46dvNoqH1S31FhGShFwXmAqxgjaoE3REvy3rLh7qjeok6jcbZrx3dt9MdYuwNBrtWD914GkT4EkgAj9xSyvmHjiiEra9AQ7yHta3USxhYPcgztYgvdsTJKeSAnKLrgnpzKcp3uPHTemkrByLZQgngMqaiBUwF4eMb4GducWjdgtth6JpVoMiujpUs3Ww1X8HFB521164tzGGLboAfJqYT3gpKjRzzbxMScSEJQFAySQu66BjXC4MYcdfkTFqNFdCfo47veKtBvtQi2QYvNYmHQkG57EVghvH1XEfMs5ejhGAL2CL4bv7ooLSyGXkdhs2mwNru6g4Yk4KhwiZcPgK8yV6oENuo5UJqeNWSX2p82JK49PpEjX6kHU7ejYnbnDhLxTYpofjgA4KviGVCntEq8yLopo2sNHW54HZSo514pBEbQ64dFC6K8Mm9dxCvnMCfGHJRe2XWTo53PJMZBkchH73PPuosvk8HrHd9Nwwgyke47Yhf3LJajYySRmfRyXXL7CvDvf8zVgchizrCnghjwhLxjxpWSmJaR4FfxXNAArfrq2xxZTiP8aytn9U53SY2KCk4TpxnQnH94UtbwKaN1Z6eqEq6KDpfPUXtQ6ugEjXiPha6nYYz8jZC5iiRVyHS9cCn3yrdZM5oac35EZrcQphWyAipSf662PxnH96eu8FyiAYH2iY4jQWiGmS6Mh9", + "programIdIndex": 22 + } + ] + } + ], + "loadedAddresses": { + "readonly": [], + "writable": [] + }, + "logMessages": [ + "Program ComputeBudget111111111111111111111111111111 invoke [1]", + "Program ComputeBudget111111111111111111111111111111 success", + "Program hemABtqNUst4MmqsVcuN217ZzBspENbGt9uueSe5jts invoke [1]", + "Program log: Instruction: IssueIotHotspotV0", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program credacwrBVewZAgCwNgowCSMbCiepuesprUWPBeLTSg invoke [2]", + "Program log: Instruction: BurnFromIssuanceV0", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]", + "Program log: Instruction: ThawAccount", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4267 of 260868 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]", + "Program log: Instruction: Burn", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4753 of 253735 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]", + "Program log: Instruction: FreezeAccount", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4265 of 246133 compute units", + "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", + "Program credacwrBVewZAgCwNgowCSMbCiepuesprUWPBeLTSg consumed 46042 of 287052 compute units", + "Program credacwrBVewZAgCwNgowCSMbCiepuesprUWPBeLTSg success", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY invoke [2]", + "Program log: Instruction: MintToCollectionV1", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s invoke [3]", + "Program log: Instruction: Bubblegum Program Set Collection Size", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s consumed 5378 of 194964 compute units", + "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s success", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV invoke [3]", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV consumed 97 of 181511 compute units", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV success", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK invoke [3]", + "Program log: Instruction: Append", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV invoke [4]", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV consumed 97 of 163110 compute units", + "Program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV success", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK consumed 11904 of 174570 compute units", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK success", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY consumed 61791 of 222636 compute units", + "Program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY success", + "Program hemABtqNUst4MmqsVcuN217ZzBspENbGt9uueSe5jts consumed 192758 of 350000 compute units", + "Program hemABtqNUst4MmqsVcuN217ZzBspENbGt9uueSe5jts success" + ], + "postBalances": [ + 21224879240, + 2032320, + 1559040, + 6222295680, + 2039280, + 2088000, + 1461600, + 5616720, + 1, + 0, + 3312960, + 2853600, + 731913600, + 3034560, + 1141440, + 1141440, + 1, + 1141440, + 2436000, + 1461600, + 1141440, + 1141440, + 1141440, + 1169280, + 1009200, + 934087680 + ], + "postTokenBalances": [ + { + "accountIndex": 4, + "mint": "EQistL7vTTXGUMu873Ff2sHDppPg7rYXrKG1D6LEBcvm", + "owner": "devTUdgycPzE5WKyeokLp9txGo7ohmxZxNYUkVDpwuZ", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "uiTokenAmount": { + "amount": "985", + "decimals": 0, + "uiAmount": 985.0, + "uiAmountString": "985" + } + } + ], + "preBalances": [ + 21226916560, + 0, + 1559040, + 6222295680, + 2039280, + 2088000, + 1461600, + 5616720, + 1, + 0, + 3312960, + 2853600, + 731913600, + 3034560, + 1141440, + 1141440, + 1, + 1141440, + 2436000, + 1461600, + 1141440, + 1141440, + 1141440, + 1169280, + 1009200, + 934087680 + ], + "preTokenBalances": [ + { + "accountIndex": 4, + "mint": "EQistL7vTTXGUMu873Ff2sHDppPg7rYXrKG1D6LEBcvm", + "owner": "devTUdgycPzE5WKyeokLp9txGo7ohmxZxNYUkVDpwuZ", + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "uiTokenAmount": { + "amount": "990", + "decimals": 0, + "uiAmount": 990.0, + "uiAmountString": "990" + } + } + ], + "rewards": [], + "status": { + "Ok": null + } + }, + "slot": 181981476, + "transaction": [ + "Ac/AYoM8uDF6r1DKMjZnN5NI9EnL1pnoner9RHcKHGK12MNZaTUA/8lZjuwNUg2+R/wxnq/6LPqlZPCqSSakkQkBABIaCWPJi7x87iRK6BaHT3Jj4G1BcfgNw4rtd0w68Tp2zlgzCpdD+bm32zuDU5+OX6NQLXZ3FmTcrq36zX9rJEU/gWoVdpVys/m1EqOX7oV4leO1Ba6grTmOizQVxzzIfPQ0fclkduBfngDTC1kVqOtIJmxv75OMSIRMzwUaQPcJMfGGxqjL9T/ACPO0uQlJsb6Xlf5ztej1X1PWvIOVD7+Nr5gH2MzW2HUkRGRNqmFzSit0kfgLNNI4wsDolzlESB87xzujtgoAjaM3JBqoQOVH6xl0XtiVXBDyBuXFO3np9ZrN80oOcB0/i10lD1zDXDBelEu8N/xlBXbloj8xICGdDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANksvVGGh1/jAC7tldzhmJmOTAdMlS8yvab8wTs7onrxfn8+mCiILSR01EKbqWOYujG9PH00S8UotQ/YATlutw4QTKWsrYj/qkdAKFCdgZSZ0JXMC1mucMVI5k+tZ6U5rjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FmXhHYt9eZ9mniNnUsIo4px+LmpIQbH1AvQpV5zAY3SGZiLgOt5NShpsiR0X1ndv4omWMoT3GiBISY1HK4HwaWlCSoT7pXEHLoIpn9axn6N9+HaEWJeHWQTf49PI4MDfxQDBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAAkvd2YCW4+Zl6Dmr5L6ugzgb+c+Mb1Ire6+F+LeI/MBCXjmaL47M8hm/k8W+M48mGKwD5tZ8Y6r2F2Xy1aISUjF2mxj+7t0xH1rCjnGXZwQVE2Q1OK3ieYUU4QD6lWKlgpp7eP3ITZacHbGLIivBHhmYKQNTed2JfbSMFQUag14C3BlsePRfEU4nVJ/awTDzVi4bHMaoP21SbbRvAP4KUYLvA/Au0fKL3TEES6UqxPPo8Y05dwX6ssDzRojzX54fAan1RcYx3TJKFZjmGkdXraLXrijm0ttXHNVWyEAAAAABqfVFxksXFEhjMlMPUrxf1ja7gibof1E49vZigAAAAAG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqelSauQAMeqBvJ9LaWRxT7COxhZhsFMxr6pLH0M5CtwCAhAABQIwVwUAFBsAAAATBwsNCgUBAgADCRIGBBURFg4PDAgZFxhA/usuIvJbj50zAAAAMTF0MVl2bTdRYnlWbm1xZENVcGZBOFhVaUdWYnBIUFZuYU50UjI1Z2I4cDJkNER6anhpAQ==", + "base64" + ] +} \ No newline at end of file diff --git a/blockbuster/tests/helpers.rs b/blockbuster/tests/helpers.rs new file mode 100644 index 000000000..953dad3a5 --- /dev/null +++ b/blockbuster/tests/helpers.rs @@ -0,0 +1,396 @@ +// Workaround since this module is only used for testing. +#![allow(dead_code)] +use blockbuster::{ + error::BlockbusterError, + instruction::{InstructionBundle, IxPair}, +}; +use borsh::ser::BorshSerialize; +use flatbuffers::{FlatBufferBuilder, WIPOffset}; +use mpl_bubblegum::LeafSchemaEvent; +use plerkle_serialization::{ + root_as_account_info, root_as_compiled_instruction, + serializer::seralize_encoded_transaction_with_status, AccountInfo, AccountInfoArgs, + CompiledInstruction as FBCompiledInstruction, CompiledInstructionBuilder, + InnerInstructionsBuilder, Pubkey as FBPubkey, TransactionInfo, TransactionInfoBuilder, +}; +use rand::Rng; +use solana_geyser_plugin_interface::geyser_plugin_interface::ReplicaAccountInfo; +use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}; +use solana_transaction_status::{ + EncodedConfirmedTransactionWithStatusMeta, InnerInstruction, InnerInstructions, +}; +use spl_account_compression::events::{ + AccountCompressionEvent, ApplicationDataEvent, ApplicationDataEventV1, +}; +use std::{fs::File, io::BufReader}; + +pub fn random_program() -> Pubkey { + Pubkey::new_unique() +} + +pub fn random_pubkey() -> Pubkey { + random_program() +} + +pub fn random_data(max: usize) -> Vec { + let mut s = rand::thread_rng(); + let x = s.gen_range(1..max); + let mut data: Vec = Vec::with_capacity(x); + for i in 0..x { + let d: u8 = s.gen_range(0..255); + data.insert(i, d); + } + data +} + +pub fn random_u8() -> u8 { + let mut s = rand::thread_rng(); + s.gen() +} + +pub fn random_u8_bound(min: u8, max: u8) -> u8 { + let mut s = rand::thread_rng(); + s.gen_range(min..max) +} + +pub fn random_list(size: usize, elem_max: u8) -> Vec { + let mut s = rand::thread_rng(); + let mut data: Vec = Vec::with_capacity(size); + for i in 0..size { + let d: u8 = s.gen_range(0..elem_max); + data.insert(i, d); + } + data +} + +pub fn random_list_of(size: usize, fun: FN) -> Vec +where + FN: Fn(u8) -> T, +{ + let mut s = rand::thread_rng(); + let mut data: Vec = Vec::with_capacity(size); + for i in 0..size { + data.insert(i, fun(s.gen())); + } + data +} + +pub fn build_random_instruction<'a>( + fbb: &mut FlatBufferBuilder<'a>, + accounts_number_in_transaction: usize, + number_of_accounts: usize, +) -> WIPOffset> { + let accounts = random_list(5, random_u8_bound(1, number_of_accounts as u8)); + let accounts = fbb.create_vector(&accounts); + let data = random_data(10); + let data = fbb.create_vector(&data); + let mut s = rand::thread_rng(); + let mut builder = CompiledInstructionBuilder::new(fbb); + builder.add_data(data); + builder.add_program_id_index(s.gen_range(0..accounts_number_in_transaction) as u8); + builder.add_accounts(accounts); + builder.finish() +} + +pub fn build_random_transaction(mut fbb: FlatBufferBuilder) -> FlatBufferBuilder { + let mut s = rand::thread_rng(); + let mut outer_instructions = vec![]; + let mut inner_instructions = vec![]; + for _ in 0..s.gen_range(2..7) { + outer_instructions.push(build_random_instruction(&mut fbb, 10, 3)); + + let mut indexed_inner_instructions = vec![]; + for _ in 0..s.gen_range(2..7) { + let ix = build_random_instruction(&mut fbb, 10, 3); + indexed_inner_instructions.push(ix); + } + + let indexed_inner_instructions = fbb.create_vector(&indexed_inner_instructions); + let mut builder = InnerInstructionsBuilder::new(&mut fbb); + builder.add_index(s.gen_range(0..7)); + builder.add_instructions(indexed_inner_instructions); + inner_instructions.push(builder.finish()); + } + + let outer_instructions = fbb.create_vector(&outer_instructions); + let inner_instructions = fbb.create_vector(&inner_instructions); + let account_keys = random_list_of(10, |_| FBPubkey(random_pubkey().to_bytes())); + let account_keys = fbb.create_vector(&account_keys); + let mut builder = TransactionInfoBuilder::new(&mut fbb); + let slot = s.gen(); + builder.add_outer_instructions(outer_instructions); + builder.add_is_vote(false); + builder.add_inner_instructions(inner_instructions); + builder.add_account_keys(account_keys); + builder.add_slot(slot); + builder.add_seen_at(s.gen()); + let builder = builder.finish(); + fbb.finish_minimal(builder); + fbb +} + +pub fn get_programs(txn_info: TransactionInfo) -> Vec { + let mut outer_keys: Vec = txn_info + .outer_instructions() + .unwrap() + .iter() + .map(|ix| { + Pubkey::new_from_array( + txn_info + .account_keys() + .unwrap() + .iter() + .collect::>() + .get(ix.program_id_index() as usize) + .unwrap() + .0, + ) + }) + .collect(); + let mut inner = vec![]; + let inner_keys = txn_info + .inner_instructions() + .unwrap() + .iter() + .fold(&mut inner, |ix, curr| { + for p in curr.instructions().unwrap() { + ix.push(Pubkey::new_from_array( + txn_info + .account_keys() + .unwrap() + .iter() + .collect::>() + .get(p.program_id_index() as usize) + .unwrap() + .0, + )) + } + ix + }); + outer_keys.append(inner_keys); + outer_keys.dedup(); + outer_keys +} + +pub fn build_instruction<'a>( + fbb: &'a mut FlatBufferBuilder<'a>, + data: &[u8], + account_indexes: &[u8], +) -> Result { + let accounts_vec = fbb.create_vector(account_indexes); + let ix_data = fbb.create_vector(data); + let mut builder = CompiledInstructionBuilder::new(fbb); + builder.add_accounts(accounts_vec); + builder.add_program_id_index(0); + builder.add_data(ix_data); + let offset = builder.finish(); + fbb.finish_minimal(offset); + let data = fbb.finished_data(); + + let cix = root_as_compiled_instruction(data)?; + Ok(CompiledInstruction { + program_id_index: cix.program_id_index(), + accounts: cix + .accounts() + .expect("failed to deserialize accounts") + .bytes() + .to_vec(), + data: cix + .data() + .expect("failed to deserialize data") + .bytes() + .to_vec(), + }) +} + +pub fn build_account_update<'a>( + fbb: &'a mut FlatBufferBuilder<'a>, + account: &ReplicaAccountInfo, + slot: u64, + is_startup: bool, +) -> Result, flatbuffers::InvalidFlatbuffer> { + // Serialize vector data. + let pubkey = FBPubkey::from(account.pubkey); + let owner = FBPubkey::from(account.owner); + + // Don't serialize a zero-length data slice. + let data = if !account.data.is_empty() { + Some(fbb.create_vector(account.data)) + } else { + None + }; + + // Serialize everything into Account Info table. + let account_info = AccountInfo::create( + fbb, + &AccountInfoArgs { + pubkey: Some(&pubkey), + lamports: account.lamports, + owner: Some(&owner), + executable: account.executable, + rent_epoch: account.rent_epoch, + data, + write_version: account.write_version, + slot, + is_startup, + seen_at: 0, + }, + ); + + // Finalize buffer + fbb.finish(account_info, None); + let data = fbb.finished_data(); + root_as_account_info(data) +} + +pub fn build_random_account_update<'a>( + fbb: &'a mut FlatBufferBuilder<'a>, + data: &[u8], +) -> Result, flatbuffers::InvalidFlatbuffer> { + // Create a `ReplicaAccountInfo` to store the account update. + // All fields except caller-specified `data` are just random values. + let replica_account_info = ReplicaAccountInfo { + pubkey: &random_pubkey().to_bytes()[..], + lamports: 1, + owner: &random_pubkey().to_bytes()[..], + executable: false, + rent_epoch: 1000, + data, + write_version: 1, + }; + + // Flatbuffer serialize the `ReplicaAccountInfo`. + build_account_update(fbb, &replica_account_info, 0, false) +} + +pub fn build_txn_from_fixture( + fixture_name: String, + fbb: FlatBufferBuilder, +) -> Result { + let file = File::open(format!( + "{}/tests/fixtures/{}.json", + env!("CARGO_MANIFEST_DIR"), + fixture_name + )) + .unwrap(); + let reader = BufReader::new(file); + let ectxn: EncodedConfirmedTransactionWithStatusMeta = serde_json::from_reader(reader).unwrap(); + Ok(seralize_encoded_transaction_with_status(fbb, ectxn) + .expect("failed serialize encoded tx with status")) +} + +#[allow(clippy::too_many_arguments)] +pub fn build_bubblegum_bundle<'a>( + fbb1: &'a mut FlatBufferBuilder<'a>, + fbb2: &'a mut FlatBufferBuilder<'a>, + fbb3: &'a mut FlatBufferBuilder<'a>, + fbb4: &'a mut FlatBufferBuilder<'a>, + accounts: &'a [Pubkey], + account_indexes: &'a [u8], + ix_data: &'a [u8], + lse: LeafSchemaEvent, + cs_event: AccountCompressionEvent, +) -> InstructionBundle<'a> { + let lse_versioned = ApplicationDataEventV1 { + application_data: lse.try_to_vec().unwrap(), + }; + let lse_event = + AccountCompressionEvent::ApplicationData(ApplicationDataEvent::V1(lse_versioned)); + let outer_ix = build_instruction(fbb1, ix_data, account_indexes).unwrap(); + + let lse = lse_event.try_to_vec().unwrap(); + let noop_bgum = spl_noop::instruction(lse).data; + let ix = build_instruction(fbb2, &noop_bgum, account_indexes).unwrap(); + let noop_bgum_ix: IxPair = (spl_noop::id(), Box::leak(Box::new(ix))); + + // The Compression Instruction here doesnt matter only the noop but we add it here to ensure we are validating that one Account compression event is happening after Bubblegum + let ix = build_instruction(fbb3, &[0; 0], account_indexes).unwrap(); + let gummy_roll_ix: IxPair = (spl_account_compression::id(), Box::leak(Box::new(ix))); + + let cs = cs_event.try_to_vec().unwrap(); + let noop_compression = spl_noop::instruction(cs).data; + let ix = build_instruction(fbb4, &noop_compression, account_indexes).unwrap(); + let noop_compression_ix: IxPair = (spl_noop::id(), Box::leak(Box::new(ix))); + + let inner_ix = vec![noop_bgum_ix, gummy_roll_ix, noop_compression_ix]; + + // `Box::leak` is ok for tests + InstructionBundle { + program: mpl_bubblegum::ID, + inner_ix: Some(Box::leak(Box::new(inner_ix))), + keys: accounts, + instruction: Some(Box::leak(Box::new(outer_ix))), + ..Default::default() + } +} + +pub fn parse_fb( + tx_info: &TransactionInfo, +) -> ( + Vec, + Vec, + Vec, +) { + let mut account_keys = vec![]; + for key in tx_info.account_keys().iter().flatten() { + account_keys.push(Pubkey::try_from(key.0.as_slice()).expect("valid key from FlatBuffer")); + } + + let mut message_instructions = vec![]; + for cix in tx_info + .outer_instructions() + .expect("valid outer_instructions") + { + message_instructions.push(CompiledInstruction { + program_id_index: cix.program_id_index(), + accounts: cix.accounts().expect("valid accounts").bytes().to_vec(), + data: cix.data().expect("valid data").bytes().to_vec(), + }); + } + + let mut meta_inner_instructions = vec![]; + if let Some(ixs) = tx_info.compiled_inner_instructions() { + for ix in ixs { + let mut instructions = vec![]; + for ix in ix.instructions().expect("valid instructions") { + let cix = ix.compiled_instruction().expect("valid instruction"); + instructions.push(InnerInstruction { + instruction: CompiledInstruction { + program_id_index: cix.program_id_index(), + accounts: cix.accounts().expect("valid accounts").bytes().to_vec(), + data: cix.data().expect("valid data").bytes().to_vec(), + }, + stack_height: Some(ix.stack_height() as u32), + }) + } + + meta_inner_instructions.push(InnerInstructions { + index: ix.index(), + instructions, + }) + } + } else if let Some(ixs) = tx_info.inner_instructions() { + for ix in ixs { + let mut instructions = vec![]; + for cix in ix.instructions().expect("valid instructions") { + instructions.push(InnerInstruction { + instruction: CompiledInstruction { + program_id_index: cix.program_id_index(), + accounts: cix.accounts().expect("valid accounts").bytes().to_vec(), + data: cix.data().expect("valid data").bytes().to_vec(), + }, + stack_height: Some(0), + }) + } + + meta_inner_instructions.push(InnerInstructions { + index: ix.index(), + instructions, + }) + } + } else { + panic!("expect valid compiled_inner_instructions/inner_instructions") + } + + (account_keys, message_instructions, meta_inner_instructions) +} diff --git a/blockbuster/tests/instructions_test.rs b/blockbuster/tests/instructions_test.rs new file mode 100644 index 000000000..b13a9d7ee --- /dev/null +++ b/blockbuster/tests/instructions_test.rs @@ -0,0 +1,227 @@ +#[cfg(test)] +mod helpers; +use anchor_lang::AnchorDeserialize; +use blockbuster::{ + instruction::{order_instructions, InstructionBundle}, + program_handler::ProgramParser, + programs::{ + bubblegum::{BubblegumParser, LeafSchemaEvent, Payload}, + ProgramParseResult, + }, +}; +use flatbuffers::FlatBufferBuilder; +use helpers::*; +use plerkle_serialization::root_as_transaction_info; +use rand::prelude::IteratorRandom; +use spl_account_compression::events::{ + AccountCompressionEvent::{self}, + ApplicationDataEvent, ApplicationDataEventV1, ChangeLogEvent, ChangeLogEventV1, +}; +use std::{collections::HashSet, env}; + +#[test] +fn test_filter() { + let mut rng = rand::thread_rng(); + let fbb = FlatBufferBuilder::new(); + let fbb = build_random_transaction(fbb); + let data = fbb.finished_data(); + let txn = root_as_transaction_info(data).expect("TODO: panic message"); + let programs = get_programs(txn); + let hs = programs + .iter() + .choose_multiple(&mut rng, 3) + .into_iter() + .copied() + .collect::>(); + let (account_keys, message_instructions, meta_inner_instructions) = parse_fb(&txn); + let res = order_instructions( + &hs, + &account_keys, + &message_instructions, + &meta_inner_instructions, + ); + + for (ib, _inner) in res.iter() { + let public_key_matches = hs.contains(&ib.0); + assert!(public_key_matches); + } + + let res = order_instructions( + &HashSet::new(), + &account_keys, + &message_instructions, + &meta_inner_instructions, + ); + assert_eq!(res.len(), 0); +} + +fn prepare_fixture<'a>(fbb: FlatBufferBuilder<'a>, fixture: &'a str) -> FlatBufferBuilder<'a> { + println!("{:?}", env::current_dir()); + let name = fixture.to_string(); + let fbb = build_txn_from_fixture(name, fbb).unwrap(); + fbb +} + +#[test] +fn helium_nested() { + let fbb = FlatBufferBuilder::new(); + let txn = prepare_fixture(fbb, "helium_nested"); + let txn = root_as_transaction_info(txn.finished_data()).expect("Fail deser"); + let mut prog = HashSet::new(); + let id = mpl_bubblegum::ID; + let slot = txn.slot(); + prog.insert(id); + let (account_keys, message_instructions, meta_inner_instructions) = parse_fb(&txn); + let res = order_instructions( + &prog, + &account_keys, + &message_instructions, + &meta_inner_instructions, + ); + + let _ix = 0; + + let contains = res.iter().any(|(ib, _inner)| ib.0 == mpl_bubblegum::ID); + assert!(contains, "Must containe bgum at hoisted root"); + let subject = BubblegumParser {}; + for (outer_ix, inner_ix) in res.into_iter() { + let (program, instruction) = outer_ix; + // let ix_accounts = instruction.accounts.iter().collect::>(); + let ix_account_len = instruction.accounts.len(); + // let _max = ix_accounts.iter().max().copied().unwrap_or(0) as usize; + let ix_accounts = + instruction + .accounts + .iter() + .fold(Vec::with_capacity(ix_account_len), |mut acc, a| { + if let Some(key) = account_keys.get(*a as usize) { + acc.push(*key); + } + //else case here is handled on 272 + acc + }); + let bundle = InstructionBundle { + txn_id: "", + program, + instruction: Some(instruction), + inner_ix: inner_ix.as_deref(), + keys: ix_accounts.as_slice(), + slot, + }; + let result = subject.handle_instruction(&bundle).unwrap(); + let res_type = result.result_type(); + let parse_result = match res_type { + ProgramParseResult::Bubblegum(parse_result) => parse_result, + _ => panic!("Wrong type"), + }; + + if let ( + Some(_le), + Some(_cl), + Some(Payload::MintV1 { + args: _, + authority: _, + tree_id: _, + }), + ) = ( + &parse_result.leaf_update, + &parse_result.tree_update, + &parse_result.payload, + ) { + } else { + panic!("Failed to parse instruction"); + } + } +} + +#[test] +fn test_double_mint() { + let fbb = FlatBufferBuilder::new(); + let txn = prepare_fixture(fbb, "double_bubblegum_mint"); + let txn = root_as_transaction_info(txn.finished_data()).expect("Fail deser"); + let mut programs = HashSet::new(); + let subject = BubblegumParser {}.key(); + programs.insert(subject); + let (account_keys, message_instructions, meta_inner_instructions) = parse_fb(&txn); + let ix = order_instructions( + &programs, + &account_keys, + &message_instructions, + &meta_inner_instructions, + ); + assert_eq!(ix.len(), 2); + let contains = ix.iter().filter(|(ib, _inner)| ib.0 == mpl_bubblegum::ID); + let mut count = 0; + contains.for_each(|(_pk, cix)| { + count += 1; + if let Some(inner) = &cix { + println!("{}", inner.len()); + for ii in inner { + println!("pp{} {:?}", count, ii.0); + } + println!("------"); + let cl = AccountCompressionEvent::try_from_slice(&inner[1].1.data).unwrap(); + if let AccountCompressionEvent::ApplicationData(ApplicationDataEvent::V1( + ApplicationDataEventV1 { application_data }, + )) = cl + { + let lse = LeafSchemaEvent::try_from_slice(&application_data).unwrap(); + println!("1 pp{} NONCE {:?}\n end", count, lse.schema.nonce()); + } + let cl = AccountCompressionEvent::try_from_slice(&inner[3].1.data).unwrap(); + if let AccountCompressionEvent::ChangeLog(ChangeLogEvent::V1(ChangeLogEventV1 { + id, + .. + })) = cl + { + println!("2 pp{} Merkle Tree {:?} \n end", count, id); + } + } + }); + assert_eq!(count, 2); +} + +#[test] +fn test_double_tree() { + let fbb = FlatBufferBuilder::new(); + let txn = prepare_fixture(fbb, "helium_mint_double_tree"); + let txn = root_as_transaction_info(txn.finished_data()).expect("Fail deser"); + let mut programs = HashSet::new(); + let subject = BubblegumParser {}.key(); + programs.insert(subject); + let (account_keys, message_instructions, meta_inner_instructions) = parse_fb(&txn); + let ix = order_instructions( + &programs, + &account_keys, + &message_instructions, + &meta_inner_instructions, + ); + let contains = ix.iter().filter(|(ib, _inner)| ib.0 == mpl_bubblegum::ID); + let mut count = 0; + contains.for_each(|(_pk, cix)| { + if let Some(inner) = &cix { + for ii in inner { + println!("pp{} {:?}", count, ii.0); + } + println!("------"); + let cl = AccountCompressionEvent::try_from_slice(&inner[1].1.data).unwrap(); + if let AccountCompressionEvent::ApplicationData(ApplicationDataEvent::V1( + ApplicationDataEventV1 { application_data }, + )) = cl + { + let lse = LeafSchemaEvent::try_from_slice(&application_data).unwrap(); + println!("1 pp{} NONCE {:?}\n end", count, lse.schema.nonce()); + } + let cl = AccountCompressionEvent::try_from_slice(&inner[3].1.data).unwrap(); + if let AccountCompressionEvent::ChangeLog(ChangeLogEvent::V1(ChangeLogEventV1 { + id, + .. + })) = cl + { + println!("2 pp{} Merkle Tree {:?} \n end", count, id); + } + } + count += 1; + }); + assert_eq!(count, 2); +}