|
|
|
|
@@ -0,0 +1,585 @@
|
|
|
|
|
// file: kb_materializer_metadata/src/materializer.rs
|
|
|
|
|
// version: 7
|
|
|
|
|
|
|
|
|
|
//! Materializer scaffold for the `kb_materializer_metadata` crate.
|
|
|
|
|
|
|
|
|
|
/// Initial materializer implementation placeholder.
|
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
|
|
|
pub struct MetadataMaterializer;
|
|
|
|
|
|
|
|
|
|
impl kb_materializer_api::Materializer for crate::MetadataMaterializer {
|
|
|
|
|
fn materializer_name(&self) -> &'static str {
|
|
|
|
|
return "kb_materializer_metadata";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn materializer_version(&self) -> &'static str {
|
|
|
|
|
return env!("CARGO_PKG_VERSION");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn accepts_event(&self, _event: &kb_model::DecodedProtocolEvent) -> bool {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn materialize_event(
|
|
|
|
|
&self,
|
|
|
|
|
event: &kb_model::DecodedProtocolEvent,
|
|
|
|
|
) -> kb_core::Result<std::vec::Vec<kb_model::MaterializedEvent>> {
|
|
|
|
|
let materialized = kb_model::MaterializedEvent {
|
|
|
|
|
signature: event.signature.clone(),
|
|
|
|
|
slot: event.slot,
|
|
|
|
|
decoded_family: event.event_family,
|
|
|
|
|
materialized_family: kb_model::MaterializedEventFamily::Metadata,
|
|
|
|
|
};
|
|
|
|
|
return std::result::Result::Ok(std::vec![materialized]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Materialize embedded Token-2022 metadata, group, and group-member extensions.
|
|
|
|
|
pub fn materialize_token_2022_metadata_snapshot(
|
|
|
|
|
account_key: &str,
|
|
|
|
|
slot: u64,
|
|
|
|
|
state: &kb_decoder_spl_token_2022::state::Token2022State,
|
|
|
|
|
) -> std::result::Result<std::vec::Vec<kb_materializer_api::MaterializedOutput>, String> {
|
|
|
|
|
if account_key.is_empty() {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Token-2022 metadata snapshot requires a non-empty account key".to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if state.kind != kb_decoder_spl_token_2022::state::Token2022StateKind::Mint {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Token-2022 metadata snapshot requires a parsed mint state".to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let mut outputs = std::vec::Vec::new();
|
|
|
|
|
for entry in &state.extensions {
|
|
|
|
|
let projection_kind = match entry.extension_name {
|
|
|
|
|
"token_metadata" => std::option::Option::Some("token_metadata"),
|
|
|
|
|
"token_group" => std::option::Option::Some("token_group"),
|
|
|
|
|
"token_group_member" => std::option::Option::Some("token_group_member"),
|
|
|
|
|
_ => std::option::Option::None,
|
|
|
|
|
};
|
|
|
|
|
let kind = match projection_kind {
|
|
|
|
|
std::option::Option::Some(value) => value,
|
|
|
|
|
std::option::Option::None => continue,
|
|
|
|
|
};
|
|
|
|
|
outputs.push(kb_materializer_api::MaterializedOutput {
|
|
|
|
|
output_key: format!("token_2022_metadata_snapshot:{account_key}:{slot}:{kind}"),
|
|
|
|
|
family: kb_model::MaterializedEventFamily::Metadata,
|
|
|
|
|
payload_json: serde_json::json!({
|
|
|
|
|
"projectionVersion": 1,
|
|
|
|
|
"domain": "spl_token_2022_embedded_metadata",
|
|
|
|
|
"projectionSemantics": "authoritative_bounded_mint_extension_snapshot",
|
|
|
|
|
"idempotenceKey": format!("token_2022_metadata:{account_key}:{slot}:{kind}"),
|
|
|
|
|
"accountKey": account_key,
|
|
|
|
|
"slot": slot,
|
|
|
|
|
"programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
|
|
|
|
|
"metadataSource": "token_2022_embedded_tlv",
|
|
|
|
|
"projectionKind": kind,
|
|
|
|
|
"extensionType": entry.extension_type,
|
|
|
|
|
"extensionName": entry.extension_name,
|
|
|
|
|
"valueHex": entry.value_hex,
|
|
|
|
|
"valueFields": entry.value_fields,
|
|
|
|
|
"externalUriFetched": false,
|
|
|
|
|
"identityAuthority": "mint_address",
|
|
|
|
|
"finalAccountStateCaptured": true,
|
|
|
|
|
"provenance": {
|
|
|
|
|
"processorName": "kb_materializer_metadata",
|
|
|
|
|
"processorVersion": env!("CARGO_PKG_VERSION"),
|
|
|
|
|
"source": "bounded_token_2022_state_parser"
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return std::result::Result::Ok(outputs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Materialize one canonical Metaplex `MetadataV1` account snapshot.
|
|
|
|
|
///
|
|
|
|
|
/// Account snapshots are authoritative for current state. Instruction observations may describe
|
|
|
|
|
/// mutations, but must not emit a second competing metadata-state projection.
|
|
|
|
|
pub fn materialize_metaplex_metadata_snapshot(
|
|
|
|
|
snapshot: &kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountSnapshot,
|
|
|
|
|
) -> std::result::Result<kb_materializer_api::MaterializedOutput, String> {
|
|
|
|
|
if !snapshot.provenance.committed {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Metaplex metadata state materialization requires committed provenance".to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if snapshot.identity.materialization
|
|
|
|
|
!= kb_decoder_metadata_metaplex_token_metadata::CanonicalMaterializationPolicy::ActiveState
|
|
|
|
|
{
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Metaplex MetadataV1 materialization requires active-state policy".to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let metadata = match &snapshot.state {
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountState::Metadata(
|
|
|
|
|
value,
|
|
|
|
|
) => value,
|
|
|
|
|
_ => {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Metaplex metadata materializer received another account layout".to_string(),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let slot = snapshot.provenance.slot.map(|value| return value.to_string());
|
|
|
|
|
let write_version = snapshot.provenance.write_version.map(|value| return value.to_string());
|
|
|
|
|
let payload = serde_json::json!({
|
|
|
|
|
"projectionVersion": 1,
|
|
|
|
|
"domain": "metaplex_token_metadata",
|
|
|
|
|
"projectionSemantics": "authoritative_committed_account_snapshot",
|
|
|
|
|
"stableId": snapshot.identity.stable_id,
|
|
|
|
|
"account": metadata.account,
|
|
|
|
|
"mint": metadata.mint,
|
|
|
|
|
"updateAuthority": metadata.update_authority,
|
|
|
|
|
"name": metadata.name,
|
|
|
|
|
"symbol": metadata.symbol,
|
|
|
|
|
"uri": metadata.uri,
|
|
|
|
|
"sellerFeeBasisPoints": metadata.seller_fee_basis_points,
|
|
|
|
|
"primarySaleHappened": metadata.primary_sale_happened,
|
|
|
|
|
"isMutable": metadata.is_mutable,
|
|
|
|
|
"layout": snapshot.identity.layout,
|
|
|
|
|
"layoutDiscriminant": snapshot.identity.discriminant,
|
|
|
|
|
"canonicalVersion": snapshot.identity.canonical_version,
|
|
|
|
|
"lifecycle": "active",
|
|
|
|
|
"materializationPolicy": "active_state",
|
|
|
|
|
"metadataSource": "metaplex_owned_account",
|
|
|
|
|
"accountState": metadata.payload_json,
|
|
|
|
|
"externalUriFetched": false,
|
|
|
|
|
"stateAuthority": "owned_account_snapshot",
|
|
|
|
|
"instructionProjectionRole": "mutation_observation_only_no_duplicate_state",
|
|
|
|
|
"provenance": {
|
|
|
|
|
"source": format!("{:?}", snapshot.provenance.source),
|
|
|
|
|
"slot": slot,
|
|
|
|
|
"writeVersion": write_version,
|
|
|
|
|
"signature": snapshot.provenance.signature,
|
|
|
|
|
"instructionIndex": snapshot.provenance.instruction_index,
|
|
|
|
|
"innerInstructionIndex": snapshot.provenance.inner_instruction_index,
|
|
|
|
|
"committed": snapshot.provenance.committed,
|
|
|
|
|
"processorName": "kb_materializer_metadata",
|
|
|
|
|
"processorVersion": env!("CARGO_PKG_VERSION")
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return std::result::Result::Ok(kb_materializer_api::MaterializedOutput {
|
|
|
|
|
output_key: format!("metaplex_metadata_state:{}", metadata.account),
|
|
|
|
|
family: kb_model::MaterializedEventFamily::Metadata,
|
|
|
|
|
payload_json: payload,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests_metaplex_metadata_state {
|
|
|
|
|
pub(super) fn snapshot(
|
|
|
|
|
committed: bool,
|
|
|
|
|
) -> kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountSnapshot {
|
|
|
|
|
let metadata = kb_decoder_metadata_metaplex_token_metadata::MetadataAccountSnapshot {
|
|
|
|
|
account: "metadata111".to_string(),
|
|
|
|
|
bump: 1,
|
|
|
|
|
mint: "mint111".to_string(),
|
|
|
|
|
update_authority: "authority111".to_string(),
|
|
|
|
|
name: "Asset".to_string(),
|
|
|
|
|
symbol: "AST".to_string(),
|
|
|
|
|
uri: "https://example.invalid/asset.json".to_string(),
|
|
|
|
|
seller_fee_basis_points: 500,
|
|
|
|
|
primary_sale_happened: true,
|
|
|
|
|
is_mutable: false,
|
|
|
|
|
payload_json: serde_json::json!({
|
|
|
|
|
"creators": [{"address":"creator111","verified":true,"share":100}],
|
|
|
|
|
"collection": {"verified":true,"key":"collection111"},
|
|
|
|
|
"uses": {"useMethod":"Single","remaining":"0","total":"1"},
|
|
|
|
|
"tokenStandard": "ProgrammableNonFungible",
|
|
|
|
|
"programmableConfig": {"ruleSet":"rules111"}
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
return kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountSnapshot::from_metadata(
|
|
|
|
|
metadata,
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalAccountProvenance {
|
|
|
|
|
source: kb_decoder_metadata_metaplex_token_metadata::CanonicalObservationSource::Backfill,
|
|
|
|
|
slot: std::option::Option::Some(42),
|
|
|
|
|
write_version: std::option::Option::Some(7),
|
|
|
|
|
signature: std::option::Option::Some("signature111".to_string()),
|
|
|
|
|
instruction_index: std::option::Option::Some(3),
|
|
|
|
|
inner_instruction_index: std::option::Option::None,
|
|
|
|
|
committed,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn account_snapshot_is_one_stable_authoritative_metadata_projection() {
|
|
|
|
|
let first = crate::materialize_metaplex_metadata_snapshot(&snapshot(true));
|
|
|
|
|
let second = crate::materialize_metaplex_metadata_snapshot(&snapshot(true));
|
|
|
|
|
assert_eq!(first, second);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
first.as_ref().map(|value| return value.output_key.as_str()),
|
|
|
|
|
std::result::Result::Ok("metaplex_metadata_state:metadata111")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
first.as_ref().map(|value| return value.payload_json["metadataSource"].clone()),
|
|
|
|
|
std::result::Result::Ok(serde_json::json!("metaplex_owned_account"))
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
first
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|value| return value.payload_json["instructionProjectionRole"].clone()),
|
|
|
|
|
std::result::Result::Ok(serde_json::json!(
|
|
|
|
|
"mutation_observation_only_no_duplicate_state"
|
|
|
|
|
))
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
first
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|value| return value.payload_json["accountState"]["creators"][0]["share"]
|
|
|
|
|
.clone()),
|
|
|
|
|
std::result::Result::Ok(serde_json::json!(100))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn uncommitted_or_non_metadata_state_is_rejected() {
|
|
|
|
|
assert!(crate::materialize_metaplex_metadata_snapshot(&snapshot(false)).is_err());
|
|
|
|
|
let mut wrong = snapshot(true);
|
|
|
|
|
wrong.state = kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountState::EditionMarker(
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::EditionMarkerAccountSnapshot {
|
|
|
|
|
account: "marker111".to_string(),
|
|
|
|
|
bump: 1,
|
|
|
|
|
mint: "mint111".to_string(),
|
|
|
|
|
kind: kb_decoder_metadata_metaplex_token_metadata::EditionMarkerAccountKind::EditionMarkerV1,
|
|
|
|
|
marker_group: std::option::Option::Some(0),
|
|
|
|
|
edition: 1,
|
|
|
|
|
byte_index: 0,
|
|
|
|
|
bit_mask: 64,
|
|
|
|
|
edition_taken: true,
|
|
|
|
|
ledger: std::vec![64],
|
|
|
|
|
payload_json: serde_json::json!({}),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
assert!(crate::materialize_metaplex_metadata_snapshot(&wrong).is_err());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Materialize any canonical Metaplex account state other than `MetadataV1`.
|
|
|
|
|
///
|
|
|
|
|
/// Every account address maps to one stable state key. Historical layouts remain materializable
|
|
|
|
|
/// with an explicit historical lifecycle, while execution policy remains independently disabled.
|
|
|
|
|
pub fn materialize_metaplex_account_snapshot(
|
|
|
|
|
snapshot: &kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountSnapshot,
|
|
|
|
|
) -> std::result::Result<kb_materializer_api::MaterializedOutput, String> {
|
|
|
|
|
if !snapshot.provenance.committed {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Metaplex account-state materialization requires committed provenance".to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let (projection_kind, fact_owner, state_json) = match &snapshot.state {
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountState::Metadata(_) => {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"MetadataV1 must use the dedicated metadata-state materializer".to_string(),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountState::Edition(value) => (
|
|
|
|
|
"edition_state",
|
|
|
|
|
"kb_materializer_metadata",
|
|
|
|
|
value.payload_json.clone(),
|
|
|
|
|
),
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountState::EditionMarker(value) => (
|
|
|
|
|
"edition_marker_state",
|
|
|
|
|
"kb_materializer_metadata",
|
|
|
|
|
value.payload_json.clone(),
|
|
|
|
|
),
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountState::TokenRecord(value) => (
|
|
|
|
|
"programmable_token_state",
|
|
|
|
|
"kb_materializer_lifecycle",
|
|
|
|
|
value.payload_json.clone(),
|
|
|
|
|
),
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountState::DelegateRecord(value) => (
|
|
|
|
|
"delegate_admin_state",
|
|
|
|
|
"kb_materializer_admin",
|
|
|
|
|
value.payload_json.clone(),
|
|
|
|
|
),
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountState::AuthorityRecord(value) => (
|
|
|
|
|
"authority_admin_state",
|
|
|
|
|
"kb_materializer_admin",
|
|
|
|
|
value.payload_json.clone(),
|
|
|
|
|
),
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountState::TokenOwnedEscrow(value) => (
|
|
|
|
|
"token_owned_escrow_state",
|
|
|
|
|
"kb_materializer_lifecycle",
|
|
|
|
|
value.payload_json.clone(),
|
|
|
|
|
),
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountState::ReservationList(value) => (
|
|
|
|
|
"historical_reservation_state",
|
|
|
|
|
"kb_materializer_metadata",
|
|
|
|
|
value.payload_json.clone(),
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
let lifecycle = match snapshot.identity.lifecycle {
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalAccountLifecycle::Active => "active",
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalAccountLifecycle::ActiveExperimental => {
|
|
|
|
|
"active_experimental"
|
|
|
|
|
},
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalAccountLifecycle::Historical => {
|
|
|
|
|
"historical"
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let materialization_policy = match snapshot.identity.materialization {
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalMaterializationPolicy::ActiveState => {
|
|
|
|
|
"active_state"
|
|
|
|
|
},
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalMaterializationPolicy::HistoricalState => {
|
|
|
|
|
"historical_state"
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let execution_policy = match snapshot.identity.execution {
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalExecutionPolicy::CurrentCandidate => {
|
|
|
|
|
"current_candidate"
|
|
|
|
|
},
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalExecutionPolicy::ExperimentalCandidate => {
|
|
|
|
|
"experimental_candidate"
|
|
|
|
|
},
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalExecutionPolicy::NeverHistorical => {
|
|
|
|
|
"never_historical"
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
return std::result::Result::Ok(kb_materializer_api::MaterializedOutput {
|
|
|
|
|
output_key: format!("metaplex_account_state:{}", snapshot.identity.account),
|
|
|
|
|
family: kb_model::MaterializedEventFamily::Metadata,
|
|
|
|
|
payload_json: serde_json::json!({
|
|
|
|
|
"projectionVersion": 1,
|
|
|
|
|
"domain": "metaplex_token_metadata",
|
|
|
|
|
"projectionKind": projection_kind,
|
|
|
|
|
"projectionSemantics": "authoritative_committed_owned_account_snapshot",
|
|
|
|
|
"stableId": snapshot.identity.stable_id,
|
|
|
|
|
"account": snapshot.identity.account,
|
|
|
|
|
"programId": snapshot.identity.program_id,
|
|
|
|
|
"layout": snapshot.identity.layout,
|
|
|
|
|
"layoutDiscriminant": snapshot.identity.discriminant,
|
|
|
|
|
"canonicalVersion": snapshot.identity.canonical_version,
|
|
|
|
|
"lifecycle": lifecycle,
|
|
|
|
|
"materializationPolicy": materialization_policy,
|
|
|
|
|
"executionPolicy": execution_policy,
|
|
|
|
|
"factOwner": fact_owner,
|
|
|
|
|
"metadataSource": "metaplex_owned_account",
|
|
|
|
|
"accountState": state_json,
|
|
|
|
|
"instructionProjectionRole": "mutation_observation_only_no_duplicate_state",
|
|
|
|
|
"provenance": {
|
|
|
|
|
"source": format!("{:?}", snapshot.provenance.source),
|
|
|
|
|
"slot": snapshot.provenance.slot.map(|value: u64| return value.to_string()),
|
|
|
|
|
"writeVersion": snapshot.provenance.write_version.map(|value: u64| return value.to_string()),
|
|
|
|
|
"signature": snapshot.provenance.signature,
|
|
|
|
|
"instructionIndex": snapshot.provenance.instruction_index,
|
|
|
|
|
"innerInstructionIndex": snapshot.provenance.inner_instruction_index,
|
|
|
|
|
"committed": snapshot.provenance.committed,
|
|
|
|
|
"processorName": "kb_materializer_metadata",
|
|
|
|
|
"processorVersion": env!("CARGO_PKG_VERSION")
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests_metaplex_account_state {
|
|
|
|
|
fn provenance() -> kb_decoder_metadata_metaplex_token_metadata::CanonicalAccountProvenance {
|
|
|
|
|
return kb_decoder_metadata_metaplex_token_metadata::CanonicalAccountProvenance {
|
|
|
|
|
source:
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::CanonicalObservationSource::Backfill,
|
|
|
|
|
slot: std::option::Option::Some(90),
|
|
|
|
|
write_version: std::option::Option::Some(2),
|
|
|
|
|
signature: std::option::Option::None,
|
|
|
|
|
instruction_index: std::option::Option::None,
|
|
|
|
|
inner_instruction_index: std::option::Option::None,
|
|
|
|
|
committed: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn historical_edition_and_reservation_are_stable_materializable_state() {
|
|
|
|
|
let edition = kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountSnapshot::from_edition(
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::EditionAccountSnapshot {
|
|
|
|
|
account: "master-v1".to_string(),
|
|
|
|
|
bump: 1,
|
|
|
|
|
mint: "mint-v1".to_string(),
|
|
|
|
|
kind: kb_decoder_metadata_metaplex_token_metadata::EditionAccountKind::MasterEditionV1,
|
|
|
|
|
supply: std::option::Option::Some(3),
|
|
|
|
|
max_supply: std::option::Option::Some(10),
|
|
|
|
|
parent: std::option::Option::None,
|
|
|
|
|
edition: std::option::Option::None,
|
|
|
|
|
printing_mint: std::option::Option::Some("printing".to_string()),
|
|
|
|
|
one_time_printing_authorization_mint: std::option::Option::Some("authorization".to_string()),
|
|
|
|
|
payload_json: serde_json::json!({"supply":"3","maxSupply":"10"}),
|
|
|
|
|
},
|
|
|
|
|
provenance(),
|
|
|
|
|
);
|
|
|
|
|
let reservation = kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountSnapshot::from_reservation_list(
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::ReservationListAccountSnapshot {
|
|
|
|
|
account: "reservation-v2".to_string(),
|
|
|
|
|
bump: 2,
|
|
|
|
|
kind: kb_decoder_metadata_metaplex_token_metadata::ReservationListAccountKind::V2,
|
|
|
|
|
master_edition: "master-v1".to_string(),
|
|
|
|
|
resource: "resource".to_string(),
|
|
|
|
|
supply_snapshot: std::option::Option::Some(3),
|
|
|
|
|
reservations: std::vec::Vec::new(),
|
|
|
|
|
total_reservation_spots: 0,
|
|
|
|
|
current_reservation_spots: 0,
|
|
|
|
|
payload_json: serde_json::json!({"reservations":[]}),
|
|
|
|
|
},
|
|
|
|
|
provenance(),
|
|
|
|
|
);
|
|
|
|
|
let first = crate::materialize_metaplex_account_snapshot(&edition);
|
|
|
|
|
let second = crate::materialize_metaplex_account_snapshot(&reservation);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
first.as_ref().map(|value| return value.payload_json["lifecycle"].clone()),
|
|
|
|
|
std::result::Result::Ok(serde_json::json!("historical"))
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
first.as_ref().map(|value| return value.payload_json["executionPolicy"].clone()),
|
|
|
|
|
std::result::Result::Ok(serde_json::json!("never_historical"))
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
second.as_ref().map(|value| return value.output_key.as_str()),
|
|
|
|
|
std::result::Result::Ok("metaplex_account_state:reservation-v2")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn programmable_delegate_authority_and_escrow_keep_unique_fact_owners() {
|
|
|
|
|
let token = kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountSnapshot::from_token_record(
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::TokenRecordAccountSnapshot {
|
|
|
|
|
account: "token-record".to_string(),
|
|
|
|
|
bump: 1,
|
|
|
|
|
mint: "mint".to_string(),
|
|
|
|
|
token: "token".to_string(),
|
|
|
|
|
stored_bump: 1,
|
|
|
|
|
state: "Locked".to_string(),
|
|
|
|
|
rule_set_revision: std::option::Option::Some(4),
|
|
|
|
|
delegate: std::option::Option::Some("delegate".to_string()),
|
|
|
|
|
delegate_role: std::option::Option::Some("Transfer".to_string()),
|
|
|
|
|
locked_transfer: std::option::Option::Some("destination".to_string()),
|
|
|
|
|
locked: true,
|
|
|
|
|
payload_json: serde_json::json!({"state":"Locked"}),
|
|
|
|
|
},
|
|
|
|
|
provenance(),
|
|
|
|
|
);
|
|
|
|
|
let output = crate::materialize_metaplex_account_snapshot(&token);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
output.as_ref().map(|value| return value.payload_json["factOwner"].clone()),
|
|
|
|
|
std::result::Result::Ok(serde_json::json!("kb_materializer_lifecycle"))
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
output.as_ref().map(|value| return value.payload_json["projectionKind"].clone()),
|
|
|
|
|
std::result::Result::Ok(serde_json::json!("programmable_token_state"))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn uncommitted_and_metadata_snapshots_use_explicit_boundaries() {
|
|
|
|
|
let mut marker = kb_decoder_metadata_metaplex_token_metadata::CanonicalMetadataAccountSnapshot::from_edition_marker(
|
|
|
|
|
kb_decoder_metadata_metaplex_token_metadata::EditionMarkerAccountSnapshot {
|
|
|
|
|
account: "marker".to_string(),
|
|
|
|
|
bump: 1,
|
|
|
|
|
mint: "mint".to_string(),
|
|
|
|
|
kind: kb_decoder_metadata_metaplex_token_metadata::EditionMarkerAccountKind::EditionMarkerV2,
|
|
|
|
|
marker_group: std::option::Option::None,
|
|
|
|
|
edition: 8,
|
|
|
|
|
byte_index: 1,
|
|
|
|
|
bit_mask: 128,
|
|
|
|
|
edition_taken: true,
|
|
|
|
|
ledger: std::vec![0, 128],
|
|
|
|
|
payload_json: serde_json::json!({"editionTaken":true}),
|
|
|
|
|
},
|
|
|
|
|
provenance(),
|
|
|
|
|
);
|
|
|
|
|
marker.provenance.committed = false;
|
|
|
|
|
assert!(crate::materialize_metaplex_account_snapshot(&marker).is_err());
|
|
|
|
|
assert!(
|
|
|
|
|
crate::materialize_metaplex_account_snapshot(
|
|
|
|
|
&super::tests_metaplex_metadata_state::snapshot(true)
|
|
|
|
|
)
|
|
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests_token_2022_state {
|
|
|
|
|
#[test]
|
|
|
|
|
fn embedded_metadata_group_and_member_have_distinct_stable_outputs() {
|
|
|
|
|
let state = kb_decoder_spl_token_2022::state::Token2022State {
|
|
|
|
|
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
|
|
|
|
base_fields: serde_json::json!({"supply":"1","decimals":0}),
|
|
|
|
|
base_hex: "00".repeat(82),
|
|
|
|
|
account_type: std::option::Option::Some(1),
|
|
|
|
|
extensions: std::vec![
|
|
|
|
|
kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
|
|
|
|
extension_type: 19,
|
|
|
|
|
extension_name: "token_metadata",
|
|
|
|
|
value_hex: "01".to_string(),
|
|
|
|
|
value_fields: serde_json::json!({"name":"Token","symbol":"TOK","uri":"https://example.invalid/token.json"}),
|
|
|
|
|
},
|
|
|
|
|
kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
|
|
|
|
extension_type: 20,
|
|
|
|
|
extension_name: "token_group",
|
|
|
|
|
value_hex: "02".to_string(),
|
|
|
|
|
value_fields: serde_json::json!({"mint":"mint111","size":"1","maxSize":"10"}),
|
|
|
|
|
},
|
|
|
|
|
kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
|
|
|
|
extension_type: 21,
|
|
|
|
|
extension_name: "token_group_member",
|
|
|
|
|
value_hex: "03".to_string(),
|
|
|
|
|
value_fields: serde_json::json!({"mint":"mint111","group":"group111","memberNumber":"1"}),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
let outputs = crate::materialize_token_2022_metadata_snapshot("mint111", 9, &state);
|
|
|
|
|
assert_eq!(outputs.as_ref().map(|values| return values.len()), std::result::Result::Ok(3));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
outputs
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|values| return values[0].payload_json["metadataSource"].clone()),
|
|
|
|
|
std::result::Result::Ok(serde_json::json!("token_2022_embedded_tlv"))
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
outputs
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|values| return values[0].payload_json["externalUriFetched"].clone()),
|
|
|
|
|
std::result::Result::Ok(serde_json::json!(false))
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
outputs
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|values| return values[1].payload_json["projectionKind"].clone()),
|
|
|
|
|
std::result::Result::Ok(serde_json::json!("token_group"))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn snapshot_is_idempotent_filters_unrelated_extensions_and_requires_mint_identity() {
|
|
|
|
|
let state = kb_decoder_spl_token_2022::state::Token2022State {
|
|
|
|
|
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
|
|
|
|
base_fields: serde_json::json!({}),
|
|
|
|
|
base_hex: "00".repeat(82),
|
|
|
|
|
account_type: std::option::Option::Some(1),
|
|
|
|
|
extensions: std::vec![kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
|
|
|
|
extension_type: 1,
|
|
|
|
|
extension_name: "transfer_fee_config",
|
|
|
|
|
value_hex: "00".to_string(),
|
|
|
|
|
value_fields: serde_json::json!({}),
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
let first = crate::materialize_token_2022_metadata_snapshot("mint111", 9, &state);
|
|
|
|
|
let second = crate::materialize_token_2022_metadata_snapshot("mint111", 9, &state);
|
|
|
|
|
assert_eq!(first, second);
|
|
|
|
|
assert_eq!(first.as_ref().map(|values| return values.len()), std::result::Result::Ok(0));
|
|
|
|
|
assert!(crate::materialize_token_2022_metadata_snapshot("", 9, &state).is_err());
|
|
|
|
|
let account_state = kb_decoder_spl_token_2022::state::Token2022State {
|
|
|
|
|
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Account,
|
|
|
|
|
base_fields: serde_json::json!({}),
|
|
|
|
|
base_hex: "00".repeat(165),
|
|
|
|
|
account_type: std::option::Option::None,
|
|
|
|
|
extensions: std::vec::Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
assert!(
|
|
|
|
|
crate::materialize_token_2022_metadata_snapshot("account111", 9, &account_state)
|
|
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|