Solo Project Building Guide
Panduan lengkap untuk mengerjakan individual project pada workshop Day 2. Pilih salah satu dari 3 opsi berikut dan ikuti step-by-step guide untuk menyelesaikan project Anda.
🪙 Option 2: Token Management System
Project Overview
Bangun sistem manajemen token kustom dengan fitur minting, burning, transfer restrictions, dan basic staking mechanism.
🎯 Learning Objectives
- Membuat custom fungible token (FT) di Stacks
- Implement minting dan burning functionality
- Menerapkan transfer restrictions
- Membangun basic staking mechanism
📋 Project Requirements
- Token dengan supply management
- Admin controls untuk minting/burning
- Transfer restrictions berdasarkan whitelist
- Basic staking dengan rewards calculation
- Unit tests untuk semua functions
Step 1: Project Setup (15 menit)
1.1 Initialize Project
cd ~/stacks-workshop
clarinet new token-management-system
cd token-management-system
1.2 Project Structure
token-management-system/
├── contracts/
│ ├── token-manager.clar # Main token contract
│ └── staking-pool.clar # Staking functionality
├── tests/
│ ├── token-manager.test.ts
│ └── staking-pool.test.ts
├── settings/
└── Clarinet.toml
1.3 Update Clarinet.toml
[contracts.token-manager]
path = "contracts/token-manager.clar"
clarity_version = 2
epoch = 2.4
[contracts.staking-pool]
path = "contracts/staking-pool.clar"
clarity_version = 2
epoch = 2.4
depends_on = ["token-manager"]
Step 2: Token Contract Foundation (30 menit)
2.1 Create contracts/simple-token.clar
;; Simple Token - SIP-010 Standard Implementation
;; Workshop Version: Minimal but compliant
;; Implement SIP-010 fungible token trait
(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)
;; Define the token
(define-fungible-token workshop-token)
;; Constants
(define-constant contract-owner tx-sender)
(define-constant err-owner-only (err u100))
(define-constant err-not-token-owner (err u101))
;; Token metadata
(define-constant token-name "Workshop Token")
(define-constant token-symbol "WTK")
(define-constant token-decimals u6)
(define-constant token-uri u"https://workshop.blockdev.id/token.json")
;; SIP-010 required functions
(define-read-only (get-name)
(ok token-name)
)
(define-read-only (get-symbol)
(ok token-symbol)
)
(define-read-only (get-decimals)
(ok token-decimals)
)
(define-read-only (get-balance (user principal))
(ok (ft-get-balance workshop-token user))
)
(define-read-only (get-total-supply)
(ok (ft-get-supply workshop-token))
)
(define-read-only (get-token-uri)
(ok (some token-uri))
)
;; SIP-010 transfer function
(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34))))
(begin
(asserts! (is-eq from tx-sender) err-not-token-owner)
(ft-transfer? workshop-token amount from to)
)
)
;; Mint function (owner only)
(define-public (mint (amount uint) (to principal))
(begin
(asserts! (is-eq tx-sender contract-owner) err-owner-only)
(ft-mint? workshop-token amount to)
)
)
2.2 Testing Your Simple Token
Update Clarinet.toml:
[contracts.simple-token]
path = "contracts/simple-token.clar"
clarity_version = 2
epoch = 2.4
Console Testing:
# Deploy and test token
clarinet console
clarinet>> ::deploy_contract simple-token contracts/simple-token.clar
# Test SIP-010 functions
clarinet>> (contract-call? .simple-token get-name)
clarinet>> (contract-call? .simple-token get-symbol)
clarinet>> (contract-call? .simple-token get-decimals)
clarinet>> (contract-call? .simple-token get-total-supply)
# Mint tokens (only owner can do this)
clarinet>> (contract-call? .simple-token mint u1000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)
# Check balance
clarinet>> (contract-call? .simple-token get-balance 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)
# Transfer tokens
clarinet>> (contract-call? .simple-token transfer u100000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 none)
Step 3: Token Project Complete! (15 menit)
🎉 Deliverables Checklist:
# ✅ Simple Token Workshop Checklist
# [ ] SIP-010 compliant token created
# [ ] Token metadata properly set
# [ ] Mint function working (owner only)
# [ ] Transfer function working
# [ ] All SIP-010 read functions working
# [ ] Local testing completed
# [ ] Ready for testnet deployment
🚀 Next Steps (Optional):
If you finish early, try adding these features:
;; Add burn function
(define-public (burn (amount uint) (from principal))
(begin
(asserts! (is-eq from tx-sender) err-not-token-owner)
(ft-burn? workshop-token amount from)
)
)
;; Add admin functions
(define-public (set-token-uri (new-uri (string-utf8 256)))
(begin
(asserts! (is-eq tx-sender contract-owner) err-owner-only)
(var-set token-uri new-uri)
(ok true)
)
)
📝 Project Documentation:
Create a TOKEN_README.md
:
# Workshop Token (WTK)
## Description
SIP-010 compliant fungible token created in Stacks workshop.
## Features
- ✅ Standard SIP-010 implementation
- ✅ Owner-controlled minting
- ✅ Standard transfer functionality
- ✅ Token metadata
## Contract Functions
- `get-name` - Returns token name
- `get-symbol` - Returns token symbol
- `get-decimals` - Returns decimal places
- `get-balance` - Get user balance
- `get-total-supply` - Get total token supply
- `transfer` - Transfer tokens between users
- `mint` - Mint new tokens (owner only)
## Testing Commands
[Include your console testing commands here]
🖼️ Option 3: Simple NFT Collection
Project Overview
Membangun NFT collection contract menggunakan SIP-009 standard dengan fokus pada basic minting dan metadata management.
🎯 Learning Objectives
- Membuat NFT contract menggunakan SIP-009 standard
- Implement minting dengan metadata sederhana
- Mengelola collection ownership
- Transfer dan ownership management
📋 Project Requirements
- NFT collection dengan metadata
- Minting functions dengan access control
- Transfer dan ownership management
- Unit tests untuk semua functions
Step 1: NFT Project Setup (15 menit)
1.1 Initialize Project
cd ~/stacks-workshop
clarinet new simple-nft-collection
cd simple-nft-collection
1.2 Update Clarinet.toml
[contracts.simple-nft]
path = "contracts/simple-nft.clar"
clarity_version = 2
epoch = 2.4
Step 2: Simple NFT Contract (30 menit)
2.1 Create contracts/simple-nft.clar
;; Simple NFT Collection - SIP-009 Standard Implementation
;; Workshop Version: Basic but compliant
;; Implement SIP-009 NFT trait
(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)
;; Define the NFT
(define-non-fungible-token workshop-nft uint)
;; Constants
(define-constant contract-owner tx-sender)
(define-constant err-owner-only (err u100))
(define-constant err-not-token-owner (err u101))
;; Data variables
(define-data-var last-token-id uint u0)
(define-data-var base-token-uri (string-ascii 256) "https://workshop.blockdev.id/nft/")
;; Data maps for metadata
(define-map token-metadata uint
{
name: (string-ascii 64),
description: (string-ascii 256),
image: (string-ascii 256)
}
)
;; SIP-009 required functions
(define-read-only (get-last-token-id)
(ok (var-get last-token-id))
)
(define-read-only (get-token-uri (token-id uint))
(ok (some (concat (var-get base-token-uri) (uint-to-ascii token-id))))
)
(define-read-only (get-owner (token-id uint))
(ok (nft-get-owner? workshop-nft token-id))
)
;; Transfer function
(define-public (transfer (token-id uint) (sender principal) (recipient principal))
(begin
(asserts! (is-eq tx-sender sender) err-not-token-owner)
(nft-transfer? workshop-nft token-id sender recipient)
)
)
;; Mint function (owner only)
(define-public (mint (to principal) (name (string-ascii 64)) (description (string-ascii 256)) (image (string-ascii 256)))
(let
(
(token-id (+ (var-get last-token-id) u1))
)
(asserts! (is-eq tx-sender contract-owner) err-owner-only)
;; Mint NFT
(try! (nft-mint? workshop-nft token-id to))
;; Store metadata
(map-set token-metadata token-id {
name: name,
description: description,
image: image
})
;; Update last token ID
(var-set last-token-id token-id)
(ok token-id)
)
)
;; Get token metadata
(define-read-only (get-token-metadata (token-id uint))
(map-get? token-metadata token-id)
)
;; Set base URI (owner only)
(define-public (set-base-uri (new-uri (string-ascii 256)))
(begin
(asserts! (is-eq tx-sender contract-owner) err-owner-only)
(ok (var-set base-token-uri new-uri))
)
)
2.2 Testing Your Simple NFT
Update Clarinet.toml:
[contracts.simple-nft]
path = "contracts/simple-nft.clar"
clarity_version = 2
epoch = 2.4
Console Testing:
# Deploy and test NFT
clarinet console
clarinet>> ::deploy_contract simple-nft contracts/simple-nft.clar
# Test SIP-009 functions
clarinet>> (contract-call? .simple-nft get-last-token-id)
clarinet>> (contract-call? .simple-nft get-token-uri u1)
# Mint NFT (only owner can do this)
clarinet>> (contract-call? .simple-nft mint 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM "Workshop NFT #1" "My first NFT in the workshop" "https://workshop.blockdev.id/nft1.jpg")
# Check ownership and metadata
clarinet>> (contract-call? .simple-nft get-owner u1)
clarinet>> (contract-call? .simple-nft get-token-metadata u1)
# Transfer NFT
clarinet>> (contract-call? .simple-nft transfer u1 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)
Step 3: NFT Project Complete! (15 menit)
🎉 Deliverables Checklist:
# ✅ Simple NFT Workshop Checklist
# [ ] SIP-009 compliant NFT created
# [ ] NFT metadata properly stored
# [ ] Mint function working (owner only)
# [ ] Transfer function working
# [ ] All SIP-009 read functions working
# [ ] Local testing completed
# [ ] Ready for testnet deployment
🚀 Next Steps (Optional):
If you finish early, try adding these features:
;; Add batch mint function
(define-public (batch-mint (recipients (list 10 principal)) (names (list 10 (string-ascii 64))) (descriptions (list 10 (string-ascii 256))) (images (list 10 (string-ascii 256))))
(let
(
(current-id (var-get last-token-id))
)
(fold batch-mint-helper (zip recipients (zip names (zip descriptions images))) current-id)
)
)
;; Add burn function
(define-public (burn (token-id uint))
(let
(
(token-owner (unwrap! (nft-get-owner? workshop-nft token-id) err-not-token-owner))
)
(asserts! (is-eq tx-sender token-owner) err-not-token-owner)
(nft-burn? workshop-nft token-id token-owner)
)
)
📝 Project Documentation:
Create a NFT_README.md
:
# Workshop NFT Collection
## Description
SIP-009 compliant NFT collection created in Stacks workshop.
## Features
- ✅ Standard SIP-009 implementation
- ✅ Owner-controlled minting
- ✅ NFT metadata storage
- ✅ Standard transfer functionality
## Contract Functions
- `get-last-token-id` - Returns last minted token ID
- `get-token-uri` - Returns token URI
- `get-owner` - Get token owner
- `transfer` - Transfer NFT between users
- `mint` - Mint new NFT (owner only)
- `get-token-metadata` - Get token metadata
## Testing Commands
[Include your console testing commands here]
🗳️ Option 4: Simple Voting System
Project Overview
Membangun sistem voting sederhana dengan governance token, proposal creation, dan basic voting mechanism.
🎯 Learning Objectives
- Membuat governance token
- Implement proposal creation dan voting
- Mengelola voting results
- Simple execution untuk approved proposals
📋 Project Requirements
- Governance token untuk voting rights
- Proposal creation dan management
- Basic voting mechanism
- Unit tests untuk semua functions
Step 1: Voting Project Setup (15 menit)
1.1 Initialize Project
cd ~/stacks-workshop
clarinet new simple-voting-system
cd simple-voting-system
1.2 Update Clarinet.toml
[contracts.simple-voting]
path = "contracts/simple-voting.clar"
clarity_version = 2
epoch = 2.4
Step 2: Simple Voting Contract (45 menit)
2.1 Create contracts/simple-voting.clar
;; Simple Voting System - Workshop Version
;; Basic governance dengan proposal dan voting functionality
;; Implement SIP-010 for governance token
(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait)
;; Define governance token
(define-fungible-token gov-token)
;; Constants
(define-constant contract-owner tx-sender)
(define-constant err-owner-only (err u100))
(define-constant err-proposal-not-found (err u101))
(define-constant err-already-voted (err u102))
(define-constant err-voting-closed (err u103))
;; Token metadata
(define-constant token-name "Workshop Gov Token")
(define-constant token-symbol "WGT")
(define-constant token-decimals u6)
;; Data variables
(define-data-var proposal-count uint u0)
;; Proposal structure
(define-map proposals uint {
title: (string-ascii 64),
description: (string-ascii 256),
end-block: uint,
yes-votes: uint,
no-votes: uint,
executed: bool
})
;; Voting records
(define-map votes { proposal-id: uint, voter: principal } bool)
;; SIP-010 Token Functions
(define-read-only (get-name)
(ok token-name)
)
(define-read-only (get-symbol)
(ok token-symbol)
)
(define-read-only (get-decimals)
(ok token-decimals)
)
(define-read-only (get-balance (user principal))
(ok (ft-get-balance gov-token user))
)
(define-read-only (get-total-supply)
(ok (ft-get-supply gov-token))
)
(define-read-only (get-token-uri)
(ok none)
)
(define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34))))
(begin
(asserts! (is-eq from tx-sender) err-owner-only)
(ft-transfer? gov-token amount from to)
)
)
;; Mint governance tokens (owner only)
(define-public (mint (amount uint) (to principal))
(begin
(asserts! (is-eq tx-sender contract-owner) err-owner-only)
(ft-mint? gov-token amount to)
)
)
;; Proposal Functions
(define-public (create-proposal (title (string-ascii 64)) (description (string-ascii 256)) (duration uint))
(let
(
(proposal-id (+ (var-get proposal-count) u1))
(end-block (+ block-height duration))
)
;; Create proposal
(map-set proposals proposal-id {
title: title,
description: description,
end-block: end-block,
yes-votes: u0,
no-votes: u0,
executed: false
})
(var-set proposal-count proposal-id)
(ok proposal-id)
)
)
;; Vote on proposal
(define-public (vote (proposal-id uint) (support bool))
(let
(
(proposal (unwrap! (map-get? proposals proposal-id) err-proposal-not-found))
(voting-power (ft-get-balance gov-token tx-sender))
(vote-key { proposal-id: proposal-id, voter: tx-sender })
)
;; Check if voting is still open
(asserts! (< block-height (get end-block proposal)) err-voting-closed)
;; Check if user hasn't voted yet
(asserts! (is-none (map-get? votes vote-key)) err-already-voted)
;; User must have tokens to vote
(asserts! (> voting-power u0) err-owner-only)
;; Record vote
(map-set votes vote-key true)
;; Update vote counts
(if support
(map-set proposals proposal-id
(merge proposal { yes-votes: (+ (get yes-votes proposal) voting-power) }))
(map-set proposals proposal-id
(merge proposal { no-votes: (+ (get no-votes proposal) voting-power) }))
)
(ok voting-power)
)
)
;; Execute approved proposal
(define-public (execute-proposal (proposal-id uint))
(let
(
(proposal (unwrap! (map-get? proposals proposal-id) err-proposal-not-found))
)
;; Check if voting period ended
(asserts! (>= block-height (get end-block proposal)) err-voting-closed)
;; Check if not already executed
(asserts! (not (get executed proposal)) err-already-voted)
;; Check if proposal passed (yes > no)
(asserts! (> (get yes-votes proposal) (get no-votes proposal)) err-owner-only)
;; Mark as executed
(map-set proposals proposal-id (merge proposal { executed: true }))
(ok true)
)
)
;; Read-only functions
(define-read-only (get-proposal (proposal-id uint))
(map-get? proposals proposal-id)
)
(define-read-only (get-user-vote (proposal-id uint) (voter principal))
(map-get? votes { proposal-id: proposal-id, voter: voter })
)
(define-read-only (get-proposal-count)
(var-get proposal-count)
)
2.2 Testing Your Voting System
Update Clarinet.toml:
[contracts.simple-voting]
path = "contracts/simple-voting.clar"
clarity_version = 2
epoch = 2.4
Console Testing:
# Deploy and test voting system
clarinet console
clarinet>> ::deploy_contract simple-voting contracts/simple-voting.clar
# Test SIP-010 token functions
clarinet>> (contract-call? .simple-voting get-name)
clarinet>> (contract-call? .simple-voting get-symbol)
clarinet>> (contract-call? .simple-voting get-total-supply)
# Mint governance tokens (only owner can do this)
clarinet>> (contract-call? .simple-voting mint u1000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)
clarinet>> (contract-call? .simple-voting mint u500000 'ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5)
# Check balances
clarinet>> (contract-call? .simple-voting get-balance 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)
# Create proposal
clarinet>> (contract-call? .simple-voting create-proposal "Workshop Proposal #1" "This is our first proposal in the voting workshop" u100)
# Vote on proposal (yes = true, no = false)
clarinet>> (contract-call? .simple-voting vote u1 true)
# Switch to different account and vote
clarinet>> ::set_tx_sender ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5
clarinet>> (contract-call? .simple-voting vote u1 false)
# Check proposal results
clarinet>> (contract-call? .simple-voting get-proposal u1)
clarinet>> (contract-call? .simple-voting get-user-vote u1 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)
Step 3: Voting Project Complete! (15 menit)
🎉 Deliverables Checklist:
# ✅ Simple Voting Workshop Checklist
# [ ] SIP-010 compliant governance token created
# [ ] Proposal creation working
# [ ] Voting mechanism working
# [ ] Vote counting accurate
# [ ] Proposal execution working
# [ ] Local testing completed
# [ ] Ready for testnet deployment
🚀 Next Steps (Optional):
If you finish early, try adding these features:
;; Add minimum token requirement for proposals
(define-constant min-proposal-tokens u10000)
;; Add proposal deposit mechanism
(define-public (create-proposal-with-deposit (title (string-ascii 64)) (description (string-ascii 256)) (duration uint) (deposit uint))
(begin
(asserts! (>= (ft-get-balance gov-token tx-sender) min-proposal-tokens) err-owner-only)
(try! (ft-transfer? gov-token deposit tx-sender (as-contract tx-sender)))
(create-proposal title description duration)
)
)
;; Add delegation system
(define-map delegations principal principal)
(define-public (delegate-to (delegate principal))
(begin
(asserts! (not (is-eq tx-sender delegate)) err-owner-only)
(ok (map-set delegations tx-sender delegate))
)
)
📝 Project Documentation:
Create a VOTING_README.md
:
# Workshop Voting System
## Description
Simple governance system dengan SIP-010 token dan basic voting functionality.
## Features
- ✅ SIP-010 compliant governance token
- ✅ Proposal creation dan management
- ✅ Token-weighted voting
- ✅ Proposal execution
## Contract Functions
- `mint` - Mint governance tokens (owner only)
- `create-proposal` - Create new proposal
- `vote` - Vote on proposal
- `execute-proposal` - Execute approved proposal
- `get-proposal` - Get proposal details
- `get-user-vote` - Check user's vote
## Testing Commands
[Include your console testing commands here]
🚀 Project Completion Checklist
For All Projects:
✅ Core Implementation
- All main functions implemented
- Error handling untuk edge cases
- Access control mechanisms
- Data validation
✅ Testing & Validation
- Console testing completed
- All functions tested manually
- Edge cases validated
- Documentation updated
✅ Code Quality
- Code comments added
- Constants defined appropriately
- Helper functions extracted
- Security best practices followed
✅ Project Documentation
- README.md dengan project overview
- Function documentation
- Usage examples
- Known limitations listed
💡 Tips for Success
Development Best Practices:
- Start Small - Implement core functions first, then add features
- Test Frequently - Use console testing after each function
- Error Handling - Always validate inputs dan handle edge cases
- Documentation - Write clear comments dan documentation
- Security - Consider access control dan validation
Time Management:
- Hour 1: Project setup dan basic structure
- Hour 2: Core functions implementation
- Hour 3: Advanced features dan testing
- Hour 4: Documentation dan presentation prep
Common Pitfalls:
- Forgetting to add access control checks
- Not handling optional values properly
- Insufficient error handling
- Over-engineering solutions
🎯 Remember: The goal is to build a working prototype that demonstrates your understanding of Clarity programming and Stacks development. Focus on core functionality first, then add enhancements if time allows!