Skip to main content

stateset_db/sqlite/
x402_payment_intents.rs

1//! SQLite x402 payment intent repository implementation
2
3use super::{
4    build_in_clause, map_db_error, params_refs, parse_datetime_opt_row, parse_datetime_row,
5    parse_decimal_row, parse_enum_row, parse_uuid_opt_row, parse_uuid_row, uuid_params,
6};
7use chrono::Utc;
8use r2d2::Pool;
9use r2d2_sqlite::SqliteConnectionManager;
10use rusqlite::{OptionalExtension, TransactionBehavior};
11use rust_decimal::Decimal;
12use stateset_core::{
13    BatchResult, CommerceError, CreateX402PaymentIntent, Result, SignX402PaymentIntent,
14    X402_DEFAULT_VALIDITY_SECONDS, X402IntentStatus, X402PaymentIntent, X402PaymentIntentFilter,
15    X402PaymentIntentRepository, X402SignatureScheme, validate_batch_size,
16};
17use std::collections::HashMap;
18use uuid::Uuid;
19
20/// SQLite implementation of `X402PaymentIntentRepository`
21#[derive(Debug)]
22pub struct SqliteX402PaymentIntentRepository {
23    pool: Pool<SqliteConnectionManager>,
24}
25
26impl SqliteX402PaymentIntentRepository {
27    #[must_use]
28    pub const fn new(pool: Pool<SqliteConnectionManager>) -> Self {
29        Self { pool }
30    }
31
32    fn conn(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>> {
33        self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))
34    }
35
36    fn validate_input(input: &CreateX402PaymentIntent) -> Result<()> {
37        if input.amount == 0 {
38            return Err(CommerceError::ValidationError(
39                "x402 amount must be greater than zero".to_string(),
40            ));
41        }
42        Ok(())
43    }
44
45    fn parse_inclusion_proof(value: Option<String>) -> rusqlite::Result<Option<Vec<String>>> {
46        value
47            .map(|raw| {
48                serde_json::from_str::<Vec<String>>(&raw).map_err(|e| {
49                    rusqlite::Error::FromSqlConversionFailure(
50                        0,
51                        rusqlite::types::Type::Text,
52                        Box::new(std::io::Error::new(
53                            std::io::ErrorKind::InvalidData,
54                            format!("Invalid JSON for x402_intent.inclusion_proof: {e}"),
55                        )),
56                    )
57                })
58            })
59            .transpose()
60    }
61
62    fn parse_signature_scheme(
63        value: Option<String>,
64    ) -> rusqlite::Result<Option<X402SignatureScheme>> {
65        value
66            .map(|raw| {
67                raw.parse::<X402SignatureScheme>().map_err(|e| {
68                    rusqlite::Error::FromSqlConversionFailure(
69                        0,
70                        rusqlite::types::Type::Text,
71                        Box::new(std::io::Error::new(
72                            std::io::ErrorKind::InvalidData,
73                            format!("Invalid x402_intent.payer_signature_scheme '{raw}': {e}"),
74                        )),
75                    )
76                })
77            })
78            .transpose()
79    }
80
81    fn parse_bundle<T: serde::de::DeserializeOwned>(
82        value: Option<String>,
83        field: &str,
84    ) -> rusqlite::Result<Option<T>> {
85        value
86            .map(|raw| {
87                serde_json::from_str::<T>(&raw).map_err(|e| {
88                    rusqlite::Error::FromSqlConversionFailure(
89                        0,
90                        rusqlite::types::Type::Text,
91                        Box::new(std::io::Error::new(
92                            std::io::ErrorKind::InvalidData,
93                            format!("Invalid JSON for x402_intent.{field}: {e}"),
94                        )),
95                    )
96                })
97            })
98            .transpose()
99    }
100
101    fn row_to_intent(row: &rusqlite::Row<'_>) -> rusqlite::Result<X402PaymentIntent> {
102        let inclusion_proof: Option<Vec<String>> =
103            Self::parse_inclusion_proof(row.get("inclusion_proof")?)?;
104
105        Ok(X402PaymentIntent {
106            id: parse_uuid_row(&row.get::<_, String>("id")?, "x402_intent", "id")?,
107            version: row.get("version")?,
108            status: parse_enum_row(&row.get::<_, String>("status")?, "x402_intent", "status")?,
109
110            payer_address: row.get("payer_address")?,
111            payee_address: row.get("payee_address")?,
112            amount: row.get::<_, i64>("amount")? as u64,
113            amount_decimal: parse_decimal_row(
114                &row.get::<_, String>("amount_decimal")?,
115                "x402_intent",
116                "amount_decimal",
117            )?,
118            asset: parse_enum_row(&row.get::<_, String>("asset")?, "x402_intent", "asset")?,
119            network: parse_enum_row(&row.get::<_, String>("network")?, "x402_intent", "network")?,
120            chain_id: row.get::<_, i64>("chain_id")? as u64,
121            token_address: row.get("token_address")?,
122
123            created_at_unix: row.get::<_, i64>("created_at_unix")? as u64,
124            valid_until: row.get::<_, i64>("valid_until")? as u64,
125            nonce: row.get::<_, i64>("nonce")? as u64,
126            idempotency_key: row.get("idempotency_key")?,
127
128            resource_uri: row.get("resource_uri")?,
129            resource_method: row.get("resource_method")?,
130            description: row.get("description")?,
131            cart_id: parse_uuid_opt_row(
132                row.get::<_, Option<String>>("cart_id")?,
133                "x402_intent",
134                "cart_id",
135            )?,
136            order_id: parse_uuid_opt_row(
137                row.get::<_, Option<String>>("order_id")?,
138                "x402_intent",
139                "order_id",
140            )?,
141            invoice_id: parse_uuid_opt_row(
142                row.get::<_, Option<String>>("invoice_id")?,
143                "x402_intent",
144                "invoice_id",
145            )?,
146            merchant_id: row.get("merchant_id")?,
147
148            signing_hash: row.get("signing_hash")?,
149            payer_signature_scheme: Self::parse_signature_scheme(
150                row.get("payer_signature_scheme")?,
151            )?,
152            payer_signature: row.get("payer_signature")?,
153            payer_public_key: row.get("payer_public_key")?,
154            payer_signature_bundle: Self::parse_bundle(
155                row.get("payer_signature_bundle")?,
156                "payer_signature_bundle",
157            )?,
158            payer_public_key_bundle: Self::parse_bundle(
159                row.get("payer_public_key_bundle")?,
160                "payer_public_key_bundle",
161            )?,
162
163            sequence_number: row.get::<_, Option<i64>>("sequence_number")?.map(|n| n as u64),
164            sequenced_at: parse_datetime_opt_row(
165                row.get::<_, Option<String>>("sequenced_at")?,
166                "x402_intent",
167                "sequenced_at",
168            )?,
169            batch_id: parse_uuid_opt_row(
170                row.get::<_, Option<String>>("batch_id")?,
171                "x402_intent",
172                "batch_id",
173            )?,
174            batch_merkle_root: row.get("batch_merkle_root")?,
175            inclusion_proof,
176
177            tx_hash: row.get("tx_hash")?,
178            block_number: row.get::<_, Option<i64>>("block_number")?.map(|n| n as u64),
179            gas_used: row.get::<_, Option<i64>>("gas_used")?.map(|n| n as u64),
180            settled_at: parse_datetime_opt_row(
181                row.get::<_, Option<String>>("settled_at")?,
182                "x402_intent",
183                "settled_at",
184            )?,
185
186            metadata: row.get("metadata")?,
187            created_at: parse_datetime_row(
188                &row.get::<_, String>("created_at")?,
189                "x402_intent",
190                "created_at",
191            )?,
192            updated_at: parse_datetime_row(
193                &row.get::<_, String>("updated_at")?,
194                "x402_intent",
195                "updated_at",
196            )?,
197        })
198    }
199
200    fn get_next_nonce_in_tx(tx: &rusqlite::Transaction<'_>, payer_address: &str) -> Result<u64> {
201        let max_nonce: Option<i64> = tx
202            .query_row(
203                "SELECT MAX(nonce) FROM x402_payment_intents WHERE payer_address = ?",
204                [payer_address],
205                |row| row.get(0),
206            )
207            .map_err(map_db_error)?;
208
209        Ok(max_nonce.map_or(0, |n| n as u64 + 1))
210    }
211}
212
213impl X402PaymentIntentRepository for SqliteX402PaymentIntentRepository {
214    fn create(&self, input: CreateX402PaymentIntent) -> Result<X402PaymentIntent> {
215        Self::validate_input(&input)?;
216        let mut conn = self.conn()?;
217        let tx =
218            conn.transaction_with_behavior(TransactionBehavior::Immediate).map_err(map_db_error)?;
219        let now = Utc::now();
220        let now_unix = now.timestamp() as u64;
221        let id = Uuid::new_v4();
222        let nonce = match input.nonce {
223            Some(n) => n,
224            None => Self::get_next_nonce_in_tx(&tx, &input.payer_address)?,
225        };
226        let validity_seconds = input.validity_seconds.unwrap_or(X402_DEFAULT_VALIDITY_SECONDS);
227        let valid_until = now_unix + validity_seconds;
228
229        let asset = input.asset;
230        let network = input.network;
231        let chain_id = network.chain_id();
232        let token_address = asset.contract_address(network).map(String::from);
233
234        // Calculate decimal amount
235        let decimals = asset.decimals();
236        let divisor = 10u64.pow(u32::from(decimals));
237        let amount_decimal = Decimal::from(input.amount) / Decimal::from(divisor);
238        let mut signing_intent = X402PaymentIntent::new(
239            input.payer_address.clone(),
240            input.payee_address.clone(),
241            input.amount,
242            asset,
243            network,
244        )
245        .with_validity(validity_seconds)
246        .with_nonce(nonce);
247        if let Some(signature_scheme) = input.signature_scheme {
248            signing_intent.payer_signature_scheme = Some(signature_scheme);
249        }
250        signing_intent.id = id;
251        signing_intent.created_at = now;
252        signing_intent.updated_at = now;
253        signing_intent.created_at_unix = now_unix;
254        signing_intent.valid_until = valid_until;
255        signing_intent.chain_id = chain_id;
256        signing_intent.token_address = token_address.clone();
257        signing_intent.resource_uri = input.resource_uri.clone();
258        signing_intent.resource_method = input.resource_method.clone();
259        let signing_hash = format!(
260            "0x{}",
261            signing_intent
262                .sequencer_signing_hash()
263                .iter()
264                .map(|b| format!("{b:02x}"))
265                .collect::<String>()
266        );
267
268        tx.execute(
269            "INSERT INTO x402_payment_intents (
270                id, version, status, payer_address, payee_address, amount, amount_decimal,
271                asset, network, chain_id, token_address, created_at_unix, valid_until, nonce,
272                idempotency_key, resource_uri, resource_method, description, cart_id, order_id,
273                invoice_id, merchant_id, signing_hash, payer_signature_scheme, metadata, created_at, updated_at
274            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
275            rusqlite::params![
276                id.to_string(),
277                "1.0",
278                X402IntentStatus::Created.to_string(),
279                input.payer_address,
280                input.payee_address,
281                input.amount as i64,
282                amount_decimal.to_string(),
283                asset.to_string().to_lowercase(),
284                network.to_string(),
285                chain_id as i64,
286                token_address,
287                now_unix as i64,
288                valid_until as i64,
289                nonce as i64,
290                input.idempotency_key,
291                input.resource_uri,
292                input.resource_method,
293                input.description,
294                input.cart_id.map(|id| id.to_string()),
295                input.order_id.map(|id| id.to_string()),
296                input.invoice_id.map(|id| id.to_string()),
297                input.merchant_id,
298                signing_hash,
299                signing_intent.signature_scheme().to_string(),
300                input.metadata,
301                now.to_rfc3339(),
302                now.to_rfc3339(),
303            ],
304        )
305        .map_err(map_db_error)?;
306
307        tx.commit().map_err(map_db_error)?;
308        self.get(id)?.ok_or(CommerceError::NotFound)
309    }
310
311    fn get(&self, id: Uuid) -> Result<Option<X402PaymentIntent>> {
312        let conn = self.conn()?;
313        let mut stmt = conn
314            .prepare("SELECT * FROM x402_payment_intents WHERE id = ?")
315            .map_err(map_db_error)?;
316
317        stmt.query_row([id.to_string()], Self::row_to_intent).optional().map_err(map_db_error)
318    }
319
320    fn get_by_idempotency_key(&self, key: &str) -> Result<Option<X402PaymentIntent>> {
321        let conn = self.conn()?;
322        let mut stmt = conn
323            .prepare("SELECT * FROM x402_payment_intents WHERE idempotency_key = ?")
324            .map_err(map_db_error)?;
325
326        stmt.query_row([key], Self::row_to_intent).optional().map_err(map_db_error)
327    }
328
329    fn sign(&self, id: Uuid, input: SignX402PaymentIntent) -> Result<X402PaymentIntent> {
330        let conn = self.conn()?;
331
332        if input.intent_id != id {
333            return Err(CommerceError::ValidationError(
334                "Sign intent_id does not match target payment intent".to_string(),
335            ));
336        }
337
338        // Verify intent exists and is in Created status
339        let intent = self.get(id)?.ok_or(CommerceError::NotFound)?;
340        if intent.status != X402IntentStatus::Created {
341            return Err(CommerceError::ValidationError(format!(
342                "Cannot sign intent in {} status",
343                intent.status
344            )));
345        }
346
347        // Check if expired
348        let now_unix = Utc::now().timestamp() as u64;
349        if now_unix > intent.valid_until {
350            return Err(CommerceError::ValidationError("Payment intent has expired".to_string()));
351        }
352
353        // Compute signing hash and store signature
354        let hash_bytes = intent.sequencer_signing_hash();
355        let signing_hash =
356            format!("0x{}", hash_bytes.iter().map(|b| format!("{b:02x}")).collect::<String>());
357        let SignX402PaymentIntent {
358            intent_id: _,
359            signature_scheme,
360            signature,
361            public_key,
362            signature_bundle,
363            public_key_bundle,
364        } = input;
365        let signature_scheme = signature_scheme.unwrap_or_else(|| intent.signature_scheme());
366        if !intent.allows_signing_scheme(signature_scheme) {
367            return Err(CommerceError::ValidationError(format!(
368                "x402 intent requires {} signatures; refusing {} authorization for this intent",
369                intent.signature_scheme(),
370                signature_scheme
371            )));
372        }
373        let signature = (!signature.trim().is_empty()).then_some(signature);
374        let public_key = (!public_key.trim().is_empty()).then_some(public_key);
375        let signature_bundle_json = signature_bundle
376            .as_ref()
377            .map(serde_json::to_string)
378            .transpose()
379            .map_err(|e| CommerceError::ValidationError(e.to_string()))?;
380        let public_key_bundle_json = public_key_bundle
381            .as_ref()
382            .map(serde_json::to_string)
383            .transpose()
384            .map_err(|e| CommerceError::ValidationError(e.to_string()))?;
385
386        // Validate signature/public key pair against the intent hash before persisting.
387        let mut signed_intent = intent;
388        signed_intent.signing_hash = Some(signing_hash.clone());
389        signed_intent.payer_signature_scheme = Some(signature_scheme);
390        signed_intent.payer_signature = signature.clone();
391        signed_intent.payer_public_key = public_key.clone();
392        signed_intent.payer_signature_bundle = signature_bundle;
393        signed_intent.payer_public_key_bundle = public_key_bundle;
394
395        let is_valid_signature = signed_intent.verify_signature().unwrap_or(false);
396        if !is_valid_signature {
397            return Err(CommerceError::ValidationError(
398                "Invalid x402 signature for payment intent".to_string(),
399            ));
400        }
401
402        conn.execute(
403            "UPDATE x402_payment_intents SET
404                status = ?, signing_hash = ?, payer_signature_scheme = ?, payer_signature = ?, payer_public_key = ?,
405                payer_signature_bundle = ?, payer_public_key_bundle = ?, updated_at = ?
406             WHERE id = ?",
407            rusqlite::params![
408                X402IntentStatus::Signed.to_string(),
409                signing_hash,
410                signature_scheme.to_string(),
411                signature,
412                public_key,
413                signature_bundle_json,
414                public_key_bundle_json,
415                Utc::now().to_rfc3339(),
416                id.to_string(),
417            ],
418        )
419        .map_err(map_db_error)?;
420
421        self.get(id)?.ok_or(CommerceError::NotFound)
422    }
423
424    fn mark_sequenced(
425        &self,
426        id: Uuid,
427        sequence_number: u64,
428        batch_id: Uuid,
429    ) -> Result<X402PaymentIntent> {
430        let conn = self.conn()?;
431
432        let intent = self.get(id)?.ok_or(CommerceError::NotFound)?;
433        if intent.status != X402IntentStatus::Signed {
434            return Err(CommerceError::ValidationError(format!(
435                "Cannot sequence intent in {} status",
436                intent.status
437            )));
438        }
439
440        conn.execute(
441            "UPDATE x402_payment_intents SET
442                status = ?, sequence_number = ?, batch_id = ?, sequenced_at = ?, updated_at = ?
443             WHERE id = ?",
444            rusqlite::params![
445                X402IntentStatus::Sequenced.to_string(),
446                sequence_number as i64,
447                batch_id.to_string(),
448                Utc::now().to_rfc3339(),
449                Utc::now().to_rfc3339(),
450                id.to_string(),
451            ],
452        )
453        .map_err(map_db_error)?;
454
455        self.get(id)?.ok_or(CommerceError::NotFound)
456    }
457
458    fn mark_settled(
459        &self,
460        id: Uuid,
461        tx_hash: &str,
462        block_number: u64,
463    ) -> Result<X402PaymentIntent> {
464        let conn = self.conn()?;
465        let intent = self.get(id)?.ok_or(CommerceError::NotFound)?;
466
467        if intent.status != X402IntentStatus::Sequenced {
468            return Err(CommerceError::ValidationError(format!(
469                "Cannot settle intent in {} status",
470                intent.status
471            )));
472        }
473
474        conn.execute(
475            "UPDATE x402_payment_intents SET
476                status = ?, tx_hash = ?, block_number = ?, settled_at = ?, updated_at = ?
477             WHERE id = ?",
478            rusqlite::params![
479                X402IntentStatus::Settled.to_string(),
480                tx_hash,
481                block_number as i64,
482                Utc::now().to_rfc3339(),
483                Utc::now().to_rfc3339(),
484                id.to_string(),
485            ],
486        )
487        .map_err(map_db_error)?;
488
489        self.get(id)?.ok_or(CommerceError::NotFound)
490    }
491
492    fn mark_failed(&self, id: Uuid, reason: &str) -> Result<X402PaymentIntent> {
493        let conn = self.conn()?;
494
495        conn.execute(
496            "UPDATE x402_payment_intents SET
497                status = ?, metadata = json_set(COALESCE(metadata, '{}'), '$.failure_reason', ?), updated_at = ?
498             WHERE id = ?",
499            rusqlite::params![
500                X402IntentStatus::Failed.to_string(),
501                reason,
502                Utc::now().to_rfc3339(),
503                id.to_string(),
504            ],
505        )
506        .map_err(map_db_error)?;
507
508        self.get(id)?.ok_or(CommerceError::NotFound)
509    }
510
511    fn mark_expired(&self, id: Uuid) -> Result<X402PaymentIntent> {
512        let conn = self.conn()?;
513
514        conn.execute(
515            "UPDATE x402_payment_intents SET status = ?, updated_at = ? WHERE id = ?",
516            rusqlite::params![
517                X402IntentStatus::Expired.to_string(),
518                Utc::now().to_rfc3339(),
519                id.to_string(),
520            ],
521        )
522        .map_err(map_db_error)?;
523
524        self.get(id)?.ok_or(CommerceError::NotFound)
525    }
526
527    fn cancel(&self, id: Uuid) -> Result<X402PaymentIntent> {
528        let conn = self.conn()?;
529
530        let intent = self.get(id)?.ok_or(CommerceError::NotFound)?;
531        if !matches!(intent.status, X402IntentStatus::Created | X402IntentStatus::Signed) {
532            return Err(CommerceError::ValidationError(format!(
533                "Cannot cancel intent in {} status",
534                intent.status
535            )));
536        }
537
538        conn.execute(
539            "UPDATE x402_payment_intents SET status = ?, updated_at = ? WHERE id = ?",
540            rusqlite::params![
541                X402IntentStatus::Cancelled.to_string(),
542                Utc::now().to_rfc3339(),
543                id.to_string(),
544            ],
545        )
546        .map_err(map_db_error)?;
547
548        self.get(id)?.ok_or(CommerceError::NotFound)
549    }
550
551    fn for_cart(&self, cart_id: Uuid) -> Result<Vec<X402PaymentIntent>> {
552        let conn = self.conn()?;
553        let mut stmt = conn
554            .prepare(
555                "SELECT * FROM x402_payment_intents WHERE cart_id = ? ORDER BY created_at DESC",
556            )
557            .map_err(map_db_error)?;
558
559        let rows =
560            stmt.query_map([cart_id.to_string()], Self::row_to_intent).map_err(map_db_error)?;
561        let mut results = Vec::new();
562        for row in rows {
563            results.push(row.map_err(map_db_error)?);
564        }
565        Ok(results)
566    }
567
568    fn for_order(&self, order_id: Uuid) -> Result<Vec<X402PaymentIntent>> {
569        let conn = self.conn()?;
570        let mut stmt = conn
571            .prepare(
572                "SELECT * FROM x402_payment_intents WHERE order_id = ? ORDER BY created_at DESC",
573            )
574            .map_err(map_db_error)?;
575
576        let rows =
577            stmt.query_map([order_id.to_string()], Self::row_to_intent).map_err(map_db_error)?;
578        let mut results = Vec::new();
579        for row in rows {
580            results.push(row.map_err(map_db_error)?);
581        }
582        Ok(results)
583    }
584
585    fn get_next_nonce(&self, payer_address: &str) -> Result<u64> {
586        let conn = self.conn()?;
587        let max_nonce: Option<i64> = conn
588            .query_row(
589                "SELECT MAX(nonce) FROM x402_payment_intents WHERE payer_address = ?",
590                [payer_address],
591                |row| row.get(0),
592            )
593            .map_err(map_db_error)?;
594
595        Ok(max_nonce.map_or(0, |n| n as u64 + 1))
596    }
597
598    fn list(&self, filter: X402PaymentIntentFilter) -> Result<Vec<X402PaymentIntent>> {
599        let conn = self.conn()?;
600        let mut conditions = vec!["1=1".to_string()];
601        let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![];
602
603        if let Some(ref payer) = filter.payer_address {
604            conditions.push("payer_address = ?".to_string());
605            params.push(Box::new(payer.clone()));
606        }
607        if let Some(ref payee) = filter.payee_address {
608            conditions.push("payee_address = ?".to_string());
609            params.push(Box::new(payee.clone()));
610        }
611        if let Some(ref status) = filter.status {
612            conditions.push("status = ?".to_string());
613            params.push(Box::new(status.to_string()));
614        }
615        if let Some(ref network) = filter.network {
616            conditions.push("network = ?".to_string());
617            params.push(Box::new(network.to_string()));
618        }
619        if let Some(ref asset) = filter.asset {
620            conditions.push("asset = ?".to_string());
621            params.push(Box::new(asset.to_string().to_lowercase()));
622        }
623        if let Some(order_id) = filter.order_id {
624            conditions.push("order_id = ?".to_string());
625            params.push(Box::new(order_id.to_string()));
626        }
627        if let Some(batch_id) = filter.batch_id {
628            conditions.push("batch_id = ?".to_string());
629            params.push(Box::new(batch_id.to_string()));
630        }
631        if let Some(ref from) = filter.from_date {
632            conditions.push("created_at >= ?".to_string());
633            params.push(Box::new(from.to_rfc3339()));
634        }
635        if let Some(ref to) = filter.to_date {
636            conditions.push("created_at <= ?".to_string());
637            params.push(Box::new(to.to_rfc3339()));
638        }
639
640        let limit = filter.limit.unwrap_or(100).min(1000);
641        let offset = filter.offset.unwrap_or(0);
642
643        let sql = format!(
644            "SELECT * FROM x402_payment_intents WHERE {} ORDER BY created_at DESC LIMIT ? OFFSET ?",
645            conditions.join(" AND ")
646        );
647        params.push(Box::new(i64::from(limit)));
648        params.push(Box::new(i64::from(offset)));
649
650        let param_refs = params_refs(&params);
651        let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
652
653        let rows = stmt
654            .query_map(rusqlite::params_from_iter(param_refs), Self::row_to_intent)
655            .map_err(map_db_error)?;
656        let mut results = Vec::new();
657        for row in rows {
658            results.push(row.map_err(map_db_error)?);
659        }
660        Ok(results)
661    }
662
663    fn count(&self, filter: X402PaymentIntentFilter) -> Result<u64> {
664        let conn = self.conn()?;
665        let mut conditions = vec!["1=1".to_string()];
666        let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![];
667
668        if let Some(ref payer) = filter.payer_address {
669            conditions.push("payer_address = ?".to_string());
670            params.push(Box::new(payer.clone()));
671        }
672        if let Some(ref payee) = filter.payee_address {
673            conditions.push("payee_address = ?".to_string());
674            params.push(Box::new(payee.clone()));
675        }
676        if let Some(ref status) = filter.status {
677            conditions.push("status = ?".to_string());
678            params.push(Box::new(status.to_string()));
679        }
680        if let Some(ref network) = filter.network {
681            conditions.push("network = ?".to_string());
682            params.push(Box::new(network.to_string()));
683        }
684        if let Some(ref asset) = filter.asset {
685            conditions.push("asset = ?".to_string());
686            params.push(Box::new(asset.to_string().to_lowercase()));
687        }
688        // Mirror `list`'s remaining predicates so a filtered count matches the
689        // filtered list (and Postgres, which applies all of these in count).
690        if let Some(order_id) = filter.order_id {
691            conditions.push("order_id = ?".to_string());
692            params.push(Box::new(order_id.to_string()));
693        }
694        if let Some(batch_id) = filter.batch_id {
695            conditions.push("batch_id = ?".to_string());
696            params.push(Box::new(batch_id.to_string()));
697        }
698        if let Some(ref from) = filter.from_date {
699            conditions.push("created_at >= ?".to_string());
700            params.push(Box::new(from.to_rfc3339()));
701        }
702        if let Some(ref to) = filter.to_date {
703            conditions.push("created_at <= ?".to_string());
704            params.push(Box::new(to.to_rfc3339()));
705        }
706
707        let sql =
708            format!("SELECT COUNT(*) FROM x402_payment_intents WHERE {}", conditions.join(" AND "));
709
710        let param_refs = params_refs(&params);
711        let count: i64 = conn
712            .query_row(&sql, rusqlite::params_from_iter(param_refs), |row| row.get(0))
713            .map_err(map_db_error)?;
714
715        Ok(count as u64)
716    }
717
718    fn expire_stale_intents(&self) -> Result<u64> {
719        let conn = self.conn()?;
720        let now_unix = Utc::now().timestamp();
721
722        let count = conn
723            .execute(
724                "UPDATE x402_payment_intents SET status = ?, updated_at = ?
725             WHERE status IN (?, ?) AND valid_until < ?",
726                rusqlite::params![
727                    X402IntentStatus::Expired.to_string(),
728                    Utc::now().to_rfc3339(),
729                    X402IntentStatus::Created.to_string(),
730                    X402IntentStatus::Signed.to_string(),
731                    now_unix,
732                ],
733            )
734            .map_err(map_db_error)?;
735
736        Ok(count as u64)
737    }
738
739    fn create_batch(
740        &self,
741        inputs: Vec<CreateX402PaymentIntent>,
742    ) -> Result<BatchResult<X402PaymentIntent>> {
743        validate_batch_size(&inputs)?;
744        let mut result = BatchResult::with_capacity(inputs.len());
745
746        for (idx, input) in inputs.into_iter().enumerate() {
747            match self.create(input) {
748                Ok(intent) => result.record_success(intent),
749                Err(e) => result.record_failure(idx, None, &e),
750            }
751        }
752
753        Ok(result)
754    }
755
756    fn create_batch_atomic(
757        &self,
758        inputs: Vec<CreateX402PaymentIntent>,
759    ) -> Result<Vec<X402PaymentIntent>> {
760        validate_batch_size(&inputs)?;
761        if inputs.is_empty() {
762            return Ok(vec![]);
763        }
764
765        let mut conn = self.conn()?;
766        let tx =
767            conn.transaction_with_behavior(TransactionBehavior::Immediate).map_err(map_db_error)?;
768        let mut ids = Vec::with_capacity(inputs.len());
769        let mut next_nonce_by_payer: HashMap<String, u64> = HashMap::new();
770
771        for input in inputs {
772            Self::validate_input(&input)?;
773            let now = Utc::now();
774            let now_unix = now.timestamp() as u64;
775            let id = Uuid::new_v4();
776            let validity_seconds = input.validity_seconds.unwrap_or(X402_DEFAULT_VALIDITY_SECONDS);
777            let valid_until = now_unix + validity_seconds;
778
779            let asset = input.asset;
780            let network = input.network;
781            let chain_id = network.chain_id();
782            let token_address = asset.contract_address(network).map(String::from);
783            let decimals = asset.decimals();
784            let divisor = 10u64.pow(u32::from(decimals));
785            let amount_decimal = Decimal::from(input.amount) / Decimal::from(divisor);
786            let payer_address = input.payer_address;
787            let nonce = match input.nonce {
788                Some(n) => n,
789                None => {
790                    if let Some(next_nonce) = next_nonce_by_payer.get_mut(&payer_address) {
791                        let allocated = *next_nonce;
792                        *next_nonce += 1;
793                        allocated
794                    } else {
795                        let next_nonce = Self::get_next_nonce_in_tx(&tx, &payer_address)?;
796                        next_nonce_by_payer.insert(payer_address.clone(), next_nonce + 1);
797                        next_nonce
798                    }
799                }
800            };
801
802            tx.execute(
803                "INSERT INTO x402_payment_intents (
804                    id, version, status, payer_address, payee_address, amount, amount_decimal,
805                    asset, network, chain_id, token_address, created_at_unix, valid_until, nonce,
806                    idempotency_key, resource_uri, resource_method, description, cart_id, order_id,
807                    invoice_id, merchant_id, metadata, created_at, updated_at
808                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
809                rusqlite::params![
810                    id.to_string(),
811                    "1.0",
812                    X402IntentStatus::Created.to_string(),
813                    payer_address,
814                    input.payee_address,
815                    input.amount as i64,
816                    amount_decimal.to_string(),
817                    asset.to_string().to_lowercase(),
818                    network.to_string(),
819                    chain_id as i64,
820                    token_address,
821                    now_unix as i64,
822                    valid_until as i64,
823                    nonce as i64,
824                    input.idempotency_key,
825                    input.resource_uri,
826                    input.resource_method,
827                    input.description,
828                    input.cart_id.map(|id| id.to_string()),
829                    input.order_id.map(|id| id.to_string()),
830                    input.invoice_id.map(|id| id.to_string()),
831                    input.merchant_id,
832                    input.metadata,
833                    now.to_rfc3339(),
834                    now.to_rfc3339(),
835                ],
836            ).map_err(map_db_error)?;
837
838            ids.push(id);
839        }
840
841        tx.commit().map_err(map_db_error)?;
842
843        ids.into_iter().map(|id| self.get(id)?.ok_or(CommerceError::NotFound)).collect()
844    }
845
846    fn get_batch(&self, ids: Vec<Uuid>) -> Result<Vec<X402PaymentIntent>> {
847        if ids.is_empty() {
848            return Ok(vec![]);
849        }
850        validate_batch_size(&ids)?;
851
852        let conn = self.conn()?;
853        let placeholders = build_in_clause(ids.len());
854        let sql = format!("SELECT * FROM x402_payment_intents WHERE id IN ({placeholders})");
855
856        let params = uuid_params(&ids);
857        let param_refs = params_refs(&params);
858        let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
859
860        let rows = stmt
861            .query_map(rusqlite::params_from_iter(param_refs), Self::row_to_intent)
862            .map_err(map_db_error)?;
863        let mut results = Vec::new();
864        for row in rows {
865            results.push(row.map_err(map_db_error)?);
866        }
867        Ok(results)
868    }
869}