From f608eb953f20746337a85bb3204c6397d614847b Mon Sep 17 00:00:00 2001 From: wisdom518 <2393347493@qq.com> Date: Wed, 1 Jul 2026 14:24:49 +0800 Subject: [PATCH] feat: add task search and filtering --- Documents/Task Bounty/API.md | 86 ++++++++++++++++++++++ Documents/Task Bounty/src/lib.rs | 35 ++++++++- Documents/Task Bounty/src/query.rs | 113 +++++++++++++++++++++++++++++ Documents/Task Bounty/src/test.rs | 91 +++++++++++++++++++++-- 4 files changed, 317 insertions(+), 8 deletions(-) create mode 100644 Documents/Task Bounty/src/query.rs diff --git a/Documents/Task Bounty/API.md b/Documents/Task Bounty/API.md index dd5d0b7..98ac287 100644 --- a/Documents/Task Bounty/API.md +++ b/Documents/Task Bounty/API.md @@ -10,6 +10,7 @@ Complete API reference for TaskBounty smart contracts. - [Events](#events) - [Errors](#errors) - [Data Structures](#data-structures) +- [Query Helpers](#query-helpers) --- @@ -265,6 +266,78 @@ console.log(task.reward); --- +#### `getAllTasks` +```solidity +function getAllTasks() + external + view + returns (Task[] memory tasks) +``` + +Return every task in creation order. + +--- + +#### `getTasksByStatus` +```solidity +function getTasksByStatus(TaskStatus status) + external + view + returns (Task[] memory tasks) +``` + +Return tasks that match the provided status. + +--- + +#### `getTasksByReward` +```solidity +function getTasksByReward(uint256 reward) + external + view + returns (Task[] memory tasks) +``` + +Return tasks whose reward matches the provided amount. + +--- + +#### `getTasksByMinReward` +```solidity +function getTasksByMinReward(uint256 minReward) + external + view + returns (Task[] memory tasks) +``` + +Return tasks whose reward is at least the provided amount. + +--- + +#### `getTasksBeforeDeadline` +```solidity +function getTasksBeforeDeadline(uint256 deadline) + external + view + returns (Task[] memory tasks) +``` + +Return tasks whose deadline is on or before the cutoff. + +--- + +#### `searchTasks` +```solidity +function searchTasks(string calldata query) + external + view + returns (Task[] memory tasks) +``` + +Return tasks whose title or description contains the query string. + +--- + #### `getSubmission` ```solidity function getSubmission(uint256 submissionId) @@ -858,4 +931,17 @@ resolver.resolveDispute(disputeId, true); --- +## Query Helpers + +The contract exposes read-only helpers for surfacing tasks in a UI: + +- `getAllTasks()` +- `getTasksByStatus(status)` +- `getTasksByReward(reward)` +- `getTasksByMinReward(minReward)` +- `getTasksBeforeDeadline(deadline)` +- `searchTasks(query)` + +--- + For more examples, see the [README.md](README.md) and test files in `test/`. diff --git a/Documents/Task Bounty/src/lib.rs b/Documents/Task Bounty/src/lib.rs index 1bec81c..f0d309d 100644 --- a/Documents/Task Bounty/src/lib.rs +++ b/Documents/Task Bounty/src/lib.rs @@ -1,5 +1,4 @@ #![no_std] - //! # TaskBounty - Decentralized Task & Reward Board //! //! A Soroban smart contract for trustless task management and bounty payments on Stellar. @@ -11,10 +10,12 @@ //! - Automatic payouts //! - Dispute resolution //! - Multi-token support (XLM and SAC tokens) +extern crate alloc; mod types; mod storage; mod task; +mod query; mod submission; mod dispute; mod events; @@ -23,7 +24,7 @@ mod events; mod test; use soroban_sdk::{contract, contractimpl, Address, Env, String, Vec}; -use types::{Task, Submission, TaskStatus, SubmissionStatus}; +use types::{Task, Submission, TaskStatus}; #[contract] pub struct TaskBountyContract; @@ -178,6 +179,36 @@ impl TaskBountyContract { storage::get_task(&env, task_id) } + /// Get every task in creation order. + pub fn get_all_tasks(env: Env) -> Vec { + query::get_all_tasks(&env) + } + + /// Filter tasks by status. + pub fn get_tasks_by_status(env: Env, status: TaskStatus) -> Vec { + query::get_tasks_by_status(&env, status) + } + + /// Filter tasks by exact reward amount. + pub fn get_tasks_by_reward(env: Env, reward: i128) -> Vec { + query::get_tasks_by_reward(&env, reward) + } + + /// Filter tasks by minimum reward amount. + pub fn get_tasks_by_min_reward(env: Env, min_reward: i128) -> Vec { + query::get_tasks_by_min_reward(&env, min_reward) + } + + /// Filter tasks whose deadline is on or before the provided cutoff. + pub fn get_tasks_before_deadline(env: Env, deadline: u64) -> Vec { + query::get_tasks_before_deadline(&env, deadline) + } + + /// Search task titles and descriptions for a query string. + pub fn search_tasks(env: Env, query_str: String) -> Vec { + query::search_tasks(&env, query_str) + } + /// Get submission details /// /// # Arguments diff --git a/Documents/Task Bounty/src/query.rs b/Documents/Task Bounty/src/query.rs new file mode 100644 index 0000000..f71e090 --- /dev/null +++ b/Documents/Task Bounty/src/query.rs @@ -0,0 +1,113 @@ +use alloc::vec::Vec as StdVec; + +use crate::{storage, types::{Task, TaskStatus}}; +use soroban_sdk::{Env, String, Vec}; + +fn all_tasks(env: &Env) -> Vec { + let count = storage::get_task_counter(env); + let mut tasks: Vec = Vec::new(env); + + if count == 0 { + return tasks; + } + + for task_id in 1..=count { + if storage::task_exists(env, task_id) { + tasks.push_back(storage::get_task(env, task_id)); + } + } + + tasks +} + +fn string_to_bytes(value: &String) -> StdVec { + let len = value.len() as usize; + let mut bytes = StdVec::with_capacity(len); + bytes.resize(len, 0); + value.copy_into_slice(&mut bytes[..]); + bytes +} + +fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool { + if needle.is_empty() { + return true; + } + + if needle.len() > haystack.len() { + return false; + } + + haystack.windows(needle.len()).any(|window| window == needle) +} + +fn task_matches_query(task: &Task, query: &String) -> bool { + let query_bytes = string_to_bytes(query); + let title_bytes = string_to_bytes(&task.title); + let description_bytes = string_to_bytes(&task.description); + + contains_bytes(&title_bytes, &query_bytes) || contains_bytes(&description_bytes, &query_bytes) +} + +pub fn get_all_tasks(env: &Env) -> Vec { + all_tasks(env) +} + +pub fn get_tasks_by_status(env: &Env, status: TaskStatus) -> Vec { + let mut results: Vec = Vec::new(env); + + for task in all_tasks(env).iter() { + if task.status == status { + results.push_back(task.clone()); + } + } + + results +} + +pub fn get_tasks_by_reward(env: &Env, reward: i128) -> Vec { + let mut results: Vec = Vec::new(env); + + for task in all_tasks(env).iter() { + if task.reward == reward { + results.push_back(task.clone()); + } + } + + results +} + +pub fn get_tasks_by_min_reward(env: &Env, min_reward: i128) -> Vec { + let mut results: Vec = Vec::new(env); + + for task in all_tasks(env).iter() { + if task.reward >= min_reward { + results.push_back(task.clone()); + } + } + + results +} + +pub fn get_tasks_before_deadline(env: &Env, deadline: u64) -> Vec { + let mut results: Vec = Vec::new(env); + + for task in all_tasks(env).iter() { + if task.deadline <= deadline { + results.push_back(task.clone()); + } + } + + results +} + +pub fn search_tasks(env: &Env, query: String) -> Vec { + let mut results: Vec = Vec::new(env); + + for task in all_tasks(env).iter() { + if task_matches_query(&task, &query) { + results.push_back(task.clone()); + } + } + + results +} diff --git a/Documents/Task Bounty/src/test.rs b/Documents/Task Bounty/src/test.rs index 38dc888..8013273 100644 --- a/Documents/Task Bounty/src/test.rs +++ b/Documents/Task Bounty/src/test.rs @@ -2,17 +2,17 @@ use super::*; use soroban_sdk::{ - testutils::{Address as _, Ledger, LedgerInfo}, + testutils::{Address as _, Ledger}, token, Address, Env, String, }; use types::{TaskStatus, SubmissionStatus}; -fn create_token_contract<'a>(env: &Env, admin: &Address) -> token::Client<'a> { - let token_address = env.register_stellar_asset_contract(admin.clone()); - token::Client::new(env, &token_address) +fn create_token_contract<'a>(env: &Env, admin: &Address) -> token::StellarAssetClient<'a> { + let token_contract = env.register_stellar_asset_contract_v2(admin.clone()); + token::StellarAssetClient::new(env, &token_contract.address()) } -fn setup_test() -> (Env, Address, Address, Address, token::Client<'static>, Address) { +fn setup_test() -> (Env, Address, Address, Address, token::StellarAssetClient<'static>, Address) { let env = Env::default(); env.mock_all_auths(); @@ -49,7 +49,7 @@ fn test_create_task() { &poster, &title, &description, - &token_client.address, + &token_client.address(), &reward, &deadline, &3, @@ -420,3 +420,82 @@ fn test_get_total_tasks() { assert_eq!(client.get_total_tasks(), 2); } + +#[test] +fn test_task_search_and_filtering() { + let (env, poster, contributor, _, token_client, contract_id) = setup_test(); + let client = TaskBountyContractClient::new(&env, &contract_id); + + let base_deadline = env.ledger().timestamp(); + + let first_id = client.create_task( + &poster, + &String::from_str(&env, "Write docs"), + &String::from_str(&env, "Document the API surface"), + &token_client.address(), + &10_000_000, + &(base_deadline + 3_600), + &2, + ); + + let second_id = client.create_task( + &poster, + &String::from_str(&env, "Build dashboard"), + &String::from_str(&env, "Filter tasks by status and reward"), + &token_client.address(), + &50_000_000, + &(base_deadline + 7_200), + &2, + ); + + let third_id = client.create_task( + &poster, + &String::from_str(&env, "Fix payout"), + &String::from_str(&env, "Verify deadline handling"), + &token_client.address, + &50_000_000, + &(base_deadline + 3_900), + &2, + ); + + let submission_id = client.submit_work( + &second_id, + &contributor, + &String::from_str(&env, "ipfs://dashboard"), + &String::from_str(&env, "First pass implementation"), + ); + + assert_eq!(client.get_all_tasks().len(), 3); + + let open_tasks = client.get_tasks_by_status(&TaskStatus::Open); + assert_eq!(open_tasks.len(), 2); + assert_eq!(open_tasks.get(0).unwrap().id, first_id); + assert_eq!(open_tasks.get(1).unwrap().id, third_id); + + let in_progress = client.get_tasks_by_status(&TaskStatus::InProgress); + assert_eq!(in_progress.len(), 1); + assert_eq!(in_progress.get(0).unwrap().id, second_id); + + let reward_matches = client.get_tasks_by_reward(&50_000_000); + assert_eq!(reward_matches.len(), 2); + assert_eq!(reward_matches.get(0).unwrap().id, second_id); + assert_eq!(reward_matches.get(1).unwrap().id, third_id); + + let min_reward = client.get_tasks_by_min_reward(&50_000_000); + assert_eq!(min_reward.len(), 2); + + let deadline_matches = client.get_tasks_before_deadline(&(base_deadline + 4_000)); + assert_eq!(deadline_matches.len(), 2); + assert_eq!(deadline_matches.get(0).unwrap().id, first_id); + assert_eq!(deadline_matches.get(1).unwrap().id, third_id); + + let title_search = client.search_tasks(&String::from_str(&env, "dashboard")); + assert_eq!(title_search.len(), 1); + assert_eq!(title_search.get(0).unwrap().id, second_id); + + let description_search = client.search_tasks(&String::from_str(&env, "deadline handling")); + assert_eq!(description_search.len(), 1); + assert_eq!(description_search.get(0).unwrap().id, third_id); + + let _ = submission_id; +}