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
1 change: 1 addition & 0 deletions contracts/earn-quest/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ pub enum Error {
NoFundsToWithdraw = 73,
QuestNotTerminal = 74,
TokenMismatch = 75,
MetadataNotFound = 76,
}
52 changes: 51 additions & 1 deletion contracts/earn-quest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ pub mod types;
pub mod validation;

use crate::errors::Error;
use crate::types::{Badge, BatchApprovalInput, BatchQuestInput, EscrowInfo, UserStats};
use crate::types::{
Badge, BatchApprovalInput, BatchQuestInput, EscrowInfo, QuestMetadata, UserStats,
};
use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, Symbol, Vec};

#[contract]
Expand Down Expand Up @@ -70,6 +72,32 @@ impl EarnQuestContract {
)
}

/// Register a new quest and attach metadata during creation.
pub fn register_quest_with_metadata(
env: Env,
id: Symbol,
creator: Address,
reward_asset: Address,
reward_amount: i128,
verifier: Address,
deadline: u64,
metadata: QuestMetadata,
) -> Result<(), Error> {
security::require_not_paused(&env)?;
creator.require_auth();

quest::register_quest_with_metadata(
&env,
&id,
&creator,
&reward_asset,
reward_amount,
&verifier,
deadline,
&metadata,
)
}

/// Register multiple quests in one transaction (gas-optimized).
/// Creator must authorize; all quests are registered to that creator.
/// Batch size is limited; on first error the entire batch reverts.
Expand Down Expand Up @@ -255,6 +283,28 @@ impl EarnQuestContract {
escrow::withdraw_unclaimed(&env, &quest_id, &creator)
}

/// Update quest metadata (quest creator or admin).
pub fn update_quest_metadata(
env: Env,
quest_id: Symbol,
updater: Address,
metadata: QuestMetadata,
) -> Result<(), Error> {
security::require_not_paused(&env)?;
updater.require_auth();
quest::update_quest_metadata(&env, &quest_id, &updater, &metadata)
}

/// Query quest metadata.
pub fn get_quest_metadata(env: Env, quest_id: Symbol) -> Result<QuestMetadata, Error> {
storage::get_quest_metadata(&env, &quest_id)
}

/// Check whether metadata exists for a quest.
pub fn has_quest_metadata(env: Env, quest_id: Symbol) -> bool {
storage::has_quest_metadata(&env, &quest_id)
}

/// Query the available escrow balance for a quest.
pub fn get_escrow_balance(env: Env, quest_id: Symbol) -> Result<i128, Error> {
escrow::get_balance(&env, &quest_id)
Expand Down
84 changes: 83 additions & 1 deletion contracts/earn-quest/src/quest.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
use crate::errors::Error;
use crate::events;
use crate::storage;
use crate::types::{BatchQuestInput, Quest, QuestStatus};
use crate::types::{BatchQuestInput, MetadataDescription, Quest, QuestMetadata, QuestStatus};
use crate::validation;
use soroban_sdk::{Address, Env, Symbol, Vec};

const MAX_METADATA_TITLE_LEN: u32 = 80;
const MAX_METADATA_CATEGORY_LEN: u32 = 40;
const MAX_METADATA_TAG_LEN: u32 = 32;
const MAX_METADATA_REQUIREMENT_LEN: u32 = 200;
const MAX_METADATA_INLINE_DESCRIPTION_LEN: u32 = 1200;
const MAX_METADATA_TAGS: u32 = 15;
const MAX_METADATA_REQUIREMENTS: u32 = 20;

/// Register a new quest with full input validation.
///
/// Validates:
Expand Down Expand Up @@ -65,6 +73,31 @@ pub fn register_quest(
Ok(())
}

/// Register a new quest and store metadata in the same transaction.
pub fn register_quest_with_metadata(
env: &Env,
id: &Symbol,
creator: &Address,
reward_asset: &Address,
reward_amount: i128,
verifier: &Address,
deadline: u64,
metadata: &QuestMetadata,
) -> Result<(), Error> {
register_quest(
env,
id,
creator,
reward_asset,
reward_amount,
verifier,
deadline,
)?;
validate_metadata(metadata)?;
storage::set_quest_metadata(env, id, metadata);
Ok(())
}

//================================================================================
// Batch registration (gas-optimized)
//================================================================================
Expand Down Expand Up @@ -106,3 +139,52 @@ pub fn register_quests_batch(

Ok(())
}

/// Update metadata for an existing quest.
/// Only the quest creator or an admin can update metadata.
pub fn update_quest_metadata(
env: &Env,
quest_id: &Symbol,
updater: &Address,
metadata: &QuestMetadata,
) -> Result<(), Error> {
let quest = storage::get_quest(env, quest_id)?;
if &quest.creator != updater && !storage::is_admin(env, updater) {
return Err(Error::Unauthorized);
}

validate_metadata(metadata)?;
storage::set_quest_metadata(env, quest_id, metadata);
Ok(())
}

fn validate_metadata(metadata: &QuestMetadata) -> Result<(), Error> {
validate_string_len(&metadata.title, MAX_METADATA_TITLE_LEN)?;
validate_string_len(&metadata.category, MAX_METADATA_CATEGORY_LEN)?;

validation::validate_array_length(metadata.tags.len(), MAX_METADATA_TAGS)?;
for i in 0..metadata.tags.len() {
validate_string_len(&metadata.tags.get(i).unwrap(), MAX_METADATA_TAG_LEN)?;
}

validation::validate_array_length(metadata.requirements.len(), MAX_METADATA_REQUIREMENTS)?;
for i in 0..metadata.requirements.len() {
validate_string_len(
&metadata.requirements.get(i).unwrap(),
MAX_METADATA_REQUIREMENT_LEN,
)?;
}

if let MetadataDescription::Inline(desc) = &metadata.description {
validate_string_len(desc, MAX_METADATA_INLINE_DESCRIPTION_LEN)?;
}

Ok(())
}

fn validate_string_len(value: &soroban_sdk::String, max: u32) -> Result<(), Error> {
if value.len() > max {
return Err(Error::StringTooLong);
}
Ok(())
}
31 changes: 30 additions & 1 deletion contracts/earn-quest/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::errors::Error;
use crate::types::{Quest, QuestStatus, Submission, SubmissionStatus, UserStats};
use crate::types::{
EscrowInfo, Quest, QuestMetadata, QuestStatus, Submission, SubmissionStatus, UserStats,
};
use soroban_sdk::{contracttype, Address, Env, Symbol, Vec};

/// Storage key definitions for the contract's persistent data.
Expand All @@ -10,6 +12,8 @@ use soroban_sdk::{contracttype, Address, Env, Symbol, Vec};
pub enum DataKey {
/// Stores individual Quest data, keyed by quest ID (Symbol)
Quest(Symbol),
/// Stores quest metadata, keyed by quest ID (Symbol)
QuestMetadata(Symbol),
/// Stores individual Submission data, keyed by quest ID and submitter address
Submission(Symbol, Address),
/// Stores UserStats data, keyed by user address
Expand Down Expand Up @@ -91,6 +95,28 @@ pub fn set_quest(env: &Env, id: &Symbol, quest: &Quest) {
.set(&DataKey::Quest(id.clone()), quest);
}

/// Checks if metadata exists for a quest.
pub fn has_quest_metadata(env: &Env, id: &Symbol) -> bool {
env.storage()
.instance()
.has(&DataKey::QuestMetadata(id.clone()))
}

/// Gets metadata for a quest, if present.
pub fn get_quest_metadata(env: &Env, id: &Symbol) -> Result<QuestMetadata, Error> {
env.storage()
.instance()
.get(&DataKey::QuestMetadata(id.clone()))
.ok_or(Error::MetadataNotFound)
}

/// Stores metadata for a quest.
pub fn set_quest_metadata(env: &Env, id: &Symbol, metadata: &QuestMetadata) {
env.storage()
.instance()
.set(&DataKey::QuestMetadata(id.clone()), metadata);
}

//================================================================================
// Submission Storage Functions
//================================================================================
Expand Down Expand Up @@ -258,6 +284,9 @@ pub fn delete_quest(env: &Env, id: &Symbol) -> Result<(), Error> {
}

env.storage().instance().remove(&DataKey::Quest(id.clone()));
env.storage()
.instance()
.remove(&DataKey::QuestMetadata(id.clone()));
Ok(())
}

Expand Down
3 changes: 1 addition & 2 deletions contracts/earn-quest/src/submission.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use crate::errors::Error;
use crate::events;
use crate::storage;
use crate::storage;
use crate::types::{BatchApprovalInput, Submission, SubmissionStatus};
use crate::validation;
use soroban_sdk::{Address, BytesN, Env, Symbol, Vec}; // already imported
use soroban_sdk::{Address, BytesN, Env, Symbol, Vec};

/// Submit proof for a quest with full input validation.
///
Expand Down
21 changes: 20 additions & 1 deletion contracts/earn-quest/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use soroban_sdk::{contracttype, Address, BytesN, Symbol, Vec};
use soroban_sdk::{contracttype, Address, BytesN, String, Symbol, Vec};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -86,6 +86,25 @@ pub struct BatchApprovalInput {
pub submitter: Address,
}

/// Description storage mode for quest metadata.
/// Inline is simpler; hash reference is cheaper for large content.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MetadataDescription {
Inline(String),
Hash(BytesN<32>),
}

/// Rich quest metadata shown to users.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QuestMetadata {
pub title: String,
pub description: MetadataDescription,
pub requirements: Vec<String>,
pub category: String,
pub tags: Vec<String>,
}
/// Escrow tracks tokens locked per quest.
/// Created when a creator calls deposit_escrow().
/// Updated when payouts happen or funds are refunded.
Expand Down
Loading