3158 lines
129 KiB
Rust
3158 lines
129 KiB
Rust
// file: ks-lib/src/decoder/metadata/metaplex_token_metadata/account.rs
|
|
// version: 16
|
|
|
|
//! Bounded decoding and PDA validation for Metaplex Token Metadata accounts.
|
|
|
|
/// Exhaustive inventory of every published Metaplex Token Metadata `Key` variant.
|
|
pub const DC_METADATA_MTM_METADATA_ACCOUNT_LAYOUT_AUDIT:
|
|
&[DcMetadataMtmMetadataAccountLayoutAuditEntry] = &[
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "Uninitialized",
|
|
discriminant: 0,
|
|
decoder: "reject_uninitialized",
|
|
lifecycle: "sentinel",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "EditionV1",
|
|
discriminant: 1,
|
|
decoder: "decode_edition_account",
|
|
lifecycle: "active",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "MasterEditionV1",
|
|
discriminant: 2,
|
|
decoder: "decode_edition_account",
|
|
lifecycle: "historical_materializable",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "ReservationListV1",
|
|
discriminant: 3,
|
|
decoder: "decode_reservation_list_account",
|
|
lifecycle: "historical_materializable",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "MetadataV1",
|
|
discriminant: 4,
|
|
decoder: "decode_metadata_account",
|
|
lifecycle: "active",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "ReservationListV2",
|
|
discriminant: 5,
|
|
decoder: "decode_reservation_list_account",
|
|
lifecycle: "historical_materializable",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "MasterEditionV2",
|
|
discriminant: 6,
|
|
decoder: "decode_edition_account",
|
|
lifecycle: "active",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "EditionMarker",
|
|
discriminant: 7,
|
|
decoder: "decode_edition_marker_account",
|
|
lifecycle: "active",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "UseAuthorityRecord",
|
|
discriminant: 8,
|
|
decoder: "decode_use_authority_record_account",
|
|
lifecycle: "active",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "CollectionAuthorityRecord",
|
|
discriminant: 9,
|
|
decoder: "decode_collection_authority_record_account",
|
|
lifecycle: "active",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "TokenOwnedEscrow",
|
|
discriminant: 10,
|
|
decoder: "decode_token_owned_escrow_account",
|
|
lifecycle: "active",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "TokenRecord",
|
|
discriminant: 11,
|
|
decoder: "decode_token_record_account",
|
|
lifecycle: "active",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "MetadataDelegate",
|
|
discriminant: 12,
|
|
decoder: "decode_metadata_delegate_record_account",
|
|
lifecycle: "active",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "EditionMarkerV2",
|
|
discriminant: 13,
|
|
decoder: "decode_edition_marker_account",
|
|
lifecycle: "active_experimental",
|
|
},
|
|
DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
key_name: "HolderDelegate",
|
|
discriminant: 14,
|
|
decoder: "decode_holder_delegate_record_account",
|
|
lifecycle: "active",
|
|
},
|
|
];
|
|
|
|
const MAX_METADATA_ACCOUNT_BYTES: usize = 1_024;
|
|
const MAX_NAME_BYTES: usize = 32;
|
|
const MAX_SYMBOL_BYTES: usize = 10;
|
|
const MAX_URI_BYTES: usize = 200;
|
|
const MAX_CREATORS: usize = 5;
|
|
const COMPACT_MASTER_EDITION_ACCOUNT_BYTES: usize = 20;
|
|
const LEGACY_MASTER_EDITION_ACCOUNT_BYTES: usize = 282;
|
|
const COMPACT_EDITION_ACCOUNT_BYTES: usize = 42;
|
|
const LEGACY_EDITION_ACCOUNT_BYTES: usize = 241;
|
|
const EDITION_MARKER_V1_BITS: u64 = 248;
|
|
const MAX_EDITION_MARKER_V2_LEDGER_BYTES: usize = 1_048_576;
|
|
|
|
/// Bounded error returned while decoding a Metadata account.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum DcMetadataMtmMetadataAccountDecodeError {
|
|
/// The account key is not a valid Solana public key.
|
|
InvalidAccountKey,
|
|
/// The account owner is not a valid Solana public key.
|
|
InvalidOwner,
|
|
/// The account is owned by another program.
|
|
ForeignOwner,
|
|
/// The account data is empty or exceeds the explicit bound.
|
|
InvalidDataLength,
|
|
/// The account payload is not a supported Metadata layout.
|
|
InvalidMetadataLayout,
|
|
/// One bounded string or vector exceeds the official maximum.
|
|
MetadataBoundsExceeded,
|
|
/// The supplied account key is not the canonical Metadata PDA for the parsed mint.
|
|
InvalidMetadataPda,
|
|
/// The parsed account key discriminant is not Metadata.
|
|
InvalidMetadataKey,
|
|
/// The supplied account key is not the canonical Edition Marker PDA.
|
|
InvalidEditionMarkerPda,
|
|
/// The supplied account key is not the canonical Token Record PDA.
|
|
InvalidTokenRecordPda,
|
|
/// The parsed account key discriminant is not Token Record.
|
|
InvalidTokenRecordKey,
|
|
/// The supplied account key is not the canonical delegate or authority record PDA.
|
|
InvalidAuthorityRecordPda,
|
|
/// The supplied account key is not the canonical Token Owned Escrow PDA.
|
|
InvalidTokenOwnedEscrowPda,
|
|
/// The parsed account key discriminant is not Token Owned Escrow.
|
|
InvalidTokenOwnedEscrowKey,
|
|
/// The parsed account key discriminant is not the expected delegate or authority record.
|
|
InvalidAuthorityRecordKey,
|
|
/// The supplied account key is not the canonical historical Reservation List PDA.
|
|
InvalidReservationListPda,
|
|
/// The parsed account key discriminant is not Reservation List V1 or V2.
|
|
InvalidReservationListKey,
|
|
/// The historical reservation vector or padded payload exceeds official bounds.
|
|
ReservationListBoundsExceeded,
|
|
/// The Edition Marker ledger exceeds the explicit decoder bound.
|
|
EditionMarkerBoundsExceeded,
|
|
/// The parsed account could not be projected to bounded JSON.
|
|
ProjectionFailed,
|
|
}
|
|
|
|
impl std::fmt::Display for DcMetadataMtmMetadataAccountDecodeError {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let message = match self {
|
|
Self::InvalidAccountKey => "metadata account key is invalid",
|
|
Self::InvalidOwner => "metadata account owner is invalid",
|
|
Self::ForeignOwner => "metadata account is owned by another program",
|
|
Self::InvalidDataLength => "metadata account data length is invalid",
|
|
Self::InvalidMetadataLayout => "metadata account layout is invalid",
|
|
Self::MetadataBoundsExceeded => "metadata account exceeds bounded fields",
|
|
Self::InvalidMetadataPda => "metadata account PDA is invalid",
|
|
Self::InvalidMetadataKey => "metadata account key discriminant is invalid",
|
|
Self::InvalidEditionMarkerPda => "edition marker account PDA is invalid",
|
|
Self::InvalidTokenRecordPda => "token record account PDA is invalid",
|
|
Self::InvalidTokenRecordKey => "token record account key discriminant is invalid",
|
|
Self::InvalidAuthorityRecordPda => "delegate or authority record PDA is invalid",
|
|
Self::InvalidTokenOwnedEscrowPda => "token owned escrow PDA is invalid",
|
|
Self::InvalidTokenOwnedEscrowKey => "token owned escrow key discriminant is invalid",
|
|
Self::InvalidAuthorityRecordKey => {
|
|
"delegate or authority record key discriminant is invalid"
|
|
},
|
|
Self::InvalidReservationListPda => "reservation list account PDA is invalid",
|
|
Self::InvalidReservationListKey => {
|
|
"reservation list account key discriminant is invalid"
|
|
},
|
|
Self::ReservationListBoundsExceeded => {
|
|
"reservation list account exceeds official bounds"
|
|
},
|
|
Self::EditionMarkerBoundsExceeded => "edition marker ledger exceeds bounded fields",
|
|
Self::ProjectionFailed => "metadata account JSON projection failed",
|
|
};
|
|
return formatter.write_str(message);
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for DcMetadataMtmMetadataAccountDecodeError {}
|
|
|
|
/// One audited Metaplex account-key layout.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct DcMetadataMtmMetadataAccountLayoutAuditEntry {
|
|
/// Published `Key` variant name.
|
|
pub key_name: &'static str,
|
|
/// Exact Borsh discriminant byte.
|
|
pub discriminant: u8,
|
|
/// Decoder entry point or explicit rejection policy.
|
|
pub decoder: &'static str,
|
|
/// Lifecycle classification used by later materialization and execution layers.
|
|
pub lifecycle: &'static str,
|
|
}
|
|
|
|
/// Canonical bounded projection of one Metadata account.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct DcMetadataMtmMetadataAccountSnapshot {
|
|
/// Canonical account address.
|
|
pub account: String,
|
|
/// Canonical Metadata PDA bump.
|
|
pub bump: u8,
|
|
/// Mint carried by the account.
|
|
pub mint: String,
|
|
/// Current update authority.
|
|
pub update_authority: String,
|
|
/// Trimmed metadata name.
|
|
pub name: String,
|
|
/// Trimmed metadata symbol.
|
|
pub symbol: String,
|
|
/// Trimmed metadata URI.
|
|
pub uri: String,
|
|
/// Seller fee in basis points.
|
|
pub seller_fee_basis_points: u16,
|
|
/// Whether primary sale happened.
|
|
pub primary_sale_happened: bool,
|
|
/// Whether the metadata is mutable.
|
|
pub is_mutable: bool,
|
|
/// Complete bounded account projection.
|
|
pub payload_json: serde_json::Value,
|
|
}
|
|
|
|
/// Decodes one Metadata account after validating owner, bounds and canonical PDA.
|
|
pub fn decoder_metadata_metaplex_token_metadata_decode_metadata_account(
|
|
account: &str,
|
|
owner: &str,
|
|
data: &[u8],
|
|
) -> std::result::Result<
|
|
DcMetadataMtmMetadataAccountSnapshot,
|
|
DcMetadataMtmMetadataAccountDecodeError,
|
|
> {
|
|
let account_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(account) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let owner_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(owner) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidOwner);
|
|
},
|
|
};
|
|
let expected_owner = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidOwner);
|
|
},
|
|
};
|
|
if owner_key != expected_owner {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::ForeignOwner);
|
|
}
|
|
if data.is_empty() || data.len() > MAX_METADATA_ACCOUNT_BYTES {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
let metadata = match mpl_token_metadata::accounts::Metadata::from_bytes(data) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
if metadata.key != mpl_token_metadata::types::Key::MetadataV1 {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataKey,
|
|
);
|
|
}
|
|
let name = metadata.name.trim_end_matches('\0').to_string();
|
|
let symbol = metadata.symbol.trim_end_matches('\0').to_string();
|
|
let uri = metadata.uri.trim_end_matches('\0').to_string();
|
|
if name.len() > MAX_NAME_BYTES || symbol.len() > MAX_SYMBOL_BYTES || uri.len() > MAX_URI_BYTES {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::MetadataBoundsExceeded,
|
|
);
|
|
}
|
|
if metadata
|
|
.creators
|
|
.as_ref()
|
|
.is_some_and(|creators| return creators.len() > MAX_CREATORS)
|
|
{
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::MetadataBoundsExceeded,
|
|
);
|
|
}
|
|
let (expected_pda, bump) = mpl_token_metadata::accounts::Metadata::find_pda(&metadata.mint);
|
|
if account_key.to_string() != expected_pda.to_string() {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataPda,
|
|
);
|
|
}
|
|
let payload_json = match serde_json::to_value(&metadata) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ProjectionFailed,
|
|
);
|
|
},
|
|
};
|
|
return std::result::Result::Ok(DcMetadataMtmMetadataAccountSnapshot {
|
|
account: account_key.to_string(),
|
|
bump,
|
|
mint: metadata.mint.to_string(),
|
|
update_authority: metadata.update_authority.to_string(),
|
|
name,
|
|
symbol,
|
|
uri,
|
|
seller_fee_basis_points: metadata.seller_fee_basis_points,
|
|
primary_sale_happened: metadata.primary_sale_happened,
|
|
is_mutable: metadata.is_mutable,
|
|
payload_json,
|
|
});
|
|
}
|
|
|
|
fn validate_known_account_allocation(
|
|
data: &[u8],
|
|
serialized_len: usize,
|
|
padded_account_lengths: &[usize],
|
|
) -> std::result::Result<(), DcMetadataMtmMetadataAccountDecodeError> {
|
|
if data.len() == serialized_len {
|
|
return std::result::Result::Ok(());
|
|
}
|
|
if data.len() < serialized_len || !padded_account_lengths.contains(&data.len()) {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
if data[serialized_len..].iter().any(|byte| return *byte != 0) {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn validate_master_edition_allocation(
|
|
data: &[u8],
|
|
serialized_len: usize,
|
|
padded_account_lengths: &[usize],
|
|
) -> std::result::Result<(), DcMetadataMtmMetadataAccountDecodeError> {
|
|
if data.len() == serialized_len {
|
|
return std::result::Result::Ok(());
|
|
}
|
|
if data.len() < serialized_len || !padded_account_lengths.contains(&data.len()) {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
let trailer_start = match data.len().checked_sub(2) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
},
|
|
};
|
|
if trailer_start < serialized_len {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
if data[serialized_len..trailer_start].iter().any(|byte| return *byte != 0) {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
}
|
|
let fee_flag = data[trailer_start];
|
|
if fee_flag > 1 {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
}
|
|
let token_standard = data[trailer_start + 1];
|
|
if token_standard != mpl_token_metadata::types::TokenStandard::NonFungible as u8
|
|
&& token_standard != mpl_token_metadata::types::TokenStandard::ProgrammableNonFungible as u8
|
|
{
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
/// Supported edition account layout.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum DcMetadataMtmEditionAccountKind {
|
|
/// Historical Master Edition V1 layout.
|
|
MasterEditionV1,
|
|
/// Current Master Edition V2 layout.
|
|
MasterEditionV2,
|
|
/// Printed Edition V1 layout.
|
|
EditionV1,
|
|
}
|
|
|
|
/// Canonical bounded projection of one edition or master-edition account.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct DcMetadataMtmEditionAccountSnapshot {
|
|
/// Canonical account address.
|
|
pub account: String,
|
|
/// Canonical edition PDA bump.
|
|
pub bump: u8,
|
|
/// Mint used to derive the edition PDA.
|
|
pub mint: String,
|
|
/// Decoded account layout.
|
|
pub kind: DcMetadataMtmEditionAccountKind,
|
|
/// Current printed supply for a master edition.
|
|
pub supply: std::option::Option<u64>,
|
|
/// Optional maximum supply for a master edition.
|
|
pub max_supply: std::option::Option<u64>,
|
|
/// Parent master edition for a printed edition.
|
|
pub parent: std::option::Option<String>,
|
|
/// Printed edition number.
|
|
pub edition: std::option::Option<u64>,
|
|
/// Historical printing mint carried by Master Edition V1.
|
|
pub printing_mint: std::option::Option<String>,
|
|
/// Historical one-time printing authorization mint carried by Master Edition V1.
|
|
pub one_time_printing_authorization_mint: std::option::Option<String>,
|
|
/// Complete bounded account projection.
|
|
pub payload_json: serde_json::Value,
|
|
}
|
|
|
|
/// Decodes one MasterEditionV1, MasterEditionV2 or Edition account with exact owner and PDA validation.
|
|
pub fn decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
account: &str,
|
|
owner: &str,
|
|
mint: &str,
|
|
data: &[u8],
|
|
) -> std::result::Result<DcMetadataMtmEditionAccountSnapshot, DcMetadataMtmMetadataAccountDecodeError>
|
|
{
|
|
let account_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(account) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let owner_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(owner) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidOwner);
|
|
},
|
|
};
|
|
let expected_owner = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidOwner);
|
|
},
|
|
};
|
|
if owner_key != expected_owner {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::ForeignOwner);
|
|
}
|
|
if data.is_empty() || data.len() > LEGACY_MASTER_EDITION_ACCOUNT_BYTES {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
let (expected_pda, bump) = match mint.parse() {
|
|
std::result::Result::Ok(mint_key) => {
|
|
mpl_token_metadata::accounts::MasterEdition::find_pda(&mint_key)
|
|
},
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
if account_key.to_string() != expected_pda.to_string() {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataPda,
|
|
);
|
|
}
|
|
let key = data[0];
|
|
if key == mpl_token_metadata::types::Key::MasterEditionV1 as u8 {
|
|
let value = match mpl_token_metadata::accounts::DeprecatedMasterEditionV1::from_bytes(data)
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let serialized_len = if value.max_supply.is_some() { 82 } else { 74 };
|
|
if let std::result::Result::Err(error) = validate_master_edition_allocation(
|
|
data,
|
|
serialized_len,
|
|
&[LEGACY_MASTER_EDITION_ACCOUNT_BYTES],
|
|
) {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let payload_json = match serde_json::to_value(&value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ProjectionFailed,
|
|
);
|
|
},
|
|
};
|
|
return std::result::Result::Ok(DcMetadataMtmEditionAccountSnapshot {
|
|
account: account_key.to_string(),
|
|
bump,
|
|
mint: mint.to_string(),
|
|
kind: DcMetadataMtmEditionAccountKind::MasterEditionV1,
|
|
supply: std::option::Option::Some(value.supply),
|
|
max_supply: value.max_supply,
|
|
parent: std::option::Option::None,
|
|
edition: std::option::Option::None,
|
|
printing_mint: std::option::Option::Some(value.printing_mint.to_string()),
|
|
one_time_printing_authorization_mint: std::option::Option::Some(
|
|
value.one_time_printing_authorization_mint.to_string(),
|
|
),
|
|
payload_json,
|
|
});
|
|
}
|
|
if key == mpl_token_metadata::types::Key::MasterEditionV2 as u8 {
|
|
let value = match mpl_token_metadata::accounts::MasterEdition::from_bytes(data) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let serialized_len = if value.max_supply.is_some() { 18 } else { 10 };
|
|
if let std::result::Result::Err(error) = validate_master_edition_allocation(
|
|
data,
|
|
serialized_len,
|
|
&[COMPACT_MASTER_EDITION_ACCOUNT_BYTES, LEGACY_MASTER_EDITION_ACCOUNT_BYTES],
|
|
) {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let payload_json = match serde_json::to_value(&value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ProjectionFailed,
|
|
);
|
|
},
|
|
};
|
|
return std::result::Result::Ok(DcMetadataMtmEditionAccountSnapshot {
|
|
account: account_key.to_string(),
|
|
bump,
|
|
mint: mint.to_string(),
|
|
kind: DcMetadataMtmEditionAccountKind::MasterEditionV2,
|
|
supply: std::option::Option::Some(value.supply),
|
|
max_supply: value.max_supply,
|
|
parent: std::option::Option::None,
|
|
edition: std::option::Option::None,
|
|
printing_mint: std::option::Option::None,
|
|
one_time_printing_authorization_mint: std::option::Option::None,
|
|
payload_json,
|
|
});
|
|
}
|
|
if key == mpl_token_metadata::types::Key::EditionV1 as u8 {
|
|
let value = match mpl_token_metadata::accounts::Edition::from_bytes(data) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
if let std::result::Result::Err(error) = validate_known_account_allocation(
|
|
data,
|
|
mpl_token_metadata::accounts::Edition::LEN,
|
|
&[COMPACT_EDITION_ACCOUNT_BYTES, LEGACY_EDITION_ACCOUNT_BYTES],
|
|
) {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let payload_json = match serde_json::to_value(&value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ProjectionFailed,
|
|
);
|
|
},
|
|
};
|
|
return std::result::Result::Ok(DcMetadataMtmEditionAccountSnapshot {
|
|
account: account_key.to_string(),
|
|
bump,
|
|
mint: mint.to_string(),
|
|
kind: DcMetadataMtmEditionAccountKind::EditionV1,
|
|
supply: std::option::Option::None,
|
|
max_supply: std::option::Option::None,
|
|
parent: std::option::Option::Some(value.parent.to_string()),
|
|
edition: std::option::Option::Some(value.edition),
|
|
printing_mint: std::option::Option::None,
|
|
one_time_printing_authorization_mint: std::option::Option::None,
|
|
payload_json,
|
|
});
|
|
}
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataKey);
|
|
}
|
|
|
|
/// Supported Edition Marker account layout.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum DcMetadataMtmEditionMarkerAccountKind {
|
|
/// Fixed 248-edition historical/current marker layout.
|
|
EditionMarkerV1,
|
|
/// Extensible marker layout with one PDA per mint.
|
|
EditionMarkerV2,
|
|
}
|
|
|
|
/// Canonical bounded projection of one Edition Marker account.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct DcMetadataMtmEditionMarkerAccountSnapshot {
|
|
/// Canonical account address.
|
|
pub account: String,
|
|
/// Canonical PDA bump.
|
|
pub bump: u8,
|
|
/// Mint used to derive the marker PDA.
|
|
pub mint: String,
|
|
/// Decoded marker layout.
|
|
pub kind: DcMetadataMtmEditionMarkerAccountKind,
|
|
/// V1 group number (`edition / 248`), absent for V2.
|
|
pub marker_group: std::option::Option<u64>,
|
|
/// Requested edition used to inspect the bitmap.
|
|
pub edition: u64,
|
|
/// Byte index inside the ledger.
|
|
pub byte_index: usize,
|
|
/// Big-endian bit mask inside the selected byte.
|
|
pub bit_mask: u8,
|
|
/// Whether the requested edition is already marked.
|
|
pub edition_taken: bool,
|
|
/// Exact bounded ledger bytes.
|
|
pub ledger: std::vec::Vec<u8>,
|
|
/// Complete bounded account projection.
|
|
pub payload_json: serde_json::Value,
|
|
}
|
|
|
|
/// Decodes EditionMarker or EditionMarkerV2 with exact owner, bounds, bitmap and PDA validation.
|
|
pub fn decoder_metadata_metaplex_token_metadata_decode_edition_marker_account(
|
|
account: &str,
|
|
owner: &str,
|
|
mint: &str,
|
|
edition: u64,
|
|
data: &[u8],
|
|
) -> std::result::Result<
|
|
DcMetadataMtmEditionMarkerAccountSnapshot,
|
|
DcMetadataMtmMetadataAccountDecodeError,
|
|
> {
|
|
let account_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(account) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let owner_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(owner) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidOwner);
|
|
},
|
|
};
|
|
let program_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidOwner);
|
|
},
|
|
};
|
|
if owner_key != program_key {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::ForeignOwner);
|
|
}
|
|
let mint_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(mint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
if data.is_empty() {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
if data[0] == mpl_token_metadata::types::Key::EditionMarker as u8 {
|
|
if data.len() != mpl_token_metadata::accounts::EditionMarker::LEN {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
let marker_group = edition / EDITION_MARKER_V1_BITS;
|
|
let marker_seed = marker_group.to_string();
|
|
let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program_key.as_ref(),
|
|
mint_key.as_ref(),
|
|
b"edition",
|
|
marker_seed.as_bytes(),
|
|
],
|
|
&program_key,
|
|
);
|
|
if account_key != expected_pda {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidEditionMarkerPda,
|
|
);
|
|
}
|
|
let value = match mpl_token_metadata::accounts::EditionMarker::from_bytes(data) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let offset = (edition % EDITION_MARKER_V1_BITS) as usize;
|
|
let byte_index = offset / 8;
|
|
let bit_mask = 1u8 << (7 - (offset % 8));
|
|
let payload_json = match serde_json::to_value(&value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ProjectionFailed,
|
|
);
|
|
},
|
|
};
|
|
return std::result::Result::Ok(DcMetadataMtmEditionMarkerAccountSnapshot {
|
|
account: account_key.to_string(),
|
|
bump,
|
|
mint: mint_key.to_string(),
|
|
kind: DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV1,
|
|
marker_group: std::option::Option::Some(marker_group),
|
|
edition,
|
|
byte_index,
|
|
bit_mask,
|
|
edition_taken: value.ledger[byte_index] & bit_mask != 0,
|
|
ledger: value.ledger.to_vec(),
|
|
payload_json,
|
|
});
|
|
}
|
|
if data[0] == mpl_token_metadata::types::Key::EditionMarkerV2 as u8 {
|
|
if data.len() < 5 || data.len() > MAX_EDITION_MARKER_V2_LEDGER_BYTES + 5 {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[b"metadata", program_key.as_ref(), mint_key.as_ref(), b"edition", b"marker"],
|
|
&program_key,
|
|
);
|
|
if account_key != expected_pda {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidEditionMarkerPda,
|
|
);
|
|
}
|
|
let value = match mpl_token_metadata::accounts::EditionMarkerV2::from_bytes(data) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
if value.ledger.len() > MAX_EDITION_MARKER_V2_LEDGER_BYTES {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::EditionMarkerBoundsExceeded,
|
|
);
|
|
}
|
|
if data.len() != value.ledger.len() + 5 {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
let byte_index_u64 = edition / 8;
|
|
let byte_index = match usize::try_from(byte_index_u64) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::EditionMarkerBoundsExceeded,
|
|
);
|
|
},
|
|
};
|
|
let bit_mask = 1u8 << (7 - (edition % 8) as usize);
|
|
let edition_taken =
|
|
value.ledger.get(byte_index).is_some_and(|entry| return entry & bit_mask != 0);
|
|
let payload_json = match serde_json::to_value(&value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ProjectionFailed,
|
|
);
|
|
},
|
|
};
|
|
return std::result::Result::Ok(DcMetadataMtmEditionMarkerAccountSnapshot {
|
|
account: account_key.to_string(),
|
|
bump,
|
|
mint: mint_key.to_string(),
|
|
kind: DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV2,
|
|
marker_group: std::option::Option::None,
|
|
edition,
|
|
byte_index,
|
|
bit_mask,
|
|
edition_taken,
|
|
ledger: value.ledger,
|
|
payload_json,
|
|
});
|
|
}
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataKey);
|
|
}
|
|
|
|
/// Canonical bounded projection of one programmable Token Record account.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct DcMetadataMtmTokenRecordAccountSnapshot {
|
|
/// Canonical Token Record account address.
|
|
pub account: String,
|
|
/// Canonical Token Record PDA bump computed from mint and token account.
|
|
pub bump: u8,
|
|
/// Mint used to derive the Token Record PDA.
|
|
pub mint: String,
|
|
/// Token account used to derive the Token Record PDA.
|
|
pub token: String,
|
|
/// Bump stored by the on-chain Token Record layout.
|
|
pub stored_bump: u8,
|
|
/// Current programmable token state.
|
|
pub state: String,
|
|
/// Optional Token Authorization Rules revision.
|
|
pub rule_set_revision: std::option::Option<u64>,
|
|
/// Optional active delegate.
|
|
pub delegate: std::option::Option<String>,
|
|
/// Optional active delegate role.
|
|
pub delegate_role: std::option::Option<String>,
|
|
/// Optional locked-transfer destination.
|
|
pub locked_transfer: std::option::Option<String>,
|
|
/// Whether the token is currently locked.
|
|
pub locked: bool,
|
|
/// Complete bounded account projection.
|
|
pub payload_json: serde_json::Value,
|
|
}
|
|
|
|
/// Decodes one Token Record account after exact owner, length, discriminant and PDA validation.
|
|
pub fn decoder_metadata_metaplex_token_metadata_decode_token_record_account(
|
|
account: &str,
|
|
owner: &str,
|
|
mint: &str,
|
|
token: &str,
|
|
data: &[u8],
|
|
) -> std::result::Result<
|
|
DcMetadataMtmTokenRecordAccountSnapshot,
|
|
DcMetadataMtmMetadataAccountDecodeError,
|
|
> {
|
|
let account_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(account) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let owner_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(owner) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidOwner);
|
|
},
|
|
};
|
|
let expected_owner = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidOwner);
|
|
},
|
|
};
|
|
if owner_key != expected_owner {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::ForeignOwner);
|
|
}
|
|
if data.len() != mpl_token_metadata::accounts::TokenRecord::LEN {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
let mint_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(mint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let token_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(token) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
expected_owner.as_ref(),
|
|
mint_key.as_ref(),
|
|
b"token_record",
|
|
token_key.as_ref(),
|
|
],
|
|
&expected_owner,
|
|
);
|
|
if account_key != expected_pda {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidTokenRecordPda,
|
|
);
|
|
}
|
|
let value = match mpl_token_metadata::accounts::TokenRecord::from_bytes(data) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
if value.key != mpl_token_metadata::types::Key::TokenRecord {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidTokenRecordKey,
|
|
);
|
|
}
|
|
if value.bump != bump {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidTokenRecordPda,
|
|
);
|
|
}
|
|
let state = format!("{:?}", value.state);
|
|
let delegate = value.delegate.map(|delegate| return delegate.to_string());
|
|
let delegate_role = value.delegate_role.as_ref().map(|role| return format!("{role:?}"));
|
|
let locked_transfer =
|
|
value.locked_transfer.map(|locked_transfer| return locked_transfer.to_string());
|
|
let locked = value.state == mpl_token_metadata::types::TokenState::Locked;
|
|
let payload_json = serde_json::json!({
|
|
"key": "TokenRecord",
|
|
"bump": value.bump,
|
|
"state": &state,
|
|
"rule_set_revision": value.rule_set_revision,
|
|
"delegate": &delegate,
|
|
"delegate_role": &delegate_role,
|
|
"locked_transfer": &locked_transfer,
|
|
});
|
|
return std::result::Result::Ok(DcMetadataMtmTokenRecordAccountSnapshot {
|
|
account: account_key.to_string(),
|
|
bump,
|
|
mint: mint_key.to_string(),
|
|
token: token_key.to_string(),
|
|
stored_bump: value.bump,
|
|
state,
|
|
rule_set_revision: value.rule_set_revision,
|
|
delegate,
|
|
delegate_role,
|
|
locked_transfer,
|
|
locked,
|
|
payload_json,
|
|
});
|
|
}
|
|
|
|
/// Canonical projection shared by Metadata and Holder delegate records.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct DcMetadataMtmDelegateRecordAccountSnapshot {
|
|
/// Canonical delegate-record account address.
|
|
pub account: String,
|
|
/// Canonical PDA bump.
|
|
pub bump: u8,
|
|
/// Delegate-record family.
|
|
pub kind: String,
|
|
/// Mint carried by the record.
|
|
pub mint: String,
|
|
/// Role seed used by the PDA.
|
|
pub role: String,
|
|
/// Authority or holder used by the PDA.
|
|
pub authority: String,
|
|
/// Delegate carried by the record.
|
|
pub delegate: String,
|
|
/// Update authority stored by the record.
|
|
pub update_authority: String,
|
|
/// Bump stored by the account.
|
|
pub stored_bump: u8,
|
|
/// Complete bounded account projection.
|
|
pub payload_json: serde_json::Value,
|
|
}
|
|
|
|
fn parse_record_boundary(
|
|
account: &str,
|
|
owner: &str,
|
|
) -> std::result::Result<
|
|
(solana_pubkey::Pubkey, solana_pubkey::Pubkey),
|
|
DcMetadataMtmMetadataAccountDecodeError,
|
|
> {
|
|
let account_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(account) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let owner_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(owner) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidOwner);
|
|
},
|
|
};
|
|
let expected_owner = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidOwner);
|
|
},
|
|
};
|
|
if owner_key != expected_owner {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::ForeignOwner);
|
|
}
|
|
return std::result::Result::Ok((account_key, expected_owner));
|
|
}
|
|
|
|
/// Decodes a Metadata Delegate Record with exact role, authority, delegate and bump validation.
|
|
pub fn decoder_metadata_metaplex_token_metadata_decode_metadata_delegate_record_account(
|
|
account: &str,
|
|
owner: &str,
|
|
role: mpl_token_metadata::types::MetadataDelegateRole,
|
|
data: &[u8],
|
|
) -> std::result::Result<
|
|
DcMetadataMtmDelegateRecordAccountSnapshot,
|
|
DcMetadataMtmMetadataAccountDecodeError,
|
|
> {
|
|
let (account_key, program) = match parse_record_boundary(account, owner) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if data.len() != mpl_token_metadata::accounts::MetadataDelegateRecord::LEN {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
let value = match mpl_token_metadata::accounts::MetadataDelegateRecord::from_bytes(data) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
if value.key != mpl_token_metadata::types::Key::MetadataDelegate {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAuthorityRecordKey,
|
|
);
|
|
}
|
|
let role_seed = role.to_string();
|
|
let mint = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(&value.mint.to_string())
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let authority = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
&value.update_authority.to_string(),
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let delegate =
|
|
match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(&value.delegate.to_string()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program.as_ref(),
|
|
mint.as_ref(),
|
|
role_seed.as_bytes(),
|
|
authority.as_ref(),
|
|
delegate.as_ref(),
|
|
],
|
|
&program,
|
|
);
|
|
if account_key != expected_pda || value.bump != bump {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAuthorityRecordPda,
|
|
);
|
|
}
|
|
let payload_json = match serde_json::to_value(&value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ProjectionFailed,
|
|
);
|
|
},
|
|
};
|
|
return std::result::Result::Ok(DcMetadataMtmDelegateRecordAccountSnapshot {
|
|
account: account_key.to_string(),
|
|
bump,
|
|
kind: "MetadataDelegateRecord".to_string(),
|
|
mint: mint.to_string(),
|
|
role: role_seed,
|
|
authority: authority.to_string(),
|
|
delegate: delegate.to_string(),
|
|
update_authority: value.update_authority.to_string(),
|
|
stored_bump: value.bump,
|
|
payload_json,
|
|
});
|
|
}
|
|
|
|
/// Decodes a Holder Delegate Record with exact role, holder, delegate and bump validation.
|
|
pub fn decoder_metadata_metaplex_token_metadata_decode_holder_delegate_record_account(
|
|
account: &str,
|
|
owner: &str,
|
|
holder: &str,
|
|
role: mpl_token_metadata::types::HolderDelegateRole,
|
|
data: &[u8],
|
|
) -> std::result::Result<
|
|
DcMetadataMtmDelegateRecordAccountSnapshot,
|
|
DcMetadataMtmMetadataAccountDecodeError,
|
|
> {
|
|
let (account_key, program) = match parse_record_boundary(account, owner) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if data.len() != mpl_token_metadata::accounts::HolderDelegateRecord::LEN {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
let value = match mpl_token_metadata::accounts::HolderDelegateRecord::from_bytes(data) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
if value.key != mpl_token_metadata::types::Key::HolderDelegate {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAuthorityRecordKey,
|
|
);
|
|
}
|
|
let holder_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(holder) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let role_seed = role.to_string();
|
|
let mint = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(&value.mint.to_string())
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let delegate =
|
|
match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(&value.delegate.to_string()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program.as_ref(),
|
|
mint.as_ref(),
|
|
role_seed.as_bytes(),
|
|
holder_key.as_ref(),
|
|
delegate.as_ref(),
|
|
],
|
|
&program,
|
|
);
|
|
if account_key != expected_pda || value.bump != bump {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAuthorityRecordPda,
|
|
);
|
|
}
|
|
let payload_json = match serde_json::to_value(&value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ProjectionFailed,
|
|
);
|
|
},
|
|
};
|
|
return std::result::Result::Ok(DcMetadataMtmDelegateRecordAccountSnapshot {
|
|
account: account_key.to_string(),
|
|
bump,
|
|
kind: "HolderDelegateRecord".to_string(),
|
|
mint: mint.to_string(),
|
|
role: role_seed,
|
|
authority: holder_key.to_string(),
|
|
delegate: delegate.to_string(),
|
|
update_authority: value.update_authority.to_string(),
|
|
stored_bump: value.bump,
|
|
payload_json,
|
|
});
|
|
}
|
|
|
|
/// Canonical projection of Collection Authority and Use Authority records.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct DcMetadataMtmAuthorityRecordAccountSnapshot {
|
|
/// Canonical record account address.
|
|
pub account: String,
|
|
/// Canonical PDA bump.
|
|
pub bump: u8,
|
|
/// Record family.
|
|
pub kind: String,
|
|
/// Mint used by the PDA.
|
|
pub mint: String,
|
|
/// Authority used by the PDA.
|
|
pub authority: String,
|
|
/// Optional update authority stored by Collection Authority Record.
|
|
pub update_authority: std::option::Option<String>,
|
|
/// Optional remaining use allowance stored by Use Authority Record.
|
|
pub allowed_uses: std::option::Option<u64>,
|
|
/// Bump stored by the account.
|
|
pub stored_bump: u8,
|
|
/// Complete bounded account projection.
|
|
pub payload_json: serde_json::Value,
|
|
}
|
|
|
|
/// Decodes a Collection Authority Record with exact PDA and bump validation.
|
|
pub fn decoder_metadata_metaplex_token_metadata_decode_collection_authority_record_account(
|
|
account: &str,
|
|
owner: &str,
|
|
mint: &str,
|
|
authority: &str,
|
|
data: &[u8],
|
|
) -> std::result::Result<
|
|
DcMetadataMtmAuthorityRecordAccountSnapshot,
|
|
DcMetadataMtmMetadataAccountDecodeError,
|
|
> {
|
|
let (account_key, program) = match parse_record_boundary(account, owner) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if data.len() != 3 && data.len() != 35 {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
let mint_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(mint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let authority_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(authority) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program.as_ref(),
|
|
mint_key.as_ref(),
|
|
b"collection_authority",
|
|
authority_key.as_ref(),
|
|
],
|
|
&program,
|
|
);
|
|
if account_key != expected_pda {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAuthorityRecordPda,
|
|
);
|
|
}
|
|
let value = match mpl_token_metadata::accounts::CollectionAuthorityRecord::from_bytes(data) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
if value.key != mpl_token_metadata::types::Key::CollectionAuthorityRecord {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAuthorityRecordKey,
|
|
);
|
|
}
|
|
if value.bump != bump {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAuthorityRecordPda,
|
|
);
|
|
}
|
|
let payload_json = match serde_json::to_value(&value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ProjectionFailed,
|
|
);
|
|
},
|
|
};
|
|
return std::result::Result::Ok(DcMetadataMtmAuthorityRecordAccountSnapshot {
|
|
account: account_key.to_string(),
|
|
bump,
|
|
kind: "CollectionAuthorityRecord".to_string(),
|
|
mint: mint_key.to_string(),
|
|
authority: authority_key.to_string(),
|
|
update_authority: value.update_authority.map(|key| return key.to_string()),
|
|
allowed_uses: std::option::Option::None,
|
|
stored_bump: value.bump,
|
|
payload_json,
|
|
});
|
|
}
|
|
|
|
/// Decodes a Use Authority Record with exact length, PDA and bump validation.
|
|
pub fn decoder_metadata_metaplex_token_metadata_decode_use_authority_record_account(
|
|
account: &str,
|
|
owner: &str,
|
|
mint: &str,
|
|
authority: &str,
|
|
data: &[u8],
|
|
) -> std::result::Result<
|
|
DcMetadataMtmAuthorityRecordAccountSnapshot,
|
|
DcMetadataMtmMetadataAccountDecodeError,
|
|
> {
|
|
let (account_key, program) = match parse_record_boundary(account, owner) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if data.len() != mpl_token_metadata::accounts::UseAuthorityRecord::LEN {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
let mint_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(mint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let authority_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(authority) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program.as_ref(),
|
|
mint_key.as_ref(),
|
|
b"user",
|
|
authority_key.as_ref(),
|
|
],
|
|
&program,
|
|
);
|
|
if account_key != expected_pda {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAuthorityRecordPda,
|
|
);
|
|
}
|
|
let value = match mpl_token_metadata::accounts::UseAuthorityRecord::from_bytes(data) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
if value.key != mpl_token_metadata::types::Key::UseAuthorityRecord {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAuthorityRecordKey,
|
|
);
|
|
}
|
|
if value.bump != bump {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAuthorityRecordPda,
|
|
);
|
|
}
|
|
let payload_json = match serde_json::to_value(&value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ProjectionFailed,
|
|
);
|
|
},
|
|
};
|
|
return std::result::Result::Ok(DcMetadataMtmAuthorityRecordAccountSnapshot {
|
|
account: account_key.to_string(),
|
|
bump,
|
|
kind: "UseAuthorityRecord".to_string(),
|
|
mint: mint_key.to_string(),
|
|
authority: authority_key.to_string(),
|
|
update_authority: std::option::Option::None,
|
|
allowed_uses: std::option::Option::Some(value.allowed_uses),
|
|
stored_bump: value.bump,
|
|
payload_json,
|
|
});
|
|
}
|
|
|
|
/// Authority form used by one Token Owned Escrow account.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum DcMetadataMtmTokenOwnedEscrowAuthorityKind {
|
|
/// Escrow controlled by the owner of the base token.
|
|
TokenOwner,
|
|
/// Escrow controlled by an explicit creator key.
|
|
Creator,
|
|
}
|
|
|
|
/// Canonical bounded projection of one Token Owned Escrow account.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct DcMetadataMtmTokenOwnedEscrowAccountSnapshot {
|
|
/// Canonical escrow account address.
|
|
pub account: String,
|
|
/// Canonical PDA bump.
|
|
pub bump: u8,
|
|
/// Base token mint stored by the account.
|
|
pub base_token: String,
|
|
/// Authority form encoded by the account.
|
|
pub authority_kind: DcMetadataMtmTokenOwnedEscrowAuthorityKind,
|
|
/// Optional creator authority.
|
|
pub creator: std::option::Option<String>,
|
|
/// Bump stored by the account.
|
|
pub stored_bump: u8,
|
|
/// Complete bounded account projection.
|
|
pub payload_json: serde_json::Value,
|
|
}
|
|
|
|
/// Decodes one Token Owned Escrow account with exact owner, layout, authority seeds and bump validation.
|
|
pub fn decoder_metadata_metaplex_token_metadata_decode_token_owned_escrow_account(
|
|
account: &str,
|
|
owner: &str,
|
|
data: &[u8],
|
|
) -> std::result::Result<
|
|
DcMetadataMtmTokenOwnedEscrowAccountSnapshot,
|
|
DcMetadataMtmMetadataAccountDecodeError,
|
|
> {
|
|
let (account_key, program) = match parse_record_boundary(account, owner) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if data.len() != 35 && data.len() != 67 {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
}
|
|
let value = match mpl_token_metadata::accounts::TokenOwnedEscrow::from_bytes(data) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
if value.key != mpl_token_metadata::types::Key::TokenOwnedEscrow {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidTokenOwnedEscrowKey,
|
|
);
|
|
}
|
|
let base_token =
|
|
match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(&value.base_token.to_string())
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let (authority_kind, creator, expected_pda, bump) = match &value.authority {
|
|
mpl_token_metadata::types::EscrowAuthority::TokenOwner => {
|
|
let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[b"metadata", program.as_ref(), base_token.as_ref(), &[0], b"escrow"],
|
|
&program,
|
|
);
|
|
(
|
|
DcMetadataMtmTokenOwnedEscrowAuthorityKind::TokenOwner,
|
|
std::option::Option::None,
|
|
expected_pda,
|
|
bump,
|
|
)
|
|
},
|
|
mpl_token_metadata::types::EscrowAuthority::Creator(creator_key) => {
|
|
let creator_text = creator_key.to_string();
|
|
let creator_key =
|
|
match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(&creator_text) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program.as_ref(),
|
|
base_token.as_ref(),
|
|
&[1],
|
|
creator_key.as_ref(),
|
|
b"escrow",
|
|
],
|
|
&program,
|
|
);
|
|
(
|
|
DcMetadataMtmTokenOwnedEscrowAuthorityKind::Creator,
|
|
std::option::Option::Some(creator_text),
|
|
expected_pda,
|
|
bump,
|
|
)
|
|
},
|
|
};
|
|
if account_key != expected_pda || value.bump != bump {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidTokenOwnedEscrowPda,
|
|
);
|
|
}
|
|
let payload_json = match serde_json::to_value(&value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ProjectionFailed,
|
|
);
|
|
},
|
|
};
|
|
return std::result::Result::Ok(DcMetadataMtmTokenOwnedEscrowAccountSnapshot {
|
|
account: account_key.to_string(),
|
|
bump,
|
|
base_token: value.base_token.to_string(),
|
|
authority_kind,
|
|
creator,
|
|
stored_bump: value.bump,
|
|
payload_json,
|
|
});
|
|
}
|
|
|
|
/// Historical Reservation List account generation.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum DcMetadataMtmReservationListAccountKind {
|
|
/// Legacy layout with `u8` spot counters.
|
|
V1,
|
|
/// Later historical layout with `u64` spot counters and cached totals.
|
|
V2,
|
|
}
|
|
|
|
/// One bounded historical reservation entry.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct DcMetadataMtmReservationEntrySnapshot {
|
|
/// Reserved address.
|
|
pub address: String,
|
|
/// Spots still available for the address.
|
|
pub spots_remaining: u64,
|
|
/// Total spots assigned to the address.
|
|
pub total_spots: u64,
|
|
}
|
|
|
|
/// Canonical decode-only projection of a historical Reservation List account.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct DcMetadataMtmReservationListAccountSnapshot {
|
|
/// Canonical account address.
|
|
pub account: String,
|
|
/// Canonical Reservation List PDA bump.
|
|
pub bump: u8,
|
|
/// Historical account generation.
|
|
pub kind: DcMetadataMtmReservationListAccountKind,
|
|
/// Master Edition stored by the account.
|
|
pub master_edition: String,
|
|
/// Resource key used by the PDA derivation.
|
|
pub resource: String,
|
|
/// Master Edition supply captured when reservations were created.
|
|
pub supply_snapshot: std::option::Option<u64>,
|
|
/// Bounded reservation entries.
|
|
pub reservations: std::vec::Vec<DcMetadataMtmReservationEntrySnapshot>,
|
|
/// Total reservation spots. V1 derives this value from entries.
|
|
pub total_reservation_spots: u64,
|
|
/// Current reservation spots. V1 derives this value from entries.
|
|
pub current_reservation_spots: u64,
|
|
/// Complete bounded JSON projection.
|
|
pub payload_json: serde_json::Value,
|
|
}
|
|
|
|
fn read_reservation_u64(data: &[u8], offset: &mut usize) -> std::option::Option<u64> {
|
|
let end = match offset.checked_add(8) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let bytes = match data.get(*offset..end) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let array = match <[u8; 8]>::try_from(bytes) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
*offset = end;
|
|
return std::option::Option::Some(u64::from_le_bytes(array));
|
|
}
|
|
|
|
fn read_reservation_pubkey(
|
|
data: &[u8],
|
|
offset: &mut usize,
|
|
) -> std::option::Option<solana_pubkey::Pubkey> {
|
|
let end = match offset.checked_add(32) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let bytes = match data.get(*offset..end) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::option::Option::None,
|
|
};
|
|
let array = match <[u8; 32]>::try_from(bytes) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
*offset = end;
|
|
return std::option::Option::Some(solana_pubkey::Pubkey::new_from_array(array));
|
|
}
|
|
|
|
/// Decodes a historical Reservation List V1 or V2 account as decode-only state.
|
|
pub fn decoder_metadata_metaplex_token_metadata_decode_reservation_list_account(
|
|
account: &str,
|
|
owner: &str,
|
|
resource: &str,
|
|
data: &[u8],
|
|
) -> std::result::Result<
|
|
DcMetadataMtmReservationListAccountSnapshot,
|
|
DcMetadataMtmMetadataAccountDecodeError,
|
|
> {
|
|
const MAX_RESERVATIONS: usize = 200;
|
|
const MAX_V1_BYTES: usize = 6_949;
|
|
const MAX_V2_BYTES: usize = 9_749;
|
|
let account_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(account) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let owner_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(owner) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidOwner);
|
|
},
|
|
};
|
|
let resource_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(resource) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidAccountKey,
|
|
);
|
|
},
|
|
};
|
|
let program_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::InvalidOwner);
|
|
},
|
|
};
|
|
if owner_key != program_key {
|
|
return std::result::Result::Err(DcMetadataMtmMetadataAccountDecodeError::ForeignOwner);
|
|
}
|
|
let key = match data.first() {
|
|
std::option::Option::Some(value) => *value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength,
|
|
);
|
|
},
|
|
};
|
|
let (kind, maximum, entry_bytes) = match key {
|
|
3 => (DcMetadataMtmReservationListAccountKind::V1, MAX_V1_BYTES, 34usize),
|
|
5 => (DcMetadataMtmReservationListAccountKind::V2, MAX_V2_BYTES, 48usize),
|
|
_ => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidReservationListKey,
|
|
);
|
|
},
|
|
};
|
|
if data.len() > maximum {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ReservationListBoundsExceeded,
|
|
);
|
|
}
|
|
let mut offset = 1usize;
|
|
let master_edition = match read_reservation_pubkey(data, &mut offset) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let option_tag = match data.get(offset) {
|
|
std::option::Option::Some(value) => *value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
offset += 1;
|
|
let supply_snapshot = match option_tag {
|
|
0 => std::option::Option::None,
|
|
1 => match read_reservation_u64(data, &mut offset) {
|
|
std::option::Option::Some(value) => std::option::Option::Some(value),
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
},
|
|
_ => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let count_end = match offset.checked_add(4) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let count_bytes = match data.get(offset..count_end) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let count_array = match <[u8; 4]>::try_from(count_bytes) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let count = u32::from_le_bytes(count_array) as usize;
|
|
offset = count_end;
|
|
if count > MAX_RESERVATIONS {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ReservationListBoundsExceeded,
|
|
);
|
|
}
|
|
let required_entries = match count.checked_mul(entry_bytes) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::ReservationListBoundsExceeded,
|
|
);
|
|
},
|
|
};
|
|
if data.len().saturating_sub(offset) < required_entries {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
}
|
|
let mut reservations = std::vec::Vec::with_capacity(count);
|
|
for _ in 0..count {
|
|
let address = match read_reservation_pubkey(data, &mut offset) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let (spots_remaining, total_spots) = match kind {
|
|
DcMetadataMtmReservationListAccountKind::V1 => {
|
|
let remaining = match data.get(offset) {
|
|
std::option::Option::Some(value) => *value as u64,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let total = match data.get(offset + 1) {
|
|
std::option::Option::Some(value) => *value as u64,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
offset += 2;
|
|
(remaining, total)
|
|
},
|
|
DcMetadataMtmReservationListAccountKind::V2 => {
|
|
let remaining = match read_reservation_u64(data, &mut offset) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let total = match read_reservation_u64(data, &mut offset) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
(remaining, total)
|
|
},
|
|
};
|
|
reservations.push(DcMetadataMtmReservationEntrySnapshot {
|
|
address: address.to_string(),
|
|
spots_remaining,
|
|
total_spots,
|
|
});
|
|
}
|
|
let derived_total = reservations
|
|
.iter()
|
|
.fold(0u64, |sum, entry| return sum.saturating_add(entry.total_spots));
|
|
let (total_reservation_spots, current_reservation_spots) = match kind {
|
|
DcMetadataMtmReservationListAccountKind::V1 => (derived_total, derived_total),
|
|
DcMetadataMtmReservationListAccountKind::V2 => {
|
|
let total = match read_reservation_u64(data, &mut offset) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
let current = match read_reservation_u64(data, &mut offset) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
},
|
|
};
|
|
(total, current)
|
|
},
|
|
};
|
|
if data
|
|
.get(offset..)
|
|
.is_some_and(|suffix| return suffix.iter().any(|byte| return *byte != 0))
|
|
{
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout,
|
|
);
|
|
}
|
|
let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program_key.as_ref(),
|
|
master_edition.as_ref(),
|
|
b"reservation",
|
|
resource_key.as_ref(),
|
|
],
|
|
&program_key,
|
|
);
|
|
if account_key != expected_pda {
|
|
return std::result::Result::Err(
|
|
DcMetadataMtmMetadataAccountDecodeError::InvalidReservationListPda,
|
|
);
|
|
}
|
|
let payload_json = serde_json::json!({
|
|
"kind": match kind { DcMetadataMtmReservationListAccountKind::V1 => "reservation_list_v1", DcMetadataMtmReservationListAccountKind::V2 => "reservation_list_v2" },
|
|
"master_edition": master_edition.to_string(),
|
|
"resource": resource_key.to_string(),
|
|
"supply_snapshot": supply_snapshot,
|
|
"reservations": reservations.iter().map(|entry| return serde_json::json!({"address": entry.address.clone(), "spots_remaining": entry.spots_remaining, "total_spots": entry.total_spots})).collect::<std::vec::Vec<_>>(),
|
|
"total_reservation_spots": total_reservation_spots,
|
|
"current_reservation_spots": current_reservation_spots,
|
|
});
|
|
return std::result::Result::Ok(DcMetadataMtmReservationListAccountSnapshot {
|
|
account: account_key.to_string(),
|
|
bump,
|
|
kind,
|
|
master_edition: master_edition.to_string(),
|
|
resource: resource_key.to_string(),
|
|
supply_snapshot,
|
|
reservations,
|
|
total_reservation_spots,
|
|
current_reservation_spots,
|
|
payload_json,
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
fn metadata() -> mpl_token_metadata::accounts::Metadata {
|
|
return mpl_token_metadata::accounts::Metadata {
|
|
key: mpl_token_metadata::types::Key::MetadataV1,
|
|
update_authority: mpl_token_metadata::ID,
|
|
mint: mpl_token_metadata::ID,
|
|
name: "Khadhroony".to_string(),
|
|
symbol: "KB".to_string(),
|
|
uri: "https://example.invalid/metadata.json".to_string(),
|
|
seller_fee_basis_points: 500,
|
|
creators: std::option::Option::None,
|
|
primary_sale_happened: false,
|
|
is_mutable: true,
|
|
edition_nonce: std::option::Option::None,
|
|
token_standard: std::option::Option::None,
|
|
collection: std::option::Option::None,
|
|
uses: std::option::Option::None,
|
|
collection_details: std::option::Option::None,
|
|
programmable_config: std::option::Option::None,
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn metadata_account_decodes_with_exact_owner_and_pda() {
|
|
let metadata = metadata();
|
|
let (account, expected_bump) =
|
|
mpl_token_metadata::accounts::Metadata::find_pda(&metadata.mint);
|
|
let data = match borsh_0_10::to_vec(&metadata) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("metadata serialization failed: {error}"),
|
|
};
|
|
let snapshot = match crate::decoder_metadata_metaplex_token_metadata_decode_metadata_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&data,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("metadata decode failed: {error}"),
|
|
};
|
|
assert_eq!(snapshot.bump, expected_bump);
|
|
assert_eq!(snapshot.mint, metadata.mint.to_string());
|
|
assert_eq!(snapshot.name, "Khadhroony");
|
|
assert_eq!(snapshot.symbol, "KB");
|
|
assert_eq!(snapshot.seller_fee_basis_points, 500);
|
|
assert!(snapshot.is_mutable);
|
|
assert!(!snapshot.primary_sale_happened);
|
|
}
|
|
|
|
#[test]
|
|
fn metadata_account_rejects_foreign_owner_wrong_pda_and_bounds() {
|
|
let metadata = metadata();
|
|
let (account, _) = mpl_token_metadata::accounts::Metadata::find_pda(&metadata.mint);
|
|
let data = match borsh_0_10::to_vec(&metadata) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("metadata serialization failed: {error}"),
|
|
};
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_metadata_account(
|
|
&account.to_string(),
|
|
&solana_pubkey::Pubkey::new_unique().to_string(),
|
|
&data
|
|
),
|
|
std::result::Result::Err(crate::DcMetadataMtmMetadataAccountDecodeError::ForeignOwner),
|
|
);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_metadata_account(
|
|
&solana_pubkey::Pubkey::new_unique().to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&data
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataPda
|
|
),
|
|
);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_metadata_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&[]
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength
|
|
),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn edition_and_master_edition_accounts_decode_with_exact_pda() {
|
|
let mint = mpl_token_metadata::ID;
|
|
let (account, expected_bump) = mpl_token_metadata::accounts::MasterEdition::find_pda(&mint);
|
|
let master_v2 = mpl_token_metadata::accounts::MasterEdition {
|
|
key: mpl_token_metadata::types::Key::MasterEditionV2,
|
|
supply: 7,
|
|
max_supply: std::option::Option::Some(100),
|
|
};
|
|
let master_v2_data = match borsh_0_10::to_vec(&master_v2) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("master edition serialization failed: {error}")
|
|
},
|
|
};
|
|
let master_v2_snapshot =
|
|
match crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&master_v2_data,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("master edition decode failed: {error}"),
|
|
};
|
|
assert_eq!(master_v2_snapshot.bump, expected_bump);
|
|
assert_eq!(
|
|
master_v2_snapshot.kind,
|
|
crate::DcMetadataMtmEditionAccountKind::MasterEditionV2
|
|
);
|
|
assert_eq!(master_v2_snapshot.supply, std::option::Option::Some(7));
|
|
assert_eq!(master_v2_snapshot.max_supply, std::option::Option::Some(100));
|
|
let edition = mpl_token_metadata::accounts::Edition {
|
|
key: mpl_token_metadata::types::Key::EditionV1,
|
|
parent: account,
|
|
edition: u64::MAX,
|
|
};
|
|
let edition_data = match borsh_0_10::to_vec(&edition) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("edition serialization failed: {error}"),
|
|
};
|
|
let edition_snapshot =
|
|
match crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&edition_data,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("edition decode failed: {error}"),
|
|
};
|
|
assert_eq!(edition_snapshot.kind, crate::DcMetadataMtmEditionAccountKind::EditionV1);
|
|
assert_eq!(edition_snapshot.parent, std::option::Option::Some(account.to_string()));
|
|
assert_eq!(edition_snapshot.edition, std::option::Option::Some(u64::MAX));
|
|
}
|
|
|
|
#[test]
|
|
fn edition_and_master_edition_accept_only_known_allocations() {
|
|
let mint = mpl_token_metadata::ID;
|
|
let (account, _) = mpl_token_metadata::accounts::MasterEdition::find_pda(&mint);
|
|
let master = mpl_token_metadata::accounts::MasterEdition {
|
|
key: mpl_token_metadata::types::Key::MasterEditionV2,
|
|
supply: 1,
|
|
max_supply: std::option::Option::Some(1),
|
|
};
|
|
let master_data = match borsh_0_10::to_vec(&master) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("master edition serialization failed: {error}")
|
|
},
|
|
};
|
|
for allocated_len in [
|
|
super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES,
|
|
super::LEGACY_MASTER_EDITION_ACCOUNT_BYTES,
|
|
] {
|
|
let mut padded = master_data.clone();
|
|
padded.resize(allocated_len, 0);
|
|
assert!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
padded.as_slice(),
|
|
)
|
|
.is_ok()
|
|
);
|
|
}
|
|
let edition = mpl_token_metadata::accounts::Edition {
|
|
key: mpl_token_metadata::types::Key::EditionV1,
|
|
parent: account,
|
|
edition: 1,
|
|
};
|
|
let edition_data = match borsh_0_10::to_vec(&edition) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("edition serialization failed: {error}"),
|
|
};
|
|
for allocated_len in
|
|
[super::COMPACT_EDITION_ACCOUNT_BYTES, super::LEGACY_EDITION_ACCOUNT_BYTES]
|
|
{
|
|
let mut padded = edition_data.clone();
|
|
padded.resize(allocated_len, 0);
|
|
assert!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
padded.as_slice(),
|
|
)
|
|
.is_ok()
|
|
);
|
|
}
|
|
let mut unexpected_len = edition_data.clone();
|
|
unexpected_len.resize(super::COMPACT_EDITION_ACCOUNT_BYTES + 1, 0);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
unexpected_len.as_slice(),
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength
|
|
),
|
|
);
|
|
let mut non_zero_padding = edition_data;
|
|
non_zero_padding.resize(super::COMPACT_EDITION_ACCOUNT_BYTES, 1);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
non_zero_padding.as_slice(),
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout
|
|
),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn compact_master_edition_trailer_accepts_pnft_and_rejects_invalid_flags() {
|
|
let mint = mpl_token_metadata::ID;
|
|
let (account, _) = mpl_token_metadata::accounts::MasterEdition::find_pda(&mint);
|
|
let master = mpl_token_metadata::accounts::MasterEdition {
|
|
key: mpl_token_metadata::types::Key::MasterEditionV2,
|
|
supply: 0,
|
|
max_supply: std::option::Option::Some(0),
|
|
};
|
|
let serialized = match borsh_0_10::to_vec(&master) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("master edition serialization failed: {error}")
|
|
},
|
|
};
|
|
let mut pnft = serialized.clone();
|
|
pnft.resize(super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES, 0);
|
|
pnft[super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES - 2] = 1;
|
|
pnft[super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES - 1] =
|
|
mpl_token_metadata::types::TokenStandard::ProgrammableNonFungible as u8;
|
|
let snapshot = match crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
pnft.as_slice(),
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("compact pNFT master edition decode failed: {error}")
|
|
},
|
|
};
|
|
assert_eq!(snapshot.kind, crate::DcMetadataMtmEditionAccountKind::MasterEditionV2);
|
|
assert_eq!(snapshot.supply, std::option::Option::Some(0));
|
|
assert_eq!(snapshot.max_supply, std::option::Option::Some(0));
|
|
let mut invalid_fee_flag = pnft.clone();
|
|
invalid_fee_flag[super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES - 2] = 2;
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
invalid_fee_flag.as_slice(),
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout
|
|
),
|
|
);
|
|
let mut invalid_token_standard = pnft;
|
|
invalid_token_standard[super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES - 1] =
|
|
mpl_token_metadata::types::TokenStandard::Fungible as u8;
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
invalid_token_standard.as_slice(),
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout
|
|
),
|
|
);
|
|
let master_without_limit = mpl_token_metadata::accounts::MasterEdition {
|
|
key: mpl_token_metadata::types::Key::MasterEditionV2,
|
|
supply: 0,
|
|
max_supply: std::option::Option::None,
|
|
};
|
|
let mut invalid_reserved = match borsh_0_10::to_vec(&master_without_limit) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("master edition serialization failed: {error}")
|
|
},
|
|
};
|
|
let serialized_len = invalid_reserved.len();
|
|
invalid_reserved.resize(super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES, 0);
|
|
invalid_reserved[serialized_len] = 1;
|
|
invalid_reserved[super::COMPACT_MASTER_EDITION_ACCOUNT_BYTES - 1] =
|
|
mpl_token_metadata::types::TokenStandard::ProgrammableNonFungible as u8;
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
invalid_reserved.as_slice(),
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout
|
|
),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn historical_master_edition_v1_and_invalid_boundaries_are_explicit() {
|
|
let mint = mpl_token_metadata::ID;
|
|
let (account, _) = mpl_token_metadata::accounts::MasterEdition::find_pda(&mint);
|
|
let historical = mpl_token_metadata::accounts::DeprecatedMasterEditionV1 {
|
|
key: mpl_token_metadata::types::Key::MasterEditionV1,
|
|
supply: 1,
|
|
max_supply: std::option::Option::None,
|
|
printing_mint: mpl_token_metadata::ID,
|
|
one_time_printing_authorization_mint: mpl_token_metadata::ID,
|
|
};
|
|
let data = match borsh_0_10::to_vec(&historical) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("historical master edition serialization failed: {error}")
|
|
},
|
|
};
|
|
let snapshot = match crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&data,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("historical master edition decode failed: {error}")
|
|
},
|
|
};
|
|
assert_eq!(snapshot.kind, crate::DcMetadataMtmEditionAccountKind::MasterEditionV1);
|
|
assert_eq!(
|
|
snapshot.printing_mint,
|
|
std::option::Option::Some(mpl_token_metadata::ID.to_string())
|
|
);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
&solana_pubkey::Pubkey::new_unique().to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&data
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataPda
|
|
),
|
|
);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&[255]
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataKey
|
|
),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn edition_marker_v1_decodes_group_bitmap_and_exact_pda() {
|
|
let mint = mpl_token_metadata::ID.to_string();
|
|
let program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
|
|
};
|
|
let mint_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(&mint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("mint parse failed: {error}"),
|
|
};
|
|
let edition = 249u64;
|
|
let group = edition / 248;
|
|
let group_seed = group.to_string();
|
|
let (account, expected_bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program.as_ref(),
|
|
mint_key.as_ref(),
|
|
b"edition",
|
|
group_seed.as_bytes(),
|
|
],
|
|
&program,
|
|
);
|
|
let mut ledger = [0u8; 31];
|
|
ledger[0] = 64;
|
|
let marker = mpl_token_metadata::accounts::EditionMarker {
|
|
key: mpl_token_metadata::types::Key::EditionMarker,
|
|
ledger,
|
|
};
|
|
let data = match borsh_0_10::to_vec(&marker) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("edition marker serialization failed: {error}")
|
|
},
|
|
};
|
|
let snapshot =
|
|
match crate::decoder_metadata_metaplex_token_metadata_decode_edition_marker_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint,
|
|
edition,
|
|
&data,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("edition marker decode failed: {error}"),
|
|
};
|
|
assert_eq!(snapshot.kind, crate::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV1);
|
|
assert_eq!(snapshot.bump, expected_bump);
|
|
assert_eq!(snapshot.marker_group, std::option::Option::Some(1));
|
|
assert_eq!(snapshot.byte_index, 0);
|
|
assert_eq!(snapshot.bit_mask, 64);
|
|
assert!(snapshot.edition_taken);
|
|
}
|
|
|
|
#[test]
|
|
fn edition_marker_v2_decodes_extensible_bitmap_and_rejects_boundaries() {
|
|
let mint = mpl_token_metadata::ID.to_string();
|
|
let program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
|
|
};
|
|
let mint_key = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(&mint) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("mint parse failed: {error}"),
|
|
};
|
|
let (account, expected_bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[b"metadata", program.as_ref(), mint_key.as_ref(), b"edition", b"marker"],
|
|
&program,
|
|
);
|
|
let marker = mpl_token_metadata::accounts::EditionMarkerV2 {
|
|
key: mpl_token_metadata::types::Key::EditionMarkerV2,
|
|
ledger: vec![0, 64],
|
|
};
|
|
let data = match borsh_0_10::to_vec(&marker) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("edition marker v2 serialization failed: {error}")
|
|
},
|
|
};
|
|
let snapshot =
|
|
match crate::decoder_metadata_metaplex_token_metadata_decode_edition_marker_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint,
|
|
9,
|
|
&data,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("edition marker v2 decode failed: {error}")
|
|
},
|
|
};
|
|
assert_eq!(snapshot.kind, crate::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV2);
|
|
assert_eq!(snapshot.bump, expected_bump);
|
|
assert_eq!(snapshot.marker_group, std::option::Option::None);
|
|
assert_eq!(snapshot.byte_index, 1);
|
|
assert_eq!(snapshot.bit_mask, 64);
|
|
assert!(snapshot.edition_taken);
|
|
let mut suffixed = data.clone();
|
|
suffixed.push(0);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_edition_marker_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint,
|
|
9,
|
|
&suffixed
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength
|
|
),
|
|
);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_edition_marker_account(
|
|
&solana_pubkey::Pubkey::new_unique().to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint,
|
|
9,
|
|
&data
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidEditionMarkerPda
|
|
),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn token_record_decodes_programmable_state_delegate_and_exact_pda() {
|
|
let mint = solana_pubkey::Pubkey::new_unique();
|
|
let token = solana_pubkey::Pubkey::new_unique();
|
|
let program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
|
|
};
|
|
let (account, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[b"metadata", program.as_ref(), mint.as_ref(), b"token_record", token.as_ref()],
|
|
&program,
|
|
);
|
|
let value = mpl_token_metadata::accounts::TokenRecord {
|
|
key: mpl_token_metadata::types::Key::TokenRecord,
|
|
bump,
|
|
state: mpl_token_metadata::types::TokenState::Locked,
|
|
rule_set_revision: std::option::Option::Some(u64::MAX),
|
|
delegate: std::option::Option::Some(mpl_token_metadata::ID),
|
|
delegate_role: std::option::Option::Some(
|
|
mpl_token_metadata::types::TokenDelegateRole::Transfer,
|
|
),
|
|
locked_transfer: std::option::Option::Some(mpl_token_metadata::ID),
|
|
};
|
|
let mut data = match borsh_0_10::to_vec(&value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("token record serialization failed: {error}"),
|
|
};
|
|
data.resize(mpl_token_metadata::accounts::TokenRecord::LEN, 0);
|
|
let snapshot =
|
|
match crate::decoder_metadata_metaplex_token_metadata_decode_token_record_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&token.to_string(),
|
|
&data,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("token record decode failed: {error}"),
|
|
};
|
|
assert_eq!(snapshot.bump, bump);
|
|
assert_eq!(snapshot.stored_bump, bump);
|
|
assert_eq!(snapshot.state, "Locked");
|
|
assert!(snapshot.locked);
|
|
assert_eq!(snapshot.rule_set_revision, std::option::Option::Some(u64::MAX));
|
|
assert_eq!(snapshot.delegate_role, std::option::Option::Some("Transfer".to_string()));
|
|
assert_eq!(
|
|
snapshot.delegate,
|
|
std::option::Option::Some(mpl_token_metadata::ID.to_string())
|
|
);
|
|
assert_eq!(
|
|
snapshot.locked_transfer,
|
|
std::option::Option::Some(mpl_token_metadata::ID.to_string())
|
|
);
|
|
assert_eq!(
|
|
snapshot.payload_json["delegate"],
|
|
serde_json::json!(mpl_token_metadata::ID.to_string())
|
|
);
|
|
assert_eq!(snapshot.payload_json["delegate_role"], serde_json::json!("Transfer"));
|
|
assert_eq!(
|
|
snapshot.payload_json["locked_transfer"],
|
|
serde_json::json!(mpl_token_metadata::ID.to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn token_record_rejects_foreign_owner_wrong_pda_bump_key_and_length() {
|
|
let mint = solana_pubkey::Pubkey::new_unique();
|
|
let token = solana_pubkey::Pubkey::new_unique();
|
|
let program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
|
|
};
|
|
let (account, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[b"metadata", program.as_ref(), mint.as_ref(), b"token_record", token.as_ref()],
|
|
&program,
|
|
);
|
|
let make_data = |key, stored_bump| {
|
|
let value = mpl_token_metadata::accounts::TokenRecord {
|
|
key,
|
|
bump: stored_bump,
|
|
state: mpl_token_metadata::types::TokenState::Unlocked,
|
|
rule_set_revision: std::option::Option::None,
|
|
delegate: std::option::Option::None,
|
|
delegate_role: std::option::Option::None,
|
|
locked_transfer: std::option::Option::None,
|
|
};
|
|
let mut bytes = match borsh_0_10::to_vec(&value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("token record serialization failed: {error}")
|
|
},
|
|
};
|
|
bytes.resize(mpl_token_metadata::accounts::TokenRecord::LEN, 0);
|
|
return bytes;
|
|
};
|
|
let data = make_data(mpl_token_metadata::types::Key::TokenRecord, bump);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_token_record_account(
|
|
&account.to_string(),
|
|
&solana_pubkey::Pubkey::new_unique().to_string(),
|
|
&mint.to_string(),
|
|
&token.to_string(),
|
|
&data
|
|
),
|
|
std::result::Result::Err(crate::DcMetadataMtmMetadataAccountDecodeError::ForeignOwner),
|
|
);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_token_record_account(
|
|
&solana_pubkey::Pubkey::new_unique().to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&token.to_string(),
|
|
&data
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidTokenRecordPda
|
|
),
|
|
);
|
|
let wrong_bump =
|
|
make_data(mpl_token_metadata::types::Key::TokenRecord, bump.wrapping_add(1));
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_token_record_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&token.to_string(),
|
|
&wrong_bump
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidTokenRecordPda
|
|
),
|
|
);
|
|
let wrong_key = make_data(mpl_token_metadata::types::Key::MetadataV1, bump);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_token_record_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&token.to_string(),
|
|
&wrong_key
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidTokenRecordKey
|
|
),
|
|
);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_token_record_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&token.to_string(),
|
|
&data[..data.len() - 1]
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength
|
|
),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn delegate_records_decode_exact_role_seeds_and_pdas() {
|
|
let program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
|
|
};
|
|
let mint = mpl_token_metadata::ID;
|
|
let authority = mpl_token_metadata::ID;
|
|
let delegate = mpl_token_metadata::ID;
|
|
let role = mpl_token_metadata::types::MetadataDelegateRole::ProgrammableConfigItem;
|
|
let role_seed = role.to_string();
|
|
let (account, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program.as_ref(),
|
|
program.as_ref(),
|
|
role_seed.as_bytes(),
|
|
program.as_ref(),
|
|
program.as_ref(),
|
|
],
|
|
&program,
|
|
);
|
|
let record = mpl_token_metadata::accounts::MetadataDelegateRecord {
|
|
key: mpl_token_metadata::types::Key::MetadataDelegate,
|
|
bump,
|
|
mint,
|
|
delegate,
|
|
update_authority: authority,
|
|
};
|
|
let data = match borsh_0_10::to_vec(&record) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("metadata delegate serialization failed: {error}")
|
|
},
|
|
};
|
|
assert_eq!(data.len(), mpl_token_metadata::accounts::MetadataDelegateRecord::LEN);
|
|
let snapshot = match crate::decoder_metadata_metaplex_token_metadata_decode_metadata_delegate_record_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
role,
|
|
&data,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("metadata delegate decode failed: {error}"),
|
|
};
|
|
assert_eq!(snapshot.role, "prog_config_item_delegate");
|
|
assert_eq!(snapshot.stored_bump, bump);
|
|
let holder_role = mpl_token_metadata::types::HolderDelegateRole::PrintDelegate;
|
|
let holder_seed = holder_role.to_string();
|
|
let (holder_account, holder_bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program.as_ref(),
|
|
program.as_ref(),
|
|
holder_seed.as_bytes(),
|
|
program.as_ref(),
|
|
program.as_ref(),
|
|
],
|
|
&program,
|
|
);
|
|
let holder_record = mpl_token_metadata::accounts::HolderDelegateRecord {
|
|
key: mpl_token_metadata::types::Key::HolderDelegate,
|
|
bump: holder_bump,
|
|
mint,
|
|
delegate,
|
|
update_authority: authority,
|
|
};
|
|
let holder_data = match borsh_0_10::to_vec(&holder_record) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("holder delegate serialization failed: {error}")
|
|
},
|
|
};
|
|
let holder_snapshot = match crate::decoder_metadata_metaplex_token_metadata_decode_holder_delegate_record_account(
|
|
&holder_account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&authority.to_string(),
|
|
holder_role,
|
|
&holder_data,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("holder delegate decode failed: {error}"),
|
|
};
|
|
assert_eq!(holder_snapshot.role, "print_delegate");
|
|
assert_eq!(holder_snapshot.stored_bump, holder_bump);
|
|
}
|
|
|
|
#[test]
|
|
fn authority_records_decode_exact_values_and_reject_invalid_boundaries() {
|
|
let program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
|
|
};
|
|
let mint = mpl_token_metadata::ID;
|
|
let authority = mpl_token_metadata::ID;
|
|
let (collection_account, collection_bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program.as_ref(),
|
|
program.as_ref(),
|
|
b"collection_authority",
|
|
program.as_ref(),
|
|
],
|
|
&program,
|
|
);
|
|
let collection = mpl_token_metadata::accounts::CollectionAuthorityRecord {
|
|
key: mpl_token_metadata::types::Key::CollectionAuthorityRecord,
|
|
bump: collection_bump,
|
|
update_authority: std::option::Option::Some(mpl_token_metadata::ID),
|
|
};
|
|
let collection_data = match borsh_0_10::to_vec(&collection) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("collection authority serialization failed: {error}")
|
|
},
|
|
};
|
|
let collection_snapshot = match crate::decoder_metadata_metaplex_token_metadata_decode_collection_authority_record_account(
|
|
&collection_account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&authority.to_string(),
|
|
&collection_data,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("collection authority decode failed: {error}")
|
|
},
|
|
};
|
|
assert_eq!(
|
|
collection_snapshot.update_authority,
|
|
std::option::Option::Some(mpl_token_metadata::ID.to_string())
|
|
);
|
|
let (use_account, use_bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[b"metadata", program.as_ref(), program.as_ref(), b"user", program.as_ref()],
|
|
&program,
|
|
);
|
|
let use_record = mpl_token_metadata::accounts::UseAuthorityRecord {
|
|
key: mpl_token_metadata::types::Key::UseAuthorityRecord,
|
|
allowed_uses: u64::MAX,
|
|
bump: use_bump,
|
|
};
|
|
let use_data = match borsh_0_10::to_vec(&use_record) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("use authority serialization failed: {error}")
|
|
},
|
|
};
|
|
let use_snapshot = match crate::decoder_metadata_metaplex_token_metadata_decode_use_authority_record_account(
|
|
&use_account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&authority.to_string(),
|
|
&use_data,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("use authority decode failed: {error}"),
|
|
};
|
|
assert_eq!(use_snapshot.allowed_uses, std::option::Option::Some(u64::MAX));
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_use_authority_record_account(
|
|
&solana_pubkey::Pubkey::new_unique().to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&authority.to_string(),
|
|
&use_data
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidAuthorityRecordPda
|
|
)
|
|
);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_use_authority_record_account(
|
|
&use_account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&mint.to_string(),
|
|
&authority.to_string(),
|
|
&use_data[..9]
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn token_owned_escrow_decodes_token_owner_and_creator_authorities() {
|
|
let program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
|
|
};
|
|
let base_token = mpl_token_metadata::ID;
|
|
let base_key = program;
|
|
let (owner_account, owner_bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[b"metadata", program.as_ref(), base_key.as_ref(), &[0], b"escrow"],
|
|
&program,
|
|
);
|
|
let owner_value = mpl_token_metadata::accounts::TokenOwnedEscrow {
|
|
key: mpl_token_metadata::types::Key::TokenOwnedEscrow,
|
|
base_token,
|
|
authority: mpl_token_metadata::types::EscrowAuthority::TokenOwner,
|
|
bump: owner_bump,
|
|
};
|
|
let owner_data = match borsh_0_10::to_vec(&owner_value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("escrow serialization failed: {error}"),
|
|
};
|
|
assert_eq!(owner_data.len(), 35);
|
|
let owner_snapshot =
|
|
match crate::decoder_metadata_metaplex_token_metadata_decode_token_owned_escrow_account(
|
|
&owner_account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&owner_data,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("escrow decode failed: {error}"),
|
|
};
|
|
assert_eq!(
|
|
owner_snapshot.authority_kind,
|
|
crate::DcMetadataMtmTokenOwnedEscrowAuthorityKind::TokenOwner
|
|
);
|
|
assert_eq!(owner_snapshot.creator, std::option::Option::None);
|
|
let creator = mpl_token_metadata::ID;
|
|
let (creator_account, creator_bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program.as_ref(),
|
|
base_key.as_ref(),
|
|
&[1],
|
|
program.as_ref(),
|
|
b"escrow",
|
|
],
|
|
&program,
|
|
);
|
|
let creator_value = mpl_token_metadata::accounts::TokenOwnedEscrow {
|
|
key: mpl_token_metadata::types::Key::TokenOwnedEscrow,
|
|
base_token,
|
|
authority: mpl_token_metadata::types::EscrowAuthority::Creator(creator),
|
|
bump: creator_bump,
|
|
};
|
|
let creator_data = match borsh_0_10::to_vec(&creator_value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
panic!("creator escrow serialization failed: {error}")
|
|
},
|
|
};
|
|
assert_eq!(creator_data.len(), 67);
|
|
let creator_snapshot =
|
|
match crate::decoder_metadata_metaplex_token_metadata_decode_token_owned_escrow_account(
|
|
&creator_account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&creator_data,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("creator escrow decode failed: {error}"),
|
|
};
|
|
assert_eq!(
|
|
creator_snapshot.authority_kind,
|
|
crate::DcMetadataMtmTokenOwnedEscrowAuthorityKind::Creator
|
|
);
|
|
assert_eq!(
|
|
creator_snapshot.creator,
|
|
std::option::Option::Some(mpl_token_metadata::ID.to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn token_owned_escrow_rejects_wrong_pda_bump_key_and_length() {
|
|
let program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
|
|
};
|
|
let (account, bump) = solana_pubkey::Pubkey::find_program_address(
|
|
&[b"metadata", program.as_ref(), program.as_ref(), &[0], b"escrow"],
|
|
&program,
|
|
);
|
|
let make = |key, stored_bump| {
|
|
let value = mpl_token_metadata::accounts::TokenOwnedEscrow {
|
|
key,
|
|
base_token: mpl_token_metadata::ID,
|
|
authority: mpl_token_metadata::types::EscrowAuthority::TokenOwner,
|
|
bump: stored_bump,
|
|
};
|
|
return match borsh_0_10::to_vec(&value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("escrow serialization failed: {error}"),
|
|
};
|
|
};
|
|
let data = make(mpl_token_metadata::types::Key::TokenOwnedEscrow, bump);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_token_owned_escrow_account(
|
|
&solana_pubkey::Pubkey::new_unique().to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&data
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidTokenOwnedEscrowPda
|
|
)
|
|
);
|
|
let wrong_bump =
|
|
make(mpl_token_metadata::types::Key::TokenOwnedEscrow, bump.wrapping_add(1));
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_token_owned_escrow_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&wrong_bump
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidTokenOwnedEscrowPda
|
|
)
|
|
);
|
|
let wrong_key = make(mpl_token_metadata::types::Key::MetadataV1, bump);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_token_owned_escrow_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&wrong_key
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidTokenOwnedEscrowKey
|
|
)
|
|
);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_token_owned_escrow_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&data[..34]
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidDataLength
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn historical_reservation_lists_decode_v1_and_v2_exactly() {
|
|
let program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
|
|
};
|
|
let master = solana_pubkey::Pubkey::new_unique();
|
|
let resource = solana_pubkey::Pubkey::new_unique();
|
|
let reserved = solana_pubkey::Pubkey::new_unique();
|
|
let (account, _) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program.as_ref(),
|
|
master.as_ref(),
|
|
b"reservation",
|
|
resource.as_ref(),
|
|
],
|
|
&program,
|
|
);
|
|
let mut v1 = vec![3u8];
|
|
v1.extend_from_slice(master.as_ref());
|
|
v1.push(1);
|
|
v1.extend_from_slice(&9u64.to_le_bytes());
|
|
v1.extend_from_slice(&1u32.to_le_bytes());
|
|
v1.extend_from_slice(reserved.as_ref());
|
|
v1.push(2);
|
|
v1.push(3);
|
|
let snapshot =
|
|
match crate::decoder_metadata_metaplex_token_metadata_decode_reservation_list_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&resource.to_string(),
|
|
&v1,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("reservation v1 decode failed: {error}"),
|
|
};
|
|
assert_eq!(snapshot.kind, crate::DcMetadataMtmReservationListAccountKind::V1);
|
|
assert_eq!(snapshot.supply_snapshot, std::option::Option::Some(9));
|
|
assert_eq!(snapshot.total_reservation_spots, 3);
|
|
let mut v2 = vec![5u8];
|
|
v2.extend_from_slice(master.as_ref());
|
|
v2.push(0);
|
|
v2.extend_from_slice(&1u32.to_le_bytes());
|
|
v2.extend_from_slice(reserved.as_ref());
|
|
v2.extend_from_slice(&u64::MAX.to_le_bytes());
|
|
v2.extend_from_slice(&7u64.to_le_bytes());
|
|
v2.extend_from_slice(&11u64.to_le_bytes());
|
|
v2.extend_from_slice(&5u64.to_le_bytes());
|
|
let snapshot =
|
|
match crate::decoder_metadata_metaplex_token_metadata_decode_reservation_list_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&resource.to_string(),
|
|
&v2,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("reservation v2 decode failed: {error}"),
|
|
};
|
|
assert_eq!(snapshot.kind, crate::DcMetadataMtmReservationListAccountKind::V2);
|
|
assert_eq!(snapshot.reservations[0].spots_remaining, u64::MAX);
|
|
assert_eq!(snapshot.total_reservation_spots, 11);
|
|
assert_eq!(snapshot.current_reservation_spots, 5);
|
|
}
|
|
|
|
#[test]
|
|
fn published_account_key_inventory_is_exact_and_exhaustive() {
|
|
let variants = [
|
|
mpl_token_metadata::types::Key::Uninitialized,
|
|
mpl_token_metadata::types::Key::EditionV1,
|
|
mpl_token_metadata::types::Key::MasterEditionV1,
|
|
mpl_token_metadata::types::Key::ReservationListV1,
|
|
mpl_token_metadata::types::Key::MetadataV1,
|
|
mpl_token_metadata::types::Key::ReservationListV2,
|
|
mpl_token_metadata::types::Key::MasterEditionV2,
|
|
mpl_token_metadata::types::Key::EditionMarker,
|
|
mpl_token_metadata::types::Key::UseAuthorityRecord,
|
|
mpl_token_metadata::types::Key::CollectionAuthorityRecord,
|
|
mpl_token_metadata::types::Key::TokenOwnedEscrow,
|
|
mpl_token_metadata::types::Key::TokenRecord,
|
|
mpl_token_metadata::types::Key::MetadataDelegate,
|
|
mpl_token_metadata::types::Key::EditionMarkerV2,
|
|
mpl_token_metadata::types::Key::HolderDelegate,
|
|
];
|
|
assert_eq!(crate::DC_METADATA_MTM_METADATA_ACCOUNT_LAYOUT_AUDIT.len(), variants.len());
|
|
for (index, variant) in variants.iter().enumerate() {
|
|
let bytes = match borsh_0_10::to_vec(variant) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("key serialization failed: {error}"),
|
|
};
|
|
assert_eq!(bytes, vec![index as u8]);
|
|
let entry = crate::DC_METADATA_MTM_METADATA_ACCOUNT_LAYOUT_AUDIT[index];
|
|
assert_eq!(entry.discriminant, index as u8);
|
|
assert!(!entry.key_name.is_empty());
|
|
assert!(!entry.decoder.is_empty());
|
|
assert!(!entry.lifecycle.is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn account_audit_matrix_covers_every_key_without_duplicate_discriminants() {
|
|
let mut discriminants = std::collections::BTreeSet::new();
|
|
let mut names = std::collections::BTreeSet::new();
|
|
for entry in crate::DC_METADATA_MTM_METADATA_ACCOUNT_LAYOUT_AUDIT {
|
|
assert!(discriminants.insert(entry.discriminant));
|
|
assert!(names.insert(entry.key_name));
|
|
if entry.discriminant == 0 {
|
|
assert_eq!(entry.decoder, "reject_uninitialized");
|
|
} else {
|
|
assert_ne!(entry.decoder, "reject_uninitialized");
|
|
}
|
|
}
|
|
let matrix: serde_json::Value = match serde_json::from_str(include_str!(
|
|
"../../../../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_MATRIX.json"
|
|
)) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("matrix parse failed: {error}"),
|
|
};
|
|
let inventory =
|
|
match matrix.get("account_key_inventory").and_then(serde_json::Value::as_array) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => panic!("account_key_inventory missing"),
|
|
};
|
|
assert_eq!(inventory.len(), crate::DC_METADATA_MTM_METADATA_ACCOUNT_LAYOUT_AUDIT.len());
|
|
for (index, item) in inventory.iter().enumerate() {
|
|
assert_eq!(
|
|
item.get("discriminant").and_then(serde_json::Value::as_u64),
|
|
std::option::Option::Some(index as u64)
|
|
);
|
|
assert_eq!(
|
|
item.get("key").and_then(serde_json::Value::as_str),
|
|
std::option::Option::Some(
|
|
crate::DC_METADATA_MTM_METADATA_ACCOUNT_LAYOUT_AUDIT[index].key_name
|
|
)
|
|
);
|
|
assert_eq!(
|
|
item.get("decoder").and_then(serde_json::Value::as_str),
|
|
std::option::Option::Some(
|
|
crate::DC_METADATA_MTM_METADATA_ACCOUNT_LAYOUT_AUDIT[index].decoder
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn historical_reservation_lists_reject_wrong_pda_key_bounds_and_suffix() {
|
|
let program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
|
|
};
|
|
let master = solana_pubkey::Pubkey::new_unique();
|
|
let resource = solana_pubkey::Pubkey::new_unique();
|
|
let (account, _) = solana_pubkey::Pubkey::find_program_address(
|
|
&[
|
|
b"metadata",
|
|
program.as_ref(),
|
|
master.as_ref(),
|
|
b"reservation",
|
|
resource.as_ref(),
|
|
],
|
|
&program,
|
|
);
|
|
let mut data = vec![3u8];
|
|
data.extend_from_slice(master.as_ref());
|
|
data.push(0);
|
|
data.extend_from_slice(&0u32.to_le_bytes());
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_reservation_list_account(
|
|
&solana_pubkey::Pubkey::new_unique().to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&resource.to_string(),
|
|
&data
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidReservationListPda
|
|
)
|
|
);
|
|
let mut wrong_key = data.clone();
|
|
wrong_key[0] = 4;
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_reservation_list_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&resource.to_string(),
|
|
&wrong_key
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidReservationListKey
|
|
)
|
|
);
|
|
let mut too_many = data.clone();
|
|
too_many[34..38].copy_from_slice(&201u32.to_le_bytes());
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_reservation_list_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&resource.to_string(),
|
|
&too_many
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::ReservationListBoundsExceeded
|
|
)
|
|
);
|
|
let mut suffix = data.clone();
|
|
suffix.push(1);
|
|
assert_eq!(
|
|
crate::decoder_metadata_metaplex_token_metadata_decode_reservation_list_account(
|
|
&account.to_string(),
|
|
ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
|
&resource.to_string(),
|
|
&suffix
|
|
),
|
|
std::result::Result::Err(
|
|
crate::DcMetadataMtmMetadataAccountDecodeError::InvalidMetadataLayout
|
|
)
|
|
);
|
|
}
|
|
}
|