Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/src/app/(overview)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -14,6 +15,7 @@ const DashboardPage = async () => {
<CampaignCreatorBadge />
<ForestReportExport />
<DashboardOverview />
<TreeMilestoneTimeline />
<FeatureCards />
<CampaignImpactCalculator />
<ImpactMapSection />
Expand Down
33 changes: 33 additions & 0 deletions apps/web/src/app/api/planter-tax-report/route.ts
Original file line number Diff line number Diff line change
@@ -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"`,
}
});
}
Original file line number Diff line number Diff line change
@@ -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 (
<div className="bg-zinc-900/50 border border-zinc-800 rounded-2xl p-6 mb-8">
<div className="mb-6">
<h3 className="text-lg font-semibold text-white">Tree Lifecycle Timeline</h3>
<p className="text-sm text-zinc-400">Track your sponsored tree's journey.</p>
</div>
<div className="relative">
<div className="absolute left-4 top-0 bottom-0 w-0.5 bg-zinc-800" />
<div className="space-y-6">
{milestones.map((milestone, index) => {
const isCompleted = index <= currentStep;
const isCurrent = index === currentStep;
return (
<div
key={milestone.id}
className={`relative flex items-start pl-12 cursor-pointer transition-opacity ${
isCompleted ? "opacity-100" : "opacity-50"
}`}
onClick={() => setCurrentStep(index)}
>
<div
className={`absolute left-2.5 top-1 h-3.5 w-3.5 rounded-full border-2 transform -translate-x-1/2 ${
isCompleted
? "bg-green-500 border-green-500"
: "bg-zinc-900 border-zinc-600"
} ${isCurrent ? "ring-4 ring-green-500/20" : ""}`}
/>
<div>
<h4 className={`text-base font-medium ${isCompleted ? "text-white" : "text-zinc-400"}`}>
{milestone.title}
</h4>
<p className="text-sm text-zinc-500 mt-1">{milestone.description}</p>
</div>
</div>
);
})}
</div>
</div>
</div>
);
};
3 changes: 3 additions & 0 deletions contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ members = [
"payment-stream",
"distributor",
"nft-stream",
"campaign-funding",
"soulbound-badge",
"verifier-penalty"
"planter",
"dispute-arbiter",
"campaign-funding",
Expand Down
13 changes: 13 additions & 0 deletions contracts/verifier-penalty/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
70 changes: 70 additions & 0 deletions contracts/verifier-penalty/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Address, Error> {
env.storage().instance().get(&DataKey::Admin).ok_or(Error::NotAdmin)
}
}
18 changes: 12 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down