diff --git a/crates/guest/assessor/assessor-guest/Cargo.lock b/crates/guest/assessor/assessor-guest/Cargo.lock index 55fc9cc624..1cce131364 100644 --- a/crates/guest/assessor/assessor-guest/Cargo.lock +++ b/crates/guest/assessor/assessor-guest/Cargo.lock @@ -1944,12 +1944,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cexpr" version = "0.6.0" @@ -2061,16 +2055,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -2909,11 +2893,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", "wasi 0.14.3+wasi-0.2.4", - "wasm-bindgen", ] [[package]] @@ -3453,28 +3435,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - [[package]] name = "jobserver" version = "0.1.34" @@ -3651,12 +3611,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "macro-string" version = "0.1.4" @@ -4310,62 +4264,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.1", - "rustls", - "socket2", - "thiserror 2.0.16", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash 2.1.1", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.16", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.52.0", -] - [[package]] name = "quote" version = "1.0.40" @@ -4579,22 +4477,16 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper", - "hyper-rustls", "hyper-util", "js-sys", "log", "mime_guess", "percent-encoding", "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", - "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -5129,37 +5021,9 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "web-time", "zeroize", ] -[[package]] -name = "rustls-platform-verifier" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework 3.3.0", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - [[package]] name = "rustls-webpki" version = "0.103.4" @@ -5215,15 +5079,6 @@ dependencies = [ "yaml-rust2", ] -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "schannel" version = "0.1.27" @@ -5818,21 +5673,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.47.1" @@ -6231,16 +6071,6 @@ dependencies = [ "libc", ] -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -6373,25 +6203,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "which" version = "4.4.2" @@ -6404,15 +6215,6 @@ dependencies = [ "rustix 0.38.44", ] -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "windows-core" version = "0.61.2" @@ -6472,15 +6274,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -6508,21 +6301,6 @@ dependencies = [ "windows-targets 0.53.3", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -6556,12 +6334,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6574,12 +6346,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6592,12 +6358,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6622,12 +6382,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6640,12 +6394,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6658,12 +6406,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6676,12 +6418,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/crates/indexer/migrations/42_request_status_status_created_index.sql b/crates/indexer/migrations/42_request_status_status_created_index.sql new file mode 100644 index 0000000000..e53d87cbc2 --- /dev/null +++ b/crates/indexer/migrations/42_request_status_status_created_index.sql @@ -0,0 +1,4 @@ +-- Index for filtering requests by status sorted by created_at +-- Complements idx_request_status_status_updated which sorts by updated_at +CREATE INDEX IF NOT EXISTS idx_request_status_status_created + ON request_status (request_status, created_at DESC); \ No newline at end of file diff --git a/crates/indexer/src/db/market.rs b/crates/indexer/src/db/market.rs index 2fddbb0ce8..591a43d345 100644 --- a/crates/indexer/src/db/market.rs +++ b/crates/indexer/src/db/market.rs @@ -860,6 +860,7 @@ pub trait IndexerDb { cursor: Option, limit: u32, sort_by: RequestSortField, + status: Option, ) -> Result<(Vec, Option), DbError>; async fn get_requests_by_request_id( @@ -3412,37 +3413,72 @@ impl IndexerDb for MarketDb { cursor: Option, limit: u32, sort_by: RequestSortField, + status: Option, ) -> Result<(Vec, Option), DbError> { let sort_field = match sort_by { RequestSortField::UpdatedAt => "updated_at", RequestSortField::CreatedAt => "created_at", }; - let rows = if let Some(c) = &cursor { - let query_str = format!( - "SELECT * FROM request_status - WHERE {} < $1 OR ({} = $1 AND request_digest < $2) - ORDER BY {} DESC, request_digest DESC - LIMIT $3", - sort_field, sort_field, sort_field - ); - sqlx::query(&query_str) - .bind(c.timestamp as i64) - .bind(&c.request_digest) - .bind(limit as i64) - .fetch_all(&self.pool) - .await? - } else { - let query_str = format!( - "SELECT * FROM request_status - ORDER BY {} DESC, request_digest DESC - LIMIT $1", - sort_field - ); - sqlx::query(&query_str).bind(limit as i64).fetch_all(&self.pool).await? + let rows = match (&cursor, &status) { + (Some(c), Some(s)) => { + let query_str = format!( + "SELECT * FROM request_status + WHERE request_status = $1 + AND ({} < $2 OR ({} = $2 AND request_digest < $3)) + ORDER BY {} DESC, request_digest DESC + LIMIT $4", + sort_field, sort_field, sort_field + ); + sqlx::query(&query_str) + .bind(s.to_string()) + .bind(c.timestamp as i64) + .bind(&c.request_digest) + .bind(limit as i64) + .fetch_all(&self.pool) + .await? + } + (Some(c), None) => { + let query_str = format!( + "SELECT * FROM request_status + WHERE {} < $1 OR ({} = $1 AND request_digest < $2) + ORDER BY {} DESC, request_digest DESC + LIMIT $3", + sort_field, sort_field, sort_field + ); + sqlx::query(&query_str) + .bind(c.timestamp as i64) + .bind(&c.request_digest) + .bind(limit as i64) + .fetch_all(&self.pool) + .await? + } + (None, Some(s)) => { + let query_str = format!( + "SELECT * FROM request_status + WHERE request_status = $1 + ORDER BY {} DESC, request_digest DESC + LIMIT $2", + sort_field + ); + sqlx::query(&query_str) + .bind(s.to_string()) + .bind(limit as i64) + .fetch_all(&self.pool) + .await? + } + (None, None) => { + let query_str = format!( + "SELECT * FROM request_status + ORDER BY {} DESC, request_digest DESC + LIMIT $1", + sort_field + ); + sqlx::query(&query_str).bind(limit as i64).fetch_all(&self.pool).await? + } }; - let mut results = Vec::new(); + let mut results = Vec::new(); for row in rows { results.push(self.row_to_request_status(&row)?); } @@ -3453,7 +3489,7 @@ impl IndexerDb for MarketDb { RequestSortField::UpdatedAt => r.updated_at, RequestSortField::CreatedAt => r.created_at, }; - RequestCursor { timestamp, request_digest: r.request_digest.to_string() } + RequestCursor { timestamp, request_digest: format!("{:x}", r.request_digest) } }) } else { None @@ -5910,6 +5946,192 @@ mod tests { assert_eq!(count, 0); } + #[sqlx::test(migrations = "./migrations")] + async fn test_list_requests_with_status_filter(pool: sqlx::PgPool) { + let test_db = test_db(pool).await; + let db: DbObj = test_db.db; + + let base_ts = 1700000000u64; + + // Create requests with different statuses + let mut status1 = create_test_status(B256::from([1; 32]), RequestStatusType::Submitted); + status1.created_at = base_ts; + status1.updated_at = base_ts; + + let mut status2 = create_test_status(B256::from([2; 32]), RequestStatusType::Locked); + status2.created_at = base_ts + 100; + status2.updated_at = base_ts + 100; + status2.lock_prover_address = Some(Address::from([0xAA; 20])); + status2.locked_at = Some(base_ts + 100); + + let mut status3 = create_test_status(B256::from([3; 32]), RequestStatusType::Fulfilled); + status3.created_at = base_ts + 200; + status3.updated_at = base_ts + 200; + status3.fulfill_prover_address = Some(Address::from([0xBB; 20])); + status3.fulfilled_at = Some(base_ts + 200); + + let mut status4 = create_test_status(B256::from([4; 32]), RequestStatusType::Expired); + status4.created_at = base_ts + 300; + status4.updated_at = base_ts + 300; + + let mut status5 = create_test_status(B256::from([5; 32]), RequestStatusType::Submitted); + status5.created_at = base_ts + 400; + status5.updated_at = base_ts + 400; + + db.upsert_request_statuses(&[ + status1.clone(), + status2.clone(), + status3.clone(), + status4.clone(), + status5.clone(), + ]) + .await + .unwrap(); + + // Filter by Submitted status + let (results, _) = db + .list_requests( + None, + 10, + RequestSortField::CreatedAt, + Some(RequestStatusType::Submitted), + ) + .await + .unwrap(); + assert_eq!(results.len(), 2); + assert!(results.iter().all(|r| r.request_status == RequestStatusType::Submitted)); + + // Filter by Locked status + let (results, _) = db + .list_requests(None, 10, RequestSortField::CreatedAt, Some(RequestStatusType::Locked)) + .await + .unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].request_status, RequestStatusType::Locked); + + // Filter by Fulfilled status + let (results, _) = db + .list_requests( + None, + 10, + RequestSortField::CreatedAt, + Some(RequestStatusType::Fulfilled), + ) + .await + .unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].request_status, RequestStatusType::Fulfilled); + + // Filter by Expired status + let (results, _) = db + .list_requests(None, 10, RequestSortField::CreatedAt, Some(RequestStatusType::Expired)) + .await + .unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].request_status, RequestStatusType::Expired); + + // No filter (all statuses) + let (results, _) = + db.list_requests(None, 10, RequestSortField::CreatedAt, None).await.unwrap(); + assert_eq!(results.len(), 5); + + // Filter with pagination - get page 1 + let (page1, cursor1) = db + .list_requests(None, 1, RequestSortField::CreatedAt, Some(RequestStatusType::Submitted)) + .await + .unwrap(); + assert_eq!(page1.len(), 1, "Page 1 should have 1 result"); + assert!(cursor1.is_some(), "Page 1 should have a cursor"); + + // Get page 2 + let (page2, cursor2) = db + .list_requests( + cursor1, + 1, + RequestSortField::CreatedAt, + Some(RequestStatusType::Submitted), + ) + .await + .unwrap(); + assert_eq!(page2.len(), 1, "Page 2 should have 1 result"); + + // Get page 3 - should be empty + let (page3, cursor3) = db + .list_requests( + cursor2, + 1, + RequestSortField::CreatedAt, + Some(RequestStatusType::Submitted), + ) + .await + .unwrap(); + assert_eq!(page3.len(), 0, "Page 3 should be empty"); + assert!(cursor3.is_none(), "Page 3 should have no cursor"); + } + + #[sqlx::test(migrations = "./migrations")] + async fn test_list_requests_pagination_same_timestamp(pool: sqlx::PgPool) { + let test_db = test_db(pool).await; + let db: DbObj = test_db.db; + + let same_ts = 1700000000u64; + + // Use digests whose first hex char is > '0' to trigger the cursor formatting bug. + // When the cursor uses 0x-prefixed hex (B256::to_string()), the SQL string comparison + // "request_digest < '0x3030...'" incorrectly excludes digests starting with '1' or '2' + // because '1' > '0' and '2' > '0' in ASCII, making them appear "greater" than the cursor. + let mut status_a = create_test_status(B256::from([0x10; 32]), RequestStatusType::Submitted); + status_a.created_at = same_ts; + status_a.updated_at = same_ts; + + let mut status_b = create_test_status(B256::from([0x20; 32]), RequestStatusType::Submitted); + status_b.created_at = same_ts; + status_b.updated_at = same_ts; + + let mut status_c = create_test_status(B256::from([0x30; 32]), RequestStatusType::Submitted); + status_c.created_at = same_ts; + status_c.updated_at = same_ts; + + db.upsert_request_statuses(&[status_a.clone(), status_b.clone(), status_c.clone()]) + .await + .unwrap(); + + // Paginate through all 3 records one at a time + let mut all_digests = Vec::new(); + let mut cursor = None; + + for page_num in 1..=4 { + let (results, next_cursor) = + db.list_requests(cursor, 1, RequestSortField::CreatedAt, None).await.unwrap(); + + if results.is_empty() { + assert!( + next_cursor.is_none(), + "Page {}: empty results should have no cursor", + page_num + ); + break; + } + + for r in &results { + all_digests.push(r.request_digest); + } + cursor = next_cursor; + } + + assert_eq!( + all_digests.len(), + 3, + "Should retrieve all 3 records via pagination, got {} (digests: {:?})", + all_digests.len(), + all_digests + ); + + // Verify all 3 distinct digests were returned + let unique: std::collections::HashSet<_> = all_digests.iter().collect(); + assert_eq!(unique.len(), 3, "All 3 records should be unique"); + } + #[sqlx::test(migrations = "./migrations")] async fn test_get_requests_comprehensive_with_multiple_fulfillments(pool: sqlx::PgPool) { let test_db = test_db(pool).await; diff --git a/crates/indexer/src/db/provers.rs b/crates/indexer/src/db/provers.rs index ab6b82cbba..dabae02d82 100644 --- a/crates/indexer/src/db/provers.rs +++ b/crates/indexer/src/db/provers.rs @@ -85,7 +85,7 @@ pub trait ProversDb: IndexerDb { RequestSortField::UpdatedAt => r.updated_at, RequestSortField::CreatedAt => r.created_at, }; - RequestCursor { timestamp, request_digest: r.request_digest.to_string() } + RequestCursor { timestamp, request_digest: format!("{:x}", r.request_digest) } }) } else { None diff --git a/crates/indexer/src/db/requestors.rs b/crates/indexer/src/db/requestors.rs index dbe7068226..0a7c7ec091 100644 --- a/crates/indexer/src/db/requestors.rs +++ b/crates/indexer/src/db/requestors.rs @@ -277,7 +277,7 @@ pub trait RequestorDb: IndexerDb { RequestSortField::UpdatedAt => r.updated_at, RequestSortField::CreatedAt => r.created_at, }; - RequestCursor { timestamp, request_digest: r.request_digest.to_string() } + RequestCursor { timestamp, request_digest: format!("{:x}", r.request_digest) } }) } else { None diff --git a/crates/lambdas/indexer-api/src/routes/market.rs b/crates/lambdas/indexer-api/src/routes/market.rs index 5957eb4aa0..b9cb8a71fd 100644 --- a/crates/lambdas/indexer-api/src/routes/market.rs +++ b/crates/lambdas/indexer-api/src/routes/market.rs @@ -32,7 +32,7 @@ use crate::{ utils::{format_eth, format_zkc, is_valid_ethereum_address}, }; use boundless_indexer::db::market::{ - RequestCursor, RequestSortField, RequestStatus, SortDirection, + RequestCursor, RequestSortField, RequestStatus, RequestStatusType, SortDirection, }; use boundless_indexer::db::{IndexerDb, ProversDb, RequestorDb}; @@ -2378,6 +2378,10 @@ pub struct RequestListParams { /// Sort field: "updated_at" or "created_at" (default "created_at") #[serde(default)] sort_by: Option, + + /// Filter by request status: "submitted", "locked", "fulfilled", or "expired" + #[serde(default)] + status: Option, } fn encode_request_cursor(cursor: &RequestCursor) -> Result { @@ -2678,7 +2682,19 @@ async fn list_requests_impl( _ => anyhow::bail!("Invalid sort_by. Must be 'updated_at' or 'created_at'"), }; - let (statuses, next_cursor) = state.market_db.list_requests(cursor, limit, sort_by).await?; + let status = match params.status.as_deref() { + Some("submitted") => Some(RequestStatusType::Submitted), + Some("locked") => Some(RequestStatusType::Locked), + Some("fulfilled") => Some(RequestStatusType::Fulfilled), + Some("expired") => Some(RequestStatusType::Expired), + None => None, + _ => anyhow::bail!( + "Invalid status. Must be 'submitted', 'locked', 'fulfilled', or 'expired'" + ), + }; + + let (statuses, next_cursor) = + state.market_db.list_requests(cursor, limit, sort_by, status).await?; let data = statuses.into_iter().map(|s| convert_request_status(s, state.chain_id)).collect::>(); diff --git a/crates/lambdas/indexer-api/tests/local_integration/market.rs b/crates/lambdas/indexer-api/tests/local_integration/market.rs index a9893081d7..0acbd635de 100644 --- a/crates/lambdas/indexer-api/tests/local_integration/market.rs +++ b/crates/lambdas/indexer-api/tests/local_integration/market.rs @@ -855,6 +855,62 @@ async fn test_market_requests_pagination() { } } +#[tokio::test] +#[cfg_attr(not(feature = "test-rpc"), ignore = "Requires BASE_MAINNET_RPC_URL")] +async fn test_market_requests_pagination_completeness() { + let env = TestEnv::market().await; + + // Fetch all requests in one go with a large limit + let all: RequestListResponse = env.get("/v1/market/requests?limit=500").await.unwrap(); + let total_count = all.data.len(); + tracing::info!("Total requests: {}", total_count); + assert!(total_count > 0, "Need at least some requests for this test"); + + let all_digests: std::collections::HashSet = + all.data.iter().map(|r| r.request_digest.clone()).collect(); + + // Paginate through with limit=1 and collect all digests + let mut paginated_digests = Vec::new(); + let mut cursor: Option = None; + let max_pages = total_count + 5; + + for _ in 0..max_pages { + let url = match &cursor { + Some(c) => format!("/v1/market/requests?limit=1&cursor={}", c), + None => "/v1/market/requests?limit=1".to_string(), + }; + let page: RequestListResponse = env.get(&url).await.unwrap(); + + for r in &page.data { + paginated_digests.push(r.request_digest.clone()); + } + + if !page.has_more { + break; + } + cursor = page.next_cursor; + } + + let paginated_set: std::collections::HashSet = + paginated_digests.iter().cloned().collect(); + + assert_eq!( + paginated_digests.len(), + paginated_set.len(), + "Pagination should not return duplicate records" + ); + + assert_eq!( + paginated_set.len(), + all_digests.len(), + "Paginating one-by-one should return the same number of records as a bulk fetch. \ + Got {} via pagination vs {} via bulk. Missing: {:?}", + paginated_set.len(), + all_digests.len(), + all_digests.difference(&paginated_set).collect::>() + ); +} + #[tokio::test] #[cfg_attr(not(feature = "test-rpc"), ignore = "Requires BASE_MAINNET_RPC_URL")] async fn test_market_requests_sorting() {