804 lines
32 KiB
Rust
804 lines
32 KiB
Rust
// 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()
|
|
);
|
|
}
|
|
}
|