// file: kb-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/escrow_campaign.rs // version: 1 //! Confirmed Devnet Token Owned Escrow campaign for current Metaplex escrow operations. /// Exact SPL state transitions exercised around the Metaplex escrow operations. #[derive(Clone, Debug, Eq, PartialEq)] pub struct DevnetMetaplexEscrowCampaignState { /// Raw fungible token amount deposited into the Token Owned Escrow ATA. pub deposit_amount_raw: u64, /// Operator attribute-token amount before the controlled deposit. pub operator_attribute_amount_before_deposit: u64, /// Operator attribute-token amount after the controlled deposit. pub operator_attribute_amount_after_deposit: u64, /// Escrow attribute-token amount after the controlled deposit. pub escrow_attribute_amount_after_deposit: u64, /// Operator attribute-token amount after `TransferOutOfEscrow`. pub operator_attribute_amount_after_transfer_out: u64, /// Whether the empty escrow attribute ATA was closed by `TransferOutOfEscrow`. pub escrow_attribute_account_closed: bool, /// Whether `CloseEscrowAccount` removed the Token Owned Escrow account. pub escrow_account_closed: bool, /// Parent NFT token amount observed after the escrow account is closed. pub parent_token_amount_after_close: u64, } /// Complete evidence retained by one current Token Owned Escrow Devnet campaign. #[derive(Clone, Debug, PartialEq)] pub struct DevnetMetaplexEscrowCampaignSummary { /// Fresh NFT whose holder controls the Token Owned Escrow. pub parent_fixture: crate::DevnetMetaplexCreateMintCampaignSummary, /// Fresh fungible asset used as the controlled escrow attribute token. pub attribute_fixture: crate::DevnetMetaplexCreateMintCampaignSummary, /// Canonical Token Owned Escrow PDA. pub escrow: std::string::String, /// Canonical classic SPL ATA owned by the escrow PDA for the attribute mint. pub escrow_attribute_token_account: std::string::String, /// Confirmed `CreateEscrowAccount` execution. pub create_escrow: crate::DevnetMetaplexTokenMetadataExecutionSummary, /// Confirmed ATA creation used only to prepare the attribute deposit. pub escrow_attribute_ata_creation: crate::DevnetSplAssociatedTokenAccountExecutionSummary, /// Confirmed classic SPL transfer depositing one raw attribute unit into the escrow ATA. pub attribute_deposit: crate::DevnetSplTokenExecutionSummary, /// Confirmed `TransferOutOfEscrow` execution. pub transfer_out: crate::DevnetMetaplexTokenMetadataExecutionSummary, /// Confirmed `CloseEscrowAccount` execution. pub close_escrow: crate::DevnetMetaplexTokenMetadataExecutionSummary, /// Exact SPL and account-presence postconditions across the campaign. pub state: crate::DevnetMetaplexEscrowCampaignState, } /// Returns the exact ordered Metaplex operations qualified by this campaign. pub fn metaplex_escrow_campaign_operation_names() -> &'static [&'static str; 3] { return &["create_escrow_account", "transfer_out_of_escrow", "close_escrow_account"]; } /// Executes a fresh Token Owned Escrow lifecycle on Devnet. #[allow(clippy::too_many_arguments)] pub async fn execute_devnet_metaplex_escrow_campaign( http_pool: &kb_onchain_transport::HttpEndpointPool, store: &S, profile: &kb_config::ProfileConfig, workspace_root: &std::path::Path, options: &crate::MetaplexCreateFixturePreparationOptions, observer: &O, ) -> kb_core::Result where S: kb_store::RawTransactionStore + kb_store::CoreExtractionStore + kb_store::DecodePipelineStore + Sync, O: crate::SolanaExecutionObserver, { if !profile.wallet.devnet_send_enabled { return std::result::Result::Err(kb_core::Error::config( "Metaplex escrow Devnet campaign requires devnet_send_enabled=true", )); } let parent_options = clean_fixture_options(options, crate::MetaplexTokenMetadataAssetFamily::Nft); let attribute_options = clean_fixture_options(options, crate::MetaplexTokenMetadataAssetFamily::Fungible); let parent_fixture = match crate::execute_devnet_metaplex_create_mint_campaign( http_pool, store, profile, workspace_root, &parent_options, observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let attribute_fixture = match crate::execute_devnet_metaplex_create_mint_campaign( http_pool, store, profile, workspace_root, &attribute_options, observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let escrow = match derive_token_owner_escrow(parent_fixture.fixture.mint.as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let escrow_attribute_token_account = match derive_classic_ata(escrow.as_str(), attribute_fixture.fixture.mint.as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let metaplex_decoders: std::vec::Vec> = std::vec![std::sync::Arc::new(kb_lib::DcMetadataMetaplexTokenMetadataDecoder)]; let metaplex_materializers: std::vec::Vec> = std::vec![ std::sync::Arc::new(kb_lib::MtAdminMaterializer), std::sync::Arc::new(kb_lib::MtComplianceAuditMaterializer), std::sync::Arc::new(kb_lib::MtLifecycleMaterializer), std::sync::Arc::new(kb_lib::MtMetadataMetaplexTokenMetadataMaterializer), std::sync::Arc::new(kb_lib::MtRiskMaterializer), ]; let create_operation = kb_lib::ExMetaplexTokenMetadataOperation::CreateEscrowAccount { escrow: kb_lib::MdPubkey(escrow.clone()), metadata: kb_lib::MdPubkey(parent_fixture.fixture.metadata.clone()), mint: kb_lib::MdPubkey(parent_fixture.fixture.mint.clone()), token_account: kb_lib::MdPubkey(parent_fixture.fixture.token_account.clone()), edition: kb_lib::MdPubkey(parent_fixture.fixture.master_edition.clone()), payer: kb_lib::MdPubkey(parent_fixture.fixture.authority.clone()), system_program: kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()), sysvar_instructions: kb_lib::MdPubkey( kb_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID.to_string(), ), authority: std::option::Option::None, }; let mut create_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new( format!("metaplex-escrow-create-{}", uuid::Uuid::new_v4()), create_operation, ); configure_metaplex_submission(&mut create_request, &parent_options); create_request.postcondition_reads = escrow_reads( escrow.as_str(), parent_fixture.fixture.metadata.as_str(), parent_options.query_role.as_str(), std::option::Option::None, ); let create_escrow = match crate::execute_devnet_metaplex_token_metadata( http_pool, store, profile, workspace_root, &create_request, metaplex_decoders.as_slice(), metaplex_materializers.as_slice(), observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let create_slot = match validate_metaplex_execution("create-escrow", &create_escrow) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if let std::result::Result::Err(error) = validate_token_owner_escrow_snapshot( create_escrow.after.as_slice(), escrow.as_str(), parent_fixture.fixture.mint.as_str(), ) { return std::result::Result::Err(error); } let ata_decoders: std::vec::Vec> = std::vec![ std::sync::Arc::new(kb_lib::DcSplAssociatedTokenAccountDecoder), std::sync::Arc::new(kb_lib::DcSplTokenDecoder), ]; let ata_materializers: std::vec::Vec> = std::vec![ std::sync::Arc::new(kb_lib::MtTokenAccountsMaterializer), std::sync::Arc::new(kb_lib::MtRiskMaterializer), ]; let mut ata_request = crate::DevnetSplAssociatedTokenAccountExecutionRequest::new( format!("metaplex-escrow-attribute-ata-{}", uuid::Uuid::new_v4()), kb_lib::ExSplAssociatedTokenAccountOperation::CreateIdempotent { wallet_owner: kb_lib::MdPubkey(escrow.clone()), mint: kb_lib::MdPubkey(attribute_fixture.fixture.mint.clone()), token_program: kb_lib::ExSplAssociatedTokenProgram::Classic, }, ); ata_request.query_role = parent_options.query_role.clone(); ata_request.transaction_role = parent_options.transaction_role.clone(); ata_request.submit = true; ata_request.operator_confirmed = true; ata_request.post_validation_max_retries = 20; let escrow_attribute_ata_creation = match crate::execute_devnet_spl_associated_token_account( http_pool, store, profile, workspace_root, &ata_request, ata_decoders.as_slice(), ata_materializers.as_slice(), observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let ata_slot = match validate_ata_setup(&escrow_attribute_ata_creation) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let empty_escrow_attribute = match crate::read_token_account_at_or_after( http_pool, parent_options.query_role.as_str(), escrow_attribute_token_account.as_str(), std::option::Option::Some(ata_slot), ) .await { std::result::Result::Ok(std::option::Option::Some(value)) => value, std::result::Result::Ok(std::option::Option::None) => { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_attribute_ata_missing", "escrow attribute ATA is missing after confirmed ATA creation", )); }, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if let std::result::Result::Err(error) = crate::validate_classic_fixture_token_account_state( &empty_escrow_attribute, attribute_fixture.fixture.mint.as_str(), escrow.as_str(), 0, spl_token_interface::state::AccountState::Initialized, ) { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_attribute_ata_invalid", format!("fresh escrow attribute ATA is invalid: {error}"), )); } const DEPOSIT_AMOUNT_RAW: u64 = 1; let operator_attribute_amount_before_deposit = attribute_fixture.fixture.mint_amount_raw; let token_decoders: std::vec::Vec> = std::vec![std::sync::Arc::new(kb_lib::DcSplTokenDecoder)]; let token_materializers: std::vec::Vec> = std::vec![std::sync::Arc::new(kb_lib::MtTokenAccountsMaterializer)]; let deposit_operation = kb_lib::ExSplClassicTokenOperation::Instruction { value: kb_lib::ExSplClassicTokenSingleOperation::TransferChecked { source: kb_lib::MdPubkey(attribute_fixture.fixture.token_account.clone()), mint: kb_lib::MdPubkey(attribute_fixture.fixture.mint.clone()), destination: kb_lib::MdPubkey(escrow_attribute_token_account.clone()), authority: kb_lib::ExSplClassicTokenAuthority { authority: kb_lib::MdPubkey(attribute_fixture.fixture.authority.clone()), multisig_signers: std::vec::Vec::new(), }, amount: kb_lib::ExSplClassicTokenAmount(DEPOSIT_AMOUNT_RAW.to_string()), decimals: attribute_fixture.fixture.mint_decimals, }, }; let mut deposit_request = crate::DevnetSplTokenExecutionRequest::new( format!("metaplex-escrow-attribute-deposit-{}", uuid::Uuid::new_v4()), deposit_operation, ); deposit_request.query_role = parent_options.query_role.clone(); deposit_request.transaction_role = parent_options.transaction_role.clone(); deposit_request.submit = true; deposit_request.operator_confirmed = true; deposit_request.post_validation_max_retries = 20; let attribute_deposit = match crate::execute_devnet_spl_token( http_pool, store, profile, workspace_root, &deposit_request, token_decoders.as_slice(), token_materializers.as_slice(), observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let deposit_slot = match validate_token_setup(&attribute_deposit) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let expected_operator_after_deposit = operator_attribute_amount_before_deposit.saturating_sub(DEPOSIT_AMOUNT_RAW); let operator_after_deposit = match read_and_validate_token_amount( http_pool, parent_options.query_role.as_str(), attribute_fixture.fixture.token_account.as_str(), attribute_fixture.fixture.mint.as_str(), attribute_fixture.fixture.authority.as_str(), expected_operator_after_deposit, deposit_slot, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let escrow_after_deposit = match read_and_validate_token_amount( http_pool, parent_options.query_role.as_str(), escrow_attribute_token_account.as_str(), attribute_fixture.fixture.mint.as_str(), escrow.as_str(), DEPOSIT_AMOUNT_RAW, deposit_slot, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let transfer_operation = kb_lib::ExMetaplexTokenMetadataOperation::TransferOutOfEscrow { escrow: kb_lib::MdPubkey(escrow.clone()), metadata: kb_lib::MdPubkey(parent_fixture.fixture.metadata.clone()), payer: kb_lib::MdPubkey(parent_fixture.fixture.authority.clone()), attribute_mint: kb_lib::MdPubkey(attribute_fixture.fixture.mint.clone()), attribute_src: kb_lib::MdPubkey(escrow_attribute_token_account.clone()), attribute_dst: kb_lib::MdPubkey(attribute_fixture.fixture.token_account.clone()), escrow_mint: kb_lib::MdPubkey(parent_fixture.fixture.mint.clone()), escrow_account: kb_lib::MdPubkey(parent_fixture.fixture.token_account.clone()), system_program: kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()), ata_program: kb_lib::MdPubkey(kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string()), token_program: kb_lib::MdPubkey(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()), sysvar_instructions: kb_lib::MdPubkey( kb_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID.to_string(), ), authority: std::option::Option::None, args: mpl_token_metadata::instructions::TransferOutOfEscrowInstructionArgs { amount: DEPOSIT_AMOUNT_RAW, }, }; let mut transfer_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new( format!("metaplex-escrow-transfer-out-{}", uuid::Uuid::new_v4()), transfer_operation, ); configure_metaplex_submission(&mut transfer_request, &parent_options); transfer_request.preflight_reads = escrow_reads( escrow.as_str(), parent_fixture.fixture.metadata.as_str(), parent_options.query_role.as_str(), std::option::Option::Some(create_slot), ); transfer_request.postcondition_reads = escrow_reads( escrow.as_str(), parent_fixture.fixture.metadata.as_str(), parent_options.query_role.as_str(), std::option::Option::Some(deposit_slot), ); let transfer_out = match crate::execute_devnet_metaplex_token_metadata( http_pool, store, profile, workspace_root, &transfer_request, metaplex_decoders.as_slice(), metaplex_materializers.as_slice(), observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let transfer_slot = match validate_metaplex_execution("transfer-out-of-escrow", &transfer_out) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if let std::result::Result::Err(error) = validate_token_owner_escrow_snapshot( transfer_out.after.as_slice(), escrow.as_str(), parent_fixture.fixture.mint.as_str(), ) { return std::result::Result::Err(error); } let operator_after_transfer_out = match read_and_validate_token_amount( http_pool, parent_options.query_role.as_str(), attribute_fixture.fixture.token_account.as_str(), attribute_fixture.fixture.mint.as_str(), attribute_fixture.fixture.authority.as_str(), operator_attribute_amount_before_deposit, transfer_slot, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let escrow_attribute_account_closed = match crate::read_token_account_at_or_after( http_pool, parent_options.query_role.as_str(), escrow_attribute_token_account.as_str(), std::option::Option::Some(transfer_slot), ) .await { std::result::Result::Ok(std::option::Option::None) => true, std::result::Result::Ok(std::option::Option::Some(_)) => false, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if !escrow_attribute_account_closed { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_attribute_source_not_closed", "TransferOutOfEscrow must close the emptied escrow attribute ATA", )); } let close_operation = kb_lib::ExMetaplexTokenMetadataOperation::CloseEscrowAccount { escrow: kb_lib::MdPubkey(escrow.clone()), metadata: kb_lib::MdPubkey(parent_fixture.fixture.metadata.clone()), mint: kb_lib::MdPubkey(parent_fixture.fixture.mint.clone()), token_account: kb_lib::MdPubkey(parent_fixture.fixture.token_account.clone()), edition: kb_lib::MdPubkey(parent_fixture.fixture.master_edition.clone()), payer: kb_lib::MdPubkey(parent_fixture.fixture.authority.clone()), system_program: kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()), sysvar_instructions: kb_lib::MdPubkey( kb_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID.to_string(), ), }; let mut close_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new( format!("metaplex-escrow-close-{}", uuid::Uuid::new_v4()), close_operation, ); configure_metaplex_submission(&mut close_request, &parent_options); close_request.preflight_reads = escrow_reads( escrow.as_str(), parent_fixture.fixture.metadata.as_str(), parent_options.query_role.as_str(), std::option::Option::Some(transfer_slot), ); close_request.postcondition_reads = metadata_read( parent_fixture.fixture.metadata.as_str(), parent_options.query_role.as_str(), std::option::Option::Some(transfer_slot), ); let close_escrow = match crate::execute_devnet_metaplex_token_metadata( http_pool, store, profile, workspace_root, &close_request, metaplex_decoders.as_slice(), metaplex_materializers.as_slice(), observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let close_slot = match validate_metaplex_execution("close-escrow", &close_escrow) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let escrow_account_closed = match metaplex_account_is_absent( http_pool, parent_options.query_role.as_str(), escrow.as_str(), close_slot, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if !escrow_account_closed { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_account_not_closed", "CloseEscrowAccount left the Token Owned Escrow account present", )); } let parent_token_amount_after_close = match read_and_validate_token_amount( http_pool, parent_options.query_role.as_str(), parent_fixture.fixture.token_account.as_str(), parent_fixture.fixture.mint.as_str(), parent_fixture.fixture.authority.as_str(), 1, close_slot, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(crate::DevnetMetaplexEscrowCampaignSummary { parent_fixture, attribute_fixture, escrow, escrow_attribute_token_account, create_escrow, escrow_attribute_ata_creation, attribute_deposit, transfer_out, close_escrow, state: crate::DevnetMetaplexEscrowCampaignState { deposit_amount_raw: DEPOSIT_AMOUNT_RAW, operator_attribute_amount_before_deposit, operator_attribute_amount_after_deposit: operator_after_deposit, escrow_attribute_amount_after_deposit: escrow_after_deposit, operator_attribute_amount_after_transfer_out: operator_after_transfer_out, escrow_attribute_account_closed, escrow_account_closed, parent_token_amount_after_close, }, }); } fn clean_fixture_options( options: &crate::MetaplexCreateFixturePreparationOptions, family: crate::MetaplexTokenMetadataAssetFamily, ) -> crate::MetaplexCreateFixturePreparationOptions { return crate::MetaplexCreateFixturePreparationOptions { query_role: options.query_role.clone(), transaction_role: options.transaction_role.clone(), wallet_dir: options.wallet_dir.clone(), asset_family: family, collection_mint: std::option::Option::None, print_supply_limit: std::option::Option::None, uses: std::option::Option::None, }; } fn configure_metaplex_submission( request: &mut crate::DevnetMetaplexTokenMetadataExecutionRequest, options: &crate::MetaplexCreateFixturePreparationOptions, ) { request.query_role = options.query_role.clone(); request.transaction_role = options.transaction_role.clone(); request.submit = true; request.operator_confirmed = true; request.materialize_after_confirmation = true; request.post_validation_max_retries = 20; } fn derive_token_owner_escrow(mint: &str) -> kb_core::Result { let program = match kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID .parse::() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::config(format!( "invalid Metaplex Program ID: {error}" ))); }, }; let mint = match mint.parse::() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::config(format!( "invalid escrow parent mint: {error}" ))); }, }; let (escrow, _) = solana_pubkey::Pubkey::find_program_address( &[b"metadata", program.as_ref(), mint.as_ref(), &[0], b"escrow"], &program, ); return std::result::Result::Ok(escrow.to_string()); } fn derive_classic_ata(owner: &str, mint: &str) -> kb_core::Result { let owner = match owner.parse::() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::config(format!( "invalid escrow ATA owner: {error}" ))); }, }; let mint = match mint.parse::() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::config(format!( "invalid escrow attribute mint: {error}" ))); }, }; let token_program = match kb_program_ids::SPL_TOKEN_PROGRAM_ID.parse::() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::config(format!( "invalid classic SPL Token Program ID: {error}" ))); }, }; return std::result::Result::Ok( spl_associated_token_account_interface::address::get_associated_token_address_with_program_id( &owner, &mint, &token_program, ) .to_string(), ); } fn escrow_reads( escrow: &str, metadata: &str, query_role: &str, min_context_slot: std::option::Option, ) -> std::vec::Vec { let mut reads = metadata_read(metadata, query_role, min_context_slot); reads.push(kb_pipeline::MetaplexTokenMetadataStatefulReadRequest { query_role: query_role.to_string(), account: kb_lib::MdPubkey(escrow.to_string()), kind: kb_pipeline::MetaplexTokenMetadataAccountKind::TokenOwnedEscrow, min_context_slot, max_data_bytes: kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES, }); return reads; } fn metadata_read( metadata: &str, query_role: &str, min_context_slot: std::option::Option, ) -> std::vec::Vec { return std::vec![kb_pipeline::MetaplexTokenMetadataStatefulReadRequest { query_role: query_role.to_string(), account: kb_lib::MdPubkey(metadata.to_string()), kind: kb_pipeline::MetaplexTokenMetadataAccountKind::Metadata, min_context_slot, max_data_bytes: kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES, }]; } fn validate_token_owner_escrow_snapshot( snapshots: &[kb_pipeline::MetaplexTokenMetadataStatefulReadResult], escrow: &str, mint: &str, ) -> kb_core::Result<()> { let snapshot = match snapshots.iter().find(|value| return value.snapshot.account.0 == escrow) { std::option::Option::Some(value) => &value.snapshot, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_stateful_snapshot_missing", "Token Owned Escrow stateful snapshot is missing", )); }, }; if snapshot.account_kind != "token_owned_escrow" || snapshot.mint.as_ref().map(|value| return value.0.as_str()) != std::option::Option::Some(mint) || snapshot.payload_json["base_token"].as_str() != std::option::Option::Some(mint) || snapshot.payload_json["authority_kind"].as_str() != std::option::Option::Some("token_owner") || !snapshot.payload_json["creator"].is_null() { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_stateful_snapshot_invalid", format!("Token Owned Escrow state mismatch: {}", snapshot.payload_json), )); } return std::result::Result::Ok(()); } async fn read_and_validate_token_amount( http_pool: &kb_onchain_transport::HttpEndpointPool, query_role: &str, account: &str, mint: &str, owner: &str, expected_amount: u64, min_context_slot: u64, ) -> kb_core::Result { let token = match crate::read_token_account_at_or_after( http_pool, query_role, account, std::option::Option::Some(min_context_slot), ) .await { std::result::Result::Ok(std::option::Option::Some(value)) => value, std::result::Result::Ok(std::option::Option::None) => { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_token_account_missing", format!("expected token account {account} is missing"), )); }, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if let std::result::Result::Err(error) = crate::validate_classic_fixture_token_account_state( &token, mint, owner, expected_amount, spl_token_interface::state::AccountState::Initialized, ) { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_token_account_invalid", format!("token account {account} failed state validation: {error}"), )); } return std::result::Result::Ok(expected_amount); } async fn metaplex_account_is_absent( http_pool: &kb_onchain_transport::HttpEndpointPool, query_role: &str, account: &str, min_context_slot: u64, ) -> kb_core::Result { let config = match kb_onchain_transport::GetAccountInfoConfig::new_with_data( kb_onchain_transport::RpcCommitmentLevel::Confirmed, std::option::Option::Some(min_context_slot), 128, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let account = kb_lib::MdPubkey(account.to_string()); let result = match http_pool.get_account_info_for_role(query_role, &account, &config).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if result.context.slot < min_context_slot { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_close_context_too_old", "escrow close account read returned a slot below confirmation", )); } return std::result::Result::Ok(result.account.is_none()); } fn validate_metaplex_execution( step: &str, execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary, ) -> kb_core::Result { if !execution.simulation.success { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_simulation_failed", format!("Metaplex escrow {step} simulation failed"), )); } let confirmation = match execution.confirmation.as_ref() { std::option::Option::Some(value) if matches!( value.status, kb_lib::ExApiExecutionConfirmationStatus::Confirmed | kb_lib::ExApiExecutionConfirmationStatus::Finalized ) => { value }, std::option::Option::Some(value) => { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_confirmation_incomplete", format!("Metaplex escrow {step} stopped at {:?}", value.status), )); }, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_confirmation_missing", format!("Metaplex escrow {step} has no confirmation evidence"), )); }, }; let diagnostic = match execution.post_execution.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_post_execution_missing", format!("Metaplex escrow {step} has no post-execution diagnostic"), )); }, }; if !diagnostic.canonical_inserted || !diagnostic.core_extracted || !diagnostic.decode_replayed || !diagnostic.materialized || execution.materializations.is_empty() || execution.materialized_snapshots.is_empty() { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_post_execution_incomplete", format!( "Metaplex escrow {step} post-execution incomplete: signature={}, slot={:?}, canonical_inserted={}, core_extracted={}, decode_replayed={}, materialized={}, materialization_rows={}, materialized_snapshots={}, diagnostics={:?}", confirmation.signature.0, confirmation.slot, diagnostic.canonical_inserted, diagnostic.core_extracted, diagnostic.decode_replayed, diagnostic.materialized, execution.materializations.len(), execution.materialized_snapshots.len(), diagnostic.diagnostics ), )); } if !idempotence_is_clean(execution.idempotence_replay.as_ref()) { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_idempotence_failed", format!("Metaplex escrow {step} second replay is not idempotent"), )); } return match confirmation.slot { std::option::Option::Some(value) => std::result::Result::Ok(value), std::option::Option::None => std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_confirmation_slot_missing", format!("Metaplex escrow {step} confirmation has no slot"), )), }; } fn validate_ata_setup( execution: &crate::DevnetSplAssociatedTokenAccountExecutionSummary, ) -> kb_core::Result { if !execution.simulation.success { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_ata_simulation_failed", "escrow attribute ATA simulation failed", )); } let confirmation = match execution.confirmation.as_ref() { std::option::Option::Some(value) if matches!( value.status, kb_lib::ExApiExecutionConfirmationStatus::Confirmed | kb_lib::ExApiExecutionConfirmationStatus::Finalized ) => { value }, _ => { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_ata_confirmation_incomplete", "escrow attribute ATA was not confirmed", )); }, }; let diagnostic = execution.post_execution.as_ref(); if !diagnostic.is_some_and(|value| { return value.canonical_inserted && value.core_extracted && value.decode_replayed && value.materialized; }) || execution.materializations.is_empty() || !idempotence_is_clean(execution.idempotence_replay.as_ref()) { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_ata_post_execution_incomplete", "escrow attribute ATA setup lacks complete pipeline evidence", )); } return match confirmation.slot { std::option::Option::Some(value) => std::result::Result::Ok(value), std::option::Option::None => std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_ata_confirmation_slot_missing", "escrow attribute ATA confirmation has no slot", )), }; } fn validate_token_setup(execution: &crate::DevnetSplTokenExecutionSummary) -> kb_core::Result { if !execution.simulation.success { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_deposit_simulation_failed", "escrow attribute deposit simulation failed", )); } let confirmation = match execution.confirmation.as_ref() { std::option::Option::Some(value) if matches!( value.status, kb_lib::ExApiExecutionConfirmationStatus::Confirmed | kb_lib::ExApiExecutionConfirmationStatus::Finalized ) => { value }, _ => { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_deposit_confirmation_incomplete", "escrow attribute deposit was not confirmed", )); }, }; let diagnostic = execution.post_execution.as_ref(); if !diagnostic.is_some_and(|value| { return value.canonical_inserted && value.core_extracted && value.decode_replayed && value.materialized; }) || execution.materializations.is_empty() || !idempotence_is_clean(execution.idempotence_replay.as_ref()) { return std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_deposit_post_execution_incomplete", "escrow attribute deposit lacks complete pipeline evidence", )); } return match confirmation.slot { std::option::Option::Some(value) => std::result::Result::Ok(value), std::option::Option::None => std::result::Result::Err(kb_core::Error::new( "metaplex_escrow_deposit_confirmation_slot_missing", "escrow attribute deposit confirmation has no slot", )), }; } fn idempotence_is_clean(replay: std::option::Option<&kb_pipeline::DecodeReplaySummary>) -> bool { return replay.is_some_and(|value| { return value.failed_inputs == 0 && value.processing_error_inputs == 0 && value.processors.iter().all(|processor| { return processor.failed == 0 && processor.processing_errors == 0 && processor.materialized_outputs == 0 && processor.materialization_refused == 0; }); }); } #[cfg(test)] mod tests { #[test] fn campaign_contract_is_exactly_create_transfer_out_close() { assert_eq!( crate::metaplex_escrow_campaign_operation_names(), &["create_escrow_account", "transfer_out_of_escrow", "close_escrow_account"] ); } #[test] fn token_owner_escrow_and_attribute_ata_derivations_are_canonical() { let mint = solana_pubkey::Pubkey::new_from_array([31_u8; 32]).to_string(); let attribute_mint = solana_pubkey::Pubkey::new_from_array([32_u8; 32]).to_string(); let escrow = crate::metadata::metaplex_token_metadata::escrow_campaign::derive_token_owner_escrow( mint.as_str(), ) .unwrap_or_else(|error| panic!("escrow derivation failed: {error}")); let ata = crate::metadata::metaplex_token_metadata::escrow_campaign::derive_classic_ata( escrow.as_str(), attribute_mint.as_str(), ) .unwrap_or_else(|error| panic!("ATA derivation failed: {error}")); assert_ne!(escrow, ata); assert_ne!(mint, attribute_mint); } #[tokio::test] async fn optional_devnet_metaplex_escrow_campaign_from_env() { if std::env::var("KB_DEVNET_METAPLEX_ESCROW_CAMPAIGN_TEST").ok().as_deref() != std::option::Option::Some("1") { return; } assert_eq!( std::env::var("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref(), std::option::Option::Some("1"), "set KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 before the escrow campaign" ); let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() { std::option::Option::Some(value) => value, std::option::Option::None => panic!("workspace root cannot be resolved"), }; let config_path = match std::env::var("KB_DEVNET_CONFIG_PATH") { std::result::Result::Ok(value) => { let path = std::path::PathBuf::from(value); if path.is_absolute() { path } else { workspace_root.join(path) } }, std::result::Result::Err(_) => workspace_root.join("config/example.config.json"), }; let config = kb_config::read_config_json_file_with_environment( config_path.as_path(), workspace_root, ) .unwrap_or_else(|error| panic!("Devnet configuration loading failed: {error}")); let profile_name = std::env::var("KB_DEVNET_PROFILE").ok(); let mut profile = crate::resolve_demo_devnet_profile(&config, profile_name.as_deref()) .unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}")); let database_url = std::env::var("KB_POSTGRES_TEST_URL") .unwrap_or_else(|error| panic!("KB_POSTGRES_TEST_URL is required: {error}")); profile.database.backend = "postgres".to_string(); profile.database.postgres.url = database_url; let http_pool = kb_onchain_transport::HttpEndpointPool::from_profile(&profile) .unwrap_or_else(|error| panic!("HTTP pool initialization failed: {error}")); let store_options = kb_store::PostgresStoreOptions::new( profile.database.postgres.url.clone(), profile.database.postgres.max_connections, profile.database.postgres.connect_timeout_ms, false, ) .unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}")); let store = kb_store::PostgresStore::connect(store_options) .await .unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}")); if let std::result::Result::Err(error) = store.initialize_store_schema().await { panic!("PostgreSQL schema initialization failed: {error}"); } let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str()); let wallet_dir = if configured_wallet_dir.is_absolute() { configured_wallet_dir } else { workspace_root.join(configured_wallet_dir) }; let options = crate::MetaplexCreateFixturePreparationOptions::new(wallet_dir); let summary = crate::execute_devnet_metaplex_escrow_campaign( &http_pool, &store, &profile, workspace_root, &options, &crate::NoopSolanaExecutionObserver, ) .await .unwrap_or_else(|error| panic!("Metaplex escrow Devnet campaign failed: {error}")); println!( "METAPLEX_ESCROW_FIXTURE parent_mint={} parent_metadata={} parent_edition={} parent_token={} attribute_mint={} attribute_token={} escrow={} escrow_attribute_token={}", summary.parent_fixture.fixture.mint, summary.parent_fixture.fixture.metadata, summary.parent_fixture.fixture.master_edition, summary.parent_fixture.fixture.token_account, summary.attribute_fixture.fixture.mint, summary.attribute_fixture.fixture.token_account, summary.escrow, summary.escrow_attribute_token_account, ); print_setup_step("attribute_ata", &summary.escrow_attribute_ata_creation); print_token_setup_step("attribute_deposit", &summary.attribute_deposit); print_metaplex_step("create_escrow", &summary.create_escrow); print_metaplex_step("transfer_out", &summary.transfer_out); print_metaplex_step("close_escrow", &summary.close_escrow); println!( "METAPLEX_ESCROW_STATE deposit_amount={} operator_before={} operator_after_deposit={} escrow_after_deposit={} operator_after_transfer_out={} escrow_attribute_closed={} escrow_closed={} parent_amount_after_close={}", summary.state.deposit_amount_raw, summary.state.operator_attribute_amount_before_deposit, summary.state.operator_attribute_amount_after_deposit, summary.state.escrow_attribute_amount_after_deposit, summary.state.operator_attribute_amount_after_transfer_out, summary.state.escrow_attribute_account_closed, summary.state.escrow_account_closed, summary.state.parent_token_amount_after_close, ); println!( "METAPLEX_ESCROW_EVIDENCE create={} transfer_out={} close={}", execution_evidence_json(&summary.create_escrow), execution_evidence_json(&summary.transfer_out), execution_evidence_json(&summary.close_escrow), ); } fn print_setup_step( label: &str, execution: &crate::DevnetSplAssociatedTokenAccountExecutionSummary, ) { let confirmation = execution .confirmation .as_ref() .unwrap_or_else(|| panic!("{label} confirmation is missing")); println!( "METAPLEX_ESCROW_SETUP label={} signature={} slot={:?} materializations={}", label, confirmation.signature.0, confirmation.slot, execution.materializations.len(), ); } fn print_token_setup_step(label: &str, execution: &crate::DevnetSplTokenExecutionSummary) { let confirmation = execution .confirmation .as_ref() .unwrap_or_else(|| panic!("{label} confirmation is missing")); println!( "METAPLEX_ESCROW_SETUP label={} signature={} slot={:?} materializations={}", label, confirmation.signature.0, confirmation.slot, execution.materializations.len(), ); } fn print_metaplex_step( label: &str, execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary, ) { let confirmation = execution .confirmation .as_ref() .unwrap_or_else(|| panic!("{label} confirmation is missing")); println!( "METAPLEX_ESCROW_STEP label={} operation={} signature={} slot={:?} materializations={}", label, execution.plan.operation_code, confirmation.signature.0, confirmation.slot, execution.materializations.len(), ); } fn execution_evidence_json( execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary, ) -> serde_json::Value { let confirmation = execution.confirmation.as_ref(); let post_execution = execution.post_execution.as_ref(); let idempotence_clean = execution.idempotence_replay.as_ref().is_some_and(|replay| { return replay.failed_inputs == 0 && replay.processing_error_inputs == 0 && replay.processors.iter().all(|processor| { return processor.failed == 0 && processor.processing_errors == 0 && processor.materialized_outputs == 0 && processor.materialization_refused == 0; }); }); return serde_json::json!({ "operation": execution.plan.operation_code.as_str(), "cluster": execution.cluster, "genesisHash": execution.genesis_hash.as_str(), "simulationContextSlot": execution.simulation_context_slot, "simulationSuccess": execution.simulation.success, "simulationLogCount": execution.simulation.logs.len(), "messageHash": execution.readiness.message_hash.as_str(), "feeLamports": execution.fee.fee_lamports, "signature": confirmation.map(|value| return value.signature.0.clone()), "confirmationStatus": confirmation.map(|value| return format!("{:?}", value.status)), "confirmationSlot": confirmation.and_then(|value| return value.slot), "beforeSnapshots": execution.before.len(), "afterSnapshots": execution.after.len(), "canonicalHydration": post_execution.is_some_and(|value| return value.canonical_inserted), "coreExtraction": post_execution.is_some_and(|value| return value.core_extracted), "decodeReplay": post_execution.is_some_and(|value| return value.decode_replayed), "materialized": post_execution.is_some_and(|value| return value.materialized), "instructionMaterializations": execution.materializations.len(), "materializedSnapshots": execution.materialized_snapshots.len(), "idempotenceReplayClean": idempotence_clean, }); } }