Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions Documents/Task Bounty/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Complete API reference for TaskBounty smart contracts.
- [Events](#events)
- [Errors](#errors)
- [Data Structures](#data-structures)
- [Query Helpers](#query-helpers)

---

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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/`.
35 changes: 33 additions & 2 deletions Documents/Task Bounty/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#![no_std]

//! # TaskBounty - Decentralized Task & Reward Board
//!
//! A Soroban smart contract for trustless task management and bounty payments on Stellar.
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<Task> {
query::get_all_tasks(&env)
}

/// Filter tasks by status.
pub fn get_tasks_by_status(env: Env, status: TaskStatus) -> Vec<Task> {
query::get_tasks_by_status(&env, status)
}

/// Filter tasks by exact reward amount.
pub fn get_tasks_by_reward(env: Env, reward: i128) -> Vec<Task> {
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<Task> {
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<Task> {
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<Task> {
query::search_tasks(&env, query_str)
}

/// Get submission details
///
/// # Arguments
Expand Down
113 changes: 113 additions & 0 deletions Documents/Task Bounty/src/query.rs
Original file line number Diff line number Diff line change
@@ -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<Task> {
let count = storage::get_task_counter(env);
let mut tasks: Vec<Task> = 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<u8> {
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<Task> {
all_tasks(env)
}

pub fn get_tasks_by_status(env: &Env, status: TaskStatus) -> Vec<Task> {
let mut results: Vec<Task> = 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<Task> {
let mut results: Vec<Task> = 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<Task> {
let mut results: Vec<Task> = 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<Task> {
let mut results: Vec<Task> = 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<Task> {
let mut results: Vec<Task> = Vec::new(env);

for task in all_tasks(env).iter() {
if task_matches_query(&task, &query) {
results.push_back(task.clone());
}
}

results
}
91 changes: 85 additions & 6 deletions Documents/Task Bounty/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -49,7 +49,7 @@ fn test_create_task() {
&poster,
&title,
&description,
&token_client.address,
&token_client.address(),
&reward,
&deadline,
&3,
Expand Down Expand Up @@ -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;
}