Challenge - Extend Crowdfunding
Waktunya hands-on! 🎯 Kita sudah punya MVP crowdfunding contract dengan 4 functions. Sekarang saatnya kamu tambahkan fitur-fitur yang missing!
⚠️ Important: Contract Updates
Contract crowdfunding kita sekarang menggunakan XLM token transfer untuk donations. Ini berarti:
initialize()function sekarang perlu parameterxlm_token: Addressdonate()function menggunakantoken::Clientuntuk transfer XLM- Untuk testnet, XLM token address:
CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC
Kenapa? Di Soroban, semua assets (termasuk native XLM) adalah token contracts. Ini memberikan consistent API untuk semua asset types.
Jangan lupa tambahkan use soroban_sdk::token; di import statements!
🎓 Format Challenge
Setiap exercise punya:
- 📋 Task: Apa yang harus dibuat
- 💡 Hint: Clues untuk implementation
- 🧪 Test Code: Complete test untuk verify solution
- ⏱️ Estimated Time: Berapa lama kira-kira
Goal: Implement function sendiri, run test, sampai pass! ✅
🔥 Exercise 1: Get Goal Amount
Difficulty: ⭐ Easy
Estimated Time: 5-10 minutes
📋 Task
Implement function untuk return campaign goal amount.
pub fn get_goal(env: Env) -> i128
💡 Hint
- Similar dengan
get_total_raised() - Read dari storage dengan key
CAMPAIGN_GOAL - Return
i128
🧪 Test Code
Add ini ke test.rs:
#[test]
fn test_get_goal() {
let env = Env::default();
let contract_id = env.register(CrowdfundingContract, ());
let client = CrowdfundingContractClient::new(&env, &contract_id);
let owner = Address::generate(&env);
let goal = 100_000_000i128; // 10 XLM
let deadline = env.ledger().timestamp() + 86400;
env.mock_all_auths();
client.initialize(&owner, &goal, &deadline);
// Test get_goal returns correct value
assert_eq!(client.get_goal(), goal);
}
✅ Solution Template
pub fn get_goal(env: Env) -> i128 {
// Your code here
// 1. Get value dari storage dengan key CAMPAIGN_GOAL
// 2. Handle case kalau tidak ada (unwrap_or(0))
// 3. Return value
}
🔥 Exercise 2: Get Deadline
Difficulty: ⭐ Easy
Estimated Time: 5-10 minutes
📋 Task
Implement function untuk return campaign deadline timestamp.
pub fn get_deadline(env: Env) -> u64
💡 Hint
- Return type adalah
u64(unsigned 64-bit integer) - Read dari storage dengan key
CAMPAIGN_DEADLINE - Use
.unwrap()karena deadline selalu ada setelah initialize
🧪 Test Code
#[test]
fn test_get_deadline() {
let env = Env::default();
let contract_id = env.register(CrowdfundingContract, ());
let client = CrowdfundingContractClient::new(&env, &contract_id);
let owner = Address::generate(&env);
let goal = 100_000_000i128;
let deadline = env.ledger().timestamp() + 86400; // 24 hours
env.mock_all_auths();
client.initialize(&owner, &goal, &deadline);
// Test get_deadline returns correct value
assert_eq!(client.get_deadline(), deadline);
}
✅ Solution Template
pub fn get_deadline(env: Env) -> u64 {
// Your code here
// Return deadline from storage
}
🔥 Exercise 3: Check if Goal Reached
Difficulty: ⭐⭐ Medium
Estimated Time: 15 minutes
📋 Task
Implement function untuk check apakah campaign sudah reach goal.
pub fn is_goal_reached(env: Env) -> bool
Return true kalau total_raised >= goal.
💡 Hint
- Get
total_raiseddari storage - Get
goaldari storage - Compare keduanya
- Return boolean
🧪 Test Code
#[test]
fn test_is_goal_reached() {
let env = Env::default();
let contract_id = env.register(CrowdfundingContract, ());
let client = CrowdfundingContractClient::new(&env, &contract_id);
let owner = Address::generate(&env);
let donor = Address::generate(&env);
let goal = 50_000_000i128; // 5 XLM
let deadline = env.ledger().timestamp() + 86400;
env.mock_all_auths();
client.initialize(&owner, &goal, &deadline);
// Before donation - should be false
assert_eq!(client.is_goal_reached(), false);
// Donate less than goal
client.donate(&donor, &30_000_000); // 3 XLM
assert_eq!(client.is_goal_reached(), false);
// Donate to reach goal
client.donate(&donor, &20_000_000); // 2 XLM more
assert_eq!(client.is_goal_reached(), true);
// Donate more than goal
client.donate(&donor, &10_000_000); // 1 XLM more
assert_eq!(client.is_goal_reached(), true); // Still true
}
✅ Solution Template
pub fn is_goal_reached(env: Env) -> bool {
// Your code here
// 1. Get total_raised
// 2. Get goal
// 3. Return comparison result
}
🔥 Exercise 4: Check if Campaign Ended
Difficulty: ⭐⭐ Medium
Estimated Time: 15 minutes
📋 Task
Implement function untuk check apakah campaign sudah berakhir (deadline passed).
pub fn is_ended(env: Env) -> bool
Return true kalau current time > deadline.
💡 Hint
- Get current timestamp:
env.ledger().timestamp() - Get deadline dari storage
- Compare timestamps
- Return boolean
🧪 Test Code
#[test]
fn test_is_ended() {
let env = Env::default();
let contract_id = env.register(CrowdfundingContract, ());
let client = CrowdfundingContractClient::new(&env, &contract_id);
let owner = Address::generate(&env);
let goal = 100_000_000i128;
let deadline = env.ledger().timestamp() + 1000; // 1000 seconds
env.mock_all_auths();
client.initialize(&owner, &goal, &deadline);
// Before deadline
assert_eq!(client.is_ended(), false);
// Fast forward time to deadline
env.ledger().with_mut(|li| {
li.timestamp = deadline;
});
assert_eq!(client.is_ended(), false); // Exactly at deadline = not ended
// Fast forward past deadline
env.ledger().with_mut(|li| {
li.timestamp = deadline + 1;
});
assert_eq!(client.is_ended(), true); // Now it's ended
}
✅ Solution Template
pub fn is_ended(env: Env) -> bool {
// Your code here
// 1. Get current timestamp
// 2. Get deadline from storage
// 3. Return comparison
}
🔥 Exercise 5: Get Progress Percentage
Difficulty: ⭐⭐⭐ Hard
Estimated Time: 20-25 minutes
📋 Task
Implement function untuk calculate progress percentage dari campaign.
pub fn get_progress_percentage(env: Env) -> i128
Formula: (total_raised * 100) / goal
💡 Hint
- Get
total_raiseddangoal - Handle division by zero (kalau goal = 0)
- Multiply by 100 before division untuk integer percentage
- Return as
i128
Important: Perhatikan urutan operasi! Multiply dulu sebelum divide untuk avoid integer truncation.
🧪 Test Code
#[test]
fn test_get_progress_percentage() {
let env = Env::default();
let contract_id = env.register(CrowdfundingContract, ());
let client = CrowdfundingContractClient::new(&env, &contract_id);
let owner = Address::generate(&env);
let donor = Address::generate(&env);
let goal = 100_000_000i128; // 10 XLM
let deadline = env.ledger().timestamp() + 86400;
env.mock_all_auths();
client.initialize(&owner, &goal, &deadline);
// 0% progress
assert_eq!(client.get_progress_percentage(), 0);
// Donate 25% of goal
client.donate(&donor, &25_000_000); // 2.5 XLM
assert_eq!(client.get_progress_percentage(), 25);
// Donate to reach 50%
client.donate(&donor, &25_000_000); // 2.5 XLM more
assert_eq!(client.get_progress_percentage(), 50);
// Donate to reach 100%
client.donate(&donor, &50_000_000); // 5 XLM more
assert_eq!(client.get_progress_percentage(), 100);
// Donate more than goal - 120%
client.donate(&donor, &20_000_000); // 2 XLM more
assert_eq!(client.get_progress_percentage(), 120);
}
✅ Solution Template
pub fn get_progress_percentage(env: Env) -> i128 {
// Your code here
// 1. Get total_raised and goal
// 2. Handle edge case: goal == 0
// 3. Calculate: (total_raised * 100) / goal
// 4. Return result
}
🎯 Bonus Challenge: Refund System
Difficulty: ⭐⭐⭐⭐ Very Hard
Estimated Time: 45+ minutes
📋 Task
Implement refund mechanism yang memungkinkan donors get uang kembali kalau:
- Campaign sudah ended (
is_ended() == true) - Goal tidak tercapai (
is_goal_reached() == false)
pub fn refund(env: Env, donor: Address) -> i128
Return amount yang di-refund.
💡 Hints
-
Validation:
- Check campaign sudah ended
- Check goal tidak reached
- Verify donor punya donation
-
Logic:
- Get donation amount dari Map
- Remove donor dari Map
- Update total_raised (kurangi amount)
- Return refunded amount
-
Security:
- Require donor authorization:
donor.require_auth() - Prevent double refund (setelah refund, donation = 0)
- Handle edge cases (donor tidak ada, amount = 0)
- Require donor authorization:
🧪 Test Code
#[test]
fn test_refund_success() {
let env = Env::default();
let contract_id = env.register(CrowdfundingContract, ());
let client = CrowdfundingContractClient::new(&env, &contract_id);
let owner = Address::generate(&env);
let donor = Address::generate(&env);
let goal = 100_000_000i128; // 10 XLM
let deadline = env.ledger().timestamp() + 100;
env.mock_all_auths();
client.initialize(&owner, &goal, &deadline);
// Make donation
let donation_amount = 30_000_000i128; // 3 XLM
client.donate(&donor, &donation_amount);
// Verify donation recorded
assert_eq!(client.get_donation(&donor), donation_amount);
assert_eq!(client.get_total_raised(), donation_amount);
// Fast forward past deadline (goal not reached)
env.ledger().with_mut(|li| {
li.timestamp = deadline + 1;
});
// Refund
let refunded = client.refund(&donor);
// Verify refund
assert_eq!(refunded, donation_amount);
assert_eq!(client.get_donation(&donor), 0);
assert_eq!(client.get_total_raised(), 0);
}
#[test]
#[should_panic(expected = "Campaign belum berakhir")]
fn test_refund_before_deadline() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(CrowdfundingContract, ());
let client = CrowdfundingContractClient::new(&env, &contract_id);
let owner = Address::generate(&env);
let donor = Address::generate(&env);
let goal = 100_000_000i128;
let deadline = env.ledger().timestamp() + 1000;
client.initialize(&owner, &goal, &deadline);
client.donate(&donor, &30_000_000);
// Try refund before deadline - should panic
client.refund(&donor);
}
#[test]
#[should_panic(expected = "Goal sudah tercapai, tidak bisa refund")]
fn test_refund_when_goal_reached() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(CrowdfundingContract, ());
let client = CrowdfundingContractClient::new(&env, &contract_id);
let owner = Address::generate(&env);
let donor = Address::generate(&env);
let goal = 50_000_000i128; // 5 XLM
let deadline = env.ledger().timestamp() + 100;
client.initialize(&owner, &goal, &deadline);
// Donate exactly goal amount
client.donate(&donor, &goal);
// Fast forward past deadline
env.ledger().with_mut(|li| {
li.timestamp = deadline + 1;
});
// Try refund when goal reached - should panic
client.refund(&donor);
}
✅ Solution Template
pub fn refund(env: Env, donor: Address) -> i128 {
// Your code here
// Step 1: Authorization
// donor.require_auth();
// Step 2: Validations
// - Check is_ended()
// - Check !is_goal_reached()
// - Get donation amount (panic if 0)
// Step 3: Process refund
// - Remove donor from Map
// - Update total_raised
// - Return refunded amount
}
📚 Learning Resources
Kalau stuck, bisa refer ke:
-
Previous Functions:
get_total_raised()- example reading from storagedonate()- example Map manipulationinitialize()- example storing data
-
Soroban SDK Docs:
-
Ask Mentor:
- Jangan ragu tanya kalau bingung!
- Workshop purpose = learning
✅ Checklist Progress
Track progress kamu:
- Exercise 1:
get_goal()⭐ - Exercise 2:
get_deadline()⭐ - Exercise 3:
is_goal_reached()⭐⭐ - Exercise 4:
is_ended()⭐⭐ - Exercise 5:
get_progress_percentage()⭐⭐⭐ - Bonus:
refund()⭐⭐⭐⭐
🎯 How to Approach
Recommended Order
- Start Easy: Do Exercise 1-2 dulu untuk warm up
- Build Confidence: Exercise 3-4 untuk practice logic
- Challenge Yourself: Exercise 5 untuk calculation practice
- Optional Bonus: Kalau ada extra time & energy
Testing Strategy
# Run specific test
cargo test test_get_goal
# Run all tests
cargo test
# Run with output
cargo test -- --nocapture
Debugging Tips
// Add logging untuk debug
log!(&env, "Total raised: {}", total);
// Check intermediate values
let progress = total * 100;
log!(&env, "Progress before division: {}", progress);
🏆 After Completing Exercises
Kalau semua exercise sudah done, contract kamu sekarang punya:
Core Functions (MVP):
- ✅
initialize()- Setup campaign - ✅
donate()- Accept donations - ✅
get_total_raised()- View progress - ✅
get_donation()- View individual contribution
Extended Functions (Your work!):
- ✅
get_goal()- View target - ✅
get_deadline()- View end time - ✅
is_goal_reached()- Check success - ✅
is_ended()- Check status - ✅
get_progress_percentage()- Visual progress - ✅
refund()- Return mechanism
Total: 10 functions = Production-grade crowdfunding contract! 🎉
🚀 Deploy Your Complete Contract
Setelah semua tests pass:
# Build
stellar contract build
# Deploy to testnet
stellar contract deploy \
--wasm target/wasm32v1-none/release/crowdfunding.wasm \
--source-account alice \
--network testnet
# Test all functions!
💡 Extension Ideas (Post-Workshop)
Kalau mau explore lebih lanjut:
-
Owner Withdraw:
- Function untuk owner withdraw funds kalau goal tercapai
- Only owner bisa call
- Only after goal reached
-
Campaign Description:
- Add metadata (title, description, category)
- Store sebagai Symbol atau String
-
Milestone System:
- Multiple goals dengan different tiers
- Unlock rewards per milestone
-
Donor List:
- Function list all donors
- Pagination support
-
Frontend Integration:
- Build React/Vue app
- Connect dengan Freighter wallet
- Real-time progress updates
🎓 What You Learned
Completing these exercises means you now understand:
- ✅ Reading/writing blockchain storage
- ✅ Working dengan Map data structures
- ✅ Time-based logic dalam smart contracts
- ✅ Boolean logic & conditional checks
- ✅ Integer arithmetic & safe calculations
- ✅ Authorization & access control
- ✅ Writing comprehensive tests
- ✅ Handling edge cases
Congrats! You’re now a Soroban developer! 🎉
Happy Coding! 🚀 Remember: Best way to learn adalah by doing. Don’t just read - code!