From 6a2d01f23ee63a66c46dcbfdd65efc4b2a50aed0 Mon Sep 17 00:00:00 2001 From: wisdom518 <2393347493@qq.com> Date: Wed, 1 Jul 2026 14:39:33 +0800 Subject: [PATCH] add task categories and tags --- Documents/Task Bounty/API.md | 40 ++++++++++ Documents/Task Bounty/src/dispute.rs | 2 +- Documents/Task Bounty/src/events.rs | 2 +- Documents/Task Bounty/src/lib.rs | 37 +++++++++- Documents/Task Bounty/src/query.rs | 55 ++++++++++++++ Documents/Task Bounty/src/storage.rs | 28 +++---- Documents/Task Bounty/src/submission.rs | 2 +- Documents/Task Bounty/src/task.rs | 36 ++++++++- Documents/Task Bounty/src/test.rs | 98 ++++++++++++++++++++++--- Documents/Task Bounty/src/types.rs | 8 +- 10 files changed, 275 insertions(+), 33 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..68144ac 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) +- [Task Metadata and Query Helpers](#task-metadata-and-query-helpers) --- @@ -858,4 +859,43 @@ resolver.resolveDispute(disputeId, true); --- +## Task Metadata and Query Helpers + +### `updateTaskCategory` +```solidity +function updateTaskCategory(uint256 taskId, string calldata category) external +``` + +Update the category assigned to a task. + +### `addTaskTag` +```solidity +function addTaskTag(uint256 taskId, string calldata tag) external +``` + +Add a custom tag to a task. + +### `getAllTasks` +```solidity +function getAllTasks() external view returns (Task[] memory tasks) +``` + +Return every task in creation order. + +### `getTasksByCategory` +```solidity +function getTasksByCategory(string calldata category) external view returns (Task[] memory tasks) +``` + +Return tasks whose category matches the provided value. + +### `getTasksByTag` +```solidity +function getTasksByTag(string calldata tag) external view returns (Task[] memory tasks) +``` + +Return tasks that contain the provided tag. + +--- + For more examples, see the [README.md](README.md) and test files in `test/`. diff --git a/Documents/Task Bounty/src/dispute.rs b/Documents/Task Bounty/src/dispute.rs index ddac14b..9e77343 100644 --- a/Documents/Task Bounty/src/dispute.rs +++ b/Documents/Task Bounty/src/dispute.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Address, Env, String}; +use soroban_sdk::{panic_with_error, Address, Env, String}; use crate::types::{Dispute, TaskStatus, SubmissionStatus, Error}; use crate::storage; use crate::events; diff --git a/Documents/Task Bounty/src/events.rs b/Documents/Task Bounty/src/events.rs index 5a5c031..828be91 100644 --- a/Documents/Task Bounty/src/events.rs +++ b/Documents/Task Bounty/src/events.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Address, Env, String, Symbol, symbol_short}; +use soroban_sdk::{symbol_short, Address, Env, String}; use crate::types::Task; /// Emit TaskCreated event diff --git a/Documents/Task Bounty/src/lib.rs b/Documents/Task Bounty/src/lib.rs index 1bec81c..075e1ad 100644 --- a/Documents/Task Bounty/src/lib.rs +++ b/Documents/Task Bounty/src/lib.rs @@ -15,6 +15,7 @@ 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; @@ -79,6 +80,20 @@ impl TaskBountyContract { ) } + /// Update the category for an existing task. + pub fn update_task_category(env: Env, task_id: u64, poster: Address, category: String) { + poster.require_auth(); + + task::update_task_category(&env, task_id, poster, category); + } + + /// Add a custom tag to an existing task. + pub fn add_task_tag(env: Env, task_id: u64, poster: Address, tag: String) { + poster.require_auth(); + + task::add_task_tag(&env, task_id, poster, tag); + } + /// Submit work for a task /// /// # Arguments @@ -178,6 +193,26 @@ 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 category. + pub fn get_tasks_by_category(env: Env, category: String) -> Vec { + query::get_tasks_by_category(&env, category) + } + + /// Filter tasks by custom tag. + pub fn get_tasks_by_tag(env: Env, tag: String) -> Vec { + query::get_tasks_by_tag(&env, tag) + } + + /// Filter tasks by status. + pub fn get_tasks_by_status(env: Env, status: TaskStatus) -> Vec { + query::get_tasks_by_status(&env, status) + } + /// 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..fccf517 --- /dev/null +++ b/Documents/Task Bounty/src/query.rs @@ -0,0 +1,55 @@ +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); + + for task_id in 1..=count { + if storage::task_exists(env, task_id) { + tasks.push_back(storage::get_task(env, task_id)); + } + } + + tasks +} + +pub fn get_all_tasks(env: &Env) -> Vec { + all_tasks(env) +} + +pub fn get_tasks_by_category(env: &Env, category: String) -> Vec { + let mut results: Vec = Vec::new(env); + + for task in all_tasks(env).iter() { + if task.category == category { + results.push_back(task.clone()); + } + } + + results +} + +pub fn get_tasks_by_tag(env: &Env, tag: String) -> Vec { + let mut results: Vec = Vec::new(env); + + for task in all_tasks(env).iter() { + if task.tags.contains(tag.clone()) { + results.push_back(task.clone()); + } + } + + results +} + +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 +} diff --git a/Documents/Task Bounty/src/storage.rs b/Documents/Task Bounty/src/storage.rs index 6d1c5c5..363fd1f 100644 --- a/Documents/Task Bounty/src/storage.rs +++ b/Documents/Task Bounty/src/storage.rs @@ -10,39 +10,39 @@ const ADMIN: &str = "ADMIN"; // Task storage pub fn get_task(env: &Env, task_id: u64) -> Task { - let key = (b"TASK", task_id); + let key = ("TASK", task_id); env.storage().persistent().get(&key).unwrap() } pub fn set_task(env: &Env, task: &Task) { - let key = (b"TASK", task.id); + let key = ("TASK", task.id); env.storage().persistent().set(&key, task); } pub fn task_exists(env: &Env, task_id: u64) -> bool { - let key = (b"TASK", task_id); + let key = ("TASK", task_id); env.storage().persistent().has(&key) } // Submission storage pub fn get_submission(env: &Env, submission_id: u64) -> Submission { - let key = (b"SUB", submission_id); + let key = ("SUB", submission_id); env.storage().persistent().get(&key).unwrap() } pub fn set_submission(env: &Env, submission: &Submission) { - let key = (b"SUB", submission.id); + let key = ("SUB", submission.id); env.storage().persistent().set(&key, submission); } pub fn submission_exists(env: &Env, submission_id: u64) -> bool { - let key = (b"SUB", submission_id); + let key = ("SUB", submission_id); env.storage().persistent().has(&key) } // Task submissions mapping pub fn get_task_submissions(env: &Env, task_id: u64) -> Vec { - let key = (b"TASK_SUBS", task_id); + let key = ("TASK_SUBS", task_id); env.storage() .persistent() .get(&key) @@ -50,7 +50,7 @@ pub fn get_task_submissions(env: &Env, task_id: u64) -> Vec { } pub fn add_task_submission(env: &Env, task_id: u64, submission_id: u64) { - let key = (b"TASK_SUBS", task_id); + let key = ("TASK_SUBS", task_id); let mut submissions = get_task_submissions(env, task_id); submissions.push_back(submission_id); env.storage().persistent().set(&key, &submissions); @@ -58,33 +58,33 @@ pub fn add_task_submission(env: &Env, task_id: u64, submission_id: u64) { // Contributor submission tracking pub fn has_contributor_submitted(env: &Env, task_id: u64, contributor: &Address) -> bool { - let key = (b"HAS_SUB", task_id, contributor); + let key = ("HAS_SUB", task_id, contributor); env.storage().persistent().has(&key) } pub fn mark_contributor_submitted(env: &Env, task_id: u64, contributor: &Address) { - let key = (b"HAS_SUB", task_id, contributor); + let key = ("HAS_SUB", task_id, contributor); env.storage().persistent().set(&key, &true); } // Dispute storage pub fn get_dispute(env: &Env, dispute_id: u64) -> Dispute { - let key = (b"DISP", dispute_id); + let key = ("DISP", dispute_id); env.storage().persistent().get(&key).unwrap() } pub fn set_dispute(env: &Env, dispute: &Dispute) { - let key = (b"DISP", dispute.id); + let key = ("DISP", dispute.id); env.storage().persistent().set(&key, dispute); } pub fn has_active_dispute(env: &Env, task_id: u64, submission_id: u64) -> bool { - let key = (b"DISP_ACT", task_id, submission_id); + let key = ("DISP_ACT", task_id, submission_id); env.storage().persistent().has(&key) } pub fn set_active_dispute(env: &Env, task_id: u64, submission_id: u64, dispute_id: u64) { - let key = (b"DISP_ACT", task_id, submission_id); + let key = ("DISP_ACT", task_id, submission_id); env.storage().persistent().set(&key, &dispute_id); } diff --git a/Documents/Task Bounty/src/submission.rs b/Documents/Task Bounty/src/submission.rs index 57f6561..f0427ca 100644 --- a/Documents/Task Bounty/src/submission.rs +++ b/Documents/Task Bounty/src/submission.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Address, Env, String, token}; +use soroban_sdk::{panic_with_error, token, Address, Env, String}; use crate::types::{Submission, SubmissionStatus, TaskStatus, Error}; use crate::storage; use crate::events; diff --git a/Documents/Task Bounty/src/task.rs b/Documents/Task Bounty/src/task.rs index b37b85e..ae7eb4f 100644 --- a/Documents/Task Bounty/src/task.rs +++ b/Documents/Task Bounty/src/task.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Address, Env, String, token}; +use soroban_sdk::{panic_with_error, token, Address, Env, String, Vec}; use crate::types::{Task, TaskStatus, Error}; use crate::storage; use crate::events; @@ -47,6 +47,8 @@ pub fn create_task( poster: poster.clone(), title: title.clone(), description, + category: String::from_str(env, "General"), + tags: Vec::new(env), token, reward, deadline, @@ -105,3 +107,35 @@ pub fn cancel_task(env: &Env, task_id: u64, poster: Address) { // Emit event events::emit_task_cancelled(env, task_id, &poster); } + +pub fn update_task_category(env: &Env, task_id: u64, poster: Address, category: String) { + if !storage::task_exists(env, task_id) { + panic_with_error!(env, Error::TaskNotFound); + } + + let mut task = storage::get_task(env, task_id); + + if task.poster != poster { + panic_with_error!(env, Error::Unauthorized); + } + + task.category = category; + storage::set_task(env, &task); +} + +pub fn add_task_tag(env: &Env, task_id: u64, poster: Address, tag: String) { + if !storage::task_exists(env, task_id) { + panic_with_error!(env, Error::TaskNotFound); + } + + let mut task = storage::get_task(env, task_id); + + if task.poster != poster { + panic_with_error!(env, Error::Unauthorized); + } + + if !task.tags.contains(tag.clone()) { + task.tags.push_back(tag); + storage::set_task(env, &task); + } +} diff --git a/Documents/Task Bounty/src/test.rs b/Documents/Task Bounty/src/test.rs index 38dc888..45df0dd 100644 --- a/Documents/Task Bounty/src/test.rs +++ b/Documents/Task Bounty/src/test.rs @@ -2,27 +2,28 @@ 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::TokenClient<'static>, Address) { let env = Env::default(); env.mock_all_auths(); let admin = Address::generate(&env); let poster = Address::generate(&env); let contributor = Address::generate(&env); - let token_client = create_token_contract(&env, &admin); + let token_admin_client = create_token_contract(&env, &admin); + let token_client = token::TokenClient::new(&env, &token_admin_client.address); // Mint tokens to poster - token_client.mint(&poster, &10_000_000_000); // 1000 XLM + token_admin_client.mint(&poster, &10_000_000_000); // 1000 XLM // Deploy contract let contract_id = env.register_contract(None, TaskBountyContract); @@ -67,7 +68,7 @@ fn test_create_task() { } #[test] -#[should_panic(expected = "InsufficientReward")] +#[should_panic(expected = "Error(Contract, #7)")] fn test_create_task_insufficient_reward() { let (env, poster, _, _, token_client, contract_id) = setup_test(); let client = TaskBountyContractClient::new(&env, &contract_id); @@ -89,11 +90,15 @@ fn test_create_task_insufficient_reward() { } #[test] -#[should_panic(expected = "InvalidDeadline")] +#[should_panic(expected = "Error(Contract, #8)")] fn test_create_task_past_deadline() { let (env, poster, _, _, token_client, contract_id) = setup_test(); let client = TaskBountyContractClient::new(&env, &contract_id); + env.ledger().with_mut(|li| { + li.timestamp = 1_000; + }); + let title = String::from_str(&env, "Task"); let description = String::from_str(&env, "Description"); let reward = 10_000_000; @@ -147,7 +152,7 @@ fn test_submit_work() { } #[test] -#[should_panic(expected = "AlreadySubmitted")] +#[should_panic(expected = "Error(Contract, #10)")] fn test_submit_work_twice() { let (env, poster, contributor, _, token_client, contract_id) = setup_test(); let client = TaskBountyContractClient::new(&env, &contract_id); @@ -170,7 +175,7 @@ fn test_submit_work_twice() { } #[test] -#[should_panic(expected = "TaskExpired")] +#[should_panic(expected = "Error(Contract, #4)")] fn test_submit_work_expired() { let (env, poster, contributor, _, token_client, contract_id) = setup_test(); let client = TaskBountyContractClient::new(&env, &contract_id); @@ -420,3 +425,74 @@ fn test_get_total_tasks() { assert_eq!(client.get_total_tasks(), 2); } + +#[test] +fn test_task_categories_and_tags() { + let (env, poster, _, _, token_client, contract_id) = setup_test(); + let client = TaskBountyContractClient::new(&env, &contract_id); + + let task1 = client.create_task( + &poster, + &String::from_str(&env, "Build landing page"), + &String::from_str(&env, "Create a polished marketing site"), + &token_client.address, + &10_000_000, + &(env.ledger().timestamp() + 86_400), + &1, + ); + + let task2 = client.create_task( + &poster, + &String::from_str(&env, "Write docs"), + &String::from_str(&env, "Document the API and workflow"), + &token_client.address, + &10_000_000, + &(env.ledger().timestamp() + 86_400), + &1, + ); + + let task3 = client.create_task( + &poster, + &String::from_str(&env, "Fix UI spacing"), + &String::from_str(&env, "Adjust layout and typography"), + &token_client.address, + &10_000_000, + &(env.ledger().timestamp() + 86_400), + &1, + ); + + client.update_task_category(&task1, &poster, &String::from_str(&env, "Design")); + client.update_task_category(&task2, &poster, &String::from_str(&env, "Writing")); + client.update_task_category(&task3, &poster, &String::from_str(&env, "Design")); + + client.add_task_tag(&task1, &poster, &String::from_str(&env, "frontend")); + client.add_task_tag(&task1, &poster, &String::from_str(&env, "ui")); + client.add_task_tag(&task2, &poster, &String::from_str(&env, "docs")); + client.add_task_tag(&task3, &poster, &String::from_str(&env, "frontend")); + client.add_task_tag(&task3, &poster, &String::from_str(&env, "bugfix")); + + let design_tasks = client.get_tasks_by_category(&String::from_str(&env, "Design")); + assert_eq!(design_tasks.len(), 2); + assert_eq!(design_tasks.get(0).unwrap().id, task1); + assert_eq!(design_tasks.get(1).unwrap().id, task3); + + let writing_tasks = client.get_tasks_by_category(&String::from_str(&env, "Writing")); + assert_eq!(writing_tasks.len(), 1); + assert_eq!(writing_tasks.get(0).unwrap().id, task2); + + let frontend_tasks = client.get_tasks_by_tag(&String::from_str(&env, "frontend")); + assert_eq!(frontend_tasks.len(), 2); + assert_eq!(frontend_tasks.get(0).unwrap().id, task1); + assert_eq!(frontend_tasks.get(1).unwrap().id, task3); + + let docs_tasks = client.get_tasks_by_tag(&String::from_str(&env, "docs")); + assert_eq!(docs_tasks.len(), 1); + assert_eq!(docs_tasks.get(0).unwrap().id, task2); + + let task = client.get_task(&task1); + assert_eq!(task.category, String::from_str(&env, "Design")); + assert!(task.tags.contains(String::from_str(&env, "frontend"))); + assert!(task.tags.contains(String::from_str(&env, "ui"))); +} + + diff --git a/Documents/Task Bounty/src/types.rs b/Documents/Task Bounty/src/types.rs index 05e40d6..2e3aba4 100644 --- a/Documents/Task Bounty/src/types.rs +++ b/Documents/Task Bounty/src/types.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contracttype, Address, String}; +use soroban_sdk::{contracterror, contracttype, Address, String, Vec}; /// Task status enum #[contracttype] @@ -28,6 +28,8 @@ pub struct Task { pub poster: Address, pub title: String, pub description: String, + pub category: String, + pub tags: Vec, pub token: Address, // Token address for reward pub reward: i128, // Reward amount pub deadline: u64, // Unix timestamp @@ -63,8 +65,8 @@ pub struct Dispute { } /// Error codes -#[contracttype] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum Error { TaskNotFound = 1,