Skip to main content

scematica_protocol/
client.rs

1/// Client-side helpers for building Scematica Protocol payment payloads.
2///
3/// Usage:
4///   1. Make a request; get back a 402 with `PaymentRequired` body.
5///   2. Pick an entry from `accepts` that your wallet supports.
6///   3. Call `build_payment_payload` to create a signed partial transaction.
7///   4. Base64-encode the `PaymentPayload` JSON and put it in the `X-Payment` header.
8///   5. Retry the original request.
9use anyhow::{bail, Result};
10use base64::Engine;
11use solana_sdk::{
12    compute_budget::ComputeBudgetInstruction, message::Message, pubkey::Pubkey, signature::Keypair,
13    signer::Signer, transaction::Transaction,
14};
15use spl_associated_token_account::get_associated_token_address;
16use spl_token::instruction::transfer_checked;
17use std::str::FromStr;
18
19use crate::types::{PaymentPayload, PaymentRequirements, SvmExactPayload, X402_VERSION};
20
21/// Build a partially-signed payment transaction for the SVM exact scheme.
22///
23/// The transaction includes:
24///   1. ComputeBudget: SetComputeUnitLimit (50_000)
25///   2. ComputeBudget: SetComputeUnitPrice (1 microlamport)
26///   3. SPL Token: TransferChecked (payer → payTo ATA, exact amount)
27///
28/// The fee payer slot is left empty (zero pubkey); the facilitator fills it at settlement.
29pub fn build_payment_payload(
30    payer: &Keypair,
31    requirements: &PaymentRequirements,
32    token_decimals: u8,
33) -> Result<PaymentPayload> {
34    let payer_pubkey = payer.pubkey();
35    let asset_mint = Pubkey::from_str(&requirements.asset)
36        .map_err(|_| anyhow::anyhow!("Invalid asset mint: {}", requirements.asset))?;
37    let pay_to = Pubkey::from_str(&requirements.pay_to)
38        .map_err(|_| anyhow::anyhow!("Invalid pay_to address: {}", requirements.pay_to))?;
39
40    let source_ata = get_associated_token_address(&payer_pubkey, &asset_mint);
41    let dest_ata = get_associated_token_address(&pay_to, &asset_mint);
42
43    // Compute budget to keep fees within spec bounds
44    let cu_limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(50_000);
45    let cu_price_ix = ComputeBudgetInstruction::set_compute_unit_price(1);
46
47    // SPL TransferChecked
48    let transfer_ix = transfer_checked(
49        &spl_token::id(),
50        &source_ata,
51        &asset_mint,
52        &dest_ata,
53        &payer_pubkey,
54        &[],
55        requirements.amount,
56        token_decimals,
57    )?;
58
59    // Build with a dummy recent blockhash — facilitator refreshes it before submission
60    let message = Message::new(
61        &[cu_limit_ix, cu_price_ix, transfer_ix],
62        None, // fee payer left empty for facilitator to fill
63    );
64    let mut tx = Transaction::new_unsigned(message);
65    tx.partial_sign(&[payer], solana_sdk::hash::Hash::default());
66
67    let tx_bytes = bincode::serialize(&tx)?;
68    let tx_b64 = base64::engine::general_purpose::STANDARD.encode(&tx_bytes);
69
70    if requirements.scheme != "exact" {
71        bail!(
72            "Only 'exact' scheme is supported; got '{}'",
73            requirements.scheme
74        );
75    }
76
77    Ok(PaymentPayload {
78        x402_version: X402_VERSION,
79        scheme: "exact".into(),
80        network: requirements.network.clone(),
81        payload: SvmExactPayload {
82            transaction: tx_b64,
83        },
84    })
85}
86
87/// Encode a `PaymentPayload` as the value for the `X-Payment` HTTP header.
88pub fn encode_payment_header(payload: &PaymentPayload) -> Result<String> {
89    let json = serde_json::to_vec(payload)?;
90    Ok(base64::engine::general_purpose::STANDARD.encode(&json))
91}