v0.4.7-pre.007
This commit is contained in:
256
kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs
Normal file
256
kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
// file: kb-pipeline/src/solana_metaplex_token_metadata_stateful.rs
|
||||
// version: 2
|
||||
|
||||
//! Bounded Metaplex Token Metadata account reads and canonical state projections.
|
||||
|
||||
/// Maximum complete Metaplex account data accepted by one RPC read.
|
||||
pub const MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES: usize = 1_048_576;
|
||||
|
||||
/// Supported Metaplex account category for one bounded stateful read.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum MetaplexTokenMetadataAccountKind {
|
||||
/// Metadata PDA derived from the mint encoded in the account.
|
||||
Metadata,
|
||||
/// Master edition or printed edition PDA derived from the supplied mint.
|
||||
Edition {
|
||||
/// Mint used to derive the edition PDA.
|
||||
mint: kb_lib::MdPubkey,
|
||||
},
|
||||
/// Programmable token record PDA derived from mint and token account.
|
||||
TokenRecord {
|
||||
/// Mint used to derive the token-record PDA.
|
||||
mint: kb_lib::MdPubkey,
|
||||
/// Token account used to derive the token-record PDA.
|
||||
token: kb_lib::MdPubkey,
|
||||
},
|
||||
}
|
||||
|
||||
/// One bounded Metaplex account read request.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MetaplexTokenMetadataStatefulReadRequest {
|
||||
/// Endpoint role used for the HTTP RPC request.
|
||||
pub query_role: std::string::String,
|
||||
/// Canonical account address.
|
||||
pub account: kb_lib::MdPubkey,
|
||||
/// Expected account category and derivation inputs.
|
||||
pub kind: crate::MetaplexTokenMetadataAccountKind,
|
||||
/// Optional minimum RPC context slot.
|
||||
pub min_context_slot: std::option::Option<u64>,
|
||||
/// Maximum decoded account bytes accepted from the endpoint.
|
||||
pub max_data_bytes: usize,
|
||||
}
|
||||
|
||||
/// Canonical bounded Metaplex state snapshot.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MetaplexTokenMetadataStatefulSnapshot {
|
||||
/// Canonical account address.
|
||||
pub account: kb_lib::MdPubkey,
|
||||
/// Stable account category.
|
||||
pub account_kind: std::string::String,
|
||||
/// Mint correlated with this account when available.
|
||||
pub mint: std::option::Option<kb_lib::MdPubkey>,
|
||||
/// RPC context slot.
|
||||
pub slot: u64,
|
||||
/// Bounded decoder-owned projection.
|
||||
pub payload_json: serde_json::Value,
|
||||
}
|
||||
|
||||
/// One bounded RPC read and its validated snapshot.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MetaplexTokenMetadataStatefulReadResult {
|
||||
/// Commitment used by the read.
|
||||
pub commitment: std::string::String,
|
||||
/// Context slot returned by the endpoint.
|
||||
pub context_slot: u64,
|
||||
/// Validated canonical snapshot.
|
||||
pub snapshot: crate::MetaplexTokenMetadataStatefulSnapshot,
|
||||
}
|
||||
|
||||
/// Reads and validates one bounded Metaplex Token Metadata account.
|
||||
pub async fn read_metaplex_token_metadata_stateful_snapshot(
|
||||
pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
|
||||
) -> kb_core::Result<crate::MetaplexTokenMetadataStatefulReadResult> {
|
||||
if request.query_role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Metaplex stateful read query_role must not be empty",
|
||||
));
|
||||
}
|
||||
let config = match kb_onchain_transport::GetAccountInfoConfig::new_with_data(
|
||||
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
|
||||
request.min_context_slot,
|
||||
request.max_data_bytes,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let result = match pool
|
||||
.get_account_info_for_role(request.query_role.as_str(), &request.account, &config)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::materialize_metaplex_token_metadata_account_info_result(request, &result);
|
||||
}
|
||||
|
||||
/// Validates one RPC account response and delegates parsing to `kb-lib`.
|
||||
pub fn materialize_metaplex_token_metadata_account_info_result(
|
||||
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
|
||||
result: &kb_onchain_transport::AccountInfoResult,
|
||||
) -> kb_core::Result<crate::MetaplexTokenMetadataStatefulReadResult> {
|
||||
if request.max_data_bytes == 0
|
||||
|| request.max_data_bytes > crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Metaplex stateful max_data_bytes must be between 1 and {}",
|
||||
crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
|
||||
)));
|
||||
}
|
||||
if let std::option::Option::Some(minimum) = request.min_context_slot {
|
||||
if result.context.slot < minimum {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_context_slot_too_old",
|
||||
"Metaplex account context slot is below the requested minimum",
|
||||
));
|
||||
}
|
||||
}
|
||||
let account = match result.account.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_account_missing",
|
||||
format!("Metaplex account {} does not exist", request.account.0),
|
||||
));
|
||||
},
|
||||
};
|
||||
if account.executable {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_account_executable",
|
||||
"Metaplex state accounts must not be executable",
|
||||
));
|
||||
}
|
||||
if account.owner.0 != kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_owner_mismatch",
|
||||
"Metaplex state account owner does not match Token Metadata",
|
||||
));
|
||||
}
|
||||
if account.space != account.data.len() as u64 || account.data.len() > request.max_data_bytes {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_account_data_invalid",
|
||||
"Metaplex account data is incomplete or above the configured bound",
|
||||
));
|
||||
}
|
||||
let snapshot = match decode_snapshot(request, result.context.slot, account.data.as_slice()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulReadResult {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot: result.context.slot,
|
||||
snapshot,
|
||||
});
|
||||
}
|
||||
|
||||
fn decode_snapshot(
|
||||
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
|
||||
slot: u64,
|
||||
data: &[u8],
|
||||
) -> kb_core::Result<crate::MetaplexTokenMetadataStatefulSnapshot> {
|
||||
let owner = kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID;
|
||||
return match &request.kind {
|
||||
crate::MetaplexTokenMetadataAccountKind::Metadata => {
|
||||
match kb_lib::decoder_metadata_metaplex_token_metadata_decode_metadata_account(
|
||||
request.account.0.as_str(),
|
||||
owner,
|
||||
data,
|
||||
) {
|
||||
std::result::Result::Ok(value) => {
|
||||
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
|
||||
account: request.account.clone(),
|
||||
account_kind: "metadata".to_string(),
|
||||
mint: std::option::Option::Some(kb_lib::MdPubkey(value.mint)),
|
||||
slot,
|
||||
payload_json: value.payload_json,
|
||||
})
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_decode_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
}
|
||||
},
|
||||
crate::MetaplexTokenMetadataAccountKind::Edition { mint } => {
|
||||
match kb_lib::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
||||
request.account.0.as_str(),
|
||||
owner,
|
||||
mint.0.as_str(),
|
||||
data,
|
||||
) {
|
||||
std::result::Result::Ok(value) => {
|
||||
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
|
||||
account: request.account.clone(),
|
||||
account_kind: "edition".to_string(),
|
||||
mint: std::option::Option::Some(mint.clone()),
|
||||
slot,
|
||||
payload_json: value.payload_json,
|
||||
})
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_decode_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
}
|
||||
},
|
||||
crate::MetaplexTokenMetadataAccountKind::TokenRecord { mint, token } => {
|
||||
match kb_lib::decoder_metadata_metaplex_token_metadata_decode_token_record_account(
|
||||
request.account.0.as_str(),
|
||||
owner,
|
||||
mint.0.as_str(),
|
||||
token.0.as_str(),
|
||||
data,
|
||||
) {
|
||||
std::result::Result::Ok(value) => {
|
||||
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
|
||||
account: request.account.clone(),
|
||||
account_kind: "token_record".to_string(),
|
||||
mint: std::option::Option::Some(mint.clone()),
|
||||
slot,
|
||||
payload_json: value.payload_json,
|
||||
})
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_stateful_decode_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn rejects_zero_and_oversize_bounds_before_account_decoding() {
|
||||
let request = crate::MetaplexTokenMetadataStatefulReadRequest {
|
||||
query_role: "rpc".to_string(),
|
||||
account: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
|
||||
kind: crate::MetaplexTokenMetadataAccountKind::Metadata,
|
||||
min_context_slot: std::option::Option::None,
|
||||
max_data_bytes: 0,
|
||||
};
|
||||
let result = kb_onchain_transport::AccountInfoResult {
|
||||
context: kb_onchain_transport::RpcResponseContext {
|
||||
slot: 1,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::None,
|
||||
};
|
||||
assert!(
|
||||
crate::materialize_metaplex_token_metadata_account_info_result(&request, &result)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user