use r402_core::error::VerificationError;
use solana_pubkey::Pubkey;
use solana_transaction::versioned::VersionedTransaction;
use super::config::SolanaExactFacilitatorConfig;
use super::verify::{TransferRequirement, transfer_amount_meets_requirement};
use crate::exact::ATA_PROGRAM_PUBKEY;
use crate::exact::error::SolanaExactError;
pub(super) const IX_TOKEN_TRANSFER_CHECKED: u8 = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ObservedTransfer {
pub program_id: Pubkey,
pub amount: u64,
pub mint: Pubkey,
pub destination: Pubkey,
pub authority: Pubkey,
}
#[must_use]
pub fn is_path1_layout_recoverable(err: &VerificationError) -> bool {
match err {
VerificationError::InvalidFormat(_) => true,
VerificationError::SimulationFailed(msg) => is_recoverable_layout_message(msg),
_ => false,
}
}
fn is_recoverable_layout_message(msg: &str) -> bool {
const NEEDLES: &[&str] = &[
"Too few instructions",
"Additional instructions not allowed",
"Instruction count exceeds maximum",
"Program not in allowed list",
"Blocked program",
"Invalid compute limit instruction",
"Invalid compute price instruction",
"Invalid token instruction",
"Empty instruction",
"Instruction at index",
"No account at index",
"CreateATA instruction not supported",
"Fee payer included in instruction accounts",
];
NEEDLES.iter().any(|n| msg.contains(n))
}
#[must_use]
pub fn parse_transfer_checked(
program_id: Pubkey,
data: &[u8],
accounts: &[Pubkey],
) -> Option<ObservedTransfer> {
if data.first().copied()? != IX_TOKEN_TRANSFER_CHECKED {
return None;
}
if data.len() < 1 + 8 + 1 {
return None;
}
let amount_bytes: [u8; 8] = data.get(1..9)?.try_into().ok()?;
let amount = u64::from_le_bytes(amount_bytes);
if accounts.len() < 4 {
return None;
}
Some(ObservedTransfer {
program_id,
amount,
mint: *accounts.get(1)?,
destination: *accounts.get(2)?,
authority: *accounts.get(3)?,
})
}
#[must_use]
pub fn extract_top_level_transfers(tx: &VersionedTransaction) -> Vec<ObservedTransfer> {
let keys = tx.message.static_account_keys();
let mut out = Vec::new();
for ix in tx.message.instructions() {
let Some(program_id) = keys.get(usize::from(ix.program_id_index)).copied() else {
continue;
};
let is_token = program_id == spl_token::ID || program_id == spl_token_2022_interface::ID;
if !is_token {
continue;
}
let mut accounts = Vec::with_capacity(ix.accounts.len());
for idx in &ix.accounts {
if let Some(k) = keys.get(usize::from(*idx)).copied() {
accounts.push(k);
}
}
if let Some(t) = parse_transfer_checked(program_id, &ix.data, &accounts) {
out.push(t);
}
}
out
}
pub fn match_required_transfer(
transfers: &[ObservedTransfer],
requirement: &TransferRequirement<'_>,
fee_payer_signers: &[Pubkey],
) -> Result<ObservedTransfer, VerificationError> {
for t in transfers {
if fee_payer_signers.contains(&t.authority) {
return Err(SolanaExactError::FeePayerTransferringFunds.into());
}
}
let expected_atas = expected_destination_atas(requirement.pay_to, requirement.asset);
if expected_atas.is_empty() {
return Err(VerificationError::InvalidFormat(
"smart_wallet_cannot_derive_destination_ata".into(),
));
}
let required_mint = *requirement.asset.pubkey();
let matching: Vec<_> = transfers
.iter()
.copied()
.filter(|t| {
t.mint == required_mint
&& expected_atas.contains(&t.destination)
&& transfer_amount_meets_requirement(t.amount, requirement.amount)
})
.collect();
match matching.as_slice() {
[] if transfers.is_empty() => Err(VerificationError::InvalidFormat(
"smart_wallet_no_transfer_in_simulation".into(),
)),
[] => Err(VerificationError::InvalidFormat(
"smart_wallet_transfer_mismatch".into(),
)),
[one] => Ok(*one),
[first, ..] => Err(VerificationError::InvalidFormat(format!(
"smart_wallet_multiple_matching_transfers (payer {})",
first.authority
))),
}
}
fn expected_destination_atas(
pay_to: &crate::chain::Address,
asset: &crate::chain::Address,
) -> Vec<Pubkey> {
let owner = *pay_to.pubkey();
let mint = *asset.pubkey();
[spl_token::ID, spl_token_2022_interface::ID]
.into_iter()
.map(|token_program| {
let (ata, _) = Pubkey::find_program_address(
&[owner.as_ref(), token_program.as_ref(), mint.as_ref()],
&ATA_PROGRAM_PUBKEY,
);
ata
})
.collect()
}
#[must_use]
pub const fn smart_wallet_enabled(config: &SolanaExactFacilitatorConfig) -> bool {
config.enable_smart_wallet_verification
}
#[cfg(test)]
#[allow(
clippy::indexing_slicing,
reason = "unit tests use fixed-length fixture vectors"
)]
mod tests {
use solana_pubkey::Pubkey;
use super::*;
use crate::chain::Address;
fn req(amount: u64) -> (Address, Address, TransferRequirement<'static>) {
let pay_to = Address::new(Pubkey::new_from_array([1u8; 32]));
let asset = Address::new(Pubkey::new_from_array([2u8; 32]));
let pay_to_static: &'static Address = Box::leak(Box::new(pay_to));
let asset_static: &'static Address = Box::leak(Box::new(asset));
let requirement = TransferRequirement {
asset: asset_static,
pay_to: pay_to_static,
amount,
};
(*pay_to_static, *asset_static, requirement)
}
#[test]
fn parse_transfer_checked_roundtrip_fields() {
let program = spl_token::ID;
let amount = 1_000_000u64;
let mut data = vec![IX_TOKEN_TRANSFER_CHECKED];
data.extend_from_slice(&amount.to_le_bytes());
data.push(6); let accounts = vec![
Pubkey::new_from_array([10; 32]),
Pubkey::new_from_array([11; 32]),
Pubkey::new_from_array([12; 32]),
Pubkey::new_from_array([13; 32]),
];
let t = parse_transfer_checked(program, &data, &accounts).unwrap();
assert_eq!(t.amount, amount);
assert_eq!(t.mint, accounts[1]);
assert_eq!(t.destination, accounts[2]);
assert_eq!(t.authority, accounts[3]);
}
#[test]
fn match_accepts_overpayment() {
let (_pay_to, asset, requirement) = req(100);
let atas = expected_destination_atas(requirement.pay_to, requirement.asset);
let dest = *atas.first().expect("ATA derived");
let t = ObservedTransfer {
program_id: spl_token::ID,
amount: 150,
mint: *asset.pubkey(),
destination: dest,
authority: Pubkey::new_from_array([9; 32]),
};
let matched = match_required_transfer(&[t], &requirement, &[]).unwrap();
assert_eq!(matched.amount, 150);
}
#[test]
fn match_rejects_underpayment() {
let (_pay_to, asset, requirement) = req(100);
let atas = expected_destination_atas(requirement.pay_to, requirement.asset);
let dest = *atas.first().expect("ATA derived");
let t = ObservedTransfer {
program_id: spl_token::ID,
amount: 50,
mint: *asset.pubkey(),
destination: dest,
authority: Pubkey::new_from_array([9; 32]),
};
let err = match_required_transfer(&[t], &requirement, &[]).unwrap_err();
assert!(matches!(
err,
VerificationError::InvalidFormat(ref m) if m.contains("smart_wallet_transfer_mismatch")
));
}
#[test]
fn layout_recoverable_classification() {
assert!(!is_path1_layout_recoverable(
&VerificationError::InvalidPaymentAmount
));
assert!(is_path1_layout_recoverable(
&VerificationError::SimulationFailed("Program not in allowed list: Abc".into())
));
assert!(!is_path1_layout_recoverable(
&VerificationError::SimulationFailed("some other sim failure".into())
));
assert!(is_path1_layout_recoverable(
&VerificationError::InvalidFormat("Too few instructions".into())
));
}
}