0.5.1-pre.002
This commit is contained in:
@@ -0,0 +1,747 @@
|
||||
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/collection_verify_campaign.rs
|
||||
// version: 2
|
||||
|
||||
//! Confirmed Devnet collection parent/member `Verify -> Unverify` campaign.
|
||||
|
||||
/// Collection relation and sized-collection state observed across verification.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct DevnetMetaplexCollectionVerifyState {
|
||||
/// Member collection flag before `Verify`.
|
||||
pub verified_before: bool,
|
||||
/// Member collection flag after `Verify`.
|
||||
pub verified_after_verify: bool,
|
||||
/// Member collection flag after `Unverify`.
|
||||
pub verified_after_unverify: bool,
|
||||
/// Collection size before `Verify`.
|
||||
pub collection_size_before: u64,
|
||||
/// Collection size after `Verify`.
|
||||
pub collection_size_after_verify: u64,
|
||||
/// Collection size after `Unverify`.
|
||||
pub collection_size_after_unverify: u64,
|
||||
}
|
||||
|
||||
/// Complete evidence produced by one collection parent/member verification campaign.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DevnetMetaplexCollectionVerifyCampaignSummary {
|
||||
/// Fully qualified collection parent fixture and `Create -> Mint` evidence.
|
||||
pub parent: crate::DevnetMetaplexCreateMintCampaignSummary,
|
||||
/// Fully qualified unverified member fixture and `Create -> Mint` evidence.
|
||||
pub member: crate::DevnetMetaplexCreateMintCampaignSummary,
|
||||
/// Confirmed collection verification evidence.
|
||||
pub verify: crate::DevnetMetaplexTokenMetadataExecutionSummary,
|
||||
/// Confirmed collection unverification evidence.
|
||||
pub unverify: crate::DevnetMetaplexTokenMetadataExecutionSummary,
|
||||
/// Exact collection relation and size transitions.
|
||||
pub state: crate::DevnetMetaplexCollectionVerifyState,
|
||||
}
|
||||
|
||||
/// Returns the exact current Metaplex operations qualified by this campaign after fixture setup.
|
||||
pub fn metaplex_collection_verify_campaign_operation_names() -> &'static [&'static str; 2] {
|
||||
return &["verify", "unverify"];
|
||||
}
|
||||
|
||||
/// Executes one fresh collection parent/member `Verify -> Unverify` campaign on Devnet.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute_devnet_metaplex_collection_verify_campaign<S, O>(
|
||||
http_pool: &ks_onchain_transport::HttpEndpointPool,
|
||||
store: &S,
|
||||
profile: &ks_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
options: &crate::MetaplexCreateFixturePreparationOptions,
|
||||
observer: &O,
|
||||
) -> ks_core::Result<crate::DevnetMetaplexCollectionVerifyCampaignSummary>
|
||||
where
|
||||
S: ks_store::RawTransactionStore
|
||||
+ ks_store::CoreExtractionStore
|
||||
+ ks_store::DecodePipelineStore
|
||||
+ Sync,
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
if options.collection_mint.is_some() {
|
||||
return std::result::Result::Err(ks_core::Error::config(
|
||||
"collection verification campaign base options must not preselect a collection mint",
|
||||
));
|
||||
}
|
||||
let mut parent_options = options.clone();
|
||||
parent_options.asset_family = crate::MetaplexTokenMetadataAssetFamily::Collection;
|
||||
parent_options.collection_mint = std::option::Option::None;
|
||||
let parent = 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 mut member_options = options.clone();
|
||||
member_options.asset_family = crate::MetaplexTokenMetadataAssetFamily::Nft;
|
||||
member_options.collection_mint = std::option::Option::Some(parent.fixture.mint.clone());
|
||||
let member = match crate::execute_devnet_metaplex_create_mint_campaign(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&member_options,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verified_before = match collection_member_verified(
|
||||
member.mint.after.as_slice(),
|
||||
member.fixture.metadata.as_str(),
|
||||
parent.fixture.mint.as_str(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let collection_size_before =
|
||||
match collection_size(parent.mint.after.as_slice(), parent.fixture.metadata.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if verified_before || collection_size_before != 0 {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_verify_initial_state_invalid",
|
||||
format!(
|
||||
"fresh collection member must start unverified with parent size 0; verified={verified_before}, size={collection_size_before}"
|
||||
),
|
||||
));
|
||||
}
|
||||
let parent_mint_slot = match confirmed_slot("parent mint", &parent.mint) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let member_mint_slot = match confirmed_slot("member mint", &member.mint) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fixture_slot = std::cmp::max(parent_mint_slot, member_mint_slot);
|
||||
let decoders: std::vec::Vec<std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>> =
|
||||
std::vec![std::sync::Arc::new(ks_lib::DcMetadataMetaplexTokenMetadataDecoder)];
|
||||
let materializers: std::vec::Vec<std::sync::Arc<dyn ks_lib::MtApiEventMaterializer>> = std::vec![
|
||||
std::sync::Arc::new(ks_lib::MtAdminMaterializer),
|
||||
std::sync::Arc::new(ks_lib::MtComplianceAuditMaterializer),
|
||||
std::sync::Arc::new(ks_lib::MtLifecycleMaterializer),
|
||||
std::sync::Arc::new(ks_lib::MtMetadataMetaplexTokenMetadataMaterializer),
|
||||
std::sync::Arc::new(ks_lib::MtRiskMaterializer),
|
||||
];
|
||||
let verify_operation = ks_lib::ExMetaplexTokenMetadataOperation::Verify {
|
||||
authority: ks_lib::MdPubkey(parent.fixture.authority.clone()),
|
||||
delegate_record: std::option::Option::None,
|
||||
metadata: ks_lib::MdPubkey(member.fixture.metadata.clone()),
|
||||
collection_mint: std::option::Option::Some(ks_lib::MdPubkey(parent.fixture.mint.clone())),
|
||||
collection_metadata: std::option::Option::Some(ks_lib::MdPubkey(
|
||||
parent.fixture.metadata.clone(),
|
||||
)),
|
||||
collection_master_edition: std::option::Option::Some(ks_lib::MdPubkey(
|
||||
parent.fixture.master_edition.clone(),
|
||||
)),
|
||||
verification_args: mpl_token_metadata::types::VerificationArgs::CollectionV1,
|
||||
};
|
||||
let mut verify_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
|
||||
format!("metaplex-collection-verify-{}", uuid::Uuid::new_v4()),
|
||||
verify_operation,
|
||||
);
|
||||
verify_request.query_role = options.query_role.clone();
|
||||
verify_request.transaction_role = options.transaction_role.clone();
|
||||
verify_request.submit = true;
|
||||
verify_request.operator_confirmed = true;
|
||||
verify_request.materialize_after_confirmation = true;
|
||||
verify_request.post_validation_max_retries = 20;
|
||||
verify_request.preflight_reads = collection_reads(
|
||||
&parent.fixture,
|
||||
&member.fixture,
|
||||
options.query_role.as_str(),
|
||||
std::option::Option::Some(fixture_slot),
|
||||
);
|
||||
verify_request.postcondition_reads = collection_reads(
|
||||
&parent.fixture,
|
||||
&member.fixture,
|
||||
options.query_role.as_str(),
|
||||
std::option::Option::Some(fixture_slot),
|
||||
);
|
||||
let verify = match crate::execute_devnet_metaplex_token_metadata(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&verify_request,
|
||||
decoders.as_slice(),
|
||||
materializers.as_slice(),
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verify_slot = match validate_confirmed_execution("verify", &verify) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verified_after_verify = match collection_member_verified(
|
||||
verify.after.as_slice(),
|
||||
member.fixture.metadata.as_str(),
|
||||
parent.fixture.mint.as_str(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let collection_size_after_verify =
|
||||
match collection_size(verify.after.as_slice(), parent.fixture.metadata.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !verified_after_verify || collection_size_after_verify != 1 {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_verify_postcondition_failed",
|
||||
format!(
|
||||
"confirmed collection Verify must set member verified=true and parent size=1; verified={verified_after_verify}, size={collection_size_after_verify}"
|
||||
),
|
||||
));
|
||||
}
|
||||
let unverify_operation = ks_lib::ExMetaplexTokenMetadataOperation::Unverify {
|
||||
authority: ks_lib::MdPubkey(parent.fixture.authority.clone()),
|
||||
delegate_record: std::option::Option::None,
|
||||
metadata: ks_lib::MdPubkey(member.fixture.metadata.clone()),
|
||||
collection_mint: std::option::Option::Some(ks_lib::MdPubkey(parent.fixture.mint.clone())),
|
||||
collection_metadata: std::option::Option::Some(ks_lib::MdPubkey(
|
||||
parent.fixture.metadata.clone(),
|
||||
)),
|
||||
verification_args: mpl_token_metadata::types::VerificationArgs::CollectionV1,
|
||||
};
|
||||
let mut unverify_request = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
|
||||
format!("metaplex-collection-unverify-{}", uuid::Uuid::new_v4()),
|
||||
unverify_operation,
|
||||
);
|
||||
unverify_request.query_role = options.query_role.clone();
|
||||
unverify_request.transaction_role = options.transaction_role.clone();
|
||||
unverify_request.submit = true;
|
||||
unverify_request.operator_confirmed = true;
|
||||
unverify_request.materialize_after_confirmation = true;
|
||||
unverify_request.post_validation_max_retries = 20;
|
||||
unverify_request.preflight_reads = collection_reads(
|
||||
&parent.fixture,
|
||||
&member.fixture,
|
||||
options.query_role.as_str(),
|
||||
std::option::Option::Some(verify_slot),
|
||||
);
|
||||
unverify_request.postcondition_reads = collection_reads(
|
||||
&parent.fixture,
|
||||
&member.fixture,
|
||||
options.query_role.as_str(),
|
||||
std::option::Option::Some(verify_slot),
|
||||
);
|
||||
let unverify = match crate::execute_devnet_metaplex_token_metadata(
|
||||
http_pool,
|
||||
store,
|
||||
profile,
|
||||
workspace_root,
|
||||
&unverify_request,
|
||||
decoders.as_slice(),
|
||||
materializers.as_slice(),
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let unverify_slot = match validate_confirmed_execution("unverify", &unverify) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verified_after_unverify = match collection_member_verified(
|
||||
unverify.after.as_slice(),
|
||||
member.fixture.metadata.as_str(),
|
||||
parent.fixture.mint.as_str(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let collection_size_after_unverify =
|
||||
match collection_size(unverify.after.as_slice(), parent.fixture.metadata.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if verified_after_unverify || collection_size_after_unverify != 0 {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_unverify_postcondition_failed",
|
||||
format!(
|
||||
"confirmed collection Unverify at slot {unverify_slot} must restore verified=false and parent size=0; verified={verified_after_unverify}, size={collection_size_after_unverify}"
|
||||
),
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(crate::DevnetMetaplexCollectionVerifyCampaignSummary {
|
||||
parent,
|
||||
member,
|
||||
verify,
|
||||
unverify,
|
||||
state: crate::DevnetMetaplexCollectionVerifyState {
|
||||
verified_before,
|
||||
verified_after_verify,
|
||||
verified_after_unverify,
|
||||
collection_size_before,
|
||||
collection_size_after_verify,
|
||||
collection_size_after_unverify,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn collection_reads(
|
||||
parent: &crate::MetaplexCreateFixturePreparationSummary,
|
||||
member: &crate::MetaplexCreateFixturePreparationSummary,
|
||||
query_role: &str,
|
||||
min_context_slot: std::option::Option<u64>,
|
||||
) -> std::vec::Vec<ks_pipeline::MetaplexTokenMetadataStatefulReadRequest> {
|
||||
return vec![
|
||||
ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
|
||||
query_role: query_role.to_string(),
|
||||
account: ks_lib::MdPubkey(member.metadata.clone()),
|
||||
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
|
||||
min_context_slot,
|
||||
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
|
||||
},
|
||||
ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
|
||||
query_role: query_role.to_string(),
|
||||
account: ks_lib::MdPubkey(parent.metadata.clone()),
|
||||
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Metadata,
|
||||
min_context_slot,
|
||||
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
|
||||
},
|
||||
ks_pipeline::MetaplexTokenMetadataStatefulReadRequest {
|
||||
query_role: query_role.to_string(),
|
||||
account: ks_lib::MdPubkey(parent.master_edition.clone()),
|
||||
kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Edition {
|
||||
mint: ks_lib::MdPubkey(parent.mint.clone()),
|
||||
},
|
||||
min_context_slot,
|
||||
max_data_bytes: ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
fn confirmed_slot(
|
||||
step: &str,
|
||||
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
|
||||
) -> ks_core::Result<u64> {
|
||||
return match execution.confirmation.as_ref().and_then(|value| return value.slot) {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_fixture_confirmation_slot_missing",
|
||||
format!("Metaplex {step} has no confirmed slot"),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
fn validate_confirmed_execution(
|
||||
step: &str,
|
||||
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
|
||||
) -> ks_core::Result<u64> {
|
||||
if !execution.simulation.success {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_simulation_failed",
|
||||
format!("Metaplex collection {step} simulation failed"),
|
||||
));
|
||||
}
|
||||
let confirmation = match execution.confirmation.as_ref() {
|
||||
std::option::Option::Some(value)
|
||||
if matches!(
|
||||
value.status,
|
||||
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
|
||||
) =>
|
||||
{
|
||||
value
|
||||
},
|
||||
std::option::Option::Some(value) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_confirmation_incomplete",
|
||||
format!("Metaplex collection {step} stopped at {:?}", value.status),
|
||||
));
|
||||
},
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_confirmation_missing",
|
||||
format!("Metaplex collection {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(ks_core::Error::new(
|
||||
"metaplex_collection_post_execution_missing",
|
||||
format!("Metaplex collection {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(ks_core::Error::new(
|
||||
"metaplex_collection_post_execution_incomplete",
|
||||
format!(
|
||||
"Metaplex collection {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
|
||||
),
|
||||
));
|
||||
}
|
||||
let second_replay = match execution.idempotence_replay.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_idempotence_missing",
|
||||
format!("Metaplex collection {step} has no idempotence replay evidence"),
|
||||
));
|
||||
},
|
||||
};
|
||||
if second_replay.failed_inputs != 0
|
||||
|| second_replay.processing_error_inputs != 0
|
||||
|| second_replay.processors.iter().any(|processor| {
|
||||
return processor.failed != 0
|
||||
|| processor.processing_errors != 0
|
||||
|| processor.materialized_outputs != 0
|
||||
|| processor.materialization_refused != 0;
|
||||
})
|
||||
{
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_idempotence_failed",
|
||||
format!("Metaplex collection {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(ks_core::Error::new(
|
||||
"metaplex_collection_confirmation_slot_missing",
|
||||
format!("Metaplex collection {step} confirmation has no slot"),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
fn metadata_payload<'a>(
|
||||
snapshots: &'a [ks_pipeline::MetaplexTokenMetadataStatefulReadResult],
|
||||
metadata: &str,
|
||||
) -> ks_core::Result<&'a serde_json::Value> {
|
||||
let snapshot = snapshots.iter().find(|value| {
|
||||
return value.snapshot.account.0.as_str() == metadata
|
||||
&& value.snapshot.account_kind == "metadata";
|
||||
});
|
||||
return match snapshot {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(&value.snapshot.payload_json),
|
||||
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_metadata_snapshot_missing",
|
||||
format!("Metaplex metadata snapshot {metadata} is missing"),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
fn collection_member_verified(
|
||||
snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult],
|
||||
member_metadata: &str,
|
||||
expected_collection_mint: &str,
|
||||
) -> ks_core::Result<bool> {
|
||||
let payload = match metadata_payload(snapshots, member_metadata) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let collection = match payload.get("collection") {
|
||||
std::option::Option::Some(value) if value.is_object() => value,
|
||||
_ => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_relation_missing",
|
||||
"member metadata does not contain a collection relation",
|
||||
));
|
||||
},
|
||||
};
|
||||
let key = collection.get("key").and_then(serde_json::Value::as_str);
|
||||
let verified = collection.get("verified").and_then(serde_json::Value::as_bool);
|
||||
if key != std::option::Option::Some(expected_collection_mint) || verified.is_none() {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_relation_invalid",
|
||||
format!(
|
||||
"member collection relation mismatch: expected key={expected_collection_mint}, got key={key:?}, verified={verified:?}"
|
||||
),
|
||||
));
|
||||
}
|
||||
return match verified {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_verified_flag_missing",
|
||||
"member collection relation does not expose a verified flag",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
fn collection_size(
|
||||
snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult],
|
||||
collection_metadata: &str,
|
||||
) -> ks_core::Result<u64> {
|
||||
let payload = match metadata_payload(snapshots, collection_metadata) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let size = payload
|
||||
.get("collection_details")
|
||||
.and_then(|value| return value.get("V1"))
|
||||
.and_then(|value| return value.get("size"))
|
||||
.and_then(serde_json::Value::as_u64);
|
||||
return match size {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
std::option::Option::None => std::result::Result::Err(ks_core::Error::new(
|
||||
"metaplex_collection_size_missing",
|
||||
"collection metadata does not expose CollectionDetails::V1 size",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn collection_campaign_contract_is_exactly_verify_then_unverify() {
|
||||
assert_eq!(
|
||||
crate::metaplex_collection_verify_campaign_operation_names(),
|
||||
&["verify", "unverify"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collection_payload_contract_tracks_relation_and_size() {
|
||||
let collection = solana_pubkey::Pubkey::new_unique().to_string();
|
||||
let relation = serde_json::json!({"collection": {"verified": false, "key": collection}});
|
||||
let details = serde_json::json!({"collection_details": {"V1": {"size": 0}}});
|
||||
assert_eq!(
|
||||
relation.pointer("/collection/verified").and_then(serde_json::Value::as_bool),
|
||||
std::option::Option::Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
details
|
||||
.pointer("/collection_details/V1/size")
|
||||
.and_then(serde_json::Value::as_u64),
|
||||
std::option::Option::Some(0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_serde_contract_uses_stateful_collection_keys() {
|
||||
let metadata = mpl_token_metadata::accounts::Metadata {
|
||||
key: mpl_token_metadata::types::Key::MetadataV1,
|
||||
update_authority: mpl_token_metadata::ID,
|
||||
mint: mpl_token_metadata::ID,
|
||||
name: "Collection".to_string(),
|
||||
symbol: "COL".to_string(),
|
||||
uri: "https://example.invalid/collection.json".to_string(),
|
||||
seller_fee_basis_points: 0,
|
||||
creators: std::option::Option::None,
|
||||
primary_sale_happened: false,
|
||||
is_mutable: true,
|
||||
edition_nonce: std::option::Option::None,
|
||||
token_standard: std::option::Option::Some(
|
||||
mpl_token_metadata::types::TokenStandard::NonFungible,
|
||||
),
|
||||
collection: std::option::Option::None,
|
||||
uses: std::option::Option::None,
|
||||
collection_details: std::option::Option::Some(
|
||||
mpl_token_metadata::types::CollectionDetails::V1 { size: 7 },
|
||||
),
|
||||
programmable_config: std::option::Option::None,
|
||||
};
|
||||
let payload = match serde_json::to_value(metadata) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("metadata serialization failed: {error}"),
|
||||
};
|
||||
assert_eq!(
|
||||
payload
|
||||
.pointer("/collection_details/V1/size")
|
||||
.and_then(serde_json::Value::as_u64),
|
||||
std::option::Option::Some(7)
|
||||
);
|
||||
assert!(payload.get("collectionDetails").is_none());
|
||||
}
|
||||
|
||||
fn execution_evidence_json(
|
||||
execution: &crate::DevnetMetaplexTokenMetadataExecutionSummary,
|
||||
) -> serde_json::Value {
|
||||
let confirmation = execution.confirmation.as_ref();
|
||||
let confirmation_status = confirmation
|
||||
.map(|value| return format!("{:?}", value.status))
|
||||
.unwrap_or_else(|| return "missing".to_string());
|
||||
let confirmation_slot = confirmation.and_then(|value| return value.slot);
|
||||
let signature = confirmation
|
||||
.map(|value| return value.signature.0.clone())
|
||||
.unwrap_or_else(|| return "missing".to_string());
|
||||
let diagnostic = 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": signature,
|
||||
"confirmationStatus": confirmation_status,
|
||||
"confirmationSlot": confirmation_slot,
|
||||
"beforeSnapshots": execution.before.len(),
|
||||
"afterSnapshots": execution.after.len(),
|
||||
"canonicalHydration": diagnostic.is_some_and(|value| return value.canonical_inserted),
|
||||
"coreExtraction": diagnostic.is_some_and(|value| return value.core_extracted),
|
||||
"decodeReplay": diagnostic.is_some_and(|value| return value.decode_replayed),
|
||||
"materialized": diagnostic.is_some_and(|value| return value.materialized),
|
||||
"instructionMaterializations": execution.materializations.len(),
|
||||
"materializedSnapshots": execution.materialized_snapshots.len(),
|
||||
"idempotenceReplayClean": idempotence_clean,
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn optional_devnet_metaplex_collection_verify_campaign_from_env() {
|
||||
if std::env::var("KB_DEVNET_METAPLEX_COLLECTION_VERIFY_CAMPAIGN_TEST")
|
||||
.ok()
|
||||
.as_deref()
|
||||
!= std::option::Option::Some("1")
|
||||
{
|
||||
return;
|
||||
}
|
||||
if std::env::var("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref()
|
||||
!= std::option::Option::Some("1")
|
||||
{
|
||||
panic!("KB_DEVNET_METAPLEX_OPERATOR_CONFIRMED=1 is required for the spending 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 = ks_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 pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile)
|
||||
.unwrap_or_else(|error| panic!("HTTP pool creation failed: {error}"));
|
||||
let store_options = ks_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 = ks_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_collection_verify_campaign(
|
||||
&pool,
|
||||
&store,
|
||||
&profile,
|
||||
workspace_root,
|
||||
&options,
|
||||
&crate::NoopSolanaExecutionObserver,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|error| {
|
||||
panic!("Metaplex collection Verify -> Unverify Devnet campaign failed: {error}")
|
||||
});
|
||||
let verify_confirmation = summary
|
||||
.verify
|
||||
.confirmation
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| panic!("Verify confirmation missing"));
|
||||
let unverify_confirmation = summary
|
||||
.unverify
|
||||
.confirmation
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| panic!("Unverify confirmation missing"));
|
||||
println!(
|
||||
"METAPLEX_COLLECTION_VERIFY_FIXTURE parent_mint={} parent_metadata={} parent_master_edition={} member_mint={} member_metadata={} member_master_edition={}",
|
||||
summary.parent.fixture.mint,
|
||||
summary.parent.fixture.metadata,
|
||||
summary.parent.fixture.master_edition,
|
||||
summary.member.fixture.mint,
|
||||
summary.member.fixture.metadata,
|
||||
summary.member.fixture.master_edition,
|
||||
);
|
||||
println!(
|
||||
"METAPLEX_COLLECTION_VERIFY_STEP operation={} signature={} slot={:?} materializations={}",
|
||||
summary.verify.plan.operation_code,
|
||||
verify_confirmation.signature.0,
|
||||
verify_confirmation.slot,
|
||||
summary.verify.materializations.len(),
|
||||
);
|
||||
println!(
|
||||
"METAPLEX_COLLECTION_VERIFY_STEP operation={} signature={} slot={:?} materializations={}",
|
||||
summary.unverify.plan.operation_code,
|
||||
unverify_confirmation.signature.0,
|
||||
unverify_confirmation.slot,
|
||||
summary.unverify.materializations.len(),
|
||||
);
|
||||
println!(
|
||||
"METAPLEX_COLLECTION_VERIFY_STATE verified_before={} verified_after_verify={} verified_after_unverify={} size_before={} size_after_verify={} size_after_unverify={}",
|
||||
summary.state.verified_before,
|
||||
summary.state.verified_after_verify,
|
||||
summary.state.verified_after_unverify,
|
||||
summary.state.collection_size_before,
|
||||
summary.state.collection_size_after_verify,
|
||||
summary.state.collection_size_after_unverify,
|
||||
);
|
||||
println!(
|
||||
"METAPLEX_COLLECTION_VERIFY_EVIDENCE verify={} unverify={}",
|
||||
execution_evidence_json(&summary.verify),
|
||||
execution_evidence_json(&summary.unverify),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user