1145 lines
49 KiB
Rust
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,
|
|
});
|
|
}
|
|
}
|