Skip to content

Commit 676aa54

Browse files
committed
chore: apply rustfmt and fix whitespace/enum formatting
1 parent 00bd3c7 commit 676aa54

File tree

6 files changed

+353
-14
lines changed

6 files changed

+353
-14
lines changed

contracts/assetsup/src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ pub enum Error {
2020
Unauthorized = 8,
2121
// Payment is not valid
2222
InvalidPayment = 9,
23+
//Subscription errors
24+
SubscriptionNotFound = 201,
25+
SubscriptionNotActive = 202,
26+
SubscriptionActive = 203,
27+
28+
//Payment errors
29+
InsufficientPayment = 300,
2330
}
2431

2532
pub fn handle_error(env: &Env, error: Error) -> ! {

contracts/assetsup/src/lib.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
use crate::error::{Error, handle_error};
44
use soroban_sdk::{Address, BytesN, Env, String, Vec, contract, contractimpl, contracttype};
5+
use crate:: subscription::{SubscriptionContract, SubscriptionService};
56

67
pub(crate) mod asset;
78
pub(crate) mod audit;
89
pub(crate) mod branch;
910
pub(crate) mod error;
11+
pub(crate) mod subscription;
1012
pub(crate) mod types;
1113

1214
pub use types::*;
@@ -319,6 +321,25 @@ impl AssetUpContract {
319321
) -> Result<Vec<audit::AuditEntry>, Error> {
320322
Ok(audit::get_asset_log(&env, &asset_id))
321323
}
324+
//creates a new subscription
325+
pub fn create_subscription(
326+
env: Env,
327+
id: BytesN<32>,
328+
user: Address,
329+
plan: crate::types::PlanType,
330+
payment_token: Address,
331+
duration_days: u32,
332+
) ->Result<Subscription,Error> {
333+
SubscriptionService::create_subscription(env, id, user, plan, payment_token, duration_days)
334+
}
335+
/// Cancels an active subscription.
336+
pub fn cancel_subscription(env: Env, id:soroban_sdk::BytesN<32>) -> Result<crate::types::Subscription, Error> {
337+
SubscriptionService::cancel_subscription(env, id)
338+
}
339+
/// Retrieves subscription details.
340+
pub fn get_subscription(env: Env, id:soroban_sdk::BytesN<32>) -> Result<crate::types::Subscription, Error> {
341+
SubscriptionService::get_subscription(env, id)
342+
}
322343
}
323344

324345
mod tests;
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use soroban_sdk::{
2+
Address, BytesN, Env, contractimpl, token
3+
};
4+
use crate::error::Error;
5+
use crate::types::{DataKey, PlanType, Subscription, SubscriptionStatus};
6+
7+
const LEDGERS_PER_DAY: u32 = 17280; //constants for ledgers
8+
9+
/// Trait defining the public contract interface for subscription operations.
10+
pub trait SubscriptionContract {
11+
/// Creates a new subscription for a user.
12+
///
13+
/// # Arguments
14+
fn create_subscription(
15+
env: Env,
16+
id: BytesN<32>,
17+
user: Address,
18+
plan: PlanType,
19+
payment_token: Address,
20+
duration_days: u32,
21+
) -> Result<Subscription, Error>;
22+
/// Cancels an active subscription.
23+
fn cancel_subscription( env: Env,id: BytesN<32>) -> Result<Subscription
24+
, Error>;
25+
/// Retrives the details of a subscription
26+
fn get_subscription(env: Env,id: BytesN<32>) -> Result<Subscription,
27+
Error>;
28+
}
29+
pub struct SubscriptionService;
30+
31+
impl SubscriptionContract for SubscriptionService {
32+
fn create_subscription(
33+
env: Env,
34+
id: BytesN<32>,
35+
user: Address,
36+
plan: PlanType,
37+
payment_token: Address,
38+
duration_days: u32,
39+
) -> Result<Subscription, Error> {
40+
// 1. Authorization check
41+
user.require_auth();
42+
43+
// 2. Existence check
44+
let sub_key = DataKey::Subscription(id.clone());
45+
if env.storage().persistent().has(&sub_key) {
46+
return Err(Error::SubscriptionAlreadyExists);
47+
}
48+
49+
// 3. Payment Logic
50+
let token_client = token::Client::new(&env, &payment_token);
51+
let amount = plan.get_price_7_decimal();
52+
let recipient = env.current_contract_address();
53+
54+
// simualate token transfer from user to contract(payment)
55+
//User has executed an 'approve' call on the token contract
56+
//The contract pulls the token
57+
token_client.transfer_from(&user, &user, &recipient, &amount);
58+
59+
// 4. Calculate Dates
60+
let current_ledger = env.ledger().sequence();
61+
//let seconds_in_day=24*60*60;
62+
//let ledgers_in_day=seconds_in_day/env.ledger().close_time_resolution();
63+
let duration_ledgers = duration_days
64+
.checked_mul(LEDGERS_PER_DAY)
65+
.unwrap_or(u32::MAX); //for simplicity, 1 day=1 ledger
66+
67+
let start_date = current_ledger;
68+
let end_date = current_ledger
69+
.checked_add(duration_ledgers)
70+
.unwrap_or(u32::MAX);
71+
72+
// 5. Create and store subscription object
73+
let new_subscription = Subscription {
74+
id: id.clone(),
75+
user: user,
76+
plan: plan,
77+
status: SubscriptionStatus::Active,
78+
payment_token: payment_token,
79+
start_date: start_date,
80+
end_date: end_date,
81+
};
82+
83+
env.storage().persistent().set(&sub_key, &new_subscription);
84+
85+
Ok(new_subscription)
86+
}
87+
88+
fn cancel_subscription(env: Env,id: BytesN<32>) -> Result<Subscription, Error> {
89+
let sub_key = DataKey::Subscription(id.clone());
90+
91+
//1. Retrieve subscription
92+
let mut subscription: Subscription = env
93+
.storage()
94+
.persistent()
95+
.get(&sub_key)
96+
.ok_or(Error::SubscriptionNotFound)?;
97+
98+
//2. Authorization check(only the subscriber can cancel)
99+
subscription.user.require_auth();
100+
101+
// 3. Status check
102+
if subscription.status != SubscriptionStatus::Active {
103+
return Err(Error::SubscriptionNotActive);
104+
}
105+
106+
// 4. Update status and date
107+
subscription.status = SubscriptionStatus::Cancelled;
108+
subscription.end_date = env.ledger().sequence(); //end immediately
109+
110+
// 5.Store update
111+
env.storage().persistent().set(&sub_key, &subscription);
112+
113+
Ok(subscription)
114+
}
115+
116+
fn get_subscription(env: Env,id: BytesN<32>) -> Result<Subscription, Error> {
117+
let sub_key = DataKey::Subscription(id.clone());
118+
env.storage()
119+
.persistent()
120+
.get(&sub_key)
121+
.ok_or(Error::SubscriptionNotFound)
122+
123+
}
124+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#![cfg(test)]
2+
3+
extern crate std;
4+
use soroban_sdk::{
5+
testutils::{Address as _, MockAuth},
6+
token,
7+
vec,
8+
Address, BytesN, Env, Symbol, IntoVal,
9+
};
10+
11+
use crate::{
12+
errors::ContractError,
13+
types::{PlanType, Subscription, SubscriptionStatus},
14+
};
15+
//import shared helper
16+
use super::common::setup_test_environment;
17+
18+
// Subscription Test Cases
19+
fn test_subscription_creation(){
20+
let (env,client, _admin, token_client)=setup_test_environment();
21+
let subscriber= Address::generate(&env);
22+
let sub_id = BytesN::from_array(&env, &[1; 32]);
23+
let plan=PlanType::Basic;
24+
let amount=plan.get_price_7_decimal();
25+
26+
// 1. Fund the subscriber and set allowance
27+
token_client.with_admin(&Address::generate(&env)).mint(&subscriber, &amount);
28+
token_client.with_source(&subscribe).approve(&subscriber, &client_contract_id,&amount, &amount);
29+
30+
// 2. Create subscription
31+
let sub:Subsciption=client.mock_auths(&[MockAuth{
32+
address: subscriber.clone(),
33+
invoke: &vec![
34+
&env,
35+
(
36+
&token_client.contract_id(),
37+
&Symbol::new(&env, "transfer_from"),
38+
vec![
39+
&env,
40+
subscriber.to_val(),
41+
subscriber.to_val(),
42+
client_contract_id.to_val(&env),
43+
amount.to_val(),
44+
],
45+
),
46+
],
47+
}])
48+
.create_subscription(&sub_id, &subscriber, &plan, &token_client.contract_id(), 30);
49+
50+
// 3. Assert properties
51+
assert_eq!(sub.id, sub_id);
52+
assert_eq!(sub.status, SubscriptionStatus::Active);
53+
assert_eq!(sub.start_date, 100);
54+
// 30 days*24h*60.*60s/5s/ledger=518400 ledgers
55+
assert_eq!(sub.end_date, 100+518400);
56+
57+
// 4. Check contract balance(payment successful)
58+
assert_eq!(token_client.balance(&client.contract_id),amount);
59+
60+
// 5. Try to create again(fails with SubscriptionAlreadyExists)
61+
let result=client
62+
.mock_auths(&[MockAuth{
63+
address: subscriber.clone(),
64+
invoke: &vec![
65+
&env,
66+
(
67+
&token_client.contract_id,
68+
&Symbol::new(&env,"transfer_from"),
69+
vec![
70+
&env,
71+
subscriber.to_val(),
72+
subscriber.to_val(),
73+
client_contract_id.to_val(&env),
74+
amount.to_val(),
75+
],
76+
),
77+
],
78+
}])
79+
.try_cancel_subscription(&sub_id, &subscriber, &plan, &token_client.contract_id,30);
80+
81+
assert_eq!(result.unwrap_err().as_error().unwrap(), ContractError::SubscriptionNotActive.into());
82+
83+
}
84+
85+
#[test]
86+
fn test_subscription_cancellation(){
87+
let (env,client, _admin, token_client)=setup_test_environment();
88+
let subscriber= Address::generate(&env);
89+
let sub_id = BytesN::from_array(&env, &[2; 32]);
90+
let plan=PlanType::Premium;
91+
let amount=plan.get_price_7_decimal();
92+
93+
// 1. Create subscription
94+
token_client.with_admin(&Address::generate(&env)).mint(&subscriber, &amount);
95+
token_client.with_source(&subscribe).approve(&subscriber, &client_contract_id, &amount);
96+
97+
client.mock_auths(&[MockAuth{
98+
address: subscriber.clone(),
99+
invoke: &vec![
100+
&env,
101+
(
102+
&token_client.contract_id(),
103+
&Symbol::new(&env, "transfer_from"),
104+
vec![
105+
&env,
106+
subscriber.to_val(),
107+
subscriber.to_val(),
108+
client_contract_id.to_val(&env),
109+
amount.to_val(),
110+
],
111+
),
112+
],
113+
}])
114+
.create_subscription(&sub_id, &subscriber, &plan, &token_client.contract_id(), 30);
115+
116+
// Advance ledger sequence to simulate time passage
117+
env.ledger().set_sequence(200);
118+
119+
// 2. Cancel the subscription
120+
let cancelled_sub: Subscription=client
121+
.mock_auths(&[MockAuth{
122+
address: subscriber.clone(),
123+
invoke: &vec![],
124+
}])
125+
.cancel_subscription(&sub_id);
126+
127+
// 3. Assert properties
128+
assert_eq!(cancelled_sub.status, SubscriptionStatus::Cancelled);
129+
assert_eq!(cancelled_sub.end_date, 200); //should be current ledger sequence
130+
131+
132+
let result=client
133+
.mock_auths(&[MockAuth{
134+
address: subscriber.clone(),
135+
invoke: &vec![&env],
136+
}])
137+
.try_cancel_subscription(&sub_id);
138+
139+
assert_eq!(result.unwrap_err().as_error().unwrap(), ContractError::SubscriptionNotActive.into());
140+
141+
}
142+
143+
#[test]
144+
fn test_get_subscription_not_found() {
145+
let (env, client, _admin, _token_client) = setup_test_environment();
146+
let sub_id = BytesN::from_array(&env, &[3; 32]);
147+
148+
// try to get a non-existent subscription
149+
let result = client.get_subscription(&sub_id);
150+
assert_eq!(
151+
result.unwrap_err().as_error().unwrap(),
152+
ContractError::SubscriptionNotFound.into()
153+
);
154+
}

contracts/assetsup/src/types.rs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#![allow(clippy::upper_case_acronyms)]
2-
use soroban_sdk::contracttype;
2+
use soroban_sdk::{ Address, BytesN, contracttype};
33

44
/// Represents the fundamental type of asset being managed
55
/// Distinguishes between physical and digital assets for different handling requirements
@@ -19,7 +19,12 @@ pub enum AssetStatus {
1919
InMaintenance,
2020
Disposed,
2121
}
22-
22+
#[contracttype]
23+
#[derive(Clone, Debug, Eq, PartialEq)]
24+
pub enum DataKey {
25+
Admin,
26+
Subscription(BytesN<32>),
27+
}
2328
/// Represents different types of actions that can be performed on assets
2429
/// Used for audit trails and tracking asset lifecycle events
2530
#[contracttype]
@@ -43,7 +48,16 @@ pub enum PlanType {
4348
Pro,
4449
Enterprise,
4550
}
46-
51+
impl PlanType {
52+
/// Returns the required monthly payment amount in 7-decimal precision tokens (e.g., USDC).
53+
pub fn get_price_7_decimal(&self) -> i128{
54+
match self{
55+
PlanType::Basic => 10_0000000,
56+
PlanType::Pro => 20_000000,
57+
PlanType::Enterprise => 50_0000000,
58+
}
59+
}
60+
}
4761
/// Represents the current status of a subscription
4862
/// Used to control access to platform features
4963
#[contracttype]
@@ -53,3 +67,22 @@ pub enum SubscriptionStatus {
5367
Expired,
5468
Cancelled,
5569
}
70+
/// Main structure holding subscription details.
71+
#[contracttype]
72+
#[derive(Clone, Debug, Eq, PartialEq)]
73+
pub struct Subscription {
74+
/// Unique identifier for the subscription.
75+
pub id: BytesN<32>,
76+
/// Address of the user/owns the subscription.
77+
pub user: Address,
78+
/// Type of plan subscribed to.
79+
pub plan: PlanType,
80+
/// Current status of the subscription.
81+
pub status: SubscriptionStatus,
82+
/// Ledger sequence number when the subscription started.
83+
pub start_date: u32,
84+
/// Ledger sequence number when the subscription is scheduled to end.
85+
pub end_date: u32,
86+
/// Address of the payment token used (e.g., USDC contract address).
87+
pub payment_token: Address,
88+
}

0 commit comments

Comments
 (0)