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
19 changes: 19 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# StellarAid worker configuration. Copy to .env and adjust: cp .env.example .env
# All values below are read in worker/src/main.rs; the defaults shown are used
# when a variable is unset (DONATION_CONTRACT_ID has no usable default).

# Soroban RPC endpoint used to simulate and submit contract invocations.
SOROBAN_RPC_URL=https://soroban-testnet.stellar.org

# Horizon API endpoint used for account and transaction lookups.
HORIZON_URL=https://horizon-testnet.stellar.org

# Network passphrase; must match the network the RPC/Horizon URLs point to.
# Mainnet: "Public Global Stellar Network ; September 2015"
SOROBAN_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"

# Contract ID (C...) of the deployed donation contract. Required for donations.
DONATION_CONTRACT_ID=

# Address and port the worker HTTP server binds to.
BIND_ADDRESS=0.0.0.0:3000
131 changes: 85 additions & 46 deletions contracts/licensing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,41 @@
#![no_std]

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

/// Typed errors returned by the contract entry points.
///
/// These replace the `assert!`/`expect` panics that previously guarded the
/// entry points, so a caller can handle each failure programmatically.
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum LicensingError {
/// The contract has not been initialized (no admin).
NotInitialized = 1,
/// No license exists for the given id.
LicenseNotFound = 2,
/// The parent license does not exist.
ParentNotFound = 3,
/// The parent license is not active.
ParentNotActive = 4,
/// The parent license does not allow sub-licensing.
NotSubLicensable = 5,
/// The license is not active.
NotActive = 6,
/// The caller is not the licensee.
NotLicensee = 7,
/// The license has expired.
Expired = 8,
/// The caller is not permitted to perform this action.
Unauthorized = 9,
/// No dispute exists for the given id.
DisputeNotFound = 10,
/// The dispute has already been resolved.
AlreadyResolved = 11,
}

// ── Data Types ────────────────────────────────────────────────────────────────

/// The type of license being granted.
Expand Down Expand Up @@ -238,21 +270,22 @@ impl LicensingMarketplace {
sub_licensee: Address,
price: i128,
expires_at: u32,
) -> u64 {
) -> Result<u64, LicensingError> {
let parent: LicenseRecord = env
.storage()
.persistent()
.get(&DataKey::License(parent_id))
.expect("parent license not found");
.ok_or(LicensingError::ParentNotFound)?;

// Only the parent licensee may sub-license.
parent.licensee.require_auth();

assert!(
parent.status == LicenseStatus::Active,
"parent license is not active"
);
assert!(parent.sub_licensable, "parent license does not allow sub-licensing");
if !(parent.status == LicenseStatus::Active) {
return Err(LicensingError::ParentNotActive);
}
if !(parent.sub_licensable) {
return Err(LicensingError::NotSubLicensable);
}

let id = Self::next_license_id(&env);
let record = LicenseRecord {
Expand Down Expand Up @@ -281,7 +314,7 @@ impl LicensingMarketplace {
(id, parent_id, sub_licensee),
);

id
Ok(id)
}

// ── License Management ────────────────────────────────────────────────
Expand All @@ -296,18 +329,17 @@ impl LicensingMarketplace {
///
/// # Panics
/// Panics if the license is missing or if `caller` is not the owner.
pub fn revoke_license(env: Env, caller: Address, license_id: u64) {
pub fn revoke_license(env: Env, caller: Address, license_id: u64) -> Result<(), LicensingError> {
caller.require_auth();
let mut record: LicenseRecord = env
.storage()
.persistent()
.get(&DataKey::License(license_id))
.expect("license not found");
.ok_or(LicensingError::LicenseNotFound)?;

assert!(
record.owner == caller,
"only the license owner can revoke"
);
if !(record.owner == caller) {
return Err(LicensingError::Unauthorized);
}

record.status = LicenseStatus::Revoked;
env.storage()
Expand All @@ -316,6 +348,7 @@ impl LicensingMarketplace {

env.events()
.publish((symbol_short!("revoked"),), license_id);
Ok(())
}

/// Mark a license as expired (admin or owner action).
Expand All @@ -329,24 +362,23 @@ impl LicensingMarketplace {
/// # Panics
/// Panics if the contract is not initialized, the license is missing, or
/// `caller` is neither the admin nor the owner.
pub fn expire_license(env: Env, caller: Address, license_id: u64) {
pub fn expire_license(env: Env, caller: Address, license_id: u64) -> Result<(), LicensingError> {
caller.require_auth();
let admin: Address = env
.storage()
.persistent()
.get(&DataKey::Admin)
.expect("contract not initialized");
.ok_or(LicensingError::NotInitialized)?;

let mut record: LicenseRecord = env
.storage()
.persistent()
.get(&DataKey::License(license_id))
.expect("license not found");
.ok_or(LicensingError::LicenseNotFound)?;

assert!(
caller == admin || caller == record.owner,
"unauthorized"
);
if !(caller == admin || caller == record.owner) {
return Err(LicensingError::Unauthorized);
}

record.status = LicenseStatus::Expired;
env.storage()
Expand All @@ -355,6 +387,7 @@ impl LicensingMarketplace {

env.events()
.publish((symbol_short!("expired"),), license_id);
Ok(())
}

// ── Usage Tracking ────────────────────────────────────────────────────
Expand All @@ -373,25 +406,28 @@ impl LicensingMarketplace {
/// # Panics
/// Panics if the license is missing or not active, if `user` is not the
/// licensee, or if the license has already expired.
pub fn record_usage(env: Env, user: Address, license_id: u64, context: String) {
pub fn record_usage(env: Env, user: Address, license_id: u64, context: String) -> Result<(), LicensingError> {
user.require_auth();

let record: LicenseRecord = env
.storage()
.persistent()
.get(&DataKey::License(license_id))
.expect("license not found");
.ok_or(LicensingError::LicenseNotFound)?;

assert!(
record.status == LicenseStatus::Active,
"license is not active"
);
assert!(record.licensee == user, "caller is not the licensee");
if !(record.status == LicenseStatus::Active) {
return Err(LicensingError::NotActive);
}
if !(record.licensee == user) {
return Err(LicensingError::NotLicensee);
}

// Check expiry.
let now = env.ledger().sequence();
if record.expires_at > 0 {
assert!(now <= record.expires_at, "license has expired");
if !(now <= record.expires_at) {
return Err(LicensingError::Expired);
}
}

let count: u64 = env
Expand All @@ -416,6 +452,7 @@ impl LicensingMarketplace {

env.events()
.publish((symbol_short!("usage"), license_id), (user, context));
Ok(())
}

/// Return the number of recorded usage events for a license.
Expand Down Expand Up @@ -454,14 +491,14 @@ impl LicensingMarketplace {
complainant: Address,
license_id: u64,
description: String,
) -> u64 {
) -> Result<u64, LicensingError> {
complainant.require_auth();

let mut record: LicenseRecord = env
.storage()
.persistent()
.get(&DataKey::License(license_id))
.expect("license not found");
.ok_or(LicensingError::LicenseNotFound)?;

// Flag the license as disputed while under review.
record.status = LicenseStatus::Disputed;
Expand All @@ -488,7 +525,7 @@ impl LicensingMarketplace {
(dispute_id, complainant, description),
);

dispute_id
Ok(dispute_id)
}

/// Resolve an open dispute. Only the admin may call this.
Expand All @@ -509,25 +546,26 @@ impl LicensingMarketplace {
admin: Address,
dispute_id: u64,
outcome: DisputeOutcome,
) {
) -> Result<(), LicensingError> {
admin.require_auth();
let stored_admin: Address = env
.storage()
.persistent()
.get(&DataKey::Admin)
.expect("contract not initialized");
assert!(admin == stored_admin, "only admin can resolve disputes");
.ok_or(LicensingError::NotInitialized)?;
if !(admin == stored_admin) {
return Err(LicensingError::Unauthorized);
}

let mut dispute: LicenseDispute = env
.storage()
.persistent()
.get(&DataKey::Dispute(dispute_id))
.expect("dispute not found");
.ok_or(LicensingError::DisputeNotFound)?;

assert!(
dispute.outcome == DisputeOutcome::Pending,
"dispute already resolved"
);
if !(dispute.outcome == DisputeOutcome::Pending) {
return Err(LicensingError::AlreadyResolved);
}

dispute.outcome = outcome.clone();
env.storage()
Expand All @@ -539,7 +577,7 @@ impl LicensingMarketplace {
.storage()
.persistent()
.get(&DataKey::License(dispute.license_id))
.expect("license not found");
.ok_or(LicensingError::LicenseNotFound)?;

record.status = match outcome {
DisputeOutcome::UpheldForLicensor => LicenseStatus::Revoked,
Expand All @@ -552,6 +590,7 @@ impl LicensingMarketplace {

env.events()
.publish((symbol_short!("resolved"),), (dispute_id, outcome));
Ok(())
}

// ── Queries ───────────────────────────────────────────────────────────
Expand All @@ -566,11 +605,11 @@ impl LicensingMarketplace {
///
/// # Panics
/// Panics if the license is not found.
pub fn get_license(env: Env, license_id: u64) -> LicenseRecord {
pub fn get_license(env: Env, license_id: u64) -> Result<LicenseRecord, LicensingError> {
env.storage()
.persistent()
.get(&DataKey::License(license_id))
.expect("license not found")
.ok_or(LicensingError::LicenseNotFound)
}

/// Retrieve a dispute record.
Expand All @@ -583,11 +622,11 @@ impl LicensingMarketplace {
///
/// # Panics
/// Panics if the dispute is not found.
pub fn get_dispute(env: Env, dispute_id: u64) -> LicenseDispute {
pub fn get_dispute(env: Env, dispute_id: u64) -> Result<LicenseDispute, LicensingError> {
env.storage()
.persistent()
.get(&DataKey::Dispute(dispute_id))
.expect("dispute not found")
.ok_or(LicensingError::DisputeNotFound)
}

/// Return a paginated slice of usage entries for a license.
Expand Down
Loading
Loading