1use 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#[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 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 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 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 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 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(¶ms);
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 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(¶ms);
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(¶ms);
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}