diff --git a/apps/web/src/app/(overview)/dashboard/page.tsx b/apps/web/src/app/(overview)/dashboard/page.tsx
index a97884ea..839c1e43 100644
--- a/apps/web/src/app/(overview)/dashboard/page.tsx
+++ b/apps/web/src/app/(overview)/dashboard/page.tsx
@@ -3,6 +3,7 @@ import DashboardOverview from "@/components/modules/dashboard/DashboardOverview"
import FeatureCards from "@/components/modules/dashboard/FeatureCards";
import ImpactComparison from "@/components/modules/dashboard/ImpactComparison";
import { ImpactMapSection } from "@/components/modules/impact-map/ImpactMapSection";
+import { TreeMilestoneTimeline } from "@/components/modules/dashboard/TreeMilestoneTimeline";
import ForestReportExport from "@/components/modules/dashboard/ForestReportExport";
import { CampaignImpactCalculator } from "@/components/modules/impact/CampaignImpactCalculator";
import CampaignCreatorBadge from "@/components/modules/dashboard/CampaignCreatorBadge";
@@ -14,6 +15,7 @@ const DashboardPage = async () => {
+
diff --git a/apps/web/src/app/api/planter-tax-report/route.ts b/apps/web/src/app/api/planter-tax-report/route.ts
new file mode 100644
index 00000000..b5bd55e3
--- /dev/null
+++ b/apps/web/src/app/api/planter-tax-report/route.ts
@@ -0,0 +1,33 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export async function GET(request: NextRequest) {
+ const searchParams = request.nextUrl.searchParams;
+ const planter = searchParams.get("planter");
+ const year = searchParams.get("year");
+
+ if (!planter || !year) {
+ return NextResponse.json({ error: "Missing planter or year parameters" }, { status: 400 });
+ }
+
+ // Mock data for the payouts
+ const payouts = [
+ { date: `${year}-01-15`, amount: "500", currency: "USDC", txHash: "0x123abc..." },
+ { date: `${year}-04-20`, amount: "1000", currency: "USDC", txHash: "0x456def..." },
+ { date: `${year}-08-10`, amount: "750", currency: "USDC", txHash: "0x789ghi..." },
+ { date: `${year}-12-05`, amount: "600", currency: "USDC", txHash: "0xabc123..." }
+ ];
+
+ const csvRows = [
+ ["Date", "Amount", "Currency", "Transaction Hash"],
+ ...payouts.map(p => [p.date, p.amount, p.currency, p.txHash])
+ ];
+
+ const csvString = csvRows.map(row => row.join(",")).join("\n");
+
+ return new NextResponse(csvString, {
+ headers: {
+ "Content-Type": "text/csv",
+ "Content-Disposition": `attachment; filename="planter_tax_report_${planter}_${year}.csv"`,
+ }
+ });
+}
diff --git a/apps/web/src/components/modules/dashboard/TreeMilestoneTimeline.tsx b/apps/web/src/components/modules/dashboard/TreeMilestoneTimeline.tsx
new file mode 100644
index 00000000..ab912249
--- /dev/null
+++ b/apps/web/src/components/modules/dashboard/TreeMilestoneTimeline.tsx
@@ -0,0 +1,56 @@
+"use client";
+
+import React, { useState } from "react";
+
+const milestones = [
+ { id: 1, title: "Pending", description: "Tree sponsorship confirmed, waiting for planting." },
+ { id: 2, title: "Planted", description: "Tree has been planted in the ground." },
+ { id: 3, title: "First photo", description: "Initial growth photo captured." },
+ { id: 4, title: "Verified", description: "Tree vitality verified by oracle." },
+ { id: 5, title: "1-year milestone", description: "Tree has reached 1 year of healthy growth." }
+];
+
+export const TreeMilestoneTimeline: React.FC = () => {
+ const [currentStep, setCurrentStep] = useState(2); // Example: currently at "First photo" index
+
+ return (
+
+
+
Tree Lifecycle Timeline
+
Track your sponsored tree's journey.
+
+
+
+
+ {milestones.map((milestone, index) => {
+ const isCompleted = index <= currentStep;
+ const isCurrent = index === currentStep;
+ return (
+
setCurrentStep(index)}
+ >
+
+
+
+ {milestone.title}
+
+
{milestone.description}
+
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml
index 17607e29..18acd09f 100644
--- a/contracts/Cargo.toml
+++ b/contracts/Cargo.toml
@@ -5,6 +5,9 @@ members = [
"payment-stream",
"distributor",
"nft-stream",
+ "campaign-funding",
+ "soulbound-badge",
+ "verifier-penalty"
"planter",
"dispute-arbiter",
"campaign-funding",
diff --git a/contracts/verifier-penalty/Cargo.toml b/contracts/verifier-penalty/Cargo.toml
new file mode 100644
index 00000000..46bb1092
--- /dev/null
+++ b/contracts/verifier-penalty/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "stellar-client-os-verifier-penalty"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+soroban-sdk = { workspace = true }
+
+[dev-dependencies]
+soroban-sdk = { workspace = true, features = ["testutils"] }
diff --git a/contracts/verifier-penalty/src/lib.rs b/contracts/verifier-penalty/src/lib.rs
new file mode 100644
index 00000000..7658ddb6
--- /dev/null
+++ b/contracts/verifier-penalty/src/lib.rs
@@ -0,0 +1,70 @@
+#![no_std]
+
+use soroban_sdk::{
+ contract, contracterror, contractimpl, contracttype, Address, Env, symbol_short
+};
+
+#[contracterror]
+#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
+#[repr(u32)]
+pub enum Error {
+ NotAdmin = 1,
+ InsufficientStake = 2,
+ NotStaked = 3,
+}
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum DataKey {
+ Admin,
+ Stake(Address),
+}
+
+#[contract]
+pub struct VerifierPenaltyContract;
+
+#[contractimpl]
+impl VerifierPenaltyContract {
+ pub fn initialize(env: Env, admin: Address) {
+ if env.storage().instance().has(&DataKey::Admin) {
+ panic!("Already initialized");
+ }
+ admin.require_auth();
+ env.storage().instance().set(&DataKey::Admin, &admin);
+ }
+
+ /// Verifier stakes a bond to become eligible for verifying trees.
+ pub fn stake(env: Env, verifier: Address, amount: i128) {
+ verifier.require_auth();
+ let current_stake = Self::get_stake(env.clone(), verifier.clone());
+ let new_stake = current_stake + amount;
+ env.storage().persistent().set(&DataKey::Stake(verifier.clone()), &new_stake);
+
+ env.events().publish((symbol_short!("staked"), verifier), amount);
+ }
+
+ /// Admin can slash a verifier's stake for fraud detection (e.g. approving a dead tree).
+ pub fn slash(env: Env, verifier: Address, slash_amount: i128) -> Result<(), Error> {
+ let admin = Self::get_admin(env.clone())?;
+ admin.require_auth();
+
+ let current_stake = Self::get_stake(env.clone(), verifier.clone());
+ if current_stake < slash_amount {
+ return Err(Error::InsufficientStake);
+ }
+
+ let new_stake = current_stake - slash_amount;
+ env.storage().persistent().set(&DataKey::Stake(verifier.clone()), &new_stake);
+
+ env.events().publish((symbol_short!("slashed"), verifier), slash_amount);
+ Ok(())
+ }
+
+ pub fn get_stake(env: Env, verifier: Address) -> i128 {
+ env.storage().persistent().get(&DataKey::Stake(verifier)).unwrap_or(0)
+ }
+
+ fn get_admin(env: Env) -> Result {
+ env.storage().instance().get(&DataKey::Admin).ok_or(Error::NotAdmin)
+ }
+}
diff --git a/docs/architecture.md b/docs/architecture.md
index b3d3b6b4..eb0bfadd 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -8,23 +8,29 @@ The following diagram illustrates the interaction between the main components of
```mermaid
graph TD
- A[User] --> B{Frontend (Next.js)};
- B --> C{SDK (@fundable/sdk)};
- C --> D{Soroban Smart Contracts};
- D --> E((Stellar Blockchain));
+ A[User] --> B{Frontend (Next.js)}
+ B <--> C{Backend (Node.js API)}
+ B --> SDK{SDK (@fundable/sdk)}
+ C --> SDK
+ SDK --> D{Soroban Smart Contracts}
+ D --> E((Stellar Blockchain))
+ B --> F[(IPFS - Decentralized Storage)]
+ C --> F
subgraph "Client-Side"
A
B
end
- subgraph "Off-Chain"
+ subgraph "Off-Chain / API"
C
+ SDK
end
- subgraph "On-Chain"
+ subgraph "Decentralized Network"
D
E
+ F
end
```