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
8 changes: 8 additions & 0 deletions contracts/assetsup/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ pub enum Error {
InvalidPurchaseValue = 37,
InvalidMetadataUri = 38,
InvalidOwnerAddress = 39,

LeaseNotFound = 40,
LeaseAlreadyExists = 41,
AssetAlreadyLeased = 42,
InvalidLeaseStatus = 43,
LeaseAlreadyStarted = 44,
LeaseNotExpired = 45,
InvalidTimestamps = 46,
}

pub fn handle_error(env: &Env, error: Error) -> ! {
Expand Down
226 changes: 226 additions & 0 deletions contracts/assetsup/src/lease.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
use soroban_sdk::{contracttype, Address, BytesN, Env, Vec};

use crate::error::Error;

// ─── Types ────────────────────────────────────────────────────────────────────

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum LeaseStatus {
Active,
Returned,
Cancelled,
Expired,
}

#[contracttype]
#[derive(Clone, Debug)]
pub struct Lease {
pub lease_id: BytesN<32>,
pub asset_id: BytesN<32>,
pub lessor: Address,
pub lessee: Address,
pub start_timestamp: u64,
pub end_timestamp: u64,
pub rent_per_period: i128,
pub deposit: i128,
pub status: LeaseStatus,
}

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

#[contracttype]
pub enum DataKey {
Lease(BytesN<32>),
AssetActiveLease(BytesN<32>),
LesseeLeases(Address),
}

// ─── Internal helpers ─────────────────────────────────────────────────────────

fn load_lease(env: &Env, lease_id: &BytesN<32>) -> Result<Lease, Error> {
env.storage()
.persistent()
.get(&DataKey::Lease(lease_id.clone()))
.ok_or(Error::LeaseNotFound)
}

fn save_lease(env: &Env, lease: &Lease) {
env.storage()
.persistent()
.set(&DataKey::Lease(lease.lease_id.clone()), lease);
}

fn set_asset_active_lease(env: &Env, asset_id: &BytesN<32>, lease_id: &BytesN<32>) {
env.storage()
.persistent()
.set(&DataKey::AssetActiveLease(asset_id.clone()), lease_id);
}

fn clear_asset_active_lease(env: &Env, asset_id: &BytesN<32>) {
env.storage()
.persistent()
.remove(&DataKey::AssetActiveLease(asset_id.clone()));
}

fn get_active_lease_id(env: &Env, asset_id: &BytesN<32>) -> Option<BytesN<32>> {
env.storage()
.persistent()
.get(&DataKey::AssetActiveLease(asset_id.clone()))
}

fn append_lessee_lease(env: &Env, lessee: &Address, lease_id: &BytesN<32>) {
let key = DataKey::LesseeLeases(lessee.clone());
let mut ids: Vec<BytesN<32>> = env
.storage()
.persistent()
.get(&key)
.unwrap_or_else(|| Vec::new(env));
ids.push_back(lease_id.clone());
env.storage().persistent().set(&key, &ids);
}

// ─── Public functions (called from lib.rs) ────────────────────────────────────

pub fn create_lease(
env: &Env,
asset_id: BytesN<32>,
lease_id: BytesN<32>,
lessor: Address,
lessee: Address,
start: u64,
end: u64,
rent: i128,
deposit: i128,
) -> Result<(), Error> {
if end <= start {
return Err(Error::InvalidTimestamps);
}

if env
.storage()
.persistent()
.has(&DataKey::Lease(lease_id.clone()))
{
return Err(Error::LeaseAlreadyExists);
}

// Asset must not already have an Active lease
if let Some(existing_id) = get_active_lease_id(env, &asset_id) {
let existing = load_lease(env, &existing_id)?;
if existing.status == LeaseStatus::Active {
return Err(Error::AssetAlreadyLeased);
}
}

let lease = Lease {
lease_id: lease_id.clone(),
asset_id: asset_id.clone(),
lessor: lessor.clone(),
lessee: lessee.clone(),
start_timestamp: start,
end_timestamp: end,
rent_per_period: rent,
deposit,
status: LeaseStatus::Active,
};

save_lease(env, &lease);
set_asset_active_lease(env, &asset_id, &lease_id);
append_lessee_lease(env, &lessee, &lease_id);

env.events().publish(
(soroban_sdk::symbol_short!("lease_new"),),
(lease_id, asset_id, lessor, lessee, env.ledger().timestamp()),
);

Ok(())
}

pub fn return_leased_asset(env: &Env, lease_id: BytesN<32>, caller: Address) -> Result<(), Error> {
let mut lease = load_lease(env, &lease_id)?;

if caller != lease.lessor && caller != lease.lessee {
return Err(Error::Unauthorized);
}

if lease.status != LeaseStatus::Active {
return Err(Error::InvalidLeaseStatus);
}

lease.status = LeaseStatus::Returned;
save_lease(env, &lease);
clear_asset_active_lease(env, &lease.asset_id);

env.events().publish(
(soroban_sdk::symbol_short!("lease_ret"),),
(lease_id, caller, env.ledger().timestamp()),
);

Ok(())
}

pub fn cancel_lease(env: &Env, lease_id: BytesN<32>, caller: Address) -> Result<(), Error> {
let mut lease = load_lease(env, &lease_id)?;

if caller != lease.lessor {
return Err(Error::Unauthorized);
}

if lease.status != LeaseStatus::Active {
return Err(Error::InvalidLeaseStatus);
}

if env.ledger().timestamp() >= lease.start_timestamp {
return Err(Error::LeaseAlreadyStarted);
}

lease.status = LeaseStatus::Cancelled;
save_lease(env, &lease);
clear_asset_active_lease(env, &lease.asset_id);

env.events().publish(
(soroban_sdk::symbol_short!("lease_can"),),
(lease_id, caller, env.ledger().timestamp()),
);

Ok(())
}

pub fn expire_lease(env: &Env, lease_id: BytesN<32>) -> Result<(), Error> {
let mut lease = load_lease(env, &lease_id)?;

if lease.status != LeaseStatus::Active {
return Err(Error::InvalidLeaseStatus);
}

if env.ledger().timestamp() <= lease.end_timestamp {
return Err(Error::LeaseNotExpired);
}

lease.status = LeaseStatus::Expired;
save_lease(env, &lease);
clear_asset_active_lease(env, &lease.asset_id);

env.events().publish(
(soroban_sdk::symbol_short!("lease_exp"),),
(lease_id, env.ledger().timestamp()),
);

Ok(())
}

pub fn get_lease(env: &Env, lease_id: BytesN<32>) -> Result<Lease, Error> {
load_lease(env, &lease_id)
}

pub fn get_asset_active_lease(env: &Env, asset_id: BytesN<32>) -> Option<Lease> {
get_active_lease_id(env, &asset_id).and_then(|id| load_lease(env, &id).ok())
}

pub fn get_lessee_leases(env: &Env, lessee: Address) -> Vec<BytesN<32>> {
env.storage()
.persistent()
.get(&DataKey::LesseeLeases(lessee))
.unwrap_or_else(|| Vec::new(env))
}
55 changes: 55 additions & 0 deletions contracts/assetsup/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub(crate) mod detokenization;
pub(crate) mod dividends;
pub(crate) mod error;
pub(crate) mod insurance;
pub(crate) mod lease;
pub(crate) mod tokenization;
pub(crate) mod transfer_restrictions;
pub(crate) mod types;
Expand Down Expand Up @@ -833,4 +834,58 @@ impl AssetUpContract {
pub fn get_asset_insurance_policies(env: Env, asset_id: BytesN<32>) -> Vec<BytesN<32>> {
insurance::get_asset_policies(env, asset_id)
}

/// Create a new lease. Lessor authenticates; asset must not already be actively leased.
pub fn create_lease(
env: Env,
asset_id: BytesN<32>,
lease_id: BytesN<32>,
lessor: Address,
lessee: Address,
start: u64,
end: u64,
rent: i128,
deposit: i128,
) -> Result<(), Error> {
lessor.require_auth();
lease::create_lease(
&env, asset_id, lease_id, lessor, lessee, start, end, rent, deposit,
)
}

/// Return a leased asset. Callable by lessor or lessee.
pub fn return_leased_asset(
env: Env,
lease_id: BytesN<32>,
caller: Address,
) -> Result<(), Error> {
caller.require_auth();
lease::return_leased_asset(&env, lease_id, caller)
}

/// Cancel a lease before it starts. Lessor only.
pub fn cancel_lease(env: Env, lease_id: BytesN<32>, caller: Address) -> Result<(), Error> {
caller.require_auth();
lease::cancel_lease(&env, lease_id, caller)
}

/// Expire a lease permissionlessly once end_timestamp has passed.
pub fn expire_lease(env: Env, lease_id: BytesN<32>) -> Result<(), Error> {
lease::expire_lease(&env, lease_id)
}

/// Fetch a lease by ID.
pub fn get_lease(env: Env, lease_id: BytesN<32>) -> Result<lease::Lease, Error> {
lease::get_lease(&env, lease_id)
}

/// Return the active lease for an asset, or None.
pub fn get_asset_active_lease(env: Env, asset_id: BytesN<32>) -> Option<lease::Lease> {
lease::get_asset_active_lease(&env, asset_id)
}

/// Return all lease IDs for a given lessee.
pub fn get_lessee_leases(env: Env, lessee: Address) -> Vec<BytesN<32>> {
lease::get_lessee_leases(&env, lessee)
}
}