|
|
|
|
@@ -0,0 +1,441 @@
|
|
|
|
|
// file: kb_materializer_fees/src/materializer.rs
|
|
|
|
|
// version: 9
|
|
|
|
|
|
|
|
|
|
//! Stable fee projections derived from exact committed Token-2022 observations.
|
|
|
|
|
|
|
|
|
|
const ACCEPTED_FAMILIES: &[kb_model::EventFamily] = &[kb_model::EventFamily::Fee];
|
|
|
|
|
const SPL_TOKEN_2022_SURFACE: &str = "spl_token_2022";
|
|
|
|
|
const TOKEN_2022_FEE_ENTRIES: &[&str] = &[
|
|
|
|
|
"initialize_transfer_fee_config",
|
|
|
|
|
"set_transfer_fee",
|
|
|
|
|
"withdraw_withheld_tokens_from_mint",
|
|
|
|
|
"withdraw_withheld_tokens_from_accounts",
|
|
|
|
|
"harvest_withheld_tokens_to_mint",
|
|
|
|
|
"transfer_checked_with_fee",
|
|
|
|
|
"initialize_confidential_transfer_fee_config",
|
|
|
|
|
"withdraw_confidential_withheld_tokens_from_mint",
|
|
|
|
|
"withdraw_confidential_withheld_tokens_from_accounts",
|
|
|
|
|
"harvest_confidential_withheld_tokens_to_mint",
|
|
|
|
|
"enable_confidential_harvest_to_mint",
|
|
|
|
|
"disable_confidential_harvest_to_mint",
|
|
|
|
|
"transfer_confidential_tokens_with_fee",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/// Fee materializer for committed Token-2022 fee instructions.
|
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
|
|
|
pub struct FeesMaterializer;
|
|
|
|
|
|
|
|
|
|
impl kb_materializer_api::Materializer for crate::FeesMaterializer {
|
|
|
|
|
fn materializer_name(&self) -> &'static str {
|
|
|
|
|
return "kb_materializer_fees";
|
|
|
|
|
}
|
|
|
|
|
fn materializer_version(&self) -> &'static str {
|
|
|
|
|
return env!("CARGO_PKG_VERSION");
|
|
|
|
|
}
|
|
|
|
|
fn accepts_event(&self, event: &kb_model::DecodedProtocolEvent) -> bool {
|
|
|
|
|
return accepts_event(event);
|
|
|
|
|
}
|
|
|
|
|
fn materialize_event(
|
|
|
|
|
&self,
|
|
|
|
|
_event: &kb_model::DecodedProtocolEvent,
|
|
|
|
|
) -> kb_core::Result<std::vec::Vec<kb_model::MaterializedEvent>> {
|
|
|
|
|
return std::result::Result::Ok(std::vec::Vec::new());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl kb_materializer_api::EventMaterializer for crate::FeesMaterializer {
|
|
|
|
|
fn identity(&self) -> kb_materializer_api::MaterializerIdentity {
|
|
|
|
|
return kb_materializer_api::MaterializerIdentity {
|
|
|
|
|
name: "fees".to_string(),
|
|
|
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
fn accepted_families(&self) -> &'static [kb_model::EventFamily] {
|
|
|
|
|
return ACCEPTED_FAMILIES;
|
|
|
|
|
}
|
|
|
|
|
fn accepts_observation(&self, observation: &kb_decoder_api::DecodedObservation) -> bool {
|
|
|
|
|
return accepts_event(&observation.event);
|
|
|
|
|
}
|
|
|
|
|
fn transaction_policy(
|
|
|
|
|
&self,
|
|
|
|
|
_family: kb_model::EventFamily,
|
|
|
|
|
) -> kb_materializer_api::MaterializationTransactionPolicy {
|
|
|
|
|
return kb_materializer_api::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
|
|
|
|
}
|
|
|
|
|
fn materialize(
|
|
|
|
|
&self,
|
|
|
|
|
observation: &kb_decoder_api::DecodedObservation,
|
|
|
|
|
) -> kb_materializer_api::MaterializerExecutionResult {
|
|
|
|
|
if !accepts_event(&observation.event) {
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult::ignored();
|
|
|
|
|
}
|
|
|
|
|
if observation.transaction_failed || !observation.observation_committed {
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult::refused(
|
|
|
|
|
"failed_transaction_fee_refused",
|
|
|
|
|
"failed or uncommitted Token-2022 observations cannot create fee outputs",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let operation = observation.event.event_name.0.clone();
|
|
|
|
|
let accounts = observation
|
|
|
|
|
.payload_json
|
|
|
|
|
.get("accounts")
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or(serde_json::Value::Null);
|
|
|
|
|
let parameters = observation
|
|
|
|
|
.payload_json
|
|
|
|
|
.get("parameters")
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or_else(|| return observation.payload_json.clone());
|
|
|
|
|
let output = kb_materializer_api::MaterializedOutput {
|
|
|
|
|
output_key: format!("token_2022_fee:{operation}:0"),
|
|
|
|
|
family: kb_model::MaterializedEventFamily::Fee,
|
|
|
|
|
payload_json: serde_json::json!({
|
|
|
|
|
"projectionVersion": 1,
|
|
|
|
|
"projectionSemantics": "committed_instruction_fee_event",
|
|
|
|
|
"domain": fee_domain(observation.event.event_name.0.as_str()),
|
|
|
|
|
"operation": operation,
|
|
|
|
|
"programId": observation.event.program_id.0.clone(),
|
|
|
|
|
"signature": observation.event.signature.0.clone(),
|
|
|
|
|
"slot": observation.event.slot.0,
|
|
|
|
|
"instructionPath": observation.event.instruction_path.0.clone(),
|
|
|
|
|
"transactionSucceeded": true,
|
|
|
|
|
"accounts": accounts,
|
|
|
|
|
"parameters": parameters,
|
|
|
|
|
"stateReadRequired": state_read_required(
|
|
|
|
|
observation.event.event_name.0.as_str(),
|
|
|
|
|
),
|
|
|
|
|
"confidentialValuesDecrypted": false,
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult {
|
|
|
|
|
status: kb_materializer_api::MaterializerOutcomeStatus::Inserted,
|
|
|
|
|
outputs: std::vec![output],
|
|
|
|
|
diagnostics: std::vec::Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn accepts_event(event: &kb_model::DecodedProtocolEvent) -> bool {
|
|
|
|
|
return event.event_family == kb_model::EventFamily::Fee
|
|
|
|
|
&& event.surface_code.0 == SPL_TOKEN_2022_SURFACE
|
|
|
|
|
&& event.program_id.0 == kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID
|
|
|
|
|
&& contains(TOKEN_2022_FEE_ENTRIES, event.event_name.0.as_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn fee_domain(operation: &str) -> &'static str {
|
|
|
|
|
if operation.contains("confidential") {
|
|
|
|
|
return "token_2022_confidential_transfer_fee";
|
|
|
|
|
}
|
|
|
|
|
return "token_2022_transfer_fee";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn state_read_required(operation: &str) -> bool {
|
|
|
|
|
return matches!(
|
|
|
|
|
operation,
|
|
|
|
|
"initialize_transfer_fee_config"
|
|
|
|
|
| "set_transfer_fee"
|
|
|
|
|
| "withdraw_withheld_tokens_from_mint"
|
|
|
|
|
| "withdraw_withheld_tokens_from_accounts"
|
|
|
|
|
| "harvest_withheld_tokens_to_mint"
|
|
|
|
|
| "initialize_confidential_transfer_fee_config"
|
|
|
|
|
| "withdraw_confidential_withheld_tokens_from_mint"
|
|
|
|
|
| "withdraw_confidential_withheld_tokens_from_accounts"
|
|
|
|
|
| "harvest_confidential_withheld_tokens_to_mint"
|
|
|
|
|
| "enable_confidential_harvest_to_mint"
|
|
|
|
|
| "disable_confidential_harvest_to_mint"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn contains(entries: &[&str], entry_code: &str) -> bool {
|
|
|
|
|
return entries.iter().any(|entry| return *entry == entry_code);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Materialize authoritative Token-2022 fee extension snapshots without decrypting confidential values.
|
|
|
|
|
pub fn materialize_token_2022_fee_state_snapshots(
|
|
|
|
|
account_key: &str,
|
|
|
|
|
slot: u64,
|
|
|
|
|
state: &kb_decoder_spl_token_2022::state::Token2022State,
|
|
|
|
|
) -> std::result::Result<std::vec::Vec<kb_materializer_api::MaterializedOutput>, String> {
|
|
|
|
|
if account_key.is_empty() {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Token-2022 fee state snapshot requires a non-empty account key".to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
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 => {
|
|
|
|
|
return std::result::Result::Err(
|
|
|
|
|
"Token-2022 multisig state cannot contain fee extensions".to_string(),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let mut outputs = std::vec::Vec::new();
|
|
|
|
|
for entry in &state.extensions {
|
|
|
|
|
let confidential = match entry.extension_name {
|
|
|
|
|
"transfer_fee_config" | "transfer_fee_amount" => false,
|
|
|
|
|
"confidential_transfer_fee_config" | "confidential_transfer_fee_amount" => true,
|
|
|
|
|
_ => continue,
|
|
|
|
|
};
|
|
|
|
|
let expected_kind = match entry.extension_name {
|
|
|
|
|
"transfer_fee_config" | "confidential_transfer_fee_config" => "mint",
|
|
|
|
|
"transfer_fee_amount" | "confidential_transfer_fee_amount" => "account",
|
|
|
|
|
_ => continue,
|
|
|
|
|
};
|
|
|
|
|
if state_kind != expected_kind {
|
|
|
|
|
return std::result::Result::Err(format!(
|
|
|
|
|
"Token-2022 {} extension requires {expected_kind} state, got {state_kind}",
|
|
|
|
|
entry.extension_name
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
outputs.push(kb_materializer_api::MaterializedOutput {
|
|
|
|
|
output_key: format!(
|
|
|
|
|
"token_2022_fee_state:{}:{account_key}:{slot}",
|
|
|
|
|
entry.extension_name
|
|
|
|
|
),
|
|
|
|
|
family: kb_model::MaterializedEventFamily::Fee,
|
|
|
|
|
payload_json: serde_json::json!({
|
|
|
|
|
"projectionVersion": 1,
|
|
|
|
|
"projectionSemantics": "authoritative_bounded_fee_state_snapshot",
|
|
|
|
|
"domain": if confidential {
|
|
|
|
|
"token_2022_confidential_transfer_fee_state"
|
|
|
|
|
} else {
|
|
|
|
|
"token_2022_transfer_fee_state"
|
|
|
|
|
},
|
|
|
|
|
"idempotenceKey": format!(
|
|
|
|
|
"token_2022_fee_state:{}:{account_key}:{slot}",
|
|
|
|
|
entry.extension_name
|
|
|
|
|
),
|
|
|
|
|
"accountKey": account_key,
|
|
|
|
|
"slot": slot,
|
|
|
|
|
"programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
|
|
|
|
|
"stateKind": state_kind,
|
|
|
|
|
"extensionType": entry.extension_type,
|
|
|
|
|
"extensionName": entry.extension_name,
|
|
|
|
|
"valueHex": entry.value_hex,
|
|
|
|
|
"valueFields": entry.value_fields,
|
|
|
|
|
"containsConfidentialState": confidential,
|
|
|
|
|
"confidentialValuesDecrypted": false,
|
|
|
|
|
"finalAccountStateCaptured": true,
|
|
|
|
|
"provenance": {
|
|
|
|
|
"processorName": "fees",
|
|
|
|
|
"processorVersion": env!("CARGO_PKG_VERSION"),
|
|
|
|
|
"source": "bounded_token_2022_state_parser"
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return std::result::Result::Ok(outputs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
fn observation(
|
|
|
|
|
surface_code: &str,
|
|
|
|
|
program_id: &str,
|
|
|
|
|
entry_code: &str,
|
|
|
|
|
transaction_failed: bool,
|
|
|
|
|
) -> kb_decoder_api::DecodedObservation {
|
|
|
|
|
return kb_decoder_api::DecodedObservation {
|
|
|
|
|
event_key: format!("{entry_code}:0"),
|
|
|
|
|
event: kb_model::DecodedProtocolEvent {
|
|
|
|
|
signature: kb_model::Signature("signature".to_string()),
|
|
|
|
|
slot: kb_model::Slot(42),
|
|
|
|
|
instruction_path: kb_model::InstructionPath("0".to_string()),
|
|
|
|
|
program_id: kb_model::ProgramId(program_id.to_string()),
|
|
|
|
|
protocol_code: kb_model::ProtocolCode("spl_token_2022".to_string()),
|
|
|
|
|
surface_code: kb_model::SurfaceCode(surface_code.to_string()),
|
|
|
|
|
event_code: kb_model::EventCode(format!("{surface_code}.{entry_code}")),
|
|
|
|
|
event_name: kb_model::EventName(entry_code.to_string()),
|
|
|
|
|
event_family: kb_model::EventFamily::Fee,
|
|
|
|
|
source_kind: kb_model::EventSourceKind::Instruction,
|
|
|
|
|
confidence: kb_model::DecoderConfidence::ManualExact,
|
|
|
|
|
},
|
|
|
|
|
payload_json: serde_json::json!({
|
|
|
|
|
"accounts": [{"position": 0, "role": "mint", "accountKey": "mint111"}],
|
|
|
|
|
"parameters": {"extension": "transfer_fee", "basisPoints": 25},
|
|
|
|
|
}),
|
|
|
|
|
transaction_failed,
|
|
|
|
|
transaction_error: if transaction_failed {
|
|
|
|
|
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
|
|
|
|
|
} else {
|
|
|
|
|
std::option::Option::None
|
|
|
|
|
},
|
|
|
|
|
observation_committed: !transaction_failed,
|
|
|
|
|
proof: kb_decoder_api::DecoderProof {
|
|
|
|
|
kind: kb_decoder_api::DecoderProofKind::Manual,
|
|
|
|
|
confidence: kb_model::DecoderConfidence::ManualExact,
|
|
|
|
|
evidence: std::vec!["fixture".to_string()],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn committed_token_2022_fee_instruction_creates_one_stable_output() {
|
|
|
|
|
let observation = observation(
|
|
|
|
|
super::SPL_TOKEN_2022_SURFACE,
|
|
|
|
|
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
|
|
|
|
|
"set_transfer_fee",
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::FeesMaterializer,
|
|
|
|
|
&observation,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
|
|
|
|
|
assert_eq!(result.outputs.len(), 1);
|
|
|
|
|
assert_eq!(result.outputs[0].family, kb_model::MaterializedEventFamily::Fee);
|
|
|
|
|
assert_eq!(result.outputs[0].output_key, "token_2022_fee:set_transfer_fee:0");
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["stateReadRequired"], true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn failed_token_2022_fee_instruction_is_refused() {
|
|
|
|
|
let observation = observation(
|
|
|
|
|
super::SPL_TOKEN_2022_SURFACE,
|
|
|
|
|
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
|
|
|
|
|
"harvest_withheld_tokens_to_mint",
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::FeesMaterializer,
|
|
|
|
|
&observation,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Refused);
|
|
|
|
|
assert!(result.outputs.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn classic_token_and_program_surface_mismatches_are_ignored() {
|
|
|
|
|
let classic = observation(
|
|
|
|
|
"spl_token",
|
|
|
|
|
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
|
|
|
|
"set_transfer_fee",
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
let wrong_program = observation(
|
|
|
|
|
super::SPL_TOKEN_2022_SURFACE,
|
|
|
|
|
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
|
|
|
|
"set_transfer_fee",
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
assert!(!kb_materializer_api::EventMaterializer::accepts_observation(
|
|
|
|
|
&crate::FeesMaterializer,
|
|
|
|
|
&classic,
|
|
|
|
|
));
|
|
|
|
|
assert!(!kb_materializer_api::EventMaterializer::accepts_observation(
|
|
|
|
|
&crate::FeesMaterializer,
|
|
|
|
|
&wrong_program,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn transfer_checked_with_fee_is_self_contained_but_state_mutations_are_not() {
|
|
|
|
|
assert!(!super::state_read_required("transfer_checked_with_fee"));
|
|
|
|
|
assert!(super::state_read_required("withdraw_withheld_tokens_from_accounts"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn committed_confidential_fee_instruction_is_projected_without_decryption() {
|
|
|
|
|
let observation = observation(
|
|
|
|
|
super::SPL_TOKEN_2022_SURFACE,
|
|
|
|
|
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
|
|
|
|
|
"initialize_confidential_transfer_fee_config",
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::FeesMaterializer,
|
|
|
|
|
&observation,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
|
|
|
|
|
assert_eq!(result.outputs.len(), 1);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
result.outputs[0].payload_json["domain"],
|
|
|
|
|
"token_2022_confidential_transfer_fee"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["stateReadRequired"], true);
|
|
|
|
|
assert_eq!(result.outputs[0].payload_json["confidentialValuesDecrypted"], false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn confidential_transfer_with_fee_is_instructionally_self_contained() {
|
|
|
|
|
assert!(!super::state_read_required("transfer_confidential_tokens_with_fee",));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
super::fee_domain("transfer_confidential_tokens_with_fee"),
|
|
|
|
|
"token_2022_confidential_transfer_fee"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn public_fee_state_snapshots_are_owned_once_and_idempotent() {
|
|
|
|
|
let mint = kb_decoder_spl_token_2022::state::Token2022State {
|
|
|
|
|
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
|
|
|
|
|
base_fields: serde_json::json!({}),
|
|
|
|
|
base_hex: "00".to_string(),
|
|
|
|
|
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 first = crate::materialize_token_2022_fee_state_snapshots("mint111", 42, &mint);
|
|
|
|
|
let second = crate::materialize_token_2022_fee_state_snapshots("mint111", 42, &mint);
|
|
|
|
|
assert!(first.is_ok());
|
|
|
|
|
assert_eq!(first, second);
|
|
|
|
|
let outputs = match first {
|
|
|
|
|
std::result::Result::Ok(outputs) => outputs,
|
|
|
|
|
std::result::Result::Err(error) => panic!("unexpected error: {error}"),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(outputs.len(), 1);
|
|
|
|
|
assert_eq!(outputs[0].payload_json["stateKind"], "mint");
|
|
|
|
|
assert_eq!(outputs[0].payload_json["valueFields"]["withheldAmount"], "7");
|
|
|
|
|
assert_eq!(outputs[0].payload_json["confidentialValuesDecrypted"], false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn confidential_fee_amount_preserves_ciphertext_without_decryption_claim() {
|
|
|
|
|
let account = kb_decoder_spl_token_2022::state::Token2022State {
|
|
|
|
|
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Account,
|
|
|
|
|
base_fields: serde_json::json!({}),
|
|
|
|
|
base_hex: "00".to_string(),
|
|
|
|
|
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 result = crate::materialize_token_2022_fee_state_snapshots("account111", 43, &account);
|
|
|
|
|
assert!(result.is_ok());
|
|
|
|
|
let outputs = match result {
|
|
|
|
|
std::result::Result::Ok(outputs) => outputs,
|
|
|
|
|
std::result::Result::Err(error) => panic!("unexpected error: {error}"),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(outputs[0].payload_json["containsConfidentialState"], true);
|
|
|
|
|
assert_eq!(outputs[0].payload_json["confidentialValuesDecrypted"], false);
|
|
|
|
|
assert_eq!(outputs[0].payload_json["valueFields"]["withheldAmount"], "ciphertext");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn fee_state_snapshot_rejects_wrong_state_kind_and_empty_identity() {
|
|
|
|
|
let wrong = kb_decoder_spl_token_2022::state::Token2022State {
|
|
|
|
|
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Account,
|
|
|
|
|
base_fields: serde_json::json!({}),
|
|
|
|
|
base_hex: "00".to_string(),
|
|
|
|
|
account_type: std::option::Option::Some(2),
|
|
|
|
|
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!({}),
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
assert!(
|
|
|
|
|
crate::materialize_token_2022_fee_state_snapshots("account111", 44, &wrong,).is_err()
|
|
|
|
|
);
|
|
|
|
|
assert!(crate::materialize_token_2022_fee_state_snapshots("", 44, &wrong,).is_err());
|
|
|
|
|
}
|
|
|
|
|
}
|