diff --git a/crates/core/migrations/2022-08-12-183150_add_index_and_taken_to_config_lines/down.sql b/crates/core/migrations/2022-08-12-183150_add_index_and_taken_to_config_lines/down.sql new file mode 100644 index 000000000..d9ada1ad3 --- /dev/null +++ b/crates/core/migrations/2022-08-12-183150_add_index_and_taken_to_config_lines/down.sql @@ -0,0 +1,11 @@ +-- clear all the data +delete from candy_machine_config_lines; + +alter table candy_machine_config_lines +rename column candy_machine_address to address; + +alter table candy_machine_config_lines +drop constraint candy_machine_address_idx_pkey, +drop column idx, +drop column taken, +add constraint candy_machine_config_lines_pkey primary key (address); \ No newline at end of file diff --git a/crates/core/migrations/2022-08-12-183150_add_index_and_taken_to_config_lines/up.sql b/crates/core/migrations/2022-08-12-183150_add_index_and_taken_to_config_lines/up.sql new file mode 100644 index 000000000..859ca87d1 --- /dev/null +++ b/crates/core/migrations/2022-08-12-183150_add_index_and_taken_to_config_lines/up.sql @@ -0,0 +1,8 @@ +alter table candy_machine_config_lines +rename column address to candy_machine_address; + +alter table candy_machine_config_lines +add column idx int not null, +add column taken bool not null, +drop constraint candy_machine_config_lines_pkey, +add constraint candy_machine_address_idx_pkey primary key (candy_machine_address, idx); \ No newline at end of file diff --git a/crates/core/src/db/models.rs b/crates/core/src/db/models.rs index e2d90810b..c788c5073 100644 --- a/crates/core/src/db/models.rs +++ b/crates/core/src/db/models.rs @@ -887,11 +887,15 @@ pub struct CandyMachineData<'a> { #[table_name = "candy_machine_config_lines"] pub struct CMConfigLine<'a> { /// ConfigLine account address - pub address: Cow<'a, str>, + pub candy_machine_address: Cow<'a, str>, /// Name pub name: Cow<'a, str>, /// URI pointing to JSON representing the asset pub uri: Cow<'a, str>, + /// The index of the config line within the candy machine data + pub idx: i32, + /// Bool indicating if this config line has been minted (true) or not minted (false) + pub taken: bool, } /// A row in the `candy_machine_creators` table diff --git a/crates/core/src/db/schema.rs b/crates/core/src/db/schema.rs index 940b57c62..50804d2c6 100644 --- a/crates/core/src/db/schema.rs +++ b/crates/core/src/db/schema.rs @@ -203,10 +203,12 @@ table! { use diesel_full_text_search::{TsVector as Tsvector, TsQuery as Tsquery}; use crate::db::custom_types::{ListingEventLifecycle as Listingeventlifecycle, Mode, ProposalState as Proposalstate, InstructionExecutionFlags as Instructionexecutionflags, ProposalVoteType as Proposalvotetype, OptionVoteResult as Optionvoteresult, MintMaxVoteType as Mintmaxvotetype, VoteTipping as Votetipping, VoteWeightV1 as Voteweightv1, VoteRecordV2Vote as Vote_record_v2_vote, VoteThresholdType as Votethresholdtype, GovernanceAccountType as Governanceaccounttype, TransactionExecutionStatus as Transactionexecutionstatus, OfferEventLifecycle as Offereventlifecycle, SettingType as Settingtype, TokenStandard as Token_standard, }; - candy_machine_config_lines (address) { - address -> Varchar, + candy_machine_config_lines (candy_machine_address, idx) { + candy_machine_address -> Varchar, name -> Text, uri -> Text, + idx -> Int4, + taken -> Bool, } } diff --git a/crates/graphql/src/schema/context.rs b/crates/graphql/src/schema/context.rs index 41e1d6e25..1728f3eee 100644 --- a/crates/graphql/src/schema/context.rs +++ b/crates/graphql/src/schema/context.rs @@ -9,9 +9,10 @@ use objects::{ ah_purchase::Purchase as AhPurchase, auction_house::AuctionHouse, bid_receipt::BidReceipt, - candymachine::{ + candy_machine::{ CandyMachine, CandyMachineCollectionPda, CandyMachineConfigLine, CandyMachineCreator, - CandyMachineEndSetting, CandyMachineWhitelistMintSetting, + CandyMachineEndSetting, CandyMachineGateKeeperConfig, CandyMachineHiddenSetting, + CandyMachineWhitelistMintSetting, }, genopets::{GenoHabitat, GenoRentalAgreement}, graph_connection::GraphConnection, @@ -34,10 +35,7 @@ use scalars::{ PublicKey, }; -use super::{ - objects::candymachine::{CandyMachineGateKeeperConfig, CandyMachineHiddenSetting}, - prelude::*, -}; +use super::prelude::*; #[derive(Clone)] pub struct AppContext { @@ -49,18 +47,18 @@ pub struct AppContext { pub auction_house_loader: Loader, Option>, pub bid_receipt_loader: Loader, Option>, pub bid_receipts_loader: Loader, Vec>, - pub candymachine_collection_pda_loader: + pub candy_machine_collection_pda_loader: Loader, Option>, - pub candymachine_config_line_loader: + pub candy_machine_config_line_loader: Loader, Vec>, - pub candymachine_creator_loader: Loader, Vec>, - pub candymachine_end_settings_loader: + pub candy_machine_creator_loader: Loader, Vec>, + pub candy_machine_end_settings_loader: Loader, Option>, - pub candymachine_gatekeeper_configs_loader: + pub candy_machine_gatekeeper_configs_loader: Loader, Option>, - pub candymachine_hidden_settings_loader: + pub candy_machine_hidden_settings_loader: Loader, Option>, - pub candymachine_whitelist_mint_settings_loader: + pub candy_machine_whitelist_mint_settings_loader: Loader, Option>, pub collection_count_loader: Loader, Option>, pub collection_floor_price_loader: Loader, Option>, @@ -90,15 +88,15 @@ pub struct AppContext { pub purchase_receipts_loader: Loader, Vec>, pub purchases_loader: Loader, Vec>, pub spl_approve_vote_choices_loader: Loader, Vec>, - pub spl_governance_loader: Loader, Option>, pub spl_governance_config_loader: Loader, Option>, + pub spl_governance_loader: Loader, Option>, pub spl_proposal_loader: Loader, Option>, pub spl_proposal_multi_choice_loader: Loader, Option>, pub spl_proposal_options_loader: Loader, Vec>, pub spl_proposalv1_loader: Loader, Option>, pub spl_proposalv2_loader: Loader, Option>, - pub spl_realm_loader: Loader, Option>, pub spl_realm_config_loader: Loader, Option>, + pub spl_realm_loader: Loader, Option>, pub spl_token_owner_record_loader: Loader, Option>, pub spl_vote_record_token_owner_loader: Loader, Vec>, @@ -126,13 +124,13 @@ impl AppContext { auction_house_loader: Loader::new(batcher.clone()), bid_receipt_loader: Loader::new(batcher.clone()), bid_receipts_loader: Loader::new(batcher.clone()), - candymachine_collection_pda_loader: Loader::new(batcher.clone()), - candymachine_config_line_loader: Loader::new(batcher.clone()), - candymachine_creator_loader: Loader::new(batcher.clone()), - candymachine_end_settings_loader: Loader::new(batcher.clone()), - candymachine_gatekeeper_configs_loader: Loader::new(batcher.clone()), - candymachine_hidden_settings_loader: Loader::new(batcher.clone()), - candymachine_whitelist_mint_settings_loader: Loader::new(batcher.clone()), + candy_machine_collection_pda_loader: Loader::new(batcher.clone()), + candy_machine_config_line_loader: Loader::new(batcher.clone()), + candy_machine_creator_loader: Loader::new(batcher.clone()), + candy_machine_end_settings_loader: Loader::new(batcher.clone()), + candy_machine_gatekeeper_configs_loader: Loader::new(batcher.clone()), + candy_machine_hidden_settings_loader: Loader::new(batcher.clone()), + candy_machine_whitelist_mint_settings_loader: Loader::new(batcher.clone()), collection_count_loader: Loader::new(batcher.clone()), collection_floor_price_loader: Loader::new(batcher.clone()), collection_loader: Loader::new(batcher.clone()), @@ -161,15 +159,15 @@ impl AppContext { purchase_receipts_loader: Loader::new(batcher.clone()), purchases_loader: Loader::new(batcher.clone()), spl_approve_vote_choices_loader: Loader::new(batcher.clone()), - spl_governance_loader: Loader::new(batcher.clone()), spl_governance_config_loader: Loader::new(batcher.clone()), + spl_governance_loader: Loader::new(batcher.clone()), spl_proposal_loader: Loader::new(batcher.clone()), spl_proposal_multi_choice_loader: Loader::new(batcher.clone()), spl_proposal_options_loader: Loader::new(batcher.clone()), spl_proposalv1_loader: Loader::new(batcher.clone()), spl_proposalv2_loader: Loader::new(batcher.clone()), - spl_realm_loader: Loader::new(batcher.clone()), spl_realm_config_loader: Loader::new(batcher.clone()), + spl_realm_loader: Loader::new(batcher.clone()), spl_token_owner_record_loader: Loader::new(batcher.clone()), spl_vote_record_token_owner_loader: Loader::new(batcher.clone()), store_auction_houses_loader: Loader::new(batcher.clone()), diff --git a/crates/graphql/src/schema/dataloaders/candymachine.rs b/crates/graphql/src/schema/dataloaders/candy_machine.rs similarity index 96% rename from crates/graphql/src/schema/dataloaders/candymachine.rs rename to crates/graphql/src/schema/dataloaders/candy_machine.rs index 360d47162..a119572d8 100644 --- a/crates/graphql/src/schema/dataloaders/candymachine.rs +++ b/crates/graphql/src/schema/dataloaders/candy_machine.rs @@ -1,4 +1,4 @@ -use objects::candymachine::{ +use objects::candy_machine::{ CandyMachine, CandyMachineCollectionPda, CandyMachineConfigLine, CandyMachineCreator, CandyMachineEndSetting, CandyMachineGateKeeperConfig, CandyMachineHiddenSetting, CandyMachineWhitelistMintSetting, @@ -61,13 +61,13 @@ impl TryBatchFn, Vec> for Batche let conn = self.db()?; let rows: Vec = candy_machine_config_lines::table - .filter(candy_machine_config_lines::address.eq(any(addresses))) + .filter(candy_machine_config_lines::candy_machine_address.eq(any(addresses))) .load(&conn) .context("Failed to candy machine config lines")?; Ok(rows .into_iter() - .map(|r| (r.address.clone(), r.try_into())) + .map(|r| (r.candy_machine_address.clone(), r.try_into())) .batch(addresses)) } } diff --git a/crates/graphql/src/schema/dataloaders/mod.rs b/crates/graphql/src/schema/dataloaders/mod.rs index 3cb4e63d1..78c16d896 100644 --- a/crates/graphql/src/schema/dataloaders/mod.rs +++ b/crates/graphql/src/schema/dataloaders/mod.rs @@ -3,7 +3,7 @@ pub mod ah_offer; pub mod ah_purchase; pub mod auction_house; pub mod bid_receipt; -pub mod candymachine; +pub mod candy_machine; pub mod collection; pub mod genopets; pub mod graph_connection; diff --git a/crates/graphql/src/schema/objects/candymachine.rs b/crates/graphql/src/schema/objects/candy_machine.rs similarity index 94% rename from crates/graphql/src/schema/objects/candymachine.rs rename to crates/graphql/src/schema/objects/candy_machine.rs index d9586aa85..caff9ddf5 100644 --- a/crates/graphql/src/schema/objects/candymachine.rs +++ b/crates/graphql/src/schema/objects/candy_machine.rs @@ -87,7 +87,7 @@ impl CandyMachine { #[graphql(description = "NOTE - this is currently bugged and will only return one creator")] pub async fn creators(&self, ctx: &AppContext) -> FieldResult> { - ctx.candymachine_creator_loader + ctx.candy_machine_creator_loader .load(self.address.clone()) .await .map_err(Into::into) @@ -97,7 +97,7 @@ impl CandyMachine { &self, ctx: &AppContext, ) -> FieldResult> { - ctx.candymachine_collection_pda_loader + ctx.candy_machine_collection_pda_loader .load(self.address.clone()) .await .map_err(Into::into) @@ -105,7 +105,7 @@ impl CandyMachine { #[graphql(description = "NOTE - this is currently bugged and will always be empty")] pub async fn config_lines(&self, ctx: &AppContext) -> FieldResult> { - ctx.candymachine_config_line_loader + ctx.candy_machine_config_line_loader .load(self.address.clone()) .await .map_err(Into::into) @@ -115,7 +115,7 @@ impl CandyMachine { &self, ctx: &AppContext, ) -> FieldResult> { - ctx.candymachine_end_settings_loader + ctx.candy_machine_end_settings_loader .load(self.address.clone()) .await .map_err(Into::into) @@ -125,7 +125,7 @@ impl CandyMachine { &self, ctx: &AppContext, ) -> FieldResult> { - ctx.candymachine_whitelist_mint_settings_loader + ctx.candy_machine_whitelist_mint_settings_loader .load(self.address.clone()) .await .map_err(Into::into) @@ -135,7 +135,7 @@ impl CandyMachine { &self, ctx: &AppContext, ) -> FieldResult> { - ctx.candymachine_hidden_settings_loader + ctx.candy_machine_hidden_settings_loader .load(self.address.clone()) .await .map_err(Into::into) @@ -145,7 +145,7 @@ impl CandyMachine { &self, ctx: &AppContext, ) -> FieldResult> { - ctx.candymachine_gatekeeper_configs_loader + ctx.candy_machine_gatekeeper_configs_loader .load(self.address.clone()) .await .map_err(Into::into) @@ -253,14 +253,26 @@ pub struct CandyMachineConfigLine { pub candy_machine_address: PublicKey, pub name: String, pub uri: String, + pub idx: i32, + pub taken: bool, } impl<'a> From> for CandyMachineConfigLine { - fn from(models::CMConfigLine { address, name, uri }: models::CMConfigLine) -> Self { + fn from( + models::CMConfigLine { + candy_machine_address, + name, + uri, + idx, + taken, + }: models::CMConfigLine, + ) -> Self { Self { - candy_machine_address: address.into(), + candy_machine_address: candy_machine_address.into(), name: name.into_owned(), uri: uri.into_owned(), + idx, + taken, } } } diff --git a/crates/graphql/src/schema/objects/mod.rs b/crates/graphql/src/schema/objects/mod.rs index 9120a8b18..b61cbcaa2 100644 --- a/crates/graphql/src/schema/objects/mod.rs +++ b/crates/graphql/src/schema/objects/mod.rs @@ -4,7 +4,7 @@ pub mod ah_purchase; pub mod auction_house; pub mod bid_receipt; pub mod bonding_change; -pub mod candymachine; +pub mod candy_machine; pub mod chart; pub mod creator; pub mod denylist; diff --git a/crates/graphql/src/schema/query_root.rs b/crates/graphql/src/schema/query_root.rs index 0d429818d..23cfffb45 100644 --- a/crates/graphql/src/schema/query_root.rs +++ b/crates/graphql/src/schema/query_root.rs @@ -11,7 +11,7 @@ use objects::{ auction_house::AuctionHouse, bid_receipt::BidReceipt, bonding_change::EnrichedBondingChange, - candymachine::CandyMachine, + candy_machine::CandyMachine, chart::PriceChart, creator::Creator, denylist::Denylist, @@ -53,6 +53,29 @@ impl From for queries::metadatas::AttributeFilter { } } +impl QueryRoot { + fn candy_machine(context: &AppContext, address: String) -> FieldResult> { + let conn = context.shared.db.get()?; + + candy_machines::table + .inner_join( + candy_machine_datas::table + .on(candy_machines::address.eq(candy_machine_datas::candy_machine_address)), + ) + .filter(candy_machines::address.eq(address)) + .select(( + candy_machines::all_columns, + candy_machine_datas::all_columns, + )) + .first::<(models::CandyMachine, models::CandyMachineData)>(&conn) + .optional() + .context("Failed to load candy machine by address.")? + .map(TryInto::try_into) + .transpose() + .map_err(Into::into) + } +} + #[graphql_object(Context = AppContext)] impl QueryRoot { #[graphql( @@ -651,30 +674,18 @@ impl QueryRoot { .map_err(Into::into) } - #[graphql(description = "Get a candymachine by the candymachine config address")] - fn candymachine( + #[graphql(deprecated = "Deprecated alias for candyMachine")] + fn candymachine(&self, ctx: &AppContext, addr: String) -> FieldResult> { + Self::candy_machine(ctx, addr) + } + + #[graphql(description = "Get a candy machine by the candy machine config address")] + fn candy_machine( &self, context: &AppContext, - #[graphql(description = "address of the candymachine config")] address: String, + #[graphql(description = "address of the candy machine config")] address: String, ) -> FieldResult> { - let conn = context.shared.db.get()?; - - candy_machines::table - .inner_join( - candy_machine_datas::table - .on(candy_machines::address.eq(candy_machine_datas::candy_machine_address)), - ) - .filter(candy_machines::address.eq(address)) - .select(( - candy_machines::all_columns, - candy_machine_datas::all_columns, - )) - .first::<(models::CandyMachine, models::CandyMachineData)>(&conn) - .optional() - .context("Failed to load candy machine by address.")? - .map(TryInto::try_into) - .transpose() - .map_err(Into::into) + Self::candy_machine(context, address) } fn storefronts(&self, context: &AppContext) -> FieldResult> { diff --git a/crates/indexer/src/geyser/accounts/candy_machine.rs b/crates/indexer/src/geyser/accounts/candy_machine.rs index 86ccaa8f8..e15c36119 100644 --- a/crates/indexer/src/geyser/accounts/candy_machine.rs +++ b/crates/indexer/src/geyser/accounts/candy_machine.rs @@ -30,6 +30,7 @@ pub(crate) async fn process( client: &Client, key: Pubkey, candy_machine: CandyMachine, + config_lines: Option>, ) -> Result<()> { let cm = DbCandyMachine { address: Owned(bs58::encode(key).into_string()), @@ -59,6 +60,10 @@ pub(crate) async fn process( Box::pin(process_creators(client, key, candy_machine.data.creators)), ]; + if let Some(config_lines) = config_lines { + futures.push(Box::pin(process_config_lines(client, key, config_lines))); + } + if let Some(es) = candy_machine.data.end_settings { futures.push(Box::pin(process_end_settings(client, key, es))); }; @@ -80,6 +85,51 @@ pub(crate) async fn process( Ok(()) } +async fn process_config_lines( + client: &Client, + key: Pubkey, + config_lines: Vec<(ConfigLine, usize, bool)>, +) -> Result<()> { + let mut db_config_lines: Vec = Vec::new(); + + for config_line in &config_lines { + let db_config_line = CMConfigLine { + candy_machine_address: Owned(key.to_string()), + name: Owned(config_line.0.name.trim_matches('\0').to_owned()), + uri: Owned(config_line.0.uri.trim_matches('\0').to_owned()), + idx: i32::try_from(config_line.1).unwrap_or(-1), + taken: config_line.2, + }; + + db_config_lines.push(db_config_line); + } + + client + .db() + .run(move |db| { + db.build_transaction().read_write().run(|| { + for cl in &db_config_lines { + insert_into(candy_machine_config_lines::table) + .values(cl) + .on_conflict(( + candy_machine_config_lines::candy_machine_address, + candy_machine_config_lines::idx, + )) + .do_update() + .set(cl) + .execute(db) + .context("Failed to insert config line")?; + } + + Result::<_>::Ok(()) + }) + }) + .await + .context("Failed to insert candy machine config lines")?; + + Ok(()) +} + async fn process_data(client: &Client, key: Pubkey, data: CandyMachineData) -> Result<()> { let cm_data = CMData { candy_machine_address: Owned(bs58::encode(key).into_string()), @@ -244,31 +294,6 @@ async fn process_whitelist_mint_settings( .context("failed to insert whitelist mint setting")?; Ok(()) } -pub(crate) async fn process_config_line( - client: &Client, - key: Pubkey, - config_line: ConfigLine, -) -> Result<()> { - let row = CMConfigLine { - address: Owned(bs58::encode(key).into_string()), - name: Owned(config_line.name), - uri: Owned(config_line.uri.trim_end_matches('\0').to_owned()), - }; - - client - .db() - .run(move |db| { - insert_into(candy_machine_config_lines::table) - .values(&row) - .on_conflict(candy_machine_config_lines::address) - .do_update() - .set(&row) - .execute(db) - }) - .await - .context("failed to insert config line")?; - Ok(()) -} pub(crate) async fn process_collection_pda( client: &Client, diff --git a/crates/indexer/src/geyser/programs/candy_machine.rs b/crates/indexer/src/geyser/programs/candy_machine.rs index 9ef4500a5..1c965b353 100644 --- a/crates/indexer/src/geyser/programs/candy_machine.rs +++ b/crates/indexer/src/geyser/programs/candy_machine.rs @@ -1,11 +1,78 @@ use anchor_lang_v0_21::{AccountDeserialize, AnchorDeserialize}; -use mpl_candy_machine::{CandyMachine, CollectionPDA, ConfigLine, CONFIG_LINE_SIZE}; +use mpl_candy_machine::{ + CandyMachine, CollectionPDA, ConfigLine, CONFIG_ARRAY_START, CONFIG_LINE_SIZE, +}; use super::{accounts::candy_machine, AccountUpdate, Client}; use crate::prelude::*; const COLLECTION_PDA_SIZE: usize = 8 + 64; +/// parse config lines out of raw candy machine accounts data +/// lines that are not "available" are ignored (this would occur if the config lines are in the process +/// of being added, or failed to be added for whatever reason) +/// returns a vector containing tuples: (`config_line`, index, taken) +/// +/// it is important that this not be called if the candy machine has hidden settings +pub fn parse_cm_config_lines( + data: &[u8], + items_available: usize, +) -> Result> { + const CONFIG_LINE_START: usize = CONFIG_ARRAY_START + 4; + let available_bitmask_start = CONFIG_LINE_START + (items_available * CONFIG_LINE_SIZE) + 4; + + // NOTE(will): you would think that (items_available / 8) incorrectly computes the length of the + // "available" bitmask. i.e. if there are 7 items available, this value would be 0. + // however, this is how metaplex has coded it. It ends up working because there are 4 bytes of padding + // left in between. It seems like probably this was intended to be a 32 bit integer indicating + // the length of the "taken" bitmask, however, in practice, it is always all zeroes and the first byte + // will get overwritten by the last byte of the "available" bitmask. When items_available is an exact + // multiple of 8 everything is as you would expect, and there are just two extra bytes + // at the end of the account (*face palm*) + let taken_bitmask_start = available_bitmask_start + (items_available / 8) + 4; + + // Sanity check to make sure we aren't going to overflow data + // This could occur if this function is called on a candy machine that uses hiddensettings instead of + // config lines + let bytes_needed_for_taken_bitmask = items_available / 8 + (items_available % 8).min(1); + let total_expected_data_len = taken_bitmask_start + bytes_needed_for_taken_bitmask; + + if total_expected_data_len >= data.len() { + bail!( + "Config line bytes would overflow available data ({} vs {})", + total_expected_data_len, + data.len() + ); + } + + // (config_line, index, taken) + let mut config_lines: Vec<(ConfigLine, usize, bool)> = Vec::new(); + for idx in 0..items_available { + let available_bitmask_byte_offset = idx / 8; + let available_bitmask_bit_offset = 7 - (idx % 8); + let available_bitmask_value = data[available_bitmask_start + available_bitmask_byte_offset]; + let available = available_bitmask_value & (1 << available_bitmask_bit_offset) != 0; + + // NOTE(will): if the config line is not available, we simply ignore it + if available { + let config_line_byte_offset = CONFIG_LINE_START + (idx * CONFIG_LINE_SIZE); + let config_line = ConfigLine::deserialize( + &mut &data[config_line_byte_offset..config_line_byte_offset + CONFIG_LINE_SIZE], + ) + .with_context(|| format!("Failed to deserialize config line at index {}", idx))?; + + let taken_bitmask_byte_offset = idx / 8; + let taken_bitmask_bit_offset = 7 - (idx % 8); + let taken_bitmask_value = data[taken_bitmask_start + taken_bitmask_byte_offset]; + let taken = taken_bitmask_value & (1 << taken_bitmask_bit_offset) != 0; + + config_lines.push((config_line, idx, taken)); + } + } + + Ok(config_lines) +} + pub async fn process_collection_pda(client: &Client, update: AccountUpdate) -> Result<()> { let collection_pda: CollectionPDA = CollectionPDA::try_deserialize(&mut update.data.as_slice()) .context("Failed to deserialize collection pda")?; @@ -13,24 +80,94 @@ pub async fn process_collection_pda(client: &Client, update: AccountUpdate) -> R candy_machine::process_collection_pda(client, update.key, collection_pda).await } -pub async fn process_config_line(client: &Client, update: AccountUpdate) -> Result<()> { - let config_line: ConfigLine = ConfigLine::deserialize(&mut update.data.as_slice()) - .context("Failed to deserialize config line")?; - - candy_machine::process_config_line(client, update.key, config_line).await -} - pub async fn process_cm(client: &Client, update: AccountUpdate) -> Result<()> { let candy_machine: CandyMachine = CandyMachine::try_deserialize(&mut update.data.as_slice()) .context("Failed to deserialize candy_machine")?; - candy_machine::process(client, update.key, candy_machine).await + let items_available = usize::try_from(candy_machine.data.items_available) + .context("Failed to convert available item count")?; + + let lines = if candy_machine.data.hidden_settings.is_none() { + Some( + parse_cm_config_lines(&update.data, items_available) + .context("Failed to parse candy machine lines")?, + ) + } else { + None + }; + + candy_machine::process(client, update.key, candy_machine, lines).await } pub(crate) async fn process(client: &Client, update: AccountUpdate) -> Result<()> { match update.data.len() { COLLECTION_PDA_SIZE => process_collection_pda(client, update).await, - CONFIG_LINE_SIZE => process_config_line(client, update).await, _ => process_cm(client, update).await, } } + +#[cfg(test)] +mod tests { + use std::{env, fs, io::prelude::*, path::Path}; + + use anchor_lang_v0_21::AccountDeserialize; + use mpl_candy_machine::CandyMachine; + + use super::parse_cm_config_lines; + use crate::prelude::*; + + fn load_account_dump(filename: impl AsRef) -> Result> { + let mut path = env::current_dir().context("Failed to get working dir")?; + path.extend(["tests", "data"]); + path.push(filename); + + println!("Loading: {:?}", path); + + let mut f = fs::File::open(&path).with_context(|| format!("Failed to open {:?}", path))?; + let mut buffer = vec![]; + f.read_to_end(&mut buffer) + .with_context(|| format!("Failed to read {:?}", path))?; + + Ok(buffer) + } + + #[test] + fn test_can_deser_candy_machine() { + let filenames = [ + "candy_machines/FhrVJL4xKNmAY53Bm5XJNqJwvBomDuDH7HGDdicgbkZY.dmp", + "candy_machines/AoHidoffmkL4xURViNgbA4YyeDw82FAYUZfomL3X5BoU.dmp", + "candy_machines/piA76RvvmCt7UWEmJSBVA6xMoXqwvEAELwJoqeHK6i3.dmp", + "candy_machines/CiBuYi3W3aVQbMWcjvfKBpwjHS6fViuuxQdSUUqkjkn4.dmp", + // This candy machine has hidden settings, but for some reason has allocated space + // for all the config lines that would be needed, however they are all empty. + // This results in returning a results array with zero as none of the config + // lines are marked as available + "candy_machines/ACDPaQ3uGy33KsBKiUH4azDX4q7Nxk3QwW3trALEdFmB.dmp", + ]; + + for filename in filenames { + println!("Reading Candy Machine {:?}", filename); + let data = load_account_dump(filename).unwrap(); + let cm = CandyMachine::try_deserialize(&mut data.as_slice()).unwrap(); + println!("Candy Machine: {}", filename); + let avail = usize::try_from(cm.data.items_available).unwrap(); + let results = parse_cm_config_lines(&data, avail).unwrap(); + + let available_count = results.len(); + let mut taken_count = 0; + for (_, _, taken) in &results { + if *taken { + taken_count += 1; + } + } + + if cm.data.hidden_settings.is_some() { + assert_eq!(available_count, 0); + assert_eq!(taken_count, 0); + } else { + assert_eq!(available_count, avail); + assert_eq!(taken_count, cm.items_redeemed); + } + } + } +} diff --git a/crates/indexer/tests/data/candy_machines/ACDPaQ3uGy33KsBKiUH4azDX4q7Nxk3QwW3trALEdFmB.dmp b/crates/indexer/tests/data/candy_machines/ACDPaQ3uGy33KsBKiUH4azDX4q7Nxk3QwW3trALEdFmB.dmp new file mode 100644 index 000000000..cb792f6e6 Binary files /dev/null and b/crates/indexer/tests/data/candy_machines/ACDPaQ3uGy33KsBKiUH4azDX4q7Nxk3QwW3trALEdFmB.dmp differ diff --git a/crates/indexer/tests/data/candy_machines/AoHidoffmkL4xURViNgbA4YyeDw82FAYUZfomL3X5BoU.dmp b/crates/indexer/tests/data/candy_machines/AoHidoffmkL4xURViNgbA4YyeDw82FAYUZfomL3X5BoU.dmp new file mode 100644 index 000000000..287559b05 Binary files /dev/null and b/crates/indexer/tests/data/candy_machines/AoHidoffmkL4xURViNgbA4YyeDw82FAYUZfomL3X5BoU.dmp differ diff --git a/crates/indexer/tests/data/candy_machines/CiBuYi3W3aVQbMWcjvfKBpwjHS6fViuuxQdSUUqkjkn4.dmp b/crates/indexer/tests/data/candy_machines/CiBuYi3W3aVQbMWcjvfKBpwjHS6fViuuxQdSUUqkjkn4.dmp new file mode 100644 index 000000000..ba3cddbde Binary files /dev/null and b/crates/indexer/tests/data/candy_machines/CiBuYi3W3aVQbMWcjvfKBpwjHS6fViuuxQdSUUqkjkn4.dmp differ diff --git a/crates/indexer/tests/data/candy_machines/FhrVJL4xKNmAY53Bm5XJNqJwvBomDuDH7HGDdicgbkZY.dmp b/crates/indexer/tests/data/candy_machines/FhrVJL4xKNmAY53Bm5XJNqJwvBomDuDH7HGDdicgbkZY.dmp new file mode 100644 index 000000000..d18ffe210 Binary files /dev/null and b/crates/indexer/tests/data/candy_machines/FhrVJL4xKNmAY53Bm5XJNqJwvBomDuDH7HGDdicgbkZY.dmp differ diff --git a/crates/indexer/tests/data/candy_machines/piA76RvvmCt7UWEmJSBVA6xMoXqwvEAELwJoqeHK6i3.dmp b/crates/indexer/tests/data/candy_machines/piA76RvvmCt7UWEmJSBVA6xMoXqwvEAELwJoqeHK6i3.dmp new file mode 100644 index 000000000..fd4c46569 Binary files /dev/null and b/crates/indexer/tests/data/candy_machines/piA76RvvmCt7UWEmJSBVA6xMoXqwvEAELwJoqeHK6i3.dmp differ