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 ```