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
67 changes: 67 additions & 0 deletions contracts/src/dex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! # Cross-Contract Token Swaps (DEX Integration) — Issue #205
//!
//! Allows a seeker who holds `XLM` (or any asset) to start a session where
//! the expert is paid in a *different* token (e.g. `USDC`). The contract
//! bridges the two assets on-the-fly by calling a Stellar DEX router
//! (Phoenix / Soroswap) through a cross-contract invocation.
//!
//! ## Storage keys (defined in `lib.rs` DataKey)
//! - `DexContractAddress` — admin-set address of the DEX router contract
//!
//! ## Public functions added to `SkillSphereContract` (in `lib.rs`)
//! - `set_dex_contract(env, dex_addr)` — admin-only, sets `DexContractAddress`
//! - `start_session_with_swap(env, seeker, expert, offer_token, ask_token,
//! path, offer_amount, metadata_cid)` — swaps then
//! starts a streaming session denominated in `ask_token`
//! - `get_dex_contract(env)` — reads the configured DEX address

use soroban_sdk::{contracttype, symbol_short, Address, Env, IntoVal, Vec};

/// Descriptor for a DEX swap leg passed into `start_session_with_swap`.
///
/// For a direct pair swap `path` is empty; multi-hop swaps list the
/// intermediate asset addresses between offer and ask.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SwapPath {
/// The token the seeker is sending (e.g. XLM).
pub offer_asset: Address,
/// The token the expert will receive (e.g. USDC).
pub ask_asset: Address,
/// Optional intermediate hops (empty Vec for a direct pair).
pub path: Vec<Address>,
}

/// Cross-contract call: invokes `swap` on the DEX router and returns the
/// `ask_asset` amount received.
///
/// The DEX router is expected to implement a function named `"swap"` with
/// signature `swap(offer_asset, ask_asset, path, offer_amount) -> i128`.
///
/// # Arguments
/// * `env` — current contract environment
/// * `dex_contract` — address of the DEX router
/// * `offer_asset` — token being sold
/// * `ask_asset` — token being bought
/// * `path` — intermediate asset hops (may be empty)
/// * `offer_amount` — amount of `offer_asset` to swap
///
/// # Returns
/// Amount of `ask_asset` received from the swap.
pub fn cross_contract_swap(
env: &Env,
dex_contract: &Address,
offer_asset: &Address,
ask_asset: &Address,
path: &Vec<Address>,
offer_amount: i128,
) -> i128 {
let args: Vec<soroban_sdk::Val> = soroban_sdk::vec![
env,
offer_asset.into_val(env),
ask_asset.into_val(env),
path.into_val(env),
offer_amount.into_val(env),
];
env.invoke_contract::<i128>(dex_contract, &symbol_short!("swap"), args)
}
36 changes: 24 additions & 12 deletions contracts/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub enum Error {
InvalidSplitBps = 17,
DisputeWindowActive = 18,
InvalidFeeConfig = 19,
InsufficientTreasuryBalance = 20,
InsuffTreasuryBal = 20,
AmountBelowMinimum = 21,
ExpertNotRegistered = 22,
ExpertUnavailable = 23,
Expand All @@ -32,21 +32,33 @@ pub enum Error {
DepositTooLow = 26,
AlreadyInitialized = 27,
InvalidRating = 28,
RatingAlreadySubmitted = 29,
RatingSubmitted = 29,
OracleNotTrusted = 30,
InvalidOracleSignature = 31,
InvalidOracleSig = 31,
InvalidSessionState = 32,
InsufficientBalance = 33,

// #213 / #214
BurnBpsExceedsFee = 34,
StakeNotFound = 35,
NoRewardsToClaim = 36,
StakeBalanceInsufficient = 37,
FixedPriceSessionAlreadyFinalised = 34,
SubscriptionNotFound = 35,
SubscriptionAlreadyCollected = 36,
SubscriptionExpired = 37,
InsuranceVaultUnset = 38,
InsufficientInsuranceBalance = 39,
ExpertOffline = 34,
DisputeAlreadyResolved = 35,
InsuffStakeBalance = 37,

// #194 / #195 / #196 / #197
FpAlreadyFinalised = 38,
SubNotFound = 39,
SubAlreadyCollected = 40,
SubscriptionExpired = 41,
ContractUnset = 42,
InsuffInsuranceBal = 43,

// #198 / #199 / #200
ExpertOffline = 44,
DisputeResolved = 45,

// #202 – Soulbound Skill Badges
BadgeAlreadyMinted = 46,
HoursThresholdNotMet = 47,
SessionFrozen = 48,
SwapFailed = 49,
}
58 changes: 58 additions & 0 deletions contracts/src/governance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//! # Community Treasury Voting Power — Issue #204
//!
//! Links treasury-spending governance votes to a user's historical
//! participation in SkillSphere sessions. Voting weight is computed as:
//!
//! ```text
//! voting_power(user) = total_spent(user) + total_earned(user)
//! ```
//!
//! Both counters are incremented automatically inside `internal_settle` in
//! `lib.rs` every time a session settles.
//!
//! ## Storage keys (defined in `lib.rs` DataKey)
//! - `UserTotalSpent(Address)` — cumulative tokens spent by this address as seeker
//! - `UserTotalEarned(Address)` — cumulative tokens earned by this address as expert
//!
//! ## Public functions added to `SkillSphereContract` (in `lib.rs`)
//! - `voting_power(env, user) -> i128` — returns `spent + earned`
//! - `get_total_spent(env, user) -> i128` — reads `UserTotalSpent`
//! - `get_total_earned(env, user) -> i128` — reads `UserTotalEarned`

use soroban_sdk::{Address, Env};

use crate::DataKey;

/// Returns the accumulated tokens spent by `user` as a seeker.
pub fn total_spent(env: &Env, user: &Address) -> i128 {
env.storage()
.persistent()
.get(&DataKey::UserTotalSpent(user.clone()))
.unwrap_or(0i128)
}

/// Returns the accumulated tokens earned by `user` as an expert.
pub fn total_earned(env: &Env, user: &Address) -> i128 {
env.storage()
.persistent()
.get(&DataKey::UserTotalEarned(user.clone()))
.unwrap_or(0i128)
}

/// Increments the seeker's `UserTotalSpent` counter by `amount`.
/// Called from `internal_settle` in `lib.rs` on every settlement.
pub fn accrue_spent(env: &Env, seeker: &Address, amount: i128) {
let prev = total_spent(env, seeker);
env.storage()
.persistent()
.set(&DataKey::UserTotalSpent(seeker.clone()), &prev.saturating_add(amount));
}

/// Increments the expert's `UserTotalEarned` counter by `amount`.
/// Called from `internal_settle` in `lib.rs` on every settlement.
pub fn accrue_earned(env: &Env, expert: &Address, amount: i128) {
let prev = total_earned(env, expert);
env.storage()
.persistent()
.set(&DataKey::UserTotalEarned(expert.clone()), &prev.saturating_add(amount));
}
Loading
Loading