0.1.0
This commit is contained in:
@@ -0,0 +1,803 @@
|
||||
// file: kb_pipeline/src/solana_token_2022_stateful.rs
|
||||
// version: 5
|
||||
|
||||
//! Contextual Token-2022 account-state validation and materialization routing.
|
||||
|
||||
/// Optional external identities required to validate cross-account Token-2022 state.
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022StatefulContext {
|
||||
/// Expected group account for one TokenGroupMember extension.
|
||||
pub expected_group_address: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// Maximum complete Token-2022 account data accepted by one bounded RPC read.
|
||||
pub const MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES: usize = 65_536;
|
||||
|
||||
/// One bounded Token-2022 account read request.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Token2022StatefulReadRequest {
|
||||
/// Endpoint role used for the HTTP RPC request.
|
||||
pub query_role: std::string::String,
|
||||
/// Canonical account address.
|
||||
pub account: kb_model::Pubkey,
|
||||
/// Expected Token-2022 base-state category.
|
||||
pub kind: kb_decoder_spl_token_2022::state::Token2022StateKind,
|
||||
/// 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,
|
||||
/// Optional external identities needed by cross-account extensions.
|
||||
pub context: crate::solana_token_2022_stateful::Token2022StatefulContext,
|
||||
}
|
||||
|
||||
/// One bounded RPC read and its contextually validated projections.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022StatefulReadResult {
|
||||
/// Commitment used for the RPC read.
|
||||
pub commitment: std::string::String,
|
||||
/// Context slot returned by the endpoint.
|
||||
pub context_slot: u64,
|
||||
/// Complete validated snapshot bundle.
|
||||
pub snapshot: crate::solana_token_2022_stateful::Token2022StatefulSnapshotBundle,
|
||||
}
|
||||
|
||||
/// Reads, validates, parses, and routes one bounded Token-2022 account snapshot.
|
||||
pub async fn read_token_2022_stateful_snapshot(
|
||||
pool: &kb_rpc::HttpEndpointPool,
|
||||
request: &crate::solana_token_2022_stateful::Token2022StatefulReadRequest,
|
||||
) -> kb_core::Result<crate::solana_token_2022_stateful::Token2022StatefulReadResult> {
|
||||
if request.query_role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Token-2022 stateful read query_role must not be empty",
|
||||
));
|
||||
}
|
||||
if request.max_data_bytes == 0
|
||||
|| request.max_data_bytes
|
||||
> crate::solana_token_2022_stateful::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 stateful read max_data_bytes must be between 1 and {}",
|
||||
crate::solana_token_2022_stateful::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES
|
||||
)));
|
||||
}
|
||||
let config = match kb_rpc::GetAccountInfoConfig::new_with_data(
|
||||
kb_rpc::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::solana_token_2022_stateful::materialize_token_2022_account_info_result(
|
||||
request, &result,
|
||||
);
|
||||
}
|
||||
|
||||
/// Validates one complete RPC account response before Token-2022 parsing and routing.
|
||||
pub fn materialize_token_2022_account_info_result(
|
||||
request: &crate::solana_token_2022_stateful::Token2022StatefulReadRequest,
|
||||
result: &kb_rpc::AccountInfoResult,
|
||||
) -> kb_core::Result<crate::solana_token_2022_stateful::Token2022StatefulReadResult> {
|
||||
if request.max_data_bytes == 0
|
||||
|| request.max_data_bytes
|
||||
> crate::solana_token_2022_stateful::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"Token-2022 stateful read max_data_bytes must be between 1 and {}",
|
||||
crate::solana_token_2022_stateful::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES
|
||||
)));
|
||||
}
|
||||
if let std::option::Option::Some(min_context_slot) = request.min_context_slot {
|
||||
if result.context.slot < min_context_slot {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_context_slot_too_old",
|
||||
format!(
|
||||
"Token-2022 account context slot {} is below requested minimum {min_context_slot}",
|
||||
result.context.slot
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
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(
|
||||
"token_2022_stateful_account_missing",
|
||||
format!("Token-2022 account {} does not exist", request.account.0),
|
||||
));
|
||||
},
|
||||
};
|
||||
if account.executable {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_account_executable",
|
||||
format!("Token-2022 state account {} must not be executable", request.account.0),
|
||||
));
|
||||
}
|
||||
if account.owner.0 != kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_owner_mismatch",
|
||||
format!(
|
||||
"Token-2022 state account {} owner must be {}, got {}",
|
||||
request.account.0,
|
||||
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
|
||||
account.owner.0
|
||||
),
|
||||
));
|
||||
}
|
||||
if account.space > request.max_data_bytes as u64 || account.data.len() > request.max_data_bytes
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_account_too_large",
|
||||
format!(
|
||||
"Token-2022 account {} reports {} bytes and returned {} bytes above limit {}",
|
||||
request.account.0,
|
||||
account.space,
|
||||
account.data.len(),
|
||||
request.max_data_bytes
|
||||
),
|
||||
));
|
||||
}
|
||||
if account.space != account.data.len() as u64 {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_account_data_incomplete",
|
||||
format!(
|
||||
"Token-2022 account {} reports {} bytes but returned {} decoded bytes",
|
||||
request.account.0,
|
||||
account.space,
|
||||
account.data.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
let state = match kb_decoder_spl_token_2022::state::parse_token_2022_state(
|
||||
request.kind,
|
||||
account.data.as_slice(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_parse_failed",
|
||||
error,
|
||||
));
|
||||
},
|
||||
};
|
||||
let snapshot = match crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot_with_context(
|
||||
request.account.0.as_str(),
|
||||
result.context.slot,
|
||||
&state,
|
||||
&request.context,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"token_2022_stateful_projection_failed",
|
||||
error,
|
||||
));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(
|
||||
crate::solana_token_2022_stateful::Token2022StatefulReadResult {
|
||||
commitment: "confirmed".to_string(),
|
||||
context_slot: result.context.slot,
|
||||
snapshot,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// One contextually validated Token-2022 state snapshot and its owned projections.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Token2022StatefulSnapshotBundle {
|
||||
/// Canonical account identity used for contextual checks and stable output keys.
|
||||
pub account_key: std::string::String,
|
||||
/// Context slot associated with the account read.
|
||||
pub slot: u64,
|
||||
/// Parsed base-state category.
|
||||
pub state_kind: std::string::String,
|
||||
/// Ordered published or future extension names retained by the parser.
|
||||
pub extension_names: std::vec::Vec<std::string::String>,
|
||||
/// Processor-owned projections routed without duplicate ownership.
|
||||
pub outputs: std::vec::Vec<kb_materializer_api::MaterializedOutput>,
|
||||
}
|
||||
|
||||
/// Parse, contextually validate, and materialize one bounded Token-2022 account snapshot.
|
||||
pub fn materialize_token_2022_stateful_snapshot(
|
||||
account_key: &str,
|
||||
owner_program_id: &str,
|
||||
slot: u64,
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind,
|
||||
data: &[u8],
|
||||
) -> std::result::Result<crate::solana_token_2022_stateful::Token2022StatefulSnapshotBundle, String>
|
||||
{
|
||||
if owner_program_id != kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID {
|
||||
return std::result::Result::Err(format!(
|
||||
"Token-2022 state snapshot owner must be {}, got {owner_program_id}",
|
||||
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID
|
||||
));
|
||||
}
|
||||
let state = match kb_decoder_spl_token_2022::state::parse_token_2022_state(kind, data) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot_with_context(
|
||||
account_key,
|
||||
slot,
|
||||
&state,
|
||||
&crate::solana_token_2022_stateful::Token2022StatefulContext::default(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Contextually validate and materialize one already parsed Token-2022 account snapshot.
|
||||
pub fn materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_key: &str,
|
||||
slot: u64,
|
||||
state: &kb_decoder_spl_token_2022::state::Token2022State,
|
||||
) -> std::result::Result<crate::solana_token_2022_stateful::Token2022StatefulSnapshotBundle, String>
|
||||
{
|
||||
return crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot_with_context(
|
||||
account_key,
|
||||
slot,
|
||||
state,
|
||||
&crate::solana_token_2022_stateful::Token2022StatefulContext::default(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Contextually validate one parsed Token-2022 snapshot with external cross-account identities.
|
||||
pub fn materialize_parsed_token_2022_stateful_snapshot_with_context(
|
||||
account_key: &str,
|
||||
slot: u64,
|
||||
state: &kb_decoder_spl_token_2022::state::Token2022State,
|
||||
context: &crate::solana_token_2022_stateful::Token2022StatefulContext,
|
||||
) -> std::result::Result<crate::solana_token_2022_stateful::Token2022StatefulSnapshotBundle, String>
|
||||
{
|
||||
if account_key.trim().is_empty() {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 stateful snapshot requires a non-empty account key".to_string(),
|
||||
);
|
||||
}
|
||||
let decoded = match bs58::decode(account_key).into_vec() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 stateful snapshot account key must be valid base58".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
if decoded.len() != 32 {
|
||||
return std::result::Result::Err(format!(
|
||||
"Token-2022 stateful snapshot account key must decode to 32 bytes, got {}",
|
||||
decoded.len()
|
||||
));
|
||||
}
|
||||
match crate::solana_token_2022_stateful::validate_embedded_mint_identity(account_key, state) {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
match crate::solana_token_2022_stateful::validate_group_member_identity(state, context) {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let account_output = match kb_materializer_token_accounts::materialize_token_2022_state_snapshot(
|
||||
account_key,
|
||||
slot,
|
||||
state,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut outputs = std::vec![account_output];
|
||||
if state.kind == kb_decoder_spl_token_2022::state::Token2022StateKind::Mint {
|
||||
let metadata_outputs =
|
||||
match kb_materializer_metadata::materialize_token_2022_metadata_snapshot(
|
||||
account_key,
|
||||
slot,
|
||||
state,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
outputs.extend(metadata_outputs);
|
||||
}
|
||||
let fee_outputs = match kb_materializer_fees::materialize_token_2022_fee_state_snapshots(
|
||||
account_key,
|
||||
slot,
|
||||
state,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
outputs.extend(fee_outputs);
|
||||
let admin_outputs = match kb_materializer_admin::materialize_token_2022_admin_state_snapshots(
|
||||
account_key,
|
||||
slot,
|
||||
state,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
outputs.extend(admin_outputs);
|
||||
let state_kind = match state.kind {
|
||||
kb_decoder_spl_token_2022::state::Token2022StateKind::Mint => "mint",
|
||||
kb_decoder_spl_token_2022::state::Token2022StateKind::Account => "account",
|
||||
kb_decoder_spl_token_2022::state::Token2022StateKind::Multisig => "multisig",
|
||||
};
|
||||
return std::result::Result::Ok(
|
||||
crate::solana_token_2022_stateful::Token2022StatefulSnapshotBundle {
|
||||
account_key: account_key.to_string(),
|
||||
slot,
|
||||
state_kind: state_kind.to_string(),
|
||||
extension_names: state
|
||||
.extensions
|
||||
.iter()
|
||||
.map(|entry| return entry.extension_name.to_string())
|
||||
.collect(),
|
||||
outputs,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_embedded_mint_identity(
|
||||
account_key: &str,
|
||||
state: &kb_decoder_spl_token_2022::state::Token2022State,
|
||||
) -> std::result::Result<(), String> {
|
||||
for entry in &state.extensions {
|
||||
if entry.extension_name != "token_metadata"
|
||||
&& entry.extension_name != "token_group"
|
||||
&& entry.extension_name != "token_group_member"
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let embedded_mint = entry.value_fields.get("mint").and_then(serde_json::Value::as_str);
|
||||
let embedded_mint = match embedded_mint {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(format!(
|
||||
"Token-2022 {} extension requires a structured mint field",
|
||||
entry.extension_name
|
||||
));
|
||||
},
|
||||
};
|
||||
if embedded_mint != account_key {
|
||||
return std::result::Result::Err(format!(
|
||||
"Token-2022 {} mint {} does not match account {account_key}",
|
||||
entry.extension_name, embedded_mint
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_group_member_identity(
|
||||
state: &kb_decoder_spl_token_2022::state::Token2022State,
|
||||
context: &crate::solana_token_2022_stateful::Token2022StatefulContext,
|
||||
) -> std::result::Result<(), String> {
|
||||
for entry in &state.extensions {
|
||||
if entry.extension_name != "token_group_member" {
|
||||
continue;
|
||||
}
|
||||
let group = entry.value_fields.get("group").and_then(serde_json::Value::as_str);
|
||||
let group = match group {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 token_group_member extension requires a structured group field"
|
||||
.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let expected = match context.expected_group_address.as_deref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
"Token-2022 token_group_member validation requires an expected group address"
|
||||
.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
if group != expected {
|
||||
return std::result::Result::Err(format!(
|
||||
"Token-2022 token_group_member group {group} does not match expected group {expected}"
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn account_key(byte: u8) -> String {
|
||||
return bs58::encode([byte; 32]).into_string();
|
||||
}
|
||||
|
||||
fn mint_state(account_key: &str) -> kb_decoder_spl_token_2022::state::Token2022State {
|
||||
return kb_decoder_spl_token_2022::state::Token2022State {
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
||||
base_fields: serde_json::json!({"supply":"1","decimals":0,"initialized":true}),
|
||||
base_hex: "00".repeat(82),
|
||||
account_type: std::option::Option::Some(1),
|
||||
extensions: std::vec![
|
||||
kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
||||
extension_type: 19,
|
||||
extension_name: "token_metadata",
|
||||
value_hex: "01".to_string(),
|
||||
value_fields: serde_json::json!({
|
||||
"mint": account_key,
|
||||
"name": "Token",
|
||||
"symbol": "TOK",
|
||||
"uri": "https://example.invalid/token.json"
|
||||
}),
|
||||
},
|
||||
kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
||||
extension_type: 20,
|
||||
extension_name: "token_group",
|
||||
value_hex: "02".to_string(),
|
||||
value_fields: serde_json::json!({
|
||||
"mint": account_key,
|
||||
"size": "1",
|
||||
"maxSize": "10"
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mint_snapshot_routes_account_and_metadata_outputs_once() {
|
||||
let account_key = account_key(7);
|
||||
let state = mint_state(account_key.as_str());
|
||||
let bundle =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_key.as_str(),
|
||||
42,
|
||||
&state,
|
||||
);
|
||||
assert_eq!(
|
||||
bundle.as_ref().map(|value| return value.outputs.len()),
|
||||
std::result::Result::Ok(3)
|
||||
);
|
||||
assert_eq!(
|
||||
bundle.as_ref().map(|value| return value.state_kind.clone()),
|
||||
std::result::Result::Ok("mint".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
bundle.as_ref().map(|value| return value.extension_names.clone()),
|
||||
std::result::Result::Ok(std::vec![
|
||||
"token_metadata".to_string(),
|
||||
"token_group".to_string()
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_metadata_mint_must_match_the_account_identity() {
|
||||
let account_address = account_key(8);
|
||||
let mut state = mint_state(account_address.as_str());
|
||||
state.extensions[0].value_fields["mint"] = serde_json::json!(account_key(9));
|
||||
let result =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_address.as_str(),
|
||||
42,
|
||||
&state,
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owner_and_account_identity_fail_closed_before_projection() {
|
||||
let account_key = account_key(10);
|
||||
let data = [0u8; 82];
|
||||
let wrong_owner =
|
||||
crate::solana_token_2022_stateful::materialize_token_2022_stateful_snapshot(
|
||||
account_key.as_str(),
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
1,
|
||||
kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
||||
&data,
|
||||
);
|
||||
assert!(wrong_owner.is_err());
|
||||
let state = mint_state(account_key.as_str());
|
||||
let malformed_key =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
"not-base58-0",
|
||||
1,
|
||||
&state,
|
||||
);
|
||||
assert!(malformed_key.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_snapshot_has_no_metadata_projection() {
|
||||
let account_key = account_key(11);
|
||||
let state = kb_decoder_spl_token_2022::state::Token2022State {
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Account,
|
||||
base_fields: serde_json::json!({"amount":"0","state":"initialized"}),
|
||||
base_hex: "00".repeat(165),
|
||||
account_type: std::option::Option::None,
|
||||
extensions: std::vec::Vec::new(),
|
||||
};
|
||||
let bundle =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_key.as_str(),
|
||||
9,
|
||||
&state,
|
||||
);
|
||||
assert_eq!(
|
||||
bundle.as_ref().map(|value| return value.outputs.len()),
|
||||
std::result::Result::Ok(1)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn group_member_requires_and_matches_external_group_identity() {
|
||||
let account_address = account_key(12);
|
||||
let group_address = account_key(13);
|
||||
let state = kb_decoder_spl_token_2022::state::Token2022State {
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
||||
base_fields: serde_json::json!({"supply":"1","decimals":0,"initialized":true}),
|
||||
base_hex: "00".repeat(82),
|
||||
account_type: std::option::Option::Some(1),
|
||||
extensions: std::vec![kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
||||
extension_type: 23,
|
||||
extension_name: "token_group_member",
|
||||
value_hex: "03".to_string(),
|
||||
value_fields: serde_json::json!({
|
||||
"mint": account_address,
|
||||
"group": group_address,
|
||||
"memberNumber": "1"
|
||||
}),
|
||||
}],
|
||||
};
|
||||
let missing =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_address.as_str(),
|
||||
44,
|
||||
&state,
|
||||
);
|
||||
assert!(missing.is_err());
|
||||
let wrong_context = crate::solana_token_2022_stateful::Token2022StatefulContext {
|
||||
expected_group_address: std::option::Option::Some(account_key(14)),
|
||||
};
|
||||
let wrong = crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot_with_context(
|
||||
account_address.as_str(),
|
||||
44,
|
||||
&state,
|
||||
&wrong_context,
|
||||
);
|
||||
assert!(wrong.is_err());
|
||||
let context = crate::solana_token_2022_stateful::Token2022StatefulContext {
|
||||
expected_group_address: std::option::Option::Some(group_address),
|
||||
};
|
||||
let valid = crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot_with_context(
|
||||
account_address.as_str(),
|
||||
44,
|
||||
&state,
|
||||
&context,
|
||||
);
|
||||
assert_eq!(
|
||||
valid.as_ref().map(|bundle| return bundle.outputs.len()),
|
||||
std::result::Result::Ok(2)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn mint_and_account_fee_extensions_route_to_the_fee_owner_once() {
|
||||
let mint_address = account_key(15);
|
||||
let mint = kb_decoder_spl_token_2022::state::Token2022State {
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
||||
base_fields: serde_json::json!({"supply":"1","decimals":0,"initialized":true}),
|
||||
base_hex: "00".repeat(82),
|
||||
account_type: std::option::Option::Some(1),
|
||||
extensions: std::vec![kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
||||
extension_type: 1,
|
||||
extension_name: "transfer_fee_config",
|
||||
value_hex: "01".to_string(),
|
||||
value_fields: serde_json::json!({"withheldAmount":"7"}),
|
||||
}],
|
||||
};
|
||||
let mint_bundle =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
mint_address.as_str(),
|
||||
50,
|
||||
&mint,
|
||||
);
|
||||
assert_eq!(
|
||||
mint_bundle.as_ref().map(|bundle| return bundle.outputs.len()),
|
||||
std::result::Result::Ok(2)
|
||||
);
|
||||
assert_eq!(
|
||||
mint_bundle
|
||||
.as_ref()
|
||||
.map(|bundle| return bundle.outputs[1].payload_json["provenance"]["processorName"]
|
||||
.clone()),
|
||||
std::result::Result::Ok(serde_json::json!("fees"))
|
||||
);
|
||||
let account_address = account_key(16);
|
||||
let account = kb_decoder_spl_token_2022::state::Token2022State {
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Account,
|
||||
base_fields: serde_json::json!({"amount":"0","state":"initialized"}),
|
||||
base_hex: "00".repeat(165),
|
||||
account_type: std::option::Option::Some(2),
|
||||
extensions: std::vec![kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
||||
extension_type: 17,
|
||||
extension_name: "confidential_transfer_fee_amount",
|
||||
value_hex: "02".to_string(),
|
||||
value_fields: serde_json::json!({"withheldAmount":"ciphertext"}),
|
||||
}],
|
||||
};
|
||||
let account_bundle =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_address.as_str(),
|
||||
51,
|
||||
&account,
|
||||
);
|
||||
assert_eq!(
|
||||
account_bundle.as_ref().map(|bundle| return bundle.outputs.len()),
|
||||
std::result::Result::Ok(2)
|
||||
);
|
||||
assert_eq!(
|
||||
account_bundle
|
||||
.as_ref()
|
||||
.map(|bundle| return bundle.outputs[1].payload_json["confidentialValuesDecrypted"]
|
||||
.clone()),
|
||||
std::result::Result::Ok(serde_json::json!(false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mint_admin_extensions_route_to_the_admin_owner_once() {
|
||||
let account_key = account_key(12);
|
||||
let mut state = mint_state(account_key.as_str());
|
||||
state.extensions.push(kb_decoder_spl_token_2022::state::Token2022TlvEntry {
|
||||
extension_type: 6,
|
||||
extension_name: "default_account_state",
|
||||
value_hex: "02".to_string(),
|
||||
value_fields: serde_json::json!({"state": 2}),
|
||||
});
|
||||
let bundle =
|
||||
crate::solana_token_2022_stateful::materialize_parsed_token_2022_stateful_snapshot(
|
||||
account_key.as_str(),
|
||||
17,
|
||||
&state,
|
||||
);
|
||||
assert_eq!(
|
||||
bundle.as_ref().map(|value| return value.outputs.len()),
|
||||
std::result::Result::Ok(4)
|
||||
);
|
||||
assert_eq!(
|
||||
bundle.as_ref().map(|value| {
|
||||
return value
|
||||
.outputs
|
||||
.iter()
|
||||
.filter(|output| {
|
||||
return output.payload_json["domain"]
|
||||
== serde_json::json!("token_2022_extension_admin_state");
|
||||
})
|
||||
.count();
|
||||
}),
|
||||
std::result::Result::Ok(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_rpc_account_routes_only_after_owner_size_and_context_validation() {
|
||||
let account = crate::solana_token_2022_stateful::tests::account_key(21);
|
||||
let request = crate::solana_token_2022_stateful::Token2022StatefulReadRequest {
|
||||
query_role: "execution".to_string(),
|
||||
account: kb_model::Pubkey(account.clone()),
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
||||
min_context_slot: std::option::Option::Some(40),
|
||||
max_data_bytes: 82,
|
||||
context: crate::solana_token_2022_stateful::Token2022StatefulContext::default(),
|
||||
};
|
||||
let result = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 41,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(kb_rpc::AccountInfoValue {
|
||||
lamports: 1,
|
||||
owner: kb_model::ProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string()),
|
||||
executable: false,
|
||||
rent_epoch: 0,
|
||||
space: 82,
|
||||
data: std::vec![0; 82],
|
||||
}),
|
||||
};
|
||||
let materialized =
|
||||
crate::solana_token_2022_stateful::materialize_token_2022_account_info_result(
|
||||
&request, &result,
|
||||
);
|
||||
assert_eq!(
|
||||
materialized.as_ref().map(|value| return value.context_slot),
|
||||
std::result::Result::Ok(41)
|
||||
);
|
||||
assert_eq!(
|
||||
materialized.as_ref().map(|value| return value.snapshot.outputs.len()),
|
||||
std::result::Result::Ok(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_foreign_executable_and_stale_rpc_accounts_fail_closed() {
|
||||
let account = crate::solana_token_2022_stateful::tests::account_key(22);
|
||||
let request = crate::solana_token_2022_stateful::Token2022StatefulReadRequest {
|
||||
query_role: "execution".to_string(),
|
||||
account: kb_model::Pubkey(account),
|
||||
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
||||
min_context_slot: std::option::Option::Some(50),
|
||||
max_data_bytes: 82,
|
||||
context: crate::solana_token_2022_stateful::Token2022StatefulContext::default(),
|
||||
};
|
||||
let base = kb_rpc::AccountInfoValue {
|
||||
lamports: 1,
|
||||
owner: kb_model::ProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string()),
|
||||
executable: false,
|
||||
rent_epoch: 0,
|
||||
space: 82,
|
||||
data: std::vec![0; 82],
|
||||
};
|
||||
let stale = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 49,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(base.clone()),
|
||||
};
|
||||
assert!(
|
||||
crate::solana_token_2022_stateful::materialize_token_2022_account_info_result(
|
||||
&request, &stale
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let mut foreign = base.clone();
|
||||
foreign.owner = kb_model::ProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string());
|
||||
let foreign = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 50,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(foreign),
|
||||
};
|
||||
assert!(
|
||||
crate::solana_token_2022_stateful::materialize_token_2022_account_info_result(
|
||||
&request, &foreign
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let mut executable = base.clone();
|
||||
executable.executable = true;
|
||||
let executable = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 50,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(executable),
|
||||
};
|
||||
assert!(
|
||||
crate::solana_token_2022_stateful::materialize_token_2022_account_info_result(
|
||||
&request,
|
||||
&executable
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let mut incomplete = base;
|
||||
incomplete.data.pop();
|
||||
let incomplete = kb_rpc::AccountInfoResult {
|
||||
context: kb_rpc::RpcResponseContext {
|
||||
slot: 50,
|
||||
api_version: std::option::Option::None,
|
||||
},
|
||||
account: std::option::Option::Some(incomplete),
|
||||
};
|
||||
assert!(
|
||||
crate::solana_token_2022_stateful::materialize_token_2022_account_info_result(
|
||||
&request,
|
||||
&incomplete
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user