Skip to content
Merged
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
73 changes: 66 additions & 7 deletions contracts/quest-engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,29 @@ pub struct SubmissionReviewed {
pub approved: bool,
}

#[contractevent]
pub struct QuestRefunded {
#[topic]
pub employer: Address,
#[topic]
pub quest_id: u32,
pub amount: i128,
}

#[contract]
pub struct QuestEngineContract;

#[contractimpl]
impl QuestEngineContract {
/// Initializes the QuestEngine contract with the token address.
pub fn initialize(env: Env, token: Address) {
pub fn initialize(env: Env, token: Address, reward_pool: Address) {
if env.storage().instance().has(&DataKey::Token) {
panic!("Already initialized");
}
env.storage().instance().set(&DataKey::Token, &token);
env.storage()
.instance()
.set(&DataKey::RewardPool, &reward_pool);
env.storage().instance().set(&DataKey::QuestCounter, &0u32);
}

Expand Down Expand Up @@ -201,13 +213,19 @@ impl QuestEngineContract {
.get(&DataKey::Token)
.expect("Not initialized");
let token_client = token::Client::new(&env, &token_address);
token_client.transfer(
&env.current_contract_address(),
&learner,
&quest.reward_amount,
);

// b. Update submission status to Approved.
let fee = (quest.reward_amount * 15) / 100;
let learner_amount = quest.reward_amount - fee;

let reward_pool: Address = env
.storage()
.instance()
.get(&DataKey::RewardPool)
.expect("Not initialized");

token_client.transfer(&env.current_contract_address(), &reward_pool, &fee);
token_client.transfer(&env.current_contract_address(), &learner, &learner_amount);

submission.status = SubmissionStatus::Approved;
} else {
// 5. If approve == false:
Expand All @@ -227,6 +245,47 @@ impl QuestEngineContract {
}
.publish(&env);
}

pub fn refund_quest(env: Env, employer: Address, quest_id: u32) {
employer.require_auth();

let mut quest: Quest = env
.storage()
.persistent()
.get(&DataKey::Quest(quest_id))
.expect("Quest not found");

if quest.employer != employer {
panic!("Unauthorized");
}
if !quest.active {
panic!("Quest already inactive");
}

quest.active = false;
env.storage()
.persistent()
.set(&DataKey::Quest(quest_id), &quest);

let token_address: Address = env
.storage()
.instance()
.get(&DataKey::Token)
.expect("Not initialized");
let token_client = token::Client::new(&env, &token_address);
token_client.transfer(
&env.current_contract_address(),
&employer,
&quest.reward_amount,
);

QuestRefunded {
employer,
quest_id,
amount: quest.reward_amount,
}
.publish(&env);
}
}

mod test;
114 changes: 85 additions & 29 deletions contracts/quest-engine/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{QuestEngineContract, QuestEngineContractClient};

// ── Helpers ───────────────────────────────────────────────────────────────────

fn setup() -> (Env, QuestEngineContractClient<'static>, Address) {
fn setup() -> (Env, QuestEngineContractClient<'static>, Address, Address) {
let env = Env::default();
env.mock_all_auths();

Expand All @@ -24,9 +24,10 @@ fn setup() -> (Env, QuestEngineContractClient<'static>, Address) {
.address();

// Initialize the contract with token
client.initialize(&token_id);
let reward_pool = Address::generate(&env);
client.initialize(&token_id, &reward_pool);

(env, client, token_id)
(env, client, token_id, reward_pool)
}

fn mint_tokens(env: &Env, token_id: &Address, to: &Address, amount: &i128) {
Expand All @@ -43,16 +44,15 @@ fn token_balance(env: &Env, token_id: &Address, of: &Address) -> i128 {
#[test]
#[should_panic(expected = "Already initialized")]
fn test_initialize_twice_panics() {
let (_env, client, token_id) = setup();
// setup() already initializes once, this second call should panic
client.initialize(&token_id);
let (_env, client, token_id, reward_pool) = setup();
client.initialize(&token_id, &reward_pool);
}

// ── create_build_quest Tests ─────────────────────────────────────────────────

#[test]
fn test_create_build_quest_success() {
let (env, client, token_id) = setup();
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let reward_amount: i128 = 1_000;
let metadata_hash = BytesN::from_array(&env, &[1u8; 32]);
Expand Down Expand Up @@ -85,7 +85,7 @@ fn test_create_build_quest_success() {

#[test]
fn test_create_build_quest_emits_event() {
let (env, client, token_id) = setup();
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let reward_amount: i128 = 500;
let metadata_hash = BytesN::from_array(&env, &[2u8; 32]);
Expand All @@ -105,7 +105,7 @@ fn test_create_build_quest_emits_event() {

#[test]
fn test_create_build_quest_increments_ids() {
let (env, client, token_id) = setup();
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let metadata_hash = BytesN::from_array(&env, &[3u8; 32]);

Expand Down Expand Up @@ -148,13 +148,13 @@ fn test_create_quest_without_init_panics() {

#[test]
fn test_get_quest_returns_none_for_nonexistent() {
let (_env, client, _token_id) = setup();
let (_env, client, _token_id, _reward_pool) = setup();
assert_eq!(client.get_quest(&999), None);
}

#[test]
fn test_create_build_quest_multiple_employers() {
let (env, client, token_id) = setup();
let (env, client, token_id, _reward_pool) = setup();
let employer1 = Address::generate(&env);
let employer2 = Address::generate(&env);
let metadata_hash = BytesN::from_array(&env, &[4u8; 32]);
Expand All @@ -181,7 +181,7 @@ fn test_create_build_quest_multiple_employers() {

#[test]
fn test_submit_proof_success() {
let (env, client, token_id) = setup();
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let learner = Address::generate(&env);
let reward_amount: i128 = 1000;
Expand All @@ -203,7 +203,7 @@ fn test_submit_proof_success() {

#[test]
fn test_submit_proof_emits_event() {
let (env, client, token_id) = setup();
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let learner = Address::generate(&env);
let reward_amount: i128 = 1000;
Expand All @@ -230,7 +230,7 @@ fn test_submit_proof_emits_event() {
#[test]
#[should_panic(expected = "Quest not found")]
fn test_submit_proof_nonexistent_quest_panics() {
let (_env, client, _token_id) = setup();
let (_env, client, _token_id, _reward_pool) = setup();
let learner = Address::generate(&_env);
let proof_hash = BytesN::from_array(&_env, &[9u8; 32]);

Expand All @@ -240,7 +240,7 @@ fn test_submit_proof_nonexistent_quest_panics() {
#[test]
#[should_panic(expected = "Submission already exists")]
fn test_submit_proof_duplicate_panics() {
let (env, client, token_id) = setup();
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let learner = Address::generate(&env);
let reward_amount: i128 = 1000;
Expand All @@ -260,7 +260,7 @@ fn test_submit_proof_duplicate_panics() {

#[test]
fn test_get_submission_returns_none_for_nonexistent() {
let (_env, client, _token_id) = setup();
let (_env, client, _token_id, _reward_pool) = setup();
let learner = Address::generate(&_env);
assert_eq!(client.get_submission(&learner, &999), None);
}
Expand All @@ -269,7 +269,7 @@ fn test_get_submission_returns_none_for_nonexistent() {

#[test]
fn test_review_submission_approve_success() {
let (env, client, token_id) = setup();
let (env, client, token_id, reward_pool) = setup();
let employer = Address::generate(&env);
let learner = Address::generate(&env);
let reward_amount: i128 = 1000;
Expand All @@ -293,18 +293,18 @@ fn test_review_submission_approve_success() {
// Approve submission
client.review_submission(&employer, &learner, &quest_id, &true);

// Verify funds transferred
assert_eq!(token_balance(&env, &token_id, &client.address), 0);
assert_eq!(token_balance(&env, &token_id, &learner), reward_amount);
// Verify fee split
let fee = (reward_amount * 15) / 100;
let learner_amount = reward_amount - fee;

// Verify submission status updated
let submission = client.get_submission(&learner, &quest_id).unwrap();
assert_eq!(submission.status, SubmissionStatus::Approved);
assert_eq!(token_balance(&env, &token_id, &client.address), 0);
assert_eq!(token_balance(&env, &token_id, &learner), learner_amount);
assert_eq!(token_balance(&env, &token_id, &reward_pool), fee);
}

#[test]
fn test_review_submission_reject_success() {
let (env, client, token_id) = setup();
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let learner = Address::generate(&env);
let reward_amount: i128 = 1000;
Expand Down Expand Up @@ -342,7 +342,7 @@ fn test_review_submission_reject_success() {

#[test]
fn test_review_submission_emits_event() {
let (env, client, token_id) = setup();
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let learner = Address::generate(&env);
let reward_amount: i128 = 1000;
Expand Down Expand Up @@ -371,7 +371,7 @@ fn test_review_submission_emits_event() {
#[test]
#[should_panic(expected = "Quest not found")]
fn test_review_submission_nonexistent_quest_panics() {
let (_env, client, _token_id) = setup();
let (_env, client, _token_id, _reward_pool) = setup();
let employer = Address::generate(&_env);
let learner = Address::generate(&_env);

Expand All @@ -381,7 +381,7 @@ fn test_review_submission_nonexistent_quest_panics() {
#[test]
#[should_panic(expected = "Only the quest employer can review submissions")]
fn test_review_submission_wrong_employer_panics() {
let (env, client, token_id) = setup();
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let wrong_employer = Address::generate(&env);
let learner = Address::generate(&env);
Expand All @@ -403,7 +403,7 @@ fn test_review_submission_wrong_employer_panics() {
#[test]
#[should_panic(expected = "Submission not found")]
fn test_review_submission_nonexistent_submission_panics() {
let (env, client, token_id) = setup();
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let learner = Address::generate(&env);
let reward_amount: i128 = 1000;
Expand All @@ -420,7 +420,7 @@ fn test_review_submission_nonexistent_submission_panics() {
#[test]
#[should_panic(expected = "Submission is not pending review")]
fn test_review_submission_already_reviewed_panics() {
let (env, client, token_id) = setup();
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let learner = Address::generate(&env);
let reward_amount: i128 = 1000;
Expand All @@ -440,3 +440,59 @@ fn test_review_submission_already_reviewed_panics() {
// Try to review again - should panic
client.review_submission(&employer, &learner, &quest_id, &false);
}

#[test]
fn test_refund_quest_success() {
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let reward_amount: i128 = 1000;
let metadata_hash = BytesN::from_array(&env, &[30u8; 32]);

mint_tokens(&env, &token_id, &employer, &reward_amount);
let quest_id = client.create_build_quest(&employer, &reward_amount, &metadata_hash);

assert_eq!(
token_balance(&env, &token_id, &client.address),
reward_amount
);
assert_eq!(token_balance(&env, &token_id, &employer), 0);

client.refund_quest(&employer, &quest_id);

assert_eq!(token_balance(&env, &token_id, &client.address), 0);
assert_eq!(token_balance(&env, &token_id, &employer), reward_amount);

let quest = client.get_quest(&quest_id).unwrap();
assert!(!quest.active);
}

#[test]
#[should_panic(expected = "Quest already inactive")]
fn test_refund_quest_already_inactive_panics() {
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let reward_amount: i128 = 1000;
let metadata_hash = BytesN::from_array(&env, &[31u8; 32]);

mint_tokens(&env, &token_id, &employer, &reward_amount);
let quest_id = client.create_build_quest(&employer, &reward_amount, &metadata_hash);

client.refund_quest(&employer, &quest_id);
// Second refund should panic
client.refund_quest(&employer, &quest_id);
}

#[test]
#[should_panic(expected = "Unauthorized")]
fn test_refund_quest_wrong_employer_panics() {
let (env, client, token_id, _reward_pool) = setup();
let employer = Address::generate(&env);
let wrong_employer = Address::generate(&env);
let reward_amount: i128 = 1000;
let metadata_hash = BytesN::from_array(&env, &[32u8; 32]);

mint_tokens(&env, &token_id, &employer, &reward_amount);
let quest_id = client.create_build_quest(&employer, &reward_amount, &metadata_hash);

client.refund_quest(&wrong_employer, &quest_id);
}
1 change: 1 addition & 0 deletions contracts/quest-engine/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ pub enum DataKey {
Submission(Address, u32), // (Submitter Address, Quest ID)
Token,
QuestCounter,
RewardPool,
}
Loading
Loading