Files
khadhroony-bot3/kb-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/escrow_campaign.rs
2026-08-08 22:28:17 +02:00

1145 lines
49 KiB
Rust

// 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<S, O>(
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<crate::DevnetMetaplexEscrowCampaignSummary>
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::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(kb_lib::DcMetadataMetaplexTokenMetadataDecoder)];
let metaplex_materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> = 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::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> = std::vec![
std::sync::Arc::new(kb_lib::DcSplAssociatedTokenAccountDecoder),
std::sync::Arc::new(kb_lib::DcSplTokenDecoder),
];
let ata_materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> = 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::sync::Arc<dyn kb_lib::DcApiInstructionDecoder>> =
std::vec![std::sync::Arc::new(kb_lib::DcSplTokenDecoder)];
let token_materializers: std::vec::Vec<std::sync::Arc<dyn kb_lib::MtApiEventMaterializer>> =
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<std::string::String> {
let program = match kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
.parse::<solana_pubkey::Pubkey>()
{
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::<solana_pubkey::Pubkey>() {
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<std::string::String> {
let owner = match owner.parse::<solana_pubkey::Pubkey>() {
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::<solana_pubkey::Pubkey>() {
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::<solana_pubkey::Pubkey>()
{
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<u64>,
) -> std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
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<u64>,
) -> std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
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<u64> {
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<bool> {
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<u64> {
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<u64> {
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<u64> {
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,
});
}
}