use r402::chain::ChainProvider;
use r402::proto::Base64Bytes;
use r402::proto::PaymentVerificationError;
use solana_client::rpc_config::RpcSimulateTransactionConfig;
use solana_client::rpc_response::{TransactionError, UiTransactionError};
use solana_commitment_config::CommitmentConfig;
use solana_compute_budget_interface::ID as ComputeBudgetInstructionId;
use solana_pubkey::Pubkey;
use solana_signature::Signature;
use solana_transaction::versioned::VersionedTransaction;
#[cfg(feature = "telemetry")]
use tracing_core::Level;
use super::config::SolanaExactFacilitatorConfig;
use crate::chain::Address;
use crate::chain::provider::{SolanaChainProviderError, SolanaChainProviderLike};
use crate::exact::ATA_PROGRAM_PUBKEY;
use crate::exact::error::SolanaExactError;
use crate::exact::types::{self, TransactionInt};
#[derive(Debug)]
pub struct VerifyTransferResult {
pub payer: Address,
pub transaction: VersionedTransaction,
}
#[derive(Debug, Clone, Copy)]
pub struct TransferCheckedInstruction {
pub amount: u64,
pub source: Pubkey,
pub mint: Pubkey,
pub destination: Pubkey,
pub authority: Pubkey,
pub token_program: Pubkey,
}
#[derive(Debug)]
pub struct TransferRequirement<'a> {
pub asset: &'a Address,
pub pay_to: &'a Address,
pub amount: u64,
}
pub fn verify_compute_limit_instruction(
transaction: &VersionedTransaction,
instruction_index: usize,
) -> Result<u32, SolanaExactError> {
let instructions = transaction.message.instructions();
let instruction = instructions
.get(instruction_index)
.ok_or(SolanaExactError::NoInstructionAtIndex(instruction_index))?;
let account = instruction.program_id(transaction.message.static_account_keys());
let data = instruction.data.as_slice();
if ComputeBudgetInstructionId.ne(account)
|| data.first().copied().unwrap_or(0) != 2
|| data.len() != 5
{
return Err(SolanaExactError::InvalidComputeLimitInstruction);
}
let mut buf = [0u8; 4];
#[allow(clippy::indexing_slicing, reason = "length == 5 checked above")]
buf.copy_from_slice(&data[1..5]);
let compute_units = u32::from_le_bytes(buf);
Ok(compute_units)
}
pub fn verify_compute_price_instruction(
max_compute_unit_price: u64,
transaction: &VersionedTransaction,
instruction_index: usize,
) -> Result<(), SolanaExactError> {
let instructions = transaction.message.instructions();
let instruction = instructions
.get(instruction_index)
.ok_or(SolanaExactError::NoInstructionAtIndex(instruction_index))?;
let account = instruction.program_id(transaction.message.static_account_keys());
let compute_budget = solana_compute_budget_interface::ID;
let data = instruction.data.as_slice();
if compute_budget.ne(account) || data.first().copied().unwrap_or(0) != 3 || data.len() != 9 {
return Err(SolanaExactError::InvalidComputePriceInstruction);
}
let mut buf = [0u8; 8];
#[allow(clippy::indexing_slicing, reason = "length == 9 checked above")]
buf.copy_from_slice(&data[1..]);
let microlamports = u64::from_le_bytes(buf);
if microlamports > max_compute_unit_price {
return Err(SolanaExactError::MaxComputeUnitPriceExceeded);
}
Ok(())
}
pub fn validate_instructions(
transaction: &VersionedTransaction,
config: &SolanaExactFacilitatorConfig,
) -> Result<(), SolanaExactError> {
let instructions = transaction.message.instructions();
if instructions.len() < 3 {
return Err(SolanaExactError::TooFewInstructions);
}
if instructions.len() > config.max_instruction_count {
return Err(SolanaExactError::InstructionCountExceedsMax(
config.max_instruction_count,
));
}
let ix2_program = get_program_id(transaction, 2);
if ix2_program == Some(ATA_PROGRAM_PUBKEY) {
return Err(SolanaExactError::CreateATANotSupported);
}
if instructions.len() > 3 {
if !config.allow_additional_instructions {
return Err(SolanaExactError::AdditionalInstructionsNotAllowed);
}
#[allow(
clippy::excessive_nesting,
reason = "program validation requires nested conditionals"
)]
for i in 3..instructions.len() {
if let Some(program_id) = get_program_id(transaction, i) {
if config.is_blocked(&program_id) {
return Err(SolanaExactError::BlockedProgram(program_id));
}
if !config.is_allowed(&program_id) {
return Err(SolanaExactError::ProgramNotAllowed(program_id));
}
}
}
}
Ok(())
}
fn get_program_id(transaction: &VersionedTransaction, index: usize) -> Option<Pubkey> {
let instruction = transaction.message.instructions().get(index)?;
let account_keys = transaction.message.static_account_keys();
Some(*instruction.program_id(account_keys))
}
pub async fn verify_transfer<P: SolanaChainProviderLike + ChainProvider>(
provider: &P,
request: &types::v2::VerifyRequest,
config: &SolanaExactFacilitatorConfig,
) -> Result<VerifyTransferResult, PaymentVerificationError> {
let payload = &request.payment_payload;
let requirements = &request.payment_requirements;
let accepted = &payload.accepted;
if !(accepted.scheme == requirements.scheme
&& accepted.network == requirements.network
&& accepted.amount == requirements.amount
&& accepted.asset == requirements.asset
&& accepted.pay_to == requirements.pay_to)
{
return Err(PaymentVerificationError::AcceptedRequirementsMismatch);
}
let chain_id = provider.chain_id();
let payload_chain_id = &accepted.network;
if payload_chain_id != &chain_id {
return Err(PaymentVerificationError::UnsupportedChain);
}
let transaction_b64_string = payload.payload.transaction.clone();
let transfer_requirement = TransferRequirement {
pay_to: &requirements.pay_to,
asset: &requirements.asset,
amount: requirements.amount.inner(),
};
verify_transaction(
provider,
transaction_b64_string,
&transfer_requirement,
config,
)
.await
}
pub async fn verify_transaction<P: SolanaChainProviderLike>(
provider: &P,
transaction_b64_string: String,
transfer_requirement: &TransferRequirement<'_>,
config: &SolanaExactFacilitatorConfig,
) -> Result<VerifyTransferResult, PaymentVerificationError> {
let bytes = Base64Bytes::from(transaction_b64_string.as_bytes())
.decode()
.map_err(|e| SolanaExactError::TransactionDecoding(e.to_string()))?;
let transaction = bincode::deserialize::<VersionedTransaction>(bytes.as_slice())
.map_err(|e| SolanaExactError::TransactionDecoding(e.to_string()))?;
let compute_units = verify_compute_limit_instruction(&transaction, 0)?;
if compute_units > provider.max_compute_unit_limit() {
return Err(SolanaExactError::MaxComputeUnitLimitExceeded.into());
}
#[cfg(feature = "telemetry")]
tracing::debug!(compute_units = compute_units, "Verified compute unit limit");
verify_compute_price_instruction(provider.max_compute_unit_price(), &transaction, 1)?;
validate_instructions(&transaction, config)?;
let transfer_instruction =
verify_transfer_instruction(provider, &transaction, 2, transfer_requirement).await?;
if config.require_fee_payer_not_in_instructions {
let fee_payer_pubkey = provider.pubkey();
for instruction in transaction.message.instructions() {
for account_idx in &instruction.accounts {
let account = transaction
.message
.static_account_keys()
.get(*account_idx as usize)
.ok_or(SolanaExactError::NoAccountAtIndex(*account_idx))?;
#[allow(
clippy::excessive_nesting,
reason = "nested iteration over instructions and accounts"
)]
if *account == fee_payer_pubkey {
return Err(SolanaExactError::FeePayerIncludedInInstructionAccounts.into());
}
}
}
}
let tx = TransactionInt::new(transaction.clone()).sign(provider)?;
let cfg = RpcSimulateTransactionConfig {
sig_verify: false,
replace_recent_blockhash: false,
commitment: Some(CommitmentConfig::confirmed()),
encoding: None,
accounts: None,
inner_instructions: false,
min_context_slot: None,
};
provider
.simulate_transaction_with_config(tx.inner(), cfg)
.await?;
let payer: Address = transfer_instruction.authority.into();
Ok(VerifyTransferResult { payer, transaction })
}
pub async fn verify_transfer_instruction<P: SolanaChainProviderLike>(
provider: &P,
transaction: &VersionedTransaction,
instruction_index: usize,
transfer_requirement: &TransferRequirement<'_>,
) -> Result<TransferCheckedInstruction, PaymentVerificationError> {
let tx = TransactionInt::new(transaction.clone());
let instruction = tx.instruction(instruction_index)?;
instruction.assert_not_empty()?;
let program_id = instruction.program_id();
let token_program = if spl_token::ID.eq(&program_id) {
spl_token::ID
} else if spl_token_2022::ID.eq(&program_id) {
spl_token_2022::ID
} else {
return Err(SolanaExactError::InvalidTokenInstruction.into());
};
let token_instruction =
spl_token::instruction::TokenInstruction::unpack(instruction.data_slice())
.map_err(|_| SolanaExactError::InvalidTokenInstruction)?;
let spl_token::instruction::TokenInstruction::TransferChecked {
amount,
decimals: _,
} = token_instruction
else {
return Err(SolanaExactError::InvalidTokenInstruction.into());
};
let transfer_checked_instruction = TransferCheckedInstruction {
amount,
source: instruction.account(0)?,
mint: instruction.account(1)?,
destination: instruction.account(2)?,
authority: instruction.account(3)?,
token_program,
};
let fee_payer_pubkey = provider.pubkey();
if transfer_checked_instruction.authority == fee_payer_pubkey {
return Err(SolanaExactError::FeePayerTransferringFunds.into());
}
if Address::new(transfer_checked_instruction.mint) != *transfer_requirement.asset {
return Err(PaymentVerificationError::AssetMismatch);
}
let expected_token_program = transfer_checked_instruction.token_program;
let (ata, _) = Pubkey::find_program_address(
&[
transfer_requirement.pay_to.as_ref(),
expected_token_program.as_ref(),
transfer_requirement.asset.as_ref(),
],
&ATA_PROGRAM_PUBKEY,
);
if transfer_checked_instruction.destination != ata {
return Err(PaymentVerificationError::RecipientMismatch);
}
let accounts = provider
.get_multiple_accounts(&[transfer_checked_instruction.source, ata])
.await?;
let is_sender_missing = accounts.first().cloned().is_none_or(|a| a.is_none());
if is_sender_missing {
return Err(SolanaExactError::MissingSenderAccount.into());
}
let is_receiver_missing = accounts.get(1).cloned().is_none_or(|a| a.is_none());
if is_receiver_missing {
return Err(PaymentVerificationError::RecipientMismatch);
}
let instruction_amount = transfer_checked_instruction.amount;
if instruction_amount < transfer_requirement.amount {
return Err(PaymentVerificationError::InvalidPaymentAmount);
}
Ok(transfer_checked_instruction)
}
pub async fn settle_transaction<P: SolanaChainProviderLike>(
provider: &P,
verification: VerifyTransferResult,
) -> Result<Signature, SolanaChainProviderError> {
let tx = TransactionInt::new(verification.transaction).sign(provider)?;
if !tx.is_fully_signed() {
#[cfg(feature = "telemetry")]
tracing::event!(Level::WARN, status = "failed", "undersigned transaction");
return Err(SolanaChainProviderError::InvalidTransaction(
UiTransactionError::from(TransactionError::SignatureFailure),
));
}
let tx_sig = tx
.send_and_confirm(provider, CommitmentConfig::confirmed())
.await?;
Ok(tx_sig)
}