Skip to content
Open
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
3 changes: 3 additions & 0 deletions subscription-service/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
tests/** linguist-vendored
vitest.config.js linguist-vendored
* text=lf
13 changes: 13 additions & 0 deletions subscription-service/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

**/settings/Mainnet.toml
**/settings/Testnet.toml
.cache/**
history.txt

logs
*.log
npm-debug.log*
coverage
*.info
costs-reports.json
node_modules
4 changes: 4 additions & 0 deletions subscription-service/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

{
"files.eol": "\n"
}
19 changes: 19 additions & 0 deletions subscription-service/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

{
"version": "2.0.0",
"tasks": [
{
"label": "check contracts",
"group": "test",
"type": "shell",
"command": "clarinet check"
},
{
"type": "npm",
"script": "test",
"group": "test",
"problemMatcher": [],
"label": "npm test"
}
]
}
19 changes: 19 additions & 0 deletions subscription-service/Clarinet.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[project]
name = 'subscription-service'
description = ''
authors = []
telemetry = true
cache_dir = '.\.cache'
requirements = []
[contracts.plan-manager]
path = 'contracts/plan-manager.clar'
clarity_version = 2
epoch = 2.5
[repl.analysis]
passes = ['check_checker']

[repl.analysis.check_checker]
strict = false
trusted_sender = false
trusted_caller = false
callee_filter = false
82 changes: 82 additions & 0 deletions subscription-service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Subscription Service Smart Contract

## Overview

This smart contract implements a subscription service on the Stacks blockchain. It allows for the creation and management of subscription plans, user subscriptions, and associated financial transactions.

## Features

1. Subscription Plan Management
- Add new subscription plans
- Update existing subscription plans
- Delete subscription plans

2. User Subscription Management
- Subscribe to a plan
- Cancel a subscription
- Renew a subscription

3. Financial Operations
- Process payments for subscriptions
- Withdraw funds from the contract

4. Read-only Functions
- Get subscription plan details
- Get user subscription details
- Check if a user's subscription is active

## Contract Functions

### Admin Functions (Contract Owner Only)

1. `add-subscription-plan`: Add a new subscription plan
2. `update-subscription-plan`: Update an existing subscription plan
3. `delete-subscription-plan`: Delete a subscription plan
4. `withdraw-contract-funds`: Withdraw funds from the contract

### User Functions

1. `subscribe-to-plan`: Subscribe to a specific plan
2. `cancel-user-subscription`: Cancel the current subscription
3. `renew-user-subscription`: Renew the current subscription

### Read-only Functions

1. `get-subscription-plan`: Get details of a specific subscription plan
2. `get-user-subscription-details`: Get details of a user's subscription
3. `is-subscription-active`: Check if a user's subscription is active

## Error Codes

- `ERR-OWNER-ONLY (u100)`: Operation restricted to contract owner
- `ERR-NOT-FOUND (u101)`: Requested item not found
- `ERR-ALREADY-EXISTS (u102)`: Item already exists
- `ERR-INSUFFICIENT-BALANCE (u103)`: Insufficient balance for the operation
- `ERR-EXPIRED (u104)`: Subscription has expired

## Usage

1. Deploy the contract to the Stacks blockchain.
2. As the contract owner, add subscription plans using `add-subscription-plan`.
3. Users can subscribe to plans using `subscribe-to-plan`.
4. Users can manage their subscriptions with `cancel-user-subscription` and `renew-user-subscription`.
5. The contract owner can manage plans and withdraw funds as needed.

## Important Considerations

- Only the contract owner can add, update, or delete subscription plans and withdraw funds.
- Users must have sufficient STX balance to subscribe to a plan or renew their subscription.
- Subscription durations and prices are measured in blocks and micro-STX, respectively.
- The contract uses the current block height for tracking subscription start and end times.

## Security

- The contract includes checks to ensure only authorized operations are performed.
- Balance checks are implemented to prevent insufficient fund issues.
- Proper error handling is in place to manage various scenarios.

## Future Improvements

- Implement a grace period for subscription renewals.
- Add functionality for tiered pricing or discounts.
- Implement a referral system or loyalty rewards.
7 changes: 7 additions & 0 deletions subscription-service/Testnet.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[network]
name = "testnet"
stacks_node_rpc_address = "https://api.testnet.hiro.so"
deployment_fee_rate = 10

[accounts.deployer]
mnemonic = "<YOUR PRIVATE TESTNET MNEMONIC HERE>"
160 changes: 160 additions & 0 deletions subscription-service/contracts/plan-manager.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
;; Subscription Service Contract

;; Constants
(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-OWNER-ONLY (err u100))
(define-constant ERR-NOT-FOUND (err u101))
(define-constant ERR-ALREADY-EXISTS (err u102))
(define-constant ERR-INSUFFICIENT-BALANCE (err u103))
(define-constant ERR-EXPIRED (err u104))
(define-constant ERR-INVALID-INPUT (err u105))

;; Data maps
(define-map subscription-plans
{ plan-id: uint }
{ plan-name: (string-ascii 50), subscription-duration: uint, plan-price: uint }
)

(define-map user-subscriptions
{ subscriber: principal }
{ subscribed-plan-id: uint, subscription-start-block: uint, subscription-end-block: uint }
)

;; Variables
(define-data-var next-available-plan-id uint u1)

;; Read-only functions
(define-read-only (get-subscription-plan (plan-id uint))
(map-get? subscription-plans { plan-id: plan-id })
)

(define-read-only (get-user-subscription-details (subscriber principal))
(map-get? user-subscriptions { subscriber: subscriber })
)

(define-read-only (is-subscription-active (subscriber principal))
(match (get-user-subscription-details subscriber)
subscription-details (> (get subscription-end-block subscription-details) block-height)
false
)
)

;; Private functions
(define-private (transfer-stx-tokens (amount uint) (sender principal) (recipient principal))
(match (stx-transfer? amount sender recipient)
transfer-success (ok true)
transfer-error (err transfer-error)
)
)

(define-private (validate-plan-input (plan-name (string-ascii 50)) (subscription-duration uint) (plan-price uint))
(and
(> (len plan-name) u0)
(< (len plan-name) u51)
(> subscription-duration u0)
(> plan-price u0)
)
)

;; Public functions
(define-public (add-subscription-plan (plan-name (string-ascii 50)) (subscription-duration uint) (plan-price uint))
(begin
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-OWNER-ONLY)
(asserts! (validate-plan-input plan-name subscription-duration plan-price) ERR-INVALID-INPUT)
(let ((new-plan-id (var-get next-available-plan-id)))
(asserts! (is-none (get-subscription-plan new-plan-id)) ERR-ALREADY-EXISTS)
(map-set subscription-plans
{ plan-id: new-plan-id }
{ plan-name: plan-name, subscription-duration: subscription-duration, plan-price: plan-price }
)
(var-set next-available-plan-id (+ new-plan-id u1))
(ok new-plan-id)
)
)
)

(define-public (update-subscription-plan (plan-id uint) (plan-name (string-ascii 50)) (subscription-duration uint) (plan-price uint))
(begin
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-OWNER-ONLY)
(asserts! (validate-plan-input plan-name subscription-duration plan-price) ERR-INVALID-INPUT)
(asserts! (is-some (get-subscription-plan plan-id)) ERR-NOT-FOUND)
(map-set subscription-plans
{ plan-id: plan-id }
{ plan-name: plan-name, subscription-duration: subscription-duration, plan-price: plan-price }
)
(ok true)
)
)

(define-public (delete-subscription-plan (plan-id uint))
(begin
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-OWNER-ONLY)
(asserts! (is-some (get-subscription-plan plan-id)) ERR-NOT-FOUND)
(map-delete subscription-plans { plan-id: plan-id })
(ok true)
)
)

(define-public (subscribe-to-plan (plan-id uint))
(let (
(selected-plan (unwrap! (get-subscription-plan plan-id) ERR-NOT-FOUND))
(plan-price (get plan-price selected-plan))
(subscription-duration (get subscription-duration selected-plan))
(subscription-start-block block-height)
(subscription-end-block (+ block-height subscription-duration))
)
(asserts! (>= (stx-get-balance tx-sender) plan-price) ERR-INSUFFICIENT-BALANCE)
(match (transfer-stx-tokens plan-price tx-sender (as-contract tx-sender))
transfer-success (begin
(map-set user-subscriptions
{ subscriber: tx-sender }
{ subscribed-plan-id: plan-id, subscription-start-block: subscription-start-block, subscription-end-block: subscription-end-block }
)
(ok true)
)
transfer-error (err transfer-error)
)
)
)

(define-public (cancel-user-subscription)
(begin
(asserts! (is-some (get-user-subscription-details tx-sender)) ERR-NOT-FOUND)
(map-delete user-subscriptions { subscriber: tx-sender })
(ok true)
)
)

(define-public (renew-user-subscription)
(let (
(current-subscription (unwrap! (get-user-subscription-details tx-sender) ERR-NOT-FOUND))
(subscribed-plan-id (get subscribed-plan-id current-subscription))
(subscription-plan (unwrap! (get-subscription-plan subscribed-plan-id) ERR-NOT-FOUND))
(renewal-price (get plan-price subscription-plan))
(renewal-duration (get subscription-duration subscription-plan))
(new-subscription-end-block (+ block-height renewal-duration))
)
(asserts! (>= (stx-get-balance tx-sender) renewal-price) ERR-INSUFFICIENT-BALANCE)
(match (transfer-stx-tokens renewal-price tx-sender (as-contract tx-sender))
transfer-success (begin
(map-set user-subscriptions
{ subscriber: tx-sender }
{ subscribed-plan-id: subscribed-plan-id, subscription-start-block: block-height, subscription-end-block: new-subscription-end-block }
)
(ok true)
)
transfer-error (err transfer-error)
)
)
)

(define-public (withdraw-contract-funds (withdrawal-amount uint))
(begin
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-OWNER-ONLY)
(asserts! (<= withdrawal-amount (stx-get-balance (as-contract tx-sender))) ERR-INSUFFICIENT-BALANCE)
(match (transfer-stx-tokens withdrawal-amount (as-contract tx-sender) CONTRACT-OWNER)
transfer-success (ok true)
transfer-error (err transfer-error)
)
)
)
24 changes: 24 additions & 0 deletions subscription-service/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

{
"name": "subscription-service-tests",
"version": "1.0.0",
"description": "Run unit tests on this project.",
"type": "module",
"private": true,
"scripts": {
"test": "vitest run",
"test:report": "vitest run -- --coverage --costs",
"test:watch": "chokidar \"tests/**/*.ts\" \"contracts/**/*.clar\" -c \"npm run test:report\""
},
"author": "",
"license": "ISC",
"dependencies": {
"@hirosystems/clarinet-sdk": "^2.3.2",
"@stacks/transactions": "^6.12.0",
"chokidar-cli": "^3.0.0",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vitest": "^1.3.1",
"vitest-environment-clarinet": "^2.0.0"
}
}
Loading