Skip to content

Commit d7f10f1

Browse files
committed
feat(subscription): Implement Subscription Service with USDC Payment and Ledger Standardization
1 parent 40fb41b commit d7f10f1

File tree

7 files changed

+469
-8
lines changed

7 files changed

+469
-8
lines changed

contracts/assetsup/src/errors.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use soroban_sdk::{contracterror};
2+
3+
#[contracterror]
4+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
5+
#[repr(u32)]
6+
pub enum ContractError{
7+
//general errors
8+
Unauthorized=100,
9+
AlreadyInitialized=101,
10+
11+
//Subscription errors
12+
SubscriptionAlreadyExists=200,
13+
SubscriptionNotFound=201,
14+
SubscriptionNotActive=202,
15+
SubscriptionActive=203,
16+
17+
//Payment errors
18+
InsufficientPayment=300,
19+
PaymentFailed=301,
20+
}

contracts/assetsup/src/lib.rs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
#![no_std]
2-
use soroban_sdk::{Address, Env, contract, contractimpl, contracttype};
2+
use soroban_sdk::{Address, Env, BytesN,contract, contractimpl};
3+
4+
//Import types and modules
5+
mod errors;
6+
mod types;
7+
mod subscription;
8+
9+
//Import types and contract logic
10+
use crate::types::DataKey;
11+
use crate::subscription::{SubscriptionContract, SubscriptionService};
312

4-
#[contracttype]
5-
#[derive(Clone, Debug, Eq, PartialEq)]
6-
pub enum DataKey {
7-
Admin,
8-
}
913

1014
#[contract]
1115
pub struct AssetUpContract;
@@ -24,6 +28,29 @@ impl AssetUpContract {
2428
pub fn get_admin(env: Env) -> Address {
2529
env.storage().persistent().get(&DataKey::Admin).unwrap()
2630
}
27-
}
2831

29-
mod tests;
32+
//creates a new subscription
33+
pub fn create_subscription(
34+
env: Env,
35+
id: BytesN<32>,
36+
user: Address,
37+
plan: crate::types::PlanType,
38+
payment_token: Address,
39+
duration_days: u32,
40+
) ->Result<crate::types::Subscription, errors::ContractError>{
41+
SubscriptionService::create_subscription(env, id, user, plan, payment_token, duration_days)
42+
}
43+
/// Cancels an active subscription.
44+
pub fn cancel_subscription(env: Env, id:soroban_sdk::BytesN<32>) -> Result<crate::types::Subscription, errors::ContractError>{
45+
SubscriptionService::cancel_subscription(env, id)
46+
}
47+
/// Retrieves subscription details.
48+
pub fn get_subscription(env: Env, id:soroban_sdk::BytesN<32>) -> Result<crate::types::Subscription, errors::ContractError>{
49+
SubscriptionService::get_subscription(env, id)
50+
}
51+
}
52+
//export contract name for testing in other modules
53+
#[cfg(test)]
54+
mod tests{
55+
56+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
use soroban_sdk::{
2+
contractimpl, token, Address, BytesN, Env,
3+
};
4+
use crate::errors::ContractError;
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, ContractError>;
22+
/// Cancels an active subscription.
23+
fn cancel_subscription(
24+
env: Env,
25+
id: BytesN<32>,
26+
) -> Result<Subscription, ContractError>;
27+
/// Retrives the details of a subscription
28+
fn get_subscription(
29+
env: Env,
30+
id: BytesN<32>,
31+
) -> Result<Subscription, ContractError>;
32+
}
33+
pub struct SubscriptionService;
34+
35+
impl SubscriptionContract for SubscriptionService {
36+
fn create_subscription(
37+
env: Env,
38+
id: BytesN<32>,
39+
user: Address,
40+
plan: PlanType,
41+
payment_token: Address,
42+
duration_days: u32,
43+
) -> Result<Subscription, ContractError> {
44+
// 1. Authorization check
45+
user.require_auth();
46+
47+
// 2. Existence check
48+
let sub_key = DataKey::Subscription(id.clone());
49+
if env.storage().persistent().has(&sub_key) {
50+
return Err(ContractError::SubscriptionAlreadyExists);
51+
}
52+
53+
// 3. Payment Logic
54+
let token_client = token::Client::new(&env, &payment_token);
55+
let amount=plan.get_price_7_decimal();
56+
let recipient=env.current_contract_address();
57+
58+
// simualate token transfer from user to contract(payment)
59+
//User has executed an 'approve' call on the token contract
60+
//The contract pulls the token
61+
token_client.transfer_from(&user,&user, &recipient, &amount);
62+
63+
// 4. Calculate Dates
64+
let current_ledger=env.ledger().sequence();
65+
//let seconds_in_day=24*60*60;
66+
//let ledgers_in_day=seconds_in_day/env.ledger().close_time_resolution();
67+
let duration_ledgers= duration_days.checked_mul(LEDGERS_PER_DAY).unwrap_or(u32::MAX); //for simplicity, 1 day=1 ledger
68+
69+
let start_date=current_ledger;
70+
let end_date=current_ledger.checked_add(duration_ledgers).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(
89+
env: Env,
90+
id: BytesN<32>,
91+
) -> Result<Subscription, ContractError> {
92+
let sub_key = DataKey::Subscription(id.clone());
93+
94+
//1. Retrieve subscription
95+
let mut subscription: Subscription = env
96+
.storage()
97+
.persistent()
98+
.get(&sub_key)
99+
.ok_or(ContractError::SubscriptionNotFound)?;
100+
101+
//2. Authorization check(only the subscriber can cancel)
102+
subscription.user.require_auth();
103+
104+
// 3. Status check
105+
if subscription.status != SubscriptionStatus::Active {
106+
return Err(ContractError::SubscriptionNotActive);
107+
}
108+
109+
// 4. Update status and date
110+
subscription.status = SubscriptionStatus::Cancelled;
111+
subscription.end_date = env.ledger().sequence(); //end immediately
112+
113+
// 5.Store update
114+
env.storage().persistent().set(&sub_key, &subscription);
115+
116+
Ok(subscription)
117+
}
118+
119+
fn get_subscription(
120+
env: Env,
121+
id: BytesN<32>,
122+
) -> Result<Subscription, ContractError> {
123+
let sub_key = DataKey::Subscription(id.clone());
124+
env.storage()
125+
.persistent()
126+
.get(&sub_key)
127+
.ok_or(ContractError::SubscriptionNotFound)
128+
129+
}
130+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#![cfg(test)]
2+
3+
extern crate std;
4+
use soroban_sdk::{
5+
testutils::Address as _,
6+
token,
7+
vec,
8+
Address, Env
9+
};
10+
11+
use crate::{AssetUpContractClient, AssetUpContract};
12+
13+
/// Helper to set up the contract and environment specifically for subscription tests.
14+
pub fn setup_subscription_test_env()->(Env, AssetUpContractClient<'static>, Address, token::Client) {
15+
let env = Env::default();
16+
env.mock_all_auths();
17+
18+
//set ledger state for predictable date/time logic
19+
env.ledger().set_sequence(100);
20+
env.ledger().set_close_time_resolution(5); // 5 seconds per ledger
21+
22+
let contract_id = env.register_contract(None, AssetUpContract);
23+
let client = AssetUpContractClient::new(&env, &contract_id);
24+
let admin= Address::generate(&env);
25+
26+
//Initialize contract
27+
client.initialize(&admin);
28+
29+
//MOck token setup(USDC)
30+
let admin = Address::generate(&env);
31+
// Use token::StellarAsset for a standard token mock
32+
let token_contract_id = env.register_contract_wasm(&token_admin, token::StellarAssetContract);
33+
let token_client=token::Client::new(&env, &token_contract_id);
34+
35+
(env,client,admin, token_client)
36+
37+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#![cfg(test)]
2+
3+
extern crate std;
4+
5+
use crate::{AssetUpContract, AssetUpContractClient};
6+
use soroban_sdk::{Address, Env, testutils::Address as _};
7+
8+
/// Setup text environment with contract and addresses
9+
10+
pub fn setup_test_environment() -> (Env, AssetUpContractClient<'static>, Address) {
11+
let env = Env::default();
12+
env.mock_all_auths();
13+
14+
let contract_id = env.register_contract(None, AssetUpContract);
15+
let client = AssetUpContractClient::new(&env, &contract_id);
16+
17+
let admin = Address::generate(&env);
18+
19+
(env, client, admin)
20+
}
21+
22+
#[test]
23+
fn test_initialize() {
24+
let (_env, client, admin) = setup_test_environment();
25+
client.initialize(&admin);
26+
let saved_admin = client.get_admin();
27+
28+
assert_eq!(admin, saved_admin);
29+
}
30+
31+
#[test]
32+
#[should_panic(expected="Contract is already initialized")]
33+
fn test_initialize_panic() {
34+
let (_env, client, admin) = setup_test_environment();
35+
client.initialize(&admin);
36+
client.initialize(&admin);
37+
}

0 commit comments

Comments
 (0)