use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Signature;
use solana_sdk::transaction::VersionedTransaction;
use super::pda::{
ASSOCIATED_TOKEN_PROGRAM_ID, COMPUTE_BUDGET_PROGRAM_ID, DevnetPdas, LEGACY_TOKEN_PROGRAM_ID,
SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, parse_pubkey,
};
use super::purchase::PURCHASE_SUBSCRIPTION_TOKEN_USDT_DISCRIMINATOR;
use crate::config::TxlineConfig;
use crate::http::models::PurchaseQuoteResponse;
use crate::{Result, TxlineError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PurchaseTransactionSafetyConfig {
pub txline_program_id: Pubkey,
pub expected_buyer: Pubkey,
pub expected_txline_amount: u64,
pub expected_backend_signer: Option<Pubkey>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PurchaseTransactionSafetyReport {
pub fee_payer: Pubkey,
pub invoked_programs: Vec<Pubkey>,
pub txline_purchase_instruction_count: usize,
pub backend_signer_present: bool,
}
impl PurchaseTransactionSafetyConfig {
pub fn devnet(
config: &TxlineConfig,
expected_buyer: Pubkey,
expected_txline_amount: u64,
expected_backend_signer: Option<Pubkey>,
) -> Result<Self> {
Ok(Self {
txline_program_id: parse_pubkey(&config.program_id)?,
expected_buyer,
expected_txline_amount,
expected_backend_signer,
})
}
}
impl PurchaseQuoteResponse {
pub fn validate_transaction_safety(
&self,
config: &PurchaseTransactionSafetyConfig,
) -> Result<PurchaseTransactionSafetyReport> {
self.validate_financial_shape()?;
let bytes = self.transaction_bytes()?;
verify_purchase_transaction_bytes(&bytes, config)
}
}
pub fn verify_purchase_transaction_base64(
transaction_base64: &str,
config: &PurchaseTransactionSafetyConfig,
) -> Result<PurchaseTransactionSafetyReport> {
let bytes = STANDARD.decode(transaction_base64)?;
verify_purchase_transaction_bytes(&bytes, config)
}
pub fn verify_purchase_transaction_bytes(
transaction_bytes: &[u8],
config: &PurchaseTransactionSafetyConfig,
) -> Result<PurchaseTransactionSafetyReport> {
let transaction = decode_versioned_transaction(transaction_bytes)?;
verify_purchase_transaction(&transaction, config)
}
pub fn verify_purchase_transaction(
transaction: &VersionedTransaction,
config: &PurchaseTransactionSafetyConfig,
) -> Result<PurchaseTransactionSafetyReport> {
transaction
.sanitize()
.map_err(|err| TxlineError::solana(format!("invalid purchase transaction: {err}")))?;
if transaction
.message
.address_table_lookups()
.is_some_and(|lookups| !lookups.is_empty())
{
return Err(TxlineError::solana(
"purchase quote uses address table lookups; SDK cannot audit dynamically loaded accounts safely",
));
}
let account_keys = transaction.message.static_account_keys();
let fee_payer = *account_keys
.first()
.ok_or_else(|| TxlineError::solana("purchase transaction has no fee payer"))?;
if fee_payer != config.expected_buyer {
return Err(TxlineError::solana(
"purchase transaction fee payer is not the expected buyer",
));
}
let backend_signer_present = match config.expected_backend_signer {
Some(backend) => signer_signature_present(transaction, &backend)?,
None => true,
};
if !backend_signer_present {
return Err(TxlineError::solana(
"purchase transaction is missing the expected backend signer signature",
));
}
let allowed_programs = allowed_purchase_program_pubkeys(config.txline_program_id)?;
let mut invoked_programs = Vec::new();
let mut purchase_instruction_count = 0usize;
for instruction in transaction.message.instructions() {
let program_id = account_keys
.get(usize::from(instruction.program_id_index))
.copied()
.ok_or_else(|| TxlineError::solana("purchase instruction program index is invalid"))?;
if !allowed_programs.contains(&program_id) {
return Err(TxlineError::solana(format!(
"purchase transaction invokes unauthorized program {program_id}"
)));
}
if !invoked_programs.contains(&program_id) {
invoked_programs.push(program_id);
}
reject_unexpected_buyer_signer(
transaction,
program_id,
config.txline_program_id,
instruction.accounts.as_slice(),
)?;
if program_id == config.txline_program_id {
purchase_instruction_count += 1;
verify_purchase_instruction_data(instruction.data.as_slice(), config)?;
verify_purchase_instruction_accounts(
account_keys,
instruction.accounts.as_slice(),
config,
)?;
}
}
if purchase_instruction_count != 1 {
return Err(TxlineError::solana(format!(
"purchase transaction must contain exactly one TxLINE purchase instruction, found {purchase_instruction_count}"
)));
}
Ok(PurchaseTransactionSafetyReport {
fee_payer,
invoked_programs,
txline_purchase_instruction_count: purchase_instruction_count,
backend_signer_present,
})
}
pub fn allowed_purchase_programs(txline_program_id: &str) -> [&str; 6] {
[
txline_program_id,
COMPUTE_BUDGET_PROGRAM_ID,
SYSTEM_PROGRAM_ID,
LEGACY_TOKEN_PROGRAM_ID,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID,
]
}
fn allowed_purchase_program_pubkeys(txline_program_id: Pubkey) -> Result<[Pubkey; 6]> {
Ok([
txline_program_id,
parse_pubkey(COMPUTE_BUDGET_PROGRAM_ID)?,
parse_pubkey(SYSTEM_PROGRAM_ID)?,
parse_pubkey(LEGACY_TOKEN_PROGRAM_ID)?,
parse_pubkey(TOKEN_2022_PROGRAM_ID)?,
parse_pubkey(ASSOCIATED_TOKEN_PROGRAM_ID)?,
])
}
fn decode_versioned_transaction(transaction_bytes: &[u8]) -> Result<VersionedTransaction> {
if transaction_bytes.is_empty() {
return Err(TxlineError::solana(
"purchase quote transaction decoded to an empty byte buffer",
));
}
wincode::deserialize(transaction_bytes)
.map_err(|err| TxlineError::solana(format!("could not decode purchase transaction: {err}")))
}
fn signer_signature_present(transaction: &VersionedTransaction, signer: &Pubkey) -> Result<bool> {
let signer_index = transaction
.message
.static_account_keys()
.iter()
.position(|key| key == signer)
.ok_or_else(|| {
TxlineError::solana("expected backend signer is not present in transaction accounts")
})?;
if !transaction.message.is_signer(signer_index) {
return Err(TxlineError::solana(
"expected backend signer account is not marked as a signer",
));
}
Ok(transaction
.signatures
.get(signer_index)
.is_some_and(|signature| *signature != Signature::default()))
}
fn reject_unexpected_buyer_signer(
transaction: &VersionedTransaction,
program_id: Pubkey,
txline_program_id: Pubkey,
instruction_accounts: &[u8],
) -> Result<()> {
let buyer_index = 0usize;
let buyer_is_instruction_signer = instruction_accounts.iter().any(|index| {
usize::from(*index) == buyer_index && transaction.message.is_signer(buyer_index)
});
if buyer_is_instruction_signer {
let txline_program_allowed = program_id == txline_program_id;
let associated_program_allowed = program_id == parse_pubkey(ASSOCIATED_TOKEN_PROGRAM_ID)?;
if !txline_program_allowed && !associated_program_allowed {
return Err(TxlineError::solana(format!(
"buyer wallet is requested as signer for unauthorized program {program_id}"
)));
}
}
Ok(())
}
fn verify_purchase_instruction_data(
data: &[u8],
config: &PurchaseTransactionSafetyConfig,
) -> Result<()> {
if data.len() != 16 {
return Err(TxlineError::solana(format!(
"purchase instruction data length is {}, expected 16",
data.len()
)));
}
if data[..8] != PURCHASE_SUBSCRIPTION_TOKEN_USDT_DISCRIMINATOR {
return Err(TxlineError::solana(
"TxLINE instruction is not purchase_subscription_token_usdt",
));
}
let mut amount = [0u8; 8];
amount.copy_from_slice(&data[8..16]);
let amount = u64::from_le_bytes(amount);
if amount != config.expected_txline_amount {
return Err(TxlineError::solana(format!(
"purchase txline_amount {amount} does not match expected {}",
config.expected_txline_amount
)));
}
Ok(())
}
fn verify_purchase_instruction_accounts(
account_keys: &[Pubkey],
instruction_accounts: &[u8],
config: &PurchaseTransactionSafetyConfig,
) -> Result<()> {
if instruction_accounts.len() != 14 {
return Err(TxlineError::solana(format!(
"purchase instruction account count is {}, expected 14",
instruction_accounts.len()
)));
}
let pdas = DevnetPdas::new()?;
let expected_backend = config.expected_backend_signer;
let expected_accounts = [
Some(config.expected_buyer),
expected_backend,
Some(pdas.usdt_mint),
Some(pdas.user_usdt_ata(&config.expected_buyer)?.address),
Some(pdas.usdt_treasury_vault_ata()?.address),
Some(pdas.usdt_treasury().address),
Some(pdas.txl_mint),
Some(pdas.token_treasury_vault_ata()?.address),
Some(pdas.token_treasury_v2().address),
Some(pdas.user_txl_ata(&config.expected_buyer)?.address),
Some(parse_pubkey(LEGACY_TOKEN_PROGRAM_ID)?),
Some(parse_pubkey(TOKEN_2022_PROGRAM_ID)?),
Some(parse_pubkey(SYSTEM_PROGRAM_ID)?),
Some(parse_pubkey(ASSOCIATED_TOKEN_PROGRAM_ID)?),
];
for (position, expected) in expected_accounts.iter().enumerate() {
let actual_index = usize::from(instruction_accounts[position]);
let actual = account_keys.get(actual_index).copied().ok_or_else(|| {
TxlineError::solana(format!(
"purchase instruction account index {actual_index} is invalid"
))
})?;
if let Some(expected) = expected
&& actual != *expected
{
return Err(TxlineError::solana(format!(
"purchase instruction account {position} is {actual}, expected {expected}"
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::DEVNET_PROGRAM_ID;
use crate::solana::purchase::{
devnet_purchase_subscription_token_usdt_accounts,
purchase_subscription_token_usdt_instruction,
};
use solana_sdk::hash::Hash;
use solana_sdk::instruction::Instruction;
use solana_sdk::signature::{Keypair, Signer};
use solana_sdk::transaction::Transaction;
#[test]
fn accepts_synthetic_devnet_purchase_transaction() {
let buyer = Keypair::new();
let backend = Keypair::new();
let amount = 1_000;
let config = safety_config(&buyer, &backend, amount);
let transaction = signed_purchase_transaction(&buyer, &backend, amount, Vec::new());
let bytes = wincode::serialize(&transaction).unwrap();
let report = verify_purchase_transaction_bytes(&bytes, &config).unwrap();
assert_eq!(report.fee_payer, buyer.pubkey());
assert_eq!(report.txline_purchase_instruction_count, 1);
assert!(report.backend_signer_present);
}
#[test]
fn rejects_purchase_amount_mismatch() {
let buyer = Keypair::new();
let backend = Keypair::new();
let transaction = signed_purchase_transaction(&buyer, &backend, 1_000, Vec::new());
let config = safety_config(&buyer, &backend, 999);
let err = verify_purchase_transaction(&transaction, &config).unwrap_err();
assert!(err.to_string().contains("txline_amount"));
}
#[test]
fn rejects_unknown_program_invocation() {
let buyer = Keypair::new();
let backend = Keypair::new();
let rogue_ix = Instruction {
program_id: Pubkey::new_unique(),
accounts: Vec::new(),
data: Vec::new(),
};
let transaction = signed_purchase_transaction(&buyer, &backend, 1_000, vec![rogue_ix]);
let config = safety_config(&buyer, &backend, 1_000);
let err = verify_purchase_transaction(&transaction, &config).unwrap_err();
assert!(err.to_string().contains("unauthorized program"));
}
#[test]
fn rejects_multiple_txline_purchase_instructions() {
let buyer = Keypair::new();
let backend = Keypair::new();
let program_id = parse_pubkey(DEVNET_PROGRAM_ID).unwrap();
let accounts =
devnet_purchase_subscription_token_usdt_accounts(buyer.pubkey(), backend.pubkey())
.unwrap();
let extra =
purchase_subscription_token_usdt_instruction(program_id, accounts, 1_000).unwrap();
let transaction = signed_purchase_transaction(&buyer, &backend, 1_000, vec![extra]);
let config = safety_config(&buyer, &backend, 1_000);
let err = verify_purchase_transaction(&transaction, &config).unwrap_err();
assert!(err.to_string().contains("exactly one"));
}
fn safety_config(
buyer: &Keypair,
backend: &Keypair,
amount: u64,
) -> PurchaseTransactionSafetyConfig {
PurchaseTransactionSafetyConfig {
txline_program_id: parse_pubkey(DEVNET_PROGRAM_ID).unwrap(),
expected_buyer: buyer.pubkey(),
expected_txline_amount: amount,
expected_backend_signer: Some(backend.pubkey()),
}
}
fn signed_purchase_transaction(
buyer: &Keypair,
backend: &Keypair,
amount: u64,
mut extra_instructions: Vec<Instruction>,
) -> VersionedTransaction {
let program_id = parse_pubkey(DEVNET_PROGRAM_ID).unwrap();
let accounts =
devnet_purchase_subscription_token_usdt_accounts(buyer.pubkey(), backend.pubkey())
.unwrap();
let purchase_ix =
purchase_subscription_token_usdt_instruction(program_id, accounts, amount).unwrap();
let mut instructions = vec![purchase_ix];
instructions.append(&mut extra_instructions);
let blockhash = Hash::new_unique();
let mut transaction = Transaction::new_with_payer(&instructions, Some(&buyer.pubkey()));
transaction.sign(&[buyer, backend], blockhash);
VersionedTransaction::from(transaction)
}
}