index

Challenge - Extend Crowdfunding

· 8min

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 parameter xlm_token: Address
  • donate() function menggunakan token::Client untuk 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_raised dari storage
  • Get goal dari 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_raised dan goal
  • 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

  1. Validation:

    • Check campaign sudah ended
    • Check goal tidak reached
    • Verify donor punya donation
  2. Logic:

    • Get donation amount dari Map
    • Remove donor dari Map
    • Update total_raised (kurangi amount)
    • Return refunded amount
  3. Security:

    • Require donor authorization: donor.require_auth()
    • Prevent double refund (setelah refund, donation = 0)
    • Handle edge cases (donor tidak ada, amount = 0)

🧪 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:

  1. Previous Functions:

    • get_total_raised() - example reading from storage
    • donate() - example Map manipulation
    • initialize() - example storing data
  2. Soroban SDK Docs:

  3. 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

  1. Start Easy: Do Exercise 1-2 dulu untuk warm up
  2. Build Confidence: Exercise 3-4 untuk practice logic
  3. Challenge Yourself: Exercise 5 untuk calculation practice
  4. 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:

  1. Owner Withdraw:

    • Function untuk owner withdraw funds kalau goal tercapai
    • Only owner bisa call
    • Only after goal reached
  2. Campaign Description:

    • Add metadata (title, description, category)
    • Store sebagai Symbol atau String
  3. Milestone System:

    • Multiple goals dengan different tiers
    • Unlock rewards per milestone
  4. Donor List:

    • Function list all donors
    • Pagination support
  5. 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!