diff --git a/src/app/(dashboard)/dashboard/treasury/page.tsx b/src/app/(dashboard)/dashboard/treasury/page.tsx index 13b228a..fd084e1 100644 --- a/src/app/(dashboard)/dashboard/treasury/page.tsx +++ b/src/app/(dashboard)/dashboard/treasury/page.tsx @@ -1,29 +1,56 @@ import BitcoinTreasury from "@/components/dashboard/bitcoin-treasury"; import { prisma } from "@/lib/db"; +export const dynamic = "force-dynamic"; + export default async function TreasuryPage() { + let org = null; + let txs: Awaited> = []; + let dbError = false; try { - const org = (await prisma.organizations.findFirst()) || ({ id: 'demo-org' } as any); - const txs = await prisma.bitcoin_transactions.findMany({ - where: { organization_id: org.id }, - orderBy: { transaction_date: 'desc' }, - take: 10 - }); - const balanceSats = txs.reduce((acc, t) => acc + Number(t.amount_sats), 0); - return ( -
- -
- ); + org = await prisma.organizations.findFirst(); + if (org) { + txs = await prisma.bitcoin_transactions.findMany({ + where: { organization_id: org.id }, + orderBy: { transaction_date: "desc" }, + take: 10, + }); + } } catch { - // Fallback demo rendering when DB is not available - const demoTxs = [ - { txid: 'demo-tx-1', amount_sats: 21000, category: 'operations', transaction_date: new Date().toISOString() }, - ] as any; + dbError = true; + } + + if (dbError || !org) { return (
- +

Treasury

+

+ {dbError + ? "The treasury register is currently unreachable. No balance can be shown." + : "No organization is registered yet, so there is no treasury to show."} +

); } + + const balanceSats = txs.reduce((acc, t) => acc + Number(t.amount_sats), 0); + return ( +
+ ({ + ...t, + amount_sats: Number(t.amount_sats), + transaction_date: + t.transaction_date instanceof Date + ? t.transaction_date.toISOString() + : String(t.transaction_date), + description: t.description ?? undefined, + to_address: t.to_address ?? undefined, + from_address: t.from_address ?? undefined, + }))} + /> +
+ ); } diff --git a/src/app/(dashboard)/dashboard/voting/VotingWalletConnector.tsx b/src/app/(dashboard)/dashboard/voting/VotingWalletConnector.tsx deleted file mode 100644 index fccbd69..0000000 --- a/src/app/(dashboard)/dashboard/voting/VotingWalletConnector.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client"; - -import WalletConnector from "@/components/bitcoin/wallet-connector"; - -export default function VotingWalletConnector() { - return ( - { - window.location.href = `/dashboard/voting?pub=${encodeURIComponent(pub)}`; - }} /> - ); -} diff --git a/src/app/(dashboard)/dashboard/voting/page.tsx b/src/app/(dashboard)/dashboard/voting/page.tsx index 8a4500a..50f1181 100644 --- a/src/app/(dashboard)/dashboard/voting/page.tsx +++ b/src/app/(dashboard)/dashboard/voting/page.tsx @@ -1,34 +1,47 @@ import VotingInterface from "@/components/dashboard/voting-interface"; -import VotingWalletConnector from "./VotingWalletConnector"; +import { Democracy } from "@/lib/solon/democracy"; import { prisma } from "@/lib/db"; -import { Suspense } from "react"; + +export const dynamic = "force-dynamic"; export default async function VotingPage() { - let session: any; + let session = null; + let dbError = false; try { - session = await prisma.voting_sessions.findFirst({ orderBy: { start_date: 'desc' } }); + session = await prisma.voting_sessions.findFirst({ orderBy: { start_date: "desc" } }); } catch { - // ignore and fallback + dbError = true; + } + + if (dbError) { + return ( +
+

Voting

+

+ The voting register is currently unreachable. No session data can be shown. +

+
+ ); } - session = session || { - id: 'demo-session', - title: 'Adopt Bitcoin-Treasury Budget 2025', - voting_type: 'simple_majority', - status: 'active', - organization_id: 'demo-org', - decision_id: null, - start_date: new Date(), - total_votes_cast: 0, - bitcoin_signature_required: true - } as any; + + if (!session) { + return ( +
+

Voting

+

+ No voting session has been opened yet. When one opens, registered members vote + here by signing the canonical vote message with their own Bitcoin wallet. +

+
+ ); + } + + const tally = await new Democracy().tally(session.id); return (

Open Vote

- - - - +
); } diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..a52cdeb --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from "next/server"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + return NextResponse.json({ status: "ok" }); +} diff --git a/src/app/api/solon/transparency/route.ts b/src/app/api/solon/transparency/route.ts deleted file mode 100644 index 81d845b..0000000 --- a/src/app/api/solon/transparency/route.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NextResponse } from "next/server"; -import { computeTransparency } from "@/lib/solon/transparency"; - -export async function GET(req: Request) { - const { searchParams } = new URL(req.url); - const orgId = searchParams.get('orgId'); - if (!orgId) return NextResponse.json({ error: 'orgId required' }, { status: 400 }); - const metrics = await computeTransparency(orgId); - return NextResponse.json({ orgId, metrics }); -} - diff --git a/src/app/governance/voting/page.tsx b/src/app/governance/voting/page.tsx index 06bf1a7..0191272 100644 --- a/src/app/governance/voting/page.tsx +++ b/src/app/governance/voting/page.tsx @@ -1,201 +1,67 @@ -"use client"; -import { useState } from 'react'; import PageLayout from '@/components/ui/page-layout'; +/** + * Explains the voting mechanism honestly. Live sessions and tallies render in + * /dashboard/voting from the database — this page fabricates nothing. + */ export default function VotingSystemPage() { - const [selectedVote, setSelectedVote] = useState(null); - return ( - -
- - {/* Live Voting Demo */} -
-

Active Votes

- -
- - - - - -
+
+
+

How a vote works

+
    +
  1. + A voting session opens on a proposal. Its rules — who is eligible, the + threshold, the quorum — are fixed when it opens. +
  2. +
  3. + A member signs the canonical vote message (session id, choice, and their own + address) with their Bitcoin wallet — Sparrow, Electrum, or Bitcoin + Core's signmessage. +
  4. +
  5. + The server recovers the public key from the signature and checks it resolves + to a registered member. No signature, no vote — there is no other way in. +
  6. +
  7. + The weighted tally is computed from stored, verified votes only, and every + vote's signature stays on record so anyone can re-verify it. +
  8. +
- {/* Voting Features */} -
- - - - - - +
+

What this gives you

+
    +
  • + Cryptographic verification — a vote is + valid because the math says so, not because an administrator does. +
  • +
  • + Replay protection — the signed text + binds session, choice, and voter, so a signature cannot be lifted onto + another vote. +
  • +
  • + Weighted voting — members carry a + voting weight; the tally is weighted accordingly and the weights are public. +
  • +
-
- - ); -} - -function VoteProposal({ - id, - title, - description, - deadline, - yesVotes, - noVotes, - totalEligible, - isActive, - onVote, - userHasVoted -}: { - id: string; - title: string; - description: string; - deadline: string; - yesVotes: number; - noVotes: number; - totalEligible: number; - isActive: boolean; - onVote: (id: string) => void; - userHasVoted: boolean; -}) { - const yesPercentage = ((yesVotes / totalEligible) * 100).toFixed(1); - const noPercentage = ((noVotes / totalEligible) * 100).toFixed(1); - return ( -
-
-
-

{title}

-

{description}

+ - - {isActive ? 'Active' : 'Completed'} -
- -
-
- Results ({yesVotes + noVotes}/{totalEligible} voted) - {deadline} -
- -
-
-
-
-
-
- -
- Yes: {yesVotes} ({yesPercentage}%) - No: {noVotes} ({noPercentage}%) -
-
- - {isActive && ( -
- {!userHasVoted ? ( - <> - - - - ) : ( -
- ✓ You have voted -
- )} - -
- )} -
+ ); } - -function FeatureBox({ title, description, icon }: { - title: string; - description: string; - icon: string; -}) { - return ( -
-
{icon}
-

{title}

-

{description}

-
- ); -} \ No newline at end of file diff --git a/src/app/integration/page.tsx b/src/app/integration/page.tsx index f4d1bc0..3d4cb97 100644 --- a/src/app/integration/page.tsx +++ b/src/app/integration/page.tsx @@ -1,119 +1,79 @@ import PageLayout from '@/components/ui/page-layout'; +/** + * Documents the API that exists today — nothing aspirational. The surface is + * small on purpose; it grows as governance features ship, and each endpoint is + * listed here only once it is live. + */ export default function IntegrationPage() { return ( - -
- - {/* Quick Start */} -
-

Quick Start Integration

- -
-
-

REST API Example

-
-
# Get organization treasury balance
-
curl -H "Authorization: Bearer YOUR_API_KEY" \
-
https://api.solon.org/v1/treasury/balance
-
-
# Response
-
{'{'}
-
"balance": "2.47851234",
-
"currency": "BTC",
-
"usd_value": "142847.23"
-
{'}'}
-
-
- -
-

SDK Integration

-
-
{'// JavaScript SDK'}
-
import
{'{ SolonAPI }'}
from
'@solon/sdk'
-
-
const
client =
new
SolonAPI(apiKey)
-
-
{'// Get treasury data'}
-
const
balance =
await
client.treasury.getBalance()
-
const
votes =
await
client.voting.getActive()
-
+
+
+

Cast a cryptographic vote

+

+ A vote is a Bitcoin signed message. Sign the canonical vote message with the + wallet that holds your registered member address, then POST the signature: +

+
+
# Message to sign (exact text):
+
Solon vote
+
session:<sessionId>
+
choice:<yes|no|abstain>
+
voter:<your-bitcoin-address>
+
+
# Submit the signed vote
+
curl -X POST /api/voting/<sessionId>/cryptographic-vote \
+
-H "Content-Type: application/json" \
+
+ -d '{'{'}"choice":"yes","address":"1...","signature":"<base64>"{'}'}'
+

+ The server recovers the public key from the signature and only stores the vote + if it resolves to a registered member. Invalid signatures return 401; verified + but ineligible votes return 422 — with the reason in both cases. +

- {/* API Endpoints */} -
- - - - - - - +
+

Live endpoints

+
    +
  • + + POST /api/voting/[sessionId]/cryptographic-vote + + cast a signed vote +
  • +
  • + + GET /api/voting/[sessionId]/cryptographic-vote + + weighted tally for a session +
  • +
  • + + GET /api/bitcoin/wallet/[orgId] + + organization wallet balance +
  • +
  • + + GET /api/health + + service health +
  • +
+

+ A broader public read API (organizations, proposals, decisions, treasury, + audit log) is being built next — endpoints appear here when they are live, + not before. +

); } - -function APIEndpointCard({ title, description, endpoints }: { - title: string; - description: string; - endpoints: string[]; -}) { - return ( -
-

{title}

-

{description}

-
    - {endpoints.map((endpoint, index) => ( -
  • - - {endpoint} - -
  • - ))} -
-
- ); -} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 72c340c..65d3de9 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,5 @@ import SolonHero from '@/components/marketing/solon-hero' import { FourPillars } from '@/components/marketing/four-pillars' -import { TransparencyDemo } from '@/components/marketing/transparency-demo' import en from '@/i18n/en.json' export default function Home() { @@ -27,21 +26,6 @@ export default function Home() { - {/* Live Demo — on light surface for rhythm */} -
-
-
-

- {t.demo_section.title} -

-

- {t.demo_section.subtitle} -

-
- -
-
- {/* Call to Action — navy anchor closes the page */}
diff --git a/src/app/treasury/bitcoin/page.tsx b/src/app/treasury/bitcoin/page.tsx index e0f10c2..cf612d0 100644 --- a/src/app/treasury/bitcoin/page.tsx +++ b/src/app/treasury/bitcoin/page.tsx @@ -1,141 +1,47 @@ import PageLayout from '@/components/ui/page-layout'; +/** + * Explains the treasury model honestly. Real balances render in + * /dashboard/treasury from recorded transactions — this page fabricates nothing. + */ export default function BitcoinTreasuryPage() { return ( - -
- - {/* Live Demo Section */} -
-

Live Treasury Demo

- -
-
-

Current Balance

-
₿ 2.47851234
-
≈ $142,847 USD
-
- -
-

This Month

-
₿ 0.15420000
-
≈ $8,924 USD received
-
- -
-

Pending Votes

-
3
-
Treasury proposals active
-
-
- - {/* Recent Transactions */} -
-

Recent Transactions

-
- - - -
-
- - {/* Interactive Demo */} -
-

Try It Yourself

-
- - -
-
+
+
+

The model

+
    +
  • + Bitcoin-native — the treasury is a + Bitcoin wallet, so its balance and history are verifiable by anyone on the + public chain, not asserted by us. +
  • +
  • + Recorded and linked — treasury + transactions are recorded with their txid and link straight to a public + block explorer. If a number here can't be checked independently, it + doesn't belong here. +
  • +
  • + Governed spending — treasury decisions + go through the same signed-vote process as everything else: proposal, open + session, cryptographically verified votes, recorded outcome. +
  • +
- {/* Features Grid */} -
-
-

Multi-Signature Security

-
    -
  • -
    - Configurable M-of-N signature requirements -
  • -
  • -
    - Hardware wallet integration support -
  • -
  • -
    - Emergency recovery procedures -
  • -
-
- -
-

Complete Transparency

-
    -
  • -
    - All transactions publicly verifiable -
  • -
  • -
    - Real-time balance updates -
  • -
  • -
    - Automated financial reporting -
  • -
-
+
); } - -function TransactionRow({ type, amount, description, timestamp, txId }: { - type: 'sent' | 'received'; - amount: string; - description: string; - timestamp: string; - txId: string; -}) { - return ( -
-
-
-
-
{description}
-
{timestamp} • {txId}
-
-
-
- {type === 'received' ? '+' : '-'}{amount} -
-
- ); -} \ No newline at end of file diff --git a/src/components/bitcoin/wallet-connector.tsx b/src/components/bitcoin/wallet-connector.tsx deleted file mode 100644 index 0790e8b..0000000 --- a/src/components/bitcoin/wallet-connector.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; -import { useState } from "react"; - -export default function WalletConnector({ onConnect }: { onConnect: (pubKey: string) => void }) { - const [pub, setPub] = useState(""); - return ( -
- setPub(e.target.value)} - className="px-3 py-2 rounded-md bg-black/20 border border-white/10 w-72" - /> - -
- ); -} - diff --git a/src/components/dashboard/bitcoin-treasury.tsx b/src/components/dashboard/bitcoin-treasury.tsx index 32f9d0f..c857eda 100644 --- a/src/components/dashboard/bitcoin-treasury.tsx +++ b/src/components/dashboard/bitcoin-treasury.tsx @@ -16,13 +16,13 @@ export default function BitcoinTreasury({ organizationId, walletBalance, recentT

Organization: {organizationId}

-
Balance (BTC)
+
Recorded balance (BTC) — sum of the transactions below
{btc}
-
-
+
+

Recent Transactions

@@ -53,15 +53,6 @@ export default function BitcoinTreasury({ organizationId, walletBalance, recentT
-
-

Budget Snapshot

-
    -
  • Operations: 40%
  • -
  • Development: 35%
  • -
  • Grants: 15%
  • -
  • Reserves: 10%
  • -
-
); diff --git a/src/components/dashboard/voting-interface.tsx b/src/components/dashboard/voting-interface.tsx index 1b22609..7aa9eca 100644 --- a/src/components/dashboard/voting-interface.tsx +++ b/src/components/dashboard/voting-interface.tsx @@ -1,14 +1,60 @@ "use client"; -import type { VotingSession } from "@/lib/solon/types"; +import { useState } from "react"; +import { voteMessage } from "@/lib/bitcoin/message"; + +type Choice = "yes" | "no" | "abstain"; +const CHOICES: Choice[] = ["yes", "no", "abstain"]; export interface VotingInterfaceProps { - session: VotingSession; - userWallet: string; - canVote: boolean; + session: { id: string; title: string; voting_type: string; status: string }; + tally: Record; +} + +interface VoteVerdict { + stored: boolean; + verified: boolean; + reason?: string; + tally?: Record; } -export default function VotingInterface({ session, userWallet, canVote }: VotingInterfaceProps) { +/** + * Casting a vote requires a real Bitcoin signed-message signature: the member + * signs the canonical vote message with their own wallet (Sparrow, Electrum, + * Bitcoin Core `signmessage`) and pastes the signature here. The server + * verifies it; nothing is stored on failure and the verdict is shown as-is. + */ +export default function VotingInterface({ session, tally }: VotingInterfaceProps) { + const [choice, setChoice] = useState("yes"); + const [address, setAddress] = useState(""); + const [signature, setSignature] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [verdict, setVerdict] = useState(null); + const [copied, setCopied] = useState(false); + + const message = address + ? voteMessage({ sessionId: session.id, choice, memberAddress: address }) + : null; + + const liveTally = verdict?.tally ?? tally; + + async function submit() { + setSubmitting(true); + setVerdict(null); + try { + const res = await fetch(`/api/voting/${session.id}/cryptographic-vote`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ choice, address, signature }), + }); + setVerdict(await res.json()); + } catch { + setVerdict({ stored: false, verified: false, reason: "network error — vote not submitted" }); + } finally { + setSubmitting(false); + } + } + return (
@@ -16,40 +62,110 @@ export default function VotingInterface({ session, userWallet, canVote }: Voting

{session.title}

Voting type: {session.voting_type}

-
- Session: {session.status} -
+
Session: {session.status}
-
-

Connected wallet: {userWallet || '—'}

-
- {['yes', 'no', 'abstain'].map((choice) => ( - - ))} +
+
+ + setAddress(e.target.value.trim())} + placeholder="1..." + className="mt-1 w-full px-3 py-2 rounded-md border border-gray-300 font-mono text-sm" + />
+ +
+ Your choice +
+ {CHOICES.map((c) => ( + + ))} +
+
+ + {message && ( +
+
+ + Sign exactly this message with your wallet + + +
+
+              {message}
+            
+
+ )} + +
+ +