fly402_core/
payment_processor.rs1use solana_client::rpc_client::RpcClient;
2use solana_sdk::{
3 commitment_config::CommitmentConfig,
4 instruction::Instruction,
5 message::Message,
6 pubkey::Pubkey,
7 signature::{Keypair, Signature, Signer},
8 transaction::Transaction,
9};
10use spl_token::instruction as token_instruction;
11use spl_associated_token_account::{
12 get_associated_token_address, instruction::create_associated_token_account,
13};
14use std::str::FromStr;
15
16use crate::{
17 errors::{X402Error, X402Result},
18 models::{PaymentAuthorization, PaymentRequest},
19};
20
21pub struct SolanaPaymentProcessor {
23 rpc_client: RpcClient,
24 #[allow(dead_code)]
25 commitment: CommitmentConfig,
26}
27
28impl SolanaPaymentProcessor {
29 pub fn new(rpc_url: &str, commitment: Option<CommitmentConfig>) -> Self {
35 Self {
36 rpc_client: RpcClient::new_with_commitment(
37 rpc_url.to_string(),
38 commitment.unwrap_or(CommitmentConfig::confirmed()),
39 ),
40 commitment: commitment.unwrap_or(CommitmentConfig::confirmed()),
41 }
42 }
43
44 pub fn default_rpc_url(network: &str) -> &'static str {
46 match network {
47 "solana-mainnet" => "https://api.mainnet-beta.solana.com",
48 "solana-devnet" => "https://api.devnet.solana.com",
49 "solana-testnet" => "https://api.testnet.solana.com",
50 _ => "https://api.devnet.solana.com",
51 }
52 }
53
54 pub async fn create_payment(
58 &self,
59 request: &PaymentRequest,
60 payer: &Keypair,
61 ) -> X402Result<PaymentAuthorization> {
62 if request.is_expired() {
64 return Err(X402Error::PaymentExpired(format!(
65 "Payment request expired at {}",
66 request.expires_at
67 )));
68 }
69
70 let token_mint = Pubkey::from_str(&request.asset_address).map_err(|e| {
72 X402Error::InvalidPaymentRequest(format!("Invalid token mint address: {}", e))
73 })?;
74
75 let recipient = Pubkey::from_str(&request.payment_address).map_err(|e| {
76 X402Error::InvalidPaymentRequest(format!("Invalid payment address: {}", e))
77 })?;
78
79 let amount = Self::parse_amount(&request.max_amount_required)?;
80
81 let sender_ata = get_associated_token_address(&payer.pubkey(), &token_mint);
83 let recipient_ata = get_associated_token_address(&recipient, &token_mint);
84
85 self.check_balance(&sender_ata, amount).await?;
87
88 let mut instructions: Vec<Instruction> = Vec::new();
90
91 if !self.account_exists(&recipient_ata).await? {
93 instructions.push(create_associated_token_account(
94 &payer.pubkey(),
95 &recipient,
96 &token_mint,
97 &spl_token::id(),
98 ));
99 }
100
101 instructions.push(
103 token_instruction::transfer_checked(
104 &spl_token::id(),
105 &sender_ata,
106 &token_mint,
107 &recipient_ata,
108 &payer.pubkey(),
109 &[],
110 amount,
111 6, )
113 .map_err(|e| {
114 X402Error::Blockchain(format!("Failed to create transfer instruction: {}", e))
115 })?,
116 );
117
118 let recent_blockhash = self
120 .rpc_client
121 .get_latest_blockhash()
122 .map_err(|e| X402Error::Network(format!("Failed to get recent blockhash: {}", e)))?;
123
124 let message = Message::new(&instructions, Some(&payer.pubkey()));
126 let mut transaction = Transaction::new_unsigned(message);
127 transaction.sign(&[payer], recent_blockhash);
128
129 let signature = self
131 .rpc_client
132 .send_and_confirm_transaction(&transaction)
133 .map_err(|e| {
134 X402Error::TransactionBroadcast(format!("Failed to broadcast transaction: {}", e))
135 })?;
136
137 Ok(PaymentAuthorization::new(
139 request.payment_id.clone(),
140 request.max_amount_required.clone(),
141 request.payment_address.clone(),
142 request.asset_address.clone(),
143 request.network.clone(),
144 signature.to_string(),
145 payer.pubkey().to_string(),
146 ))
147 }
148
149 pub async fn verify_payment(
153 &self,
154 authorization: &PaymentAuthorization,
155 expected_amount: &str,
156 ) -> X402Result<bool> {
157 let signature = Signature::from_str(&authorization.signature).map_err(|e| {
158 X402Error::InvalidPaymentAuthorization(format!("Invalid signature: {}", e))
159 })?;
160
161 let transaction = self
163 .rpc_client
164 .get_transaction(&signature, solana_transaction_status::UiTransactionEncoding::Json)
165 .map_err(|e| {
166 X402Error::PaymentVerification(format!("Failed to fetch transaction: {}", e))
167 })?;
168
169 if transaction.transaction.meta.as_ref().and_then(|m| m.err.as_ref()).is_some() {
171 return Err(X402Error::PaymentVerification(
172 "Transaction failed on-chain".to_string(),
173 ));
174 }
175
176 let expected = Self::parse_amount(expected_amount)?;
178 let actual = Self::parse_amount(&authorization.actual_amount)?;
179
180 if actual < expected {
181 return Err(X402Error::PaymentVerification(format!(
182 "Payment amount {} is less than required {}",
183 authorization.actual_amount, expected_amount
184 )));
185 }
186
187 Ok(true)
188 }
189
190 pub async fn get_token_balance(&self, token_account: &Pubkey) -> X402Result<u64> {
192 let balance = self
193 .rpc_client
194 .get_token_account_balance(token_account)
195 .map_err(|e| X402Error::Network(format!("Failed to get token balance: {}", e)))?;
196
197 balance
198 .amount
199 .parse::<u64>()
200 .map_err(|e| X402Error::Blockchain(format!("Failed to parse balance: {}", e)))
201 }
202
203 async fn account_exists(&self, account: &Pubkey) -> X402Result<bool> {
205 match self.rpc_client.get_account(account) {
206 Ok(_) => Ok(true),
207 Err(e) => {
208 if e.to_string().contains("AccountNotFound") {
210 Ok(false)
211 } else {
212 Err(X402Error::Network(format!(
213 "Failed to check account existence: {}",
214 e
215 )))
216 }
217 }
218 }
219 }
220
221 async fn check_balance(&self, token_account: &Pubkey, required_amount: u64) -> X402Result<()> {
223 let balance = self.get_token_balance(token_account).await?;
224
225 if balance < required_amount {
226 return Err(X402Error::InsufficientFunds(format!(
227 "Insufficient balance: {} required, {} available",
228 required_amount, balance
229 )));
230 }
231
232 Ok(())
233 }
234
235 fn parse_amount(amount_str: &str) -> X402Result<u64> {
237 let amount: f64 = amount_str.parse().map_err(|e| {
238 X402Error::InvalidPaymentRequest(format!("Invalid amount format: {}", e))
239 })?;
240
241 let lamports = (amount * 1_000_000.0) as u64;
243 Ok(lamports)
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_parse_amount() {
253 assert_eq!(SolanaPaymentProcessor::parse_amount("0.10").unwrap(), 100_000);
254 assert_eq!(SolanaPaymentProcessor::parse_amount("1.0").unwrap(), 1_000_000);
255 assert_eq!(
256 SolanaPaymentProcessor::parse_amount("0.000001").unwrap(),
257 1
258 );
259 }
260
261 #[test]
262 fn test_default_rpc_url() {
263 assert_eq!(
264 SolanaPaymentProcessor::default_rpc_url("solana-mainnet"),
265 "https://api.mainnet-beta.solana.com"
266 );
267 assert_eq!(
268 SolanaPaymentProcessor::default_rpc_url("solana-devnet"),
269 "https://api.devnet.solana.com"
270 );
271 }
272}