use base64::Engine;
use compact_str::CompactString;
use r402_core::wire::VerifyResponse;
use serde_json::Value;
use crate::chain::codec::{SignedTransaction, Transaction, TxnType, decode_signed};
use crate::chain::rpc::AlgodRpc;
use crate::chain::signer::verify_txn_signature;
use crate::chain::types::{AlgorandAddress, normalize_algorand_network};
use crate::exact::error::{AlgorandInvalid, invalid};
use crate::max_reasonable_group_fee;
pub async fn verify_request_json<R, F>(
rpc: &R,
facilitator_addresses: &[String],
request: &Value,
sign: F,
) -> VerifyResponse
where
R: AlgodRpc,
F: FnMut(&Transaction, &str) -> Result<Vec<u8>, AlgorandInvalid>,
{
match verify_inner(rpc, facilitator_addresses, request, sign).await {
Ok(payer) => VerifyResponse::valid(payer),
Err(invalid) => invalid.into_response(),
}
}
#[allow(
clippy::too_many_lines,
clippy::cognitive_complexity,
reason = "sequential AVM verify checklist stays in one function"
)]
async fn verify_inner<R, F>(
rpc: &R,
facilitator_addresses: &[String],
request: &Value,
mut sign: F,
) -> Result<CompactString, AlgorandInvalid>
where
R: AlgodRpc,
F: FnMut(&Transaction, &str) -> Result<Vec<u8>, AlgorandInvalid>,
{
let payload = request
.get("paymentPayload")
.ok_or_else(|| invalid("invalid_exact_avm_payload"))?;
let requirements = request
.get("paymentRequirements")
.ok_or_else(|| invalid("invalid_exact_avm_payload"))?;
let payload_version = payload.get("x402Version").and_then(Value::as_u64);
if payload_version != Some(2) {
return Err(invalid("invalid_exact_avm_invalid_version")
.with_message(format!("Expected x402Version 2, got {payload_version:?}")));
}
let accepted = payload
.get("accepted")
.ok_or_else(|| invalid("invalid_exact_avm_payload"))?;
let accepted_scheme = accepted.get("scheme").and_then(Value::as_str);
let req_scheme = requirements.get("scheme").and_then(Value::as_str);
if accepted_scheme != Some("exact") || req_scheme != Some("exact") {
return Err(invalid("invalid_exact_avm_scheme").with_message(format!(
"Expected scheme \"exact\", got payload=\"{accepted_scheme:?}\" requirements=\"{req_scheme:?}\""
)));
}
let req_network = requirements
.get("network")
.and_then(Value::as_str)
.unwrap_or("");
let accepted_network = accepted
.get("network")
.and_then(Value::as_str)
.unwrap_or("");
let req_chain = normalize_algorand_network(req_network)
.ok_or_else(|| invalid("invalid_exact_avm_payload").with_message("unsupported network"))?;
let accepted_chain = normalize_algorand_network(accepted_network).ok_or_else(|| {
invalid("invalid_exact_avm_payload").with_message("unsupported payload network")
})?;
if req_chain != accepted_chain {
return Err(
invalid("invalid_exact_avm_network_mismatch").with_message(format!(
"Network mismatch: payload=\"{accepted_network}\" requirements=\"{req_network}\""
)),
);
}
let inner = payload
.get("payload")
.ok_or_else(|| invalid("invalid_exact_avm_payload"))?;
let payment_group = inner
.get("paymentGroup")
.and_then(Value::as_array)
.ok_or_else(|| invalid("invalid_exact_avm_payload"))?;
if !payment_group.iter().all(Value::is_string) {
return Err(invalid("invalid_exact_avm_payload"));
}
let payment_index = inner
.get("paymentIndex")
.and_then(Value::as_u64)
.ok_or_else(|| invalid("invalid_exact_avm_payload"))?;
let group_n = payment_group.len();
if group_n != 1 && group_n != 2 {
return Err(
invalid("invalid_exact_avm_group_size_exceeded").with_message(format!(
"exact Algorand group must be payment only, or payment + unsigned facilitator fee pay (got {group_n})"
)),
);
}
let group_len = u64::try_from(payment_group.len()).unwrap_or(u64::MAX);
if payment_index >= group_len {
return Err(
invalid("invalid_exact_avm_payment_index").with_message(format!(
"Payment index {payment_index} out of bounds for group of {}",
payment_group.len()
)),
);
}
let decoded = decode_transaction_group(payment_group, facilitator_addresses)?;
let expected_gh = req_chain.genesis_hash();
for (i, stxn) in decoded.iter().enumerate() {
if stxn.txn.genesis_hash != expected_gh {
return Err(
invalid("invalid_exact_avm_network_mismatch").with_message(format!(
"transaction {i} genesis hash does not match payment network"
)),
);
}
}
let payment_idx = usize::try_from(payment_index).unwrap_or(usize::MAX);
let payment = decoded
.get(payment_idx)
.ok_or_else(|| invalid("invalid_exact_avm_payment_index"))?;
let payer = payment.txn.sender.to_string();
if facilitator_addresses.iter().any(|a| a == &payer) {
return Err(invalid("invalid_exact_avm_facilitator_transferring")
.with_payer(payer)
.with_message("Facilitator signer cannot be the payment sender"));
}
let encoded_payment = payment_group
.get(payment_idx)
.and_then(Value::as_str)
.ok_or_else(|| invalid("invalid_exact_avm_payload"))?;
verify_payment_transaction(payment, requirements, encoded_payment)?;
if group_n == 2 {
let extra_idx = 1 - payment_idx;
let extra = decoded
.get(extra_idx)
.ok_or_else(|| invalid("invalid_exact_avm_payload"))?;
if extra.has_signature() {
return Err(invalid("invalid_exact_avm_payload").with_message(
"only an unsigned facilitator 0-amount self-pay may accompany the payment",
));
}
verify_fee_payer_transaction(&extra.txn)?;
}
let prepared =
prepare_signed_group_with(&decoded, payment_group, facilitator_addresses, &mut sign)?;
let sim = rpc
.simulate_group(&prepared)
.await
.map_err(|e| invalid("invalid_exact_avm_simulation_failed").with_message(e.to_string()))?;
if let Some(message) = sim.failure_message {
return Err(invalid("invalid_exact_avm_simulation_failed")
.with_payer(payer)
.with_message(message));
}
Ok(CompactString::from(payer))
}
pub(crate) fn decode_transaction_group(
payment_group: &[Value],
facilitator_addresses: &[String],
) -> Result<Vec<SignedTransaction>, AlgorandInvalid> {
let mut txns = Vec::with_capacity(payment_group.len());
for (i, item) in payment_group.iter().enumerate() {
let encoded = item
.as_str()
.ok_or_else(|| invalid("invalid_exact_avm_payload"))?;
let bytes = base64::engine::general_purpose::STANDARD
.decode(encoded)
.map_err(|_| {
invalid("invalid_exact_avm_invalid_transaction")
.with_message(format!("Failed to decode transaction at index {i}"))
})?;
let stxn = decode_signed(&bytes).map_err(|_| {
invalid("invalid_exact_avm_invalid_transaction")
.with_message(format!("Failed to decode transaction at index {i}"))
})?;
if !stxn.has_signature() {
let sender = stxn.txn.sender.to_string();
if !facilitator_addresses.iter().any(|a| a == &sender) {
return Err(invalid("invalid_exact_avm_unsigned_non_facilitator")
.with_message(format!(
"Unsigned transaction at index {i} from {sender} is not a facilitator address"
)));
}
}
txns.push(stxn);
}
if txns.len() > 1 {
let first = txns.first().and_then(|t| t.txn.group);
for txn in txns.iter().skip(1) {
if txn.txn.group != first {
return Err(invalid("invalid_exact_avm_invalid_group_id")
.with_message("Transactions have inconsistent group IDs"));
}
}
}
Ok(txns)
}
fn verify_payment_transaction(
stxn: &SignedTransaction,
requirements: &Value,
_encoded: &str,
) -> Result<(), AlgorandInvalid> {
if stxn.txn.txn_type != TxnType::Axfer {
return Err(
invalid("invalid_exact_avm_not_asset_transfer").with_message(format!(
"Expected asset transfer, got \"{}\"",
stxn.txn.txn_type.as_str()
)),
);
}
let expected_amount = requirements
.get("amount")
.and_then(Value::as_str)
.unwrap_or("");
let actual = stxn.txn.asset_amount.to_string();
if actual != expected_amount {
return Err(invalid("invalid_exact_avm_amount_mismatch")
.with_message(format!("Expected {expected_amount}, got {actual}")));
}
let expected_pay_to = requirements
.get("payTo")
.and_then(Value::as_str)
.unwrap_or("");
let receiver = stxn
.txn
.asset_receiver
.unwrap_or(AlgorandAddress::ZERO)
.to_string();
if receiver != expected_pay_to {
return Err(invalid("invalid_exact_avm_receiver_mismatch")
.with_message(format!("Expected {expected_pay_to}, got {receiver}")));
}
let expected_asset = requirements
.get("asset")
.and_then(Value::as_str)
.unwrap_or("");
let asset_id = stxn.txn.asset_id.to_string();
if asset_id != expected_asset {
return Err(invalid("invalid_exact_avm_asset_mismatch")
.with_message(format!("Expected asset {expected_asset}, got {asset_id}")));
}
let Some(sig) = stxn.sig else {
return Err(invalid("invalid_exact_avm_payment_not_signed")
.with_message("Payment transaction is not signed"));
};
if !verify_txn_signature(&stxn.txn, &sig) {
return Err(invalid("invalid_exact_avm_invalid_signature")
.with_message("Payment transaction signature does not match sender"));
}
if stxn.txn.asset_close_to.is_some() {
return Err(
invalid("invalid_exact_avm_payload").with_message("aclose not allowed on payment")
);
}
if stxn.txn.asset_sender.is_some() {
return Err(
invalid("invalid_exact_avm_payload").with_message("asnd not allowed on payment")
);
}
if stxn.txn.rekey_to.is_some() {
return Err(
invalid("invalid_exact_avm_payload").with_message("rekey not allowed on payment")
);
}
if stxn.txn.close_remainder_to.is_some() {
return Err(
invalid("invalid_exact_avm_payload").with_message("close not allowed on payment")
);
}
Ok(())
}
fn verify_fee_payer_transaction(txn: &Transaction) -> Result<(), AlgorandInvalid> {
if txn.txn_type != TxnType::Pay {
return Err(
invalid("invalid_exact_avm_invalid_fee_payer").with_message(format!(
"Expected payment transaction, got {}",
txn.txn_type.as_str()
)),
);
}
if txn.amount > 0 {
return Err(invalid("invalid_exact_avm_invalid_fee_payer")
.with_message("Fee payer amount must be 0"));
}
if let Some(receiver) = txn.receiver
&& receiver != txn.sender
{
return Err(invalid("invalid_exact_avm_invalid_fee_payer")
.with_message("Fee payer receiver must be same as sender (self-payment)"));
}
if txn.close_remainder_to.is_some() {
return Err(invalid("invalid_exact_avm_invalid_fee_payer")
.with_message("closeRemainderTo not allowed on fee payer"));
}
if txn.rekey_to.is_some() {
return Err(invalid("invalid_exact_avm_invalid_fee_payer")
.with_message("rekeyTo not allowed on fee payer"));
}
Ok(())
}
pub(crate) fn prepare_signed_group_with<F>(
decoded: &[SignedTransaction],
payment_group: &[Value],
facilitator_addresses: &[String],
sign: &mut F,
) -> Result<Vec<Vec<u8>>, AlgorandInvalid>
where
F: FnMut(&Transaction, &str) -> Result<Vec<u8>, AlgorandInvalid>,
{
let mut signed = Vec::with_capacity(decoded.len());
let mut facilitator_fees = 0_u64;
for (i, stxn) in decoded.iter().enumerate() {
let sender = stxn.txn.sender.to_string();
if facilitator_addresses.iter().any(|a| a == &sender) {
verify_fee_payer_transaction(&stxn.txn)?;
facilitator_fees = facilitator_fees.saturating_add(stxn.txn.fee);
signed.push(sign(&stxn.txn, &sender)?);
} else {
let encoded = payment_group
.get(i)
.and_then(Value::as_str)
.ok_or_else(|| invalid("invalid_exact_avm_payload"))?;
let bytes = base64::engine::general_purpose::STANDARD
.decode(encoded)
.map_err(|_| invalid("invalid_exact_avm_invalid_transaction"))?;
signed.push(bytes);
}
}
let max_fee = max_reasonable_group_fee(decoded.len());
if facilitator_fees > max_fee {
return Err(
invalid("invalid_exact_avm_fee_too_high").with_message(format!(
"Facilitator fees {facilitator_fees} exceed maximum {max_fee} ({} txns × 5000 µAlgo)",
decoded.len()
)),
);
}
Ok(signed)
}