diff --git a/contracts/bounty/src/lib.rs b/contracts/bounty/src/lib.rs index adacfa7..65cedaf 100644 --- a/contracts/bounty/src/lib.rs +++ b/contracts/bounty/src/lib.rs @@ -1,5 +1,18 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, Address, Env, token, symbol_short, Symbol, Map}; +use soroban_sdk::{ + contract, contractimpl, contracttype, symbol_short, token, Address, Env, Map, Symbol, +}; + +#[cfg(test)] +mod test; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BountyInfo { + pub token: Address, + pub amount: i128, + pub deadline: u64, +} #[contract] pub struct BountyContract; @@ -12,26 +25,68 @@ impl BountyContract { client.transfer(&from, &to, &amount); } - pub fn create_bounty(env: Env, token: Address, maintainer: Address, issue_id: Symbol, amount: i128) { + pub fn create_bounty( + env: Env, + token: Address, + maintainer: Address, + issue_id: Symbol, + amount: i128, + deadline: u64, + ) { maintainer.require_auth(); - + assert!(amount > 0, "Bounty amount must be positive"); + assert!( + deadline > env.ledger().timestamp(), + "Bounty deadline must be in the future" + ); + let client = token::Client::new(&env, &token); client.transfer(&maintainer, &env.current_contract_address(), &amount); - + let key = (symbol_short!("bounty"), issue_id.clone()); - let mut bounty: Map
= env.storage().persistent().get(&key).unwrap_or(Map::new(&env)); - + let mut bounty: Map = env + .storage() + .persistent() + .get(&key) + .unwrap_or(Map::new(&env)); + if bounty.contains_key(maintainer.clone()) { panic!("Bounty already exists for this issue and maintainer"); } - - bounty.set(maintainer.clone(), amount); + + bounty.set( + maintainer.clone(), + BountyInfo { + token, + amount, + deadline, + }, + ); env.storage().persistent().set(&key, &bounty); } - pub fn cancel_bounty(env: Env, token: Address, maintainer: Address, amount: i128) { + pub fn cancel_bounty(env: Env, issue_id: Symbol, maintainer: Address) { maintainer.require_auth(); - let client = token::Client::new(&env, &token); - client.transfer(&env.current_contract_address(), &maintainer, &amount); + + let key = (symbol_short!("bounty"), issue_id.clone()); + let mut bounty: Map = env + .storage() + .persistent() + .get(&key) + .unwrap_or(Map::new(&env)); + let info = bounty + .get(maintainer.clone()) + .expect("Bounty does not exist for this issue and maintainer"); + + assert!( + env.ledger().timestamp() >= info.deadline, + "Bounty deadline has not passed" + ); + + let client = token::Client::new(&env, &info.token); + client.transfer(&env.current_contract_address(), &maintainer, &info.amount); + + bounty.remove(maintainer); + env.storage().persistent().set(&key, &bounty); } } diff --git a/contracts/bounty/src/test.rs b/contracts/bounty/src/test.rs index 0bdcba0..5fa15ab 100644 --- a/contracts/bounty/src/test.rs +++ b/contracts/bounty/src/test.rs @@ -1,21 +1,81 @@ #![cfg(test)] use super::*; -use soroban_sdk::{vec, Env, String}; +use soroban_sdk::{ + symbol_short, + testutils::{Address as _, Ledger}, + token, Address, Env, +}; -#[test] -fn test() { +fn setup() -> ( + Env, + BountyContractClient<'static>, + token::Client<'static>, + token::StellarAssetClient<'static>, + Address, + Address, +) { let env = Env::default(); - let contract_id = env.register(Contract, ()); - let client = ContractClient::new(&env, &contract_id); - - let words = client.hello(&String::from_str(&env, "Dev")); - assert_eq!( - words, - vec![ - &env, - String::from_str(&env, "Hello"), - String::from_str(&env, "Dev"), - ] + env.mock_all_auths(); + env.ledger().set_timestamp(1_000); + + let contract_id = env.register(BountyContract, ()); + let bounty_client = BountyContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let maintainer = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract_v2(admin); + let token_client = token::Client::new(&env, &token_contract.address()); + let token_admin = token::StellarAssetClient::new(&env, &token_contract.address()); + token_admin.mint(&maintainer, &1_000); + + ( + env, + bounty_client, + token_client, + token_admin, + maintainer, + contract_id, + ) +} + +#[test] +fn create_bounty_locks_tokens_until_deadline() { + let (env, bounty_client, token_client, _token_admin, maintainer, contract_id) = setup(); + let issue_id = symbol_short!("ISSUE3"); + + bounty_client.create_bounty(&token_client.address, &maintainer, &issue_id, &250, &1_500); + + assert_eq!(token_client.balance(&maintainer), 750); + assert_eq!(token_client.balance(&contract_id), 250); + + env.ledger().set_timestamp(1_500); + bounty_client.cancel_bounty(&issue_id, &maintainer); + + assert_eq!(token_client.balance(&maintainer), 1_000); + assert_eq!(token_client.balance(&contract_id), 0); +} + +#[test] +#[should_panic(expected = "Bounty deadline must be in the future")] +fn create_bounty_rejects_past_deadline() { + let (_env, bounty_client, token_client, _token_admin, maintainer, _contract_id) = setup(); + + bounty_client.create_bounty( + &token_client.address, + &maintainer, + &symbol_short!("ISSUE3"), + &250, + &1_000, ); } + +#[test] +#[should_panic(expected = "Bounty deadline has not passed")] +fn cancel_bounty_rejects_early_refund() { + let (_env, bounty_client, token_client, _token_admin, maintainer, _contract_id) = setup(); + let issue_id = symbol_short!("ISSUE3"); + + bounty_client.create_bounty(&token_client.address, &maintainer, &issue_id, &250, &1_500); + bounty_client.cancel_bounty(&issue_id, &maintainer); +}