Skip to content
Closed
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
7 changes: 7 additions & 0 deletions contracts/assetsup/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ pub enum Error {
Unauthorized = 8,
// Payment is not valid
InvalidPayment = 9,
//Subscription errors
SubscriptionNotFound = 201,
SubscriptionNotActive = 202,
SubscriptionActive = 203,

//Payment errors
InsufficientPayment = 300,
}

pub fn handle_error(env: &Env, error: Error) -> ! {
Expand Down
27 changes: 27 additions & 0 deletions contracts/assetsup/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#![no_std]

use crate::error::{Error, handle_error};
use crate::subscription::SubscriptionService;
use soroban_sdk::{Address, BytesN, Env, String, Vec, contract, contractimpl, contracttype};

pub(crate) mod asset;
pub(crate) mod audit;
pub(crate) mod branch;
pub(crate) mod error;
pub(crate) mod subscription;
pub(crate) mod types;

pub use types::*;
Expand Down Expand Up @@ -319,6 +321,31 @@ impl AssetUpContract {
) -> Result<Vec<audit::AuditEntry>, Error> {
Ok(audit::get_asset_log(&env, &asset_id))
}
//creates a new subscription
pub fn create_subscription(
env: Env,
id: BytesN<32>,
user: Address,
plan: crate::types::PlanType,
payment_token: Address,
duration_days: u32,
) -> Result<Subscription, Error> {
SubscriptionService::create_subscription(env, id, user, plan, payment_token, duration_days)
}
/// Cancels an active subscription.
pub fn cancel_subscription(
env: Env,
id: soroban_sdk::BytesN<32>,
) -> Result<crate::types::Subscription, Error> {
SubscriptionService::cancel_subscription(env, id)
}
/// Retrieves subscription details.
pub fn get_subscription(
env: Env,
id: soroban_sdk::BytesN<32>,
) -> Result<crate::types::Subscription, Error> {
SubscriptionService::get_subscription(env, id)
}
}

mod tests;
95 changes: 95 additions & 0 deletions contracts/assetsup/src/subscription.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use crate::error::Error;
use crate::types::{DataKey, PlanType, Subscription, SubscriptionStatus};
use soroban_sdk::{Address, BytesN, Env, token};

const LEDGERS_PER_DAY: u32 = 17280; //constants for ledgers
pub struct SubscriptionService;

impl SubscriptionService {
pub fn create_subscription(
env: Env,
id: BytesN<32>,
user: Address,
plan: PlanType,
payment_token: Address,
duration_days: u32,
) -> Result<Subscription, Error> {
// 1. Authorization check
//user.require_auth();

// 2. Existence check
let sub_key = DataKey::Subscription(id.clone());
if env.storage().persistent().has(&sub_key) {
return Err(Error::SubscriptionAlreadyExists);
}

// 3. Payment Logic
let token_client = token::Client::new(&env, &payment_token);
let amount = plan.get_price_7_decimal();
let recipient = env.current_contract_address();

// simualate token transfer from user to contract(payment)
//User has executed an 'approve' call on the token contract
//The contract pulls the token
token_client.transfer_from(&user, &user, &recipient, &amount);

// 4. Calculate Dates
let current_ledger = env.ledger().sequence();
//let seconds_in_day=24*60*60;
//let ledgers_in_day=seconds_in_day/env.ledger().close_time_resolution();
let duration_ledgers = duration_days.saturating_mul(LEDGERS_PER_DAY);
let start_date = current_ledger;
let end_date = current_ledger.saturating_add(duration_ledgers);

// 5. Create and store subscription object
let new_subscription = Subscription {
id,
user,
plan,
status: SubscriptionStatus::Active,
payment_token,
start_date,
end_date,
};

env.storage().persistent().set(&sub_key, &new_subscription);

Ok(new_subscription)
}

pub fn cancel_subscription(env: Env, id: BytesN<32>) -> Result<Subscription, Error> {
let sub_key = DataKey::Subscription(id.clone());

//1. Retrieve subscription
let mut subscription: Subscription = env
.storage()
.persistent()
.get(&sub_key)
.ok_or(Error::SubscriptionNotFound)?;

//2. Authorization check(only the subscriber can cancel)
subscription.user.require_auth();

// 3. Status check
if subscription.status != SubscriptionStatus::Active {
return Err(Error::SubscriptionNotActive);
}

// 4. Update status and date
subscription.status = SubscriptionStatus::Cancelled;
//subscription.end_date = env.ledger().sequence(); //end immediately

// 5.Store update
env.storage().persistent().set(&sub_key, &subscription);

Ok(subscription)
}

pub fn get_subscription(env: Env, id: BytesN<32>) -> Result<Subscription, Error> {
let sub_key = DataKey::Subscription(id.clone());
env.storage()
.persistent()
.get(&sub_key)
.ok_or(Error::SubscriptionNotFound)
}
}
154 changes: 154 additions & 0 deletions contracts/assetsup/src/subscription_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#![cfg(test)]

extern crate std;
use soroban_sdk::{
testutils::{Address as _, MockAuth},
token,
vec,
Address, BytesN, Env, Symbol, IntoVal,
};

use crate::{
errors::ContractError,
types::{PlanType, Subscription, SubscriptionStatus},
};
//import shared helper
use super::common::setup_test_environment;

// Subscription Test Cases
fn test_subscription_creation(){
let (env,client, _admin, token_client)=setup_test_environment();
let subscriber= Address::generate(&env);
let sub_id = BytesN::from_array(&env, &[1; 32]);
let plan=PlanType::Basic;
let amount=plan.get_price_7_decimal();

// 1. Fund the subscriber and set allowance
token_client.with_admin(&Address::generate(&env)).mint(&subscriber, &amount);
token_client.with_source(&subscribe).approve(&subscriber, &client_contract_id,&amount, &amount);

// 2. Create subscription
let sub:Subsciption=client.mock_auths(&[MockAuth{
address: subscriber.clone(),
invoke: &vec![
&env,
(
&token_client.contract_id(),
&Symbol::new(&env, "transfer_from"),
vec![
&env,
subscriber.to_val(),
subscriber.to_val(),
client_contract_id.to_val(&env),
amount.to_val(),
],
),
],
}])
.create_subscription(&sub_id, &subscriber, &plan, &token_client.contract_id(), 30);

// 3. Assert properties
assert_eq!(sub.id, sub_id);
assert_eq!(sub.status, SubscriptionStatus::Active);
assert_eq!(sub.start_date, 100);
// 30 days*24h*60.*60s/5s/ledger=518400 ledgers
assert_eq!(sub.end_date, 100+518400);

// 4. Check contract balance(payment successful)
assert_eq!(token_client.balance(&client.contract_id),amount);

// 5. Try to create again(fails with SubscriptionAlreadyExists)
let result=client
.mock_auths(&[MockAuth{
address: subscriber.clone(),
invoke: &vec![
&env,
(
&token_client.contract_id,
&Symbol::new(&env,"transfer_from"),
vec![
&env,
subscriber.to_val(),
subscriber.to_val(),
client_contract_id.to_val(&env),
amount.to_val(),
],
),
],
}])
.try_cancel_subscription(&sub_id, &subscriber, &plan, &token_client.contract_id,30);

assert_eq!(result.unwrap_err().as_error().unwrap(), ContractError::SubscriptionNotActive.into());

}

#[test]
fn test_subscription_cancellation(){
let (env,client, _admin, token_client)=setup_test_environment();
let subscriber= Address::generate(&env);
let sub_id = BytesN::from_array(&env, &[2; 32]);
let plan=PlanType::Premium;
let amount=plan.get_price_7_decimal();

// 1. Create subscription
token_client.with_admin(&Address::generate(&env)).mint(&subscriber, &amount);
token_client.with_source(&subscribe).approve(&subscriber, &client_contract_id, &amount);

client.mock_auths(&[MockAuth{
address: subscriber.clone(),
invoke: &vec![
&env,
(
&token_client.contract_id(),
&Symbol::new(&env, "transfer_from"),
vec![
&env,
subscriber.to_val(),
subscriber.to_val(),
client_contract_id.to_val(&env),
amount.to_val(),
],
),
],
}])
.create_subscription(&sub_id, &subscriber, &plan, &token_client.contract_id(), 30);

// Advance ledger sequence to simulate time passage
env.ledger().set_sequence(200);

// 2. Cancel the subscription
let cancelled_sub: Subscription=client
.mock_auths(&[MockAuth{
address: subscriber.clone(),
invoke: &vec![],
}])
.cancel_subscription(&sub_id);

// 3. Assert properties
assert_eq!(cancelled_sub.status, SubscriptionStatus::Cancelled);
assert_eq!(cancelled_sub.end_date, 200); //should be current ledger sequence


let result=client
.mock_auths(&[MockAuth{
address: subscriber.clone(),
invoke: &vec![&env],
}])
.try_cancel_subscription(&sub_id);

assert_eq!(result.unwrap_err().as_error().unwrap(), ContractError::SubscriptionNotActive.into());

}

#[test]
fn test_get_subscription_not_found() {
let (env, client, _admin, _token_client) = setup_test_environment();
let sub_id = BytesN::from_array(&env, &[3; 32]);

// try to get a non-existent subscription
let result = client.get_subscription(&sub_id);
assert_eq!(
result.unwrap_err().as_error().unwrap(),
ContractError::SubscriptionNotFound.into()
);
}
39 changes: 36 additions & 3 deletions contracts/assetsup/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![allow(clippy::upper_case_acronyms)]
use soroban_sdk::contracttype;
use soroban_sdk::{Address, BytesN, contracttype};

/// Represents the fundamental type of asset being managed
/// Distinguishes between physical and digital assets for different handling requirements
Expand All @@ -19,7 +19,12 @@ pub enum AssetStatus {
InMaintenance,
Disposed,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DataKey {
Admin,
Subscription(BytesN<32>),
}
/// Represents different types of actions that can be performed on assets
/// Used for audit trails and tracking asset lifecycle events
#[contracttype]
Expand All @@ -43,7 +48,16 @@ pub enum PlanType {
Pro,
Enterprise,
}

impl PlanType {
/// Returns the required monthly payment amount in 7-decimal precision tokens (e.g., USDC).
pub fn get_price_7_decimal(&self) -> i128 {
match self {
PlanType::Basic => 10_0000000,
PlanType::Pro => 20_000000,
PlanType::Enterprise => 50_0000000,
}
}
}
/// Represents the current status of a subscription
/// Used to control access to platform features
#[contracttype]
Expand All @@ -53,3 +67,22 @@ pub enum SubscriptionStatus {
Expired,
Cancelled,
}
/// Main structure holding subscription details.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Subscription {
/// Unique identifier for the subscription.
pub id: BytesN<32>,
/// Address of the user/owns the subscription.
pub user: Address,
/// Type of plan subscribed to.
pub plan: PlanType,
/// Current status of the subscription.
pub status: SubscriptionStatus,
/// Ledger sequence number when the subscription started.
pub start_date: u32,
/// Ledger sequence number when the subscription is scheduled to end.
pub end_date: u32,
/// Address of the payment token used (e.g., USDC contract address).
pub payment_token: Address,
}
Loading