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
42 changes: 42 additions & 0 deletions .github/workflows/contract-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Contract CI

on:
push:
paths:
- "contract/**"
- ".github/workflows/contract-ci.yml"
pull_request:
paths:
- "contract/**"
- ".github/workflows/contract-ci.yml"

jobs:
build-and-test:
name: Build & Test Soroban Contract
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
contract/target
key: ${{ runner.os }}-cargo-${{ hashFiles('contract/Cargo.toml') }}
restore-keys: ${{ runner.os }}-cargo-

- name: Build contract (WASM)
working-directory: contract
run: cargo build --target wasm32-unknown-unknown --release

- name: Run tests
working-directory: contract
run: cargo test
21 changes: 19 additions & 2 deletions contract/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
[package]
name = "contract"
name = "skillsync_contract"
version = "0.1.0"
edition = "2024"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
soroban-sdk = { version = "22.0.0", features = ["alloc"] }

[dev-dependencies]
soroban-sdk = { version = "22.0.0", features = ["testutils", "alloc"] }

[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true
184 changes: 184 additions & 0 deletions contract/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#![no_std]

use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, Env};

// ── Storage Keys ──────────────────────────────────────────────────────────────

#[contracttype]
pub enum DataKey {
Admin,
Treasury,
PlatformFeeBps,
DisputeWindow,
Initialized,
}

// ── Contract ──────────────────────────────────────────────────────────────────

#[contract]
pub struct SkillSyncContract;

#[contractimpl]
impl SkillSyncContract {
// ── Issue #748: initialize ────────────────────────────────────────────────

/// Sets up initial contract state. Can only be called once.
pub fn initialize(env: Env, admin: Address, treasury: Address) {
if env.storage().persistent().has(&DataKey::Initialized) {
panic!("already initialized");
}
admin.require_auth();

env.storage().persistent().set(&DataKey::Admin, &admin);
env.storage().persistent().set(&DataKey::Treasury, &treasury);
env.storage().persistent().set(&DataKey::PlatformFeeBps, &0u32);
env.storage().persistent().set(&DataKey::DisputeWindow, &1000u32);
env.storage().persistent().set(&DataKey::Initialized, &true);

env.events().publish(
(symbol_short!("init"),),
(admin, treasury),
);
}

// ── Issue #749: platform fee ──────────────────────────────────────────────

/// Sets the platform fee in basis points (0–1000). Admin only.
pub fn set_platform_fee(env: Env, new_fee_bps: u32) {
Self::require_admin(&env);
assert!(new_fee_bps <= 1000, "fee exceeds 10%");
env.storage().persistent().set(&DataKey::PlatformFeeBps, &new_fee_bps);
env.events().publish(
(symbol_short!("fee_upd"),),
new_fee_bps,
);
}

/// Returns the current platform fee in basis points.
pub fn get_platform_fee(env: Env) -> u32 {
env.storage()
.persistent()
.get(&DataKey::PlatformFeeBps)
.unwrap_or(0)
}

// ── Issue #750: treasury wallet ───────────────────────────────────────────

/// Updates the treasury wallet address. Admin only.
pub fn set_treasury(env: Env, new_treasury: Address) {
Self::require_admin(&env);
env.storage().persistent().set(&DataKey::Treasury, &new_treasury);
env.events().publish(
(symbol_short!("treas"),),
new_treasury,
);
}

/// Returns the current treasury wallet address.
pub fn get_treasury(env: Env) -> Address {
env.storage()
.persistent()
.get(&DataKey::Treasury)
.expect("treasury not set")
}

// ── Issue #751: dispute window ────────────────────────────────────────────

/// Sets the dispute resolution window in ledgers. Admin only.
pub fn set_dispute_window(env: Env, window_ledgers: u32) {
Self::require_admin(&env);
env.storage().persistent().set(&DataKey::DisputeWindow, &window_ledgers);
env.events().publish(
(symbol_short!("disp_win"),),
window_ledgers,
);
}

/// Returns the current dispute window in ledgers (default: 1000).
pub fn get_dispute_window(env: Env) -> u32 {
env.storage()
.persistent()
.get(&DataKey::DisputeWindow)
.unwrap_or(1000)
}

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

fn require_admin(env: &Env) {
let admin: Address = env
.storage()
.persistent()
.get(&DataKey::Admin)
.expect("not initialized");
admin.require_auth();
}
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::testutils::Address as _;
use soroban_sdk::Env;

fn setup() -> (Env, Address, Address, SkillSyncContractClient<'static>) {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(SkillSyncContract, ());
let client = SkillSyncContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let treasury = Address::generate(&env);
(env, admin, treasury, client)
}

#[test]
fn test_initialize() {
let (_, admin, treasury, client) = setup();
client.initialize(&admin, &treasury);
assert_eq!(client.get_treasury(), treasury);
assert_eq!(client.get_platform_fee(), 0);
assert_eq!(client.get_dispute_window(), 1000);
}

#[test]
#[should_panic(expected = "already initialized")]
fn test_initialize_once() {
let (_, admin, treasury, client) = setup();
client.initialize(&admin, &treasury);
client.initialize(&admin, &treasury);
}

#[test]
fn test_platform_fee() {
let (_, admin, treasury, client) = setup();
client.initialize(&admin, &treasury);
client.set_platform_fee(&250);
assert_eq!(client.get_platform_fee(), 250);
}

#[test]
#[should_panic(expected = "fee exceeds 10%")]
fn test_platform_fee_max() {
let (_, admin, treasury, client) = setup();
client.initialize(&admin, &treasury);
client.set_platform_fee(&1001);
}

#[test]
fn test_treasury() {
let (env, admin, treasury, client) = setup();
client.initialize(&admin, &treasury);
let new_treasury = Address::generate(&env);
client.set_treasury(&new_treasury);
assert_eq!(client.get_treasury(), new_treasury);
}

#[test]
fn test_dispute_window() {
let (_, admin, treasury, client) = setup();
client.initialize(&admin, &treasury);
client.set_dispute_window(&2000);
assert_eq!(client.get_dispute_window(), 2000);
}
}
3 changes: 0 additions & 3 deletions contract/src/main.rs

This file was deleted.

Loading