Merge commit '655bd921784bd5aaa339cffc6b72a37879fb6534' as 'website'

This commit is contained in:
anonpenguin23 2026-03-26 18:14:59 +02:00
commit c536e45d0f
239 changed files with 45982 additions and 0 deletions

2
website/.env.example Normal file
View File

@ -0,0 +1,2 @@
VITE_INVEST_API_URL=http://localhost:8090
VITE_HELIUS_API_KEY=your-helius-api-key

3
website/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
dist/
node_modules/
.env

16
website/index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Orama — Decentralized Cloud Infrastructure</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,5 @@
PORT=8090
DB_PATH=invest.db
JWT_SECRET=change-me-to-a-random-32-byte-string
HELIUS_API_KEY=your-helius-api-key
HELIUS_RPC_URL=https://mainnet.helius-rpc.com/?api-key=your-helius-api-key

5
website/invest-api/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
invest-api
*.db
*.db-shm
*.db-wal
.env

View File

@ -0,0 +1,25 @@
package auth
import "context"
type contextKey string
const (
walletKey contextKey = "wallet"
chainKey contextKey = "chain"
)
func WithWallet(ctx context.Context, wallet, chain string) context.Context {
ctx = context.WithValue(ctx, walletKey, wallet)
ctx = context.WithValue(ctx, chainKey, chain)
return ctx
}
func WalletFromContext(ctx context.Context) (wallet, chain string, ok bool) {
w, wOk := ctx.Value(walletKey).(string)
c, cOk := ctx.Value(chainKey).(string)
if !wOk || !cOk || w == "" || c == "" {
return "", "", false
}
return w, c, true
}

View File

@ -0,0 +1,120 @@
package auth
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)
type LoginRequest struct {
Wallet string `json:"wallet"`
Chain string `json:"chain"`
Message string `json:"message"`
Signature string `json:"signature"`
}
type LoginResponse struct {
Token string `json:"token"`
Wallet string `json:"wallet"`
Chain string `json:"chain"`
ExpiresAt string `json:"expires_at"`
}
func LoginHandler(database *sql.DB, jwtSecret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonError(w, http.StatusBadRequest, "invalid request body")
return
}
req.Chain = strings.ToLower(req.Chain)
if req.Wallet == "" || req.Message == "" || req.Signature == "" {
jsonError(w, http.StatusBadRequest, "wallet, message, and signature are required")
return
}
if req.Chain != "sol" && req.Chain != "evm" {
jsonError(w, http.StatusBadRequest, "chain must be 'sol' or 'evm'")
return
}
// Validate message timestamp (anti-replay: within 5 minutes)
if err := validateMessageTimestamp(req.Message); err != nil {
jsonError(w, http.StatusBadRequest, fmt.Sprintf("message validation failed: %v", err))
return
}
// Verify signature
var verifyErr error
if req.Chain == "sol" {
verifyErr = VerifySolana(req.Wallet, req.Message, req.Signature)
} else {
verifyErr = VerifyEVM(req.Wallet, req.Message, req.Signature)
}
if verifyErr != nil {
jsonError(w, http.StatusUnauthorized, fmt.Sprintf("signature verification failed: %v", verifyErr))
return
}
// Upsert wallet
database.Exec(
`INSERT INTO wallets (wallet, chain) VALUES (?, ?)
ON CONFLICT(wallet) DO UPDATE SET last_seen = CURRENT_TIMESTAMP, chain = ?`,
req.Wallet, req.Chain, req.Chain,
)
// Generate JWT
token, expiresAt, err := GenerateToken(req.Wallet, req.Chain, jwtSecret)
if err != nil {
jsonError(w, http.StatusInternalServerError, "failed to generate token")
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(LoginResponse{
Token: token,
Wallet: req.Wallet,
Chain: req.Chain,
ExpiresAt: expiresAt.Format(time.RFC3339),
})
}
}
func validateMessageTimestamp(message string) error {
// Extract timestamp from message format:
// "...Timestamp: 2026-03-21T12:00:00.000Z"
idx := strings.Index(message, "Timestamp: ")
if idx == -1 {
return fmt.Errorf("message does not contain a timestamp")
}
tsStr := strings.TrimSpace(message[idx+len("Timestamp: "):])
// Handle potential trailing content
if newline := strings.Index(tsStr, "\n"); newline != -1 {
tsStr = tsStr[:newline]
}
ts, err := time.Parse(time.RFC3339Nano, tsStr)
if err != nil {
ts, err = time.Parse("2006-01-02T15:04:05.000Z", tsStr)
if err != nil {
return fmt.Errorf("invalid timestamp format: %s", tsStr)
}
}
if time.Since(ts) > 5*time.Minute {
return fmt.Errorf("message timestamp expired (older than 5 minutes)")
}
return nil
}
func jsonError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}

View File

@ -0,0 +1,57 @@
package auth
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
const TokenExpiry = 24 * time.Hour
type Claims struct {
Wallet string `json:"wallet"`
Chain string `json:"chain"`
jwt.RegisteredClaims
}
func GenerateToken(wallet, chain, secret string) (string, time.Time, error) {
expiresAt := time.Now().Add(TokenExpiry)
claims := Claims{
Wallet: wallet,
Chain: chain,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "orama-invest",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString([]byte(secret))
if err != nil {
return "", time.Time{}, fmt.Errorf("failed to sign JWT: %w", err)
}
return signed, expiresAt, nil
}
func ParseToken(tokenStr, secret string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(secret), nil
})
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token claims")
}
return claims, nil
}

View File

@ -0,0 +1,52 @@
package auth
import (
"encoding/hex"
"fmt"
"strings"
"github.com/ethereum/go-ethereum/crypto"
)
// VerifyEVM verifies an Ethereum personal_sign signature.
func VerifyEVM(wallet, message, signatureHex string) error {
// Remove 0x prefix if present
sigHex := strings.TrimPrefix(signatureHex, "0x")
sigBytes, err := hex.DecodeString(sigHex)
if err != nil {
return fmt.Errorf("failed to decode signature hex: %w", err)
}
if len(sigBytes) != 65 {
return fmt.Errorf("invalid signature length: got %d, want 65", len(sigBytes))
}
// Ethereum personal_sign prefix
prefixed := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(message), message)
hash := crypto.Keccak256Hash([]byte(prefixed))
// Fix recovery ID: some wallets return v=27/28, need v=0/1
if sigBytes[64] >= 27 {
sigBytes[64] -= 27
}
pubKeyBytes, err := crypto.Ecrecover(hash.Bytes(), sigBytes)
if err != nil {
return fmt.Errorf("ecrecover failed: %w", err)
}
pubKey, err := crypto.UnmarshalPubkey(pubKeyBytes)
if err != nil {
return fmt.Errorf("failed to unmarshal public key: %w", err)
}
recoveredAddr := crypto.PubkeyToAddress(*pubKey)
expectedAddr := strings.ToLower(strings.TrimPrefix(wallet, "0x"))
recoveredHex := strings.ToLower(recoveredAddr.Hex()[2:])
if recoveredHex != expectedAddr {
return fmt.Errorf("signature verification failed: recovered %s, expected %s", recoveredHex, expectedAddr)
}
return nil
}

View File

@ -0,0 +1,42 @@
package auth
import (
"crypto/ed25519"
"encoding/base64"
"fmt"
"github.com/mr-tron/base58"
)
// VerifySolana verifies a Solana wallet signature.
// The signature should be base58 or base64 encoded.
// The wallet is the base58-encoded public key.
func VerifySolana(wallet, message, signature string) error {
pubKeyBytes, err := base58.Decode(wallet)
if err != nil {
return fmt.Errorf("invalid wallet address: %w", err)
}
if len(pubKeyBytes) != ed25519.PublicKeySize {
return fmt.Errorf("invalid public key length: got %d, want %d", len(pubKeyBytes), ed25519.PublicKeySize)
}
// Try base58 first (Phantom default), then base64
sigBytes, err := base58.Decode(signature)
if err != nil || len(sigBytes) != ed25519.SignatureSize {
sigBytes, err = base64.StdEncoding.DecodeString(signature)
if err != nil {
return fmt.Errorf("failed to decode signature: %w", err)
}
}
if len(sigBytes) != ed25519.SignatureSize {
return fmt.Errorf("invalid signature length: got %d, want %d", len(sigBytes), ed25519.SignatureSize)
}
if !ed25519.Verify(pubKeyBytes, []byte(message), sigBytes) {
return fmt.Errorf("signature verification failed")
}
return nil
}

View File

@ -0,0 +1,50 @@
package config
import (
"fmt"
"os"
"github.com/joho/godotenv"
)
type Config struct {
Port string
DBPath string
JWTSecret string
HeliusAPIKey string
HeliusRPCURL string
}
func Load() (*Config, error) {
// Load .env file if it exists (ignore error if missing)
_ = godotenv.Load()
cfg := &Config{
Port: envOrDefault("PORT", "8090"),
DBPath: envOrDefault("DB_PATH", "invest.db"),
JWTSecret: os.Getenv("JWT_SECRET"),
HeliusAPIKey: os.Getenv("HELIUS_API_KEY"),
HeliusRPCURL: os.Getenv("HELIUS_RPC_URL"),
}
if cfg.JWTSecret == "" {
return nil, fmt.Errorf("JWT_SECRET environment variable is required")
}
if cfg.HeliusAPIKey == "" {
return nil, fmt.Errorf("HELIUS_API_KEY environment variable is required")
}
if cfg.HeliusRPCURL == "" {
cfg.HeliusRPCURL = "https://mainnet.helius-rpc.com/?api-key=" + cfg.HeliusAPIKey
}
return cfg, nil
}
func envOrDefault(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}

View File

@ -0,0 +1,21 @@
package db
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
func Open(path string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", path+"?_journal_mode=WAL&_busy_timeout=5000")
if err != nil {
return nil, fmt.Errorf("failed to open database at %s: %w", path, err)
}
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return db, nil
}

View File

@ -0,0 +1,164 @@
package db
import (
"database/sql"
"fmt"
"log"
)
func Migrate(db *sql.DB) error {
schema := `
CREATE TABLE IF NOT EXISTS wallets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet TEXT NOT NULL UNIQUE,
chain TEXT NOT NULL CHECK(chain IN ('sol', 'evm')),
first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS token_purchases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet TEXT NOT NULL,
chain TEXT NOT NULL CHECK(chain IN ('sol', 'evm')),
amount_paid REAL NOT NULL,
pay_currency TEXT NOT NULL,
tokens_allocated REAL NOT NULL,
tx_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS license_purchases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet TEXT NOT NULL,
chain TEXT NOT NULL CHECK(chain IN ('sol', 'evm')),
amount_paid REAL NOT NULL,
pay_currency TEXT NOT NULL,
tx_hash TEXT NOT NULL,
license_number INTEGER NOT NULL,
claimed_via_nft INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS whitelist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet TEXT NOT NULL UNIQUE,
chain TEXT NOT NULL CHECK(chain IN ('sol', 'evm')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS activity_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
wallet TEXT NOT NULL,
detail TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS anchat_claims (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet TEXT NOT NULL UNIQUE,
anchat_balance_at_claim REAL NOT NULL,
orama_amount REAL NOT NULL,
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'flagged', 'warned', 'revoked')),
flagged_at DATETIME,
warned_at DATETIME,
revoked_at DATETIME,
claimed_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS nft_license_verification (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet TEXT NOT NULL UNIQUE,
license_id INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'flagged', 'warned', 'revoked')),
flagged_at DATETIME,
warned_at DATETIME,
revoked_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_wallets_wallet ON wallets(wallet);
CREATE INDEX IF NOT EXISTS idx_token_purchases_wallet ON token_purchases(wallet);
CREATE INDEX IF NOT EXISTS idx_license_purchases_wallet ON license_purchases(wallet);
CREATE INDEX IF NOT EXISTS idx_whitelist_wallet ON whitelist(wallet);
CREATE INDEX IF NOT EXISTS idx_activity_log_created ON activity_log(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_anchat_claims_wallet ON anchat_claims(wallet);
CREATE INDEX IF NOT EXISTS idx_anchat_claims_status ON anchat_claims(status);
CREATE INDEX IF NOT EXISTS idx_nft_license_verification_wallet ON nft_license_verification(wallet);
CREATE INDEX IF NOT EXISTS idx_nft_license_verification_status ON nft_license_verification(status);
`
_, err := db.Exec(schema)
if err != nil {
return err
}
// Add columns if migrating from old schema (SQLite doesn't support IF NOT EXISTS for ALTER)
for _, col := range []struct{ table, column, def string }{
{"anchat_claims", "status", "TEXT NOT NULL DEFAULT 'active'"},
{"anchat_claims", "flagged_at", "DATETIME"},
{"anchat_claims", "warned_at", "DATETIME"},
{"anchat_claims", "revoked_at", "DATETIME"},
} {
db.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", col.table, col.column, col.def))
}
return nil
}
// Seed populates initial data if tables are empty (idempotent).
func Seed(database *sql.DB) error {
var count int
database.QueryRow("SELECT COUNT(*) FROM token_purchases").Scan(&count)
if count > 0 {
return nil // Already seeded
}
log.Println("Seeding database with initial investor data...")
tx, err := database.Begin()
if err != nil {
return fmt.Errorf("failed to begin seed transaction: %w", err)
}
defer tx.Rollback()
// DeBros — 9JEsuEeRpxrJyFVkx1RRXBTG7V6zTeApr33nGrBpcYt3
debrosWallet := "9JEsuEeRpxrJyFVkx1RRXBTG7V6zTeApr33nGrBpcYt3"
tx.Exec("INSERT OR IGNORE INTO wallets (wallet, chain) VALUES (?, 'sol')", debrosWallet)
tx.Exec(
"INSERT INTO token_purchases (wallet, chain, amount_paid, pay_currency, tokens_allocated, tx_hash) VALUES (?, 'sol', 25000, 'SOL', 500000, 'seed-debros-token')",
debrosWallet,
)
tx.Exec(
"INSERT INTO activity_log (event_type, wallet, detail) VALUES ('token_purchase', '9JEs...pcYt3', '$25,000 — 500,000 $ORAMA')",
)
// ICXCNIKA — AXXGYMTVS7UGens718mKPvuWpRfX9zf4tGzsAqY5TgwZ
icxcWallet := "AXXGYMTVS7UGens718mKPvuWpRfX9zf4tGzsAqY5TgwZ"
tx.Exec("INSERT OR IGNORE INTO wallets (wallet, chain) VALUES (?, 'sol')", icxcWallet)
for i := 1; i <= 3; i++ {
tx.Exec(
"INSERT INTO license_purchases (wallet, chain, amount_paid, pay_currency, tx_hash, license_number, claimed_via_nft) VALUES (?, 'sol', 3000, 'SOL', ?, ?, 0)",
icxcWallet, fmt.Sprintf("seed-icxcnika-license-%d", i), i,
)
tx.Exec(
"INSERT INTO activity_log (event_type, wallet, detail) VALUES ('license_purchase', 'AXXG...TgwZ', ?)",
fmt.Sprintf("License #%d — $3,000", i),
)
}
tx.Exec(
"INSERT INTO token_purchases (wallet, chain, amount_paid, pay_currency, tokens_allocated, tx_hash) VALUES (?, 'sol', 1000, 'SOL', 20000, 'seed-icxcnika-token')",
icxcWallet,
)
tx.Exec(
"INSERT INTO activity_log (event_type, wallet, detail) VALUES ('token_purchase', 'AXXG...TgwZ', '$1,000 — 20,000 $ORAMA')",
)
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit seed data: %w", err)
}
log.Println("Seed data inserted: DeBros ($25K tokens) + ICXCNIKA (3 licenses + $1K tokens)")
return nil
}

View File

@ -0,0 +1,75 @@
package db
// Constants shared across handlers.
const (
TotalTokenAllocation = 10_000_000
TokenPrice = 0.05
MinTokenPurchaseUSD = 50.0
TotalLicenses = 500
LicensePrice = 3000.0
TeamNFTCollection = "4vd4ct4ohhSsjKdi2QKdRTcEPsgFHMTNwCUR27xNrA73"
CommunityNFTCollection = "DV8pjrqEKx7ET5FSFY2pCugnzyZmcDFrWFxzDLqdSzmp"
AnchatMint = "EZGb7aaSbCHbE6DcfynezKRc9Bkzxs8stP97H6WEpump"
AnchatClaimRate = 0.0025 // 0.25%
AnchatMinBalance = 10_000 // Minimum $ANCHAT to be eligible for claim
TreasurySOL = "CBRv5D69LKD8xmnjcNNcz1jgehEtKzhUdrxdFaBs14CP"
TreasuryETH = "0xA3b5Cba1dBD45951F718d1720bE19718159f5B0D"
TreasuryBTC = "bc1qzpkjguxh4pl9pdhj76zeztur42prhfed2hd22z"
)
type StatsResponse struct {
TokensSold float64 `json:"tokens_sold"`
TokensRemaining float64 `json:"tokens_remaining"`
TokenRaised float64 `json:"token_raised"`
LicensesSold int `json:"licenses_sold"`
LicensesLeft int `json:"licenses_left"`
LicenseRaised float64 `json:"license_raised"`
TotalRaised float64 `json:"total_raised"`
WhitelistCount int `json:"whitelist_count"`
}
type ActivityEntry struct {
EventType string `json:"event_type"`
Wallet string `json:"wallet"`
Detail string `json:"detail"`
CreatedAt string `json:"created_at"`
}
type MeResponse struct {
Wallet string `json:"wallet"`
Chain string `json:"chain"`
TokensPurchased float64 `json:"tokens_purchased"`
TokenSpent float64 `json:"tokens_spent"`
Licenses []LicenseInfo `json:"licenses"`
OnWhitelist bool `json:"on_whitelist"`
PurchaseHistory []PurchaseRecord `json:"purchase_history"`
AnchatClaimed bool `json:"anchat_claimed"`
AnchatOrama float64 `json:"anchat_orama_amount"`
}
type LicenseInfo struct {
LicenseNumber int `json:"license_number"`
ClaimedViaNFT bool `json:"claimed_via_nft"`
PurchasedAt string `json:"purchased_at"`
}
type PurchaseRecord struct {
Type string `json:"type"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
TxHash string `json:"tx_hash"`
CreatedAt string `json:"created_at"`
}
type NftStatusResponse struct {
HasTeamNFT bool `json:"has_team_nft"`
HasCommunityNFT bool `json:"has_community_nft"`
NFTCount int `json:"nft_count"`
}
type AnchatBalanceResponse struct {
Balance float64 `json:"balance"`
ClaimableOrama float64 `json:"claimable_orama"`
}

18
website/invest-api/go.mod Normal file
View File

@ -0,0 +1,18 @@
module github.com/debros/orama-website/invest-api
go 1.24.4
require (
github.com/ethereum/go-ethereum v1.15.11
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.37
github.com/mr-tron/base58 v1.2.0
)
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
)

20
website/invest-api/go.sum Normal file
View File

@ -0,0 +1,20 @@
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/ethereum/go-ethereum v1.15.11 h1:JK73WKeu0WC0O1eyX+mdQAVHUV+UR1a9VB/domDngBU=
github.com/ethereum/go-ethereum v1.15.11/go.mod h1:mf8YiHIb0GR4x4TipcvBUPxJLw1mFdmxzoDi11sDRoI=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@ -0,0 +1,30 @@
package handler
import (
"database/sql"
"net/http"
"github.com/debros/orama-website/invest-api/db"
)
func ActivityHandler(database *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := database.Query(
"SELECT event_type, wallet, COALESCE(detail, ''), created_at FROM activity_log ORDER BY created_at DESC LIMIT 50",
)
if err != nil {
jsonError(w, http.StatusInternalServerError, "failed to query activity log")
return
}
defer rows.Close()
entries := []db.ActivityEntry{}
for rows.Next() {
var e db.ActivityEntry
rows.Scan(&e.EventType, &e.Wallet, &e.Detail, &e.CreatedAt)
entries = append(entries, e)
}
jsonResponse(w, http.StatusOK, entries)
}
}

View File

@ -0,0 +1,129 @@
package handler
import (
"database/sql"
"fmt"
"math"
"net/http"
"github.com/debros/orama-website/invest-api/auth"
dbpkg "github.com/debros/orama-website/invest-api/db"
"github.com/debros/orama-website/invest-api/helius"
)
func AnchatBalanceHandler(database *sql.DB, heliusClient *helius.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
wallet, chain, ok := auth.WalletFromContext(r.Context())
if !ok {
jsonError(w, http.StatusUnauthorized, "wallet required")
return
}
if chain != "sol" {
jsonError(w, http.StatusBadRequest, "$ANCHAT is a Solana token — connect with a Solana wallet")
return
}
// Check current claim status
var status string
err := database.QueryRow("SELECT status FROM anchat_claims WHERE wallet = ?", wallet).Scan(&status)
hasActiveClaim := err == nil && status == "active"
hasRevokedClaim := err == nil && status == "revoked"
balance, err := heliusClient.GetTokenBalance(wallet, dbpkg.AnchatMint)
if err != nil {
jsonError(w, http.StatusInternalServerError, fmt.Sprintf("failed to check $ANCHAT balance: %v", err))
return
}
claimable := 0.0
canClaim := false
if balance >= dbpkg.AnchatMinBalance && !hasActiveClaim {
claimable = math.Floor(balance * dbpkg.AnchatClaimRate)
canClaim = true
}
jsonResponse(w, http.StatusOK, map[string]any{
"balance": balance,
"claimable_orama": claimable,
"already_claimed": hasActiveClaim,
"was_revoked": hasRevokedClaim,
"can_claim": canClaim,
"min_balance": dbpkg.AnchatMinBalance,
})
}
}
func AnchatClaimHandler(database *sql.DB, heliusClient *helius.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
wallet, chain, ok := auth.WalletFromContext(r.Context())
if !ok {
jsonError(w, http.StatusUnauthorized, "wallet required")
return
}
if chain != "sol" {
jsonError(w, http.StatusBadRequest, "$ANCHAT is a Solana token — connect with a Solana wallet")
return
}
// Check if there's an active claim already
var status string
err := database.QueryRow("SELECT status FROM anchat_claims WHERE wallet = ?", wallet).Scan(&status)
if err == nil && status == "active" {
jsonError(w, http.StatusConflict, "you already have an active $ORAMA claim")
return
}
// Fresh balance (bypass cache)
balance, err := heliusClient.GetTokenBalanceFresh(wallet, dbpkg.AnchatMint)
if err != nil {
jsonError(w, http.StatusInternalServerError, fmt.Sprintf("failed to verify $ANCHAT balance: %v", err))
return
}
if balance < dbpkg.AnchatMinBalance {
jsonError(w, http.StatusBadRequest,
fmt.Sprintf("minimum %s $ANCHAT required (you have %.0f)",
formatInt(dbpkg.AnchatMinBalance), balance))
return
}
oramaAmount := math.Floor(balance * dbpkg.AnchatClaimRate)
if oramaAmount <= 0 {
jsonError(w, http.StatusBadRequest, "$ANCHAT balance too low to claim $ORAMA")
return
}
// Upsert: if previously revoked, update to active; otherwise insert
_, err = database.Exec(`
INSERT INTO anchat_claims (wallet, anchat_balance_at_claim, orama_amount, status, flagged_at, warned_at, revoked_at)
VALUES (?, ?, ?, 'active', NULL, NULL, NULL)
ON CONFLICT(wallet) DO UPDATE SET
anchat_balance_at_claim = ?,
orama_amount = ?,
status = 'active',
flagged_at = NULL,
warned_at = NULL,
revoked_at = NULL,
claimed_at = CURRENT_TIMESTAMP
`, wallet, balance, oramaAmount, balance, oramaAmount)
if err != nil {
jsonError(w, http.StatusInternalServerError, "failed to record claim")
return
}
logActivity(database, "anchat_claim", wallet, fmt.Sprintf("%.0f $ANCHAT → %.0f $ORAMA", balance, oramaAmount))
jsonResponse(w, http.StatusCreated, map[string]any{
"orama_amount": oramaAmount,
})
}
}
func formatInt(n float64) string {
if n >= 1000 {
return fmt.Sprintf("%.0fK", n/1000)
}
return fmt.Sprintf("%.0f", n)
}

View File

@ -0,0 +1,28 @@
package handler
import (
"database/sql"
"encoding/json"
"net/http"
)
func jsonResponse(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func jsonError(w http.ResponseWriter, status int, msg string) {
jsonResponse(w, status, map[string]string{"error": msg})
}
func logActivity(database *sql.DB, eventType, wallet, detail string) {
truncated := wallet
if len(wallet) > 10 {
truncated = wallet[:6] + "..." + wallet[len(wallet)-4:]
}
database.Exec(
"INSERT INTO activity_log (event_type, wallet, detail) VALUES (?, ?, ?)",
eventType, truncated, detail,
)
}

View File

@ -0,0 +1,79 @@
package handler
import (
"database/sql"
"net/http"
"github.com/debros/orama-website/invest-api/auth"
"github.com/debros/orama-website/invest-api/db"
)
func MeHandler(database *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
wallet, chain, ok := auth.WalletFromContext(r.Context())
if !ok {
jsonError(w, http.StatusUnauthorized, "wallet not found in context")
return
}
resp := db.MeResponse{
Wallet: wallet,
Chain: chain,
Licenses: []db.LicenseInfo{},
PurchaseHistory: []db.PurchaseRecord{},
}
// Token totals
database.QueryRow(
"SELECT COALESCE(SUM(tokens_allocated), 0), COALESCE(SUM(amount_paid), 0) FROM token_purchases WHERE wallet = ?",
wallet,
).Scan(&resp.TokensPurchased, &resp.TokenSpent)
// Licenses
rows, _ := database.Query(
"SELECT license_number, claimed_via_nft, created_at FROM license_purchases WHERE wallet = ? ORDER BY created_at",
wallet,
)
if rows != nil {
defer rows.Close()
for rows.Next() {
var l db.LicenseInfo
var claimed int
rows.Scan(&l.LicenseNumber, &claimed, &l.PurchasedAt)
l.ClaimedViaNFT = claimed == 1
resp.Licenses = append(resp.Licenses, l)
}
}
// Whitelist
var count int
database.QueryRow("SELECT COUNT(*) FROM whitelist WHERE wallet = ?", wallet).Scan(&count)
resp.OnWhitelist = count > 0
// ANCHAT claim status
var claimCount int
database.QueryRow("SELECT COUNT(*) FROM anchat_claims WHERE wallet = ?", wallet).Scan(&claimCount)
resp.AnchatClaimed = claimCount > 0
if resp.AnchatClaimed {
database.QueryRow("SELECT orama_amount FROM anchat_claims WHERE wallet = ?", wallet).Scan(&resp.AnchatOrama)
}
// Purchase history (combined)
histRows, _ := database.Query(`
SELECT 'token' as type, amount_paid, pay_currency, tx_hash, created_at FROM token_purchases WHERE wallet = ?
UNION ALL
SELECT 'license' as type, amount_paid, pay_currency, tx_hash, created_at FROM license_purchases WHERE wallet = ?
ORDER BY created_at DESC
`, wallet, wallet)
if histRows != nil {
defer histRows.Close()
for histRows.Next() {
var p db.PurchaseRecord
histRows.Scan(&p.Type, &p.Amount, &p.Currency, &p.TxHash, &p.CreatedAt)
resp.PurchaseHistory = append(resp.PurchaseHistory, p)
}
}
jsonResponse(w, http.StatusOK, resp)
}
}

View File

@ -0,0 +1,32 @@
package handler
import (
"fmt"
"net/http"
"github.com/debros/orama-website/invest-api/auth"
"github.com/debros/orama-website/invest-api/db"
"github.com/debros/orama-website/invest-api/helius"
)
func NftStatusHandler(heliusClient *helius.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
wallet, _, ok := auth.WalletFromContext(r.Context())
if !ok {
jsonError(w, http.StatusUnauthorized, "wallet required")
return
}
result, err := heliusClient.CheckNFTs(wallet, db.TeamNFTCollection, db.CommunityNFTCollection)
if err != nil {
jsonError(w, http.StatusInternalServerError, fmt.Sprintf("failed to check NFTs: %v", err))
return
}
jsonResponse(w, http.StatusOK, db.NftStatusResponse{
HasTeamNFT: result.HasTeamNFT,
HasCommunityNFT: result.HasCommunityNFT,
NFTCount: result.Count,
})
}
}

View File

@ -0,0 +1,95 @@
package handler
import (
"encoding/json"
"io"
"net/http"
"sync"
"time"
)
type PriceResponse struct {
SolUSD float64 `json:"sol_usd"`
EthUSD float64 `json:"eth_usd"`
}
var (
priceCache PriceResponse
priceCacheTime time.Time
priceMu sync.RWMutex
priceTTL = 60 * time.Second
)
func PriceHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
prices, err := getCachedPrices()
if err != nil {
jsonError(w, http.StatusInternalServerError, "failed to fetch prices: "+err.Error())
return
}
jsonResponse(w, http.StatusOK, prices)
}
}
func getCachedPrices() (PriceResponse, error) {
priceMu.RLock()
if time.Since(priceCacheTime) < priceTTL && priceCache.SolUSD > 0 {
cached := priceCache
priceMu.RUnlock()
return cached, nil
}
priceMu.RUnlock()
prices, err := fetchCoinGeckoPrices()
if err != nil {
return PriceResponse{}, err
}
priceMu.Lock()
priceCache = prices
priceCacheTime = time.Now()
priceMu.Unlock()
return prices, nil
}
func fetchCoinGeckoPrices() (PriceResponse, error) {
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("https://api.coingecko.com/api/v3/simple/price?ids=solana,ethereum&vs_currencies=usd")
if err != nil {
return PriceResponse{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return PriceResponse{}, err
}
var data struct {
Solana struct{ USD float64 `json:"usd"` } `json:"solana"`
Ethereum struct{ USD float64 `json:"usd"` } `json:"ethereum"`
}
if err := json.Unmarshal(body, &data); err != nil {
return PriceResponse{}, err
}
return PriceResponse{
SolUSD: data.Solana.USD,
EthUSD: data.Ethereum.USD,
}, nil
}
// GetSOLPrice returns the cached SOL price (used by purchase verification).
func GetSOLPrice() float64 {
priceMu.RLock()
defer priceMu.RUnlock()
return priceCache.SolUSD
}
// GetETHPrice returns the cached ETH price.
func GetETHPrice() float64 {
priceMu.RLock()
defer priceMu.RUnlock()
return priceCache.EthUSD
}

View File

@ -0,0 +1,194 @@
package handler
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/debros/orama-website/invest-api/auth"
"github.com/debros/orama-website/invest-api/db"
"github.com/debros/orama-website/invest-api/helius"
)
type TokenPurchaseRequest struct {
AmountPaid float64 `json:"amount_paid"`
PayCurrency string `json:"pay_currency"`
TxHash string `json:"tx_hash"`
}
type LicensePurchaseRequest struct {
AmountPaid float64 `json:"amount_paid"`
PayCurrency string `json:"pay_currency"`
TxHash string `json:"tx_hash"`
ClaimedViaNFT bool `json:"claimed_via_nft"`
}
func TokenPurchaseHandler(database *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
wallet, chain, ok := auth.WalletFromContext(r.Context())
if !ok {
jsonError(w, http.StatusUnauthorized, "wallet required")
return
}
var req TokenPurchaseRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.TxHash == "" {
jsonError(w, http.StatusBadRequest, "tx_hash is required")
return
}
if req.AmountPaid < db.MinTokenPurchaseUSD {
jsonError(w, http.StatusBadRequest, fmt.Sprintf("minimum purchase is $%.0f", db.MinTokenPurchaseUSD))
return
}
tokensAllocated := req.AmountPaid / db.TokenPrice
var sold float64
database.QueryRow("SELECT COALESCE(SUM(tokens_allocated), 0) FROM token_purchases").Scan(&sold)
if sold+tokensAllocated > db.TotalTokenAllocation {
jsonError(w, http.StatusConflict, "not enough tokens remaining in pre-sale allocation")
return
}
var existing int
database.QueryRow("SELECT COUNT(*) FROM token_purchases WHERE tx_hash = ?", req.TxHash).Scan(&existing)
if existing > 0 {
jsonError(w, http.StatusConflict, "transaction already recorded")
return
}
_, err := database.Exec(
"INSERT INTO token_purchases (wallet, chain, amount_paid, pay_currency, tokens_allocated, tx_hash) VALUES (?, ?, ?, ?, ?, ?)",
wallet, chain, req.AmountPaid, req.PayCurrency, tokensAllocated, req.TxHash,
)
if err != nil {
jsonError(w, http.StatusInternalServerError, "failed to record purchase")
return
}
logActivity(database, "token_purchase", wallet, fmt.Sprintf("%.0f $ORAMA for $%.2f %s", tokensAllocated, req.AmountPaid, req.PayCurrency))
jsonResponse(w, http.StatusCreated, map[string]any{
"tokens_allocated": tokensAllocated,
"tx_hash": req.TxHash,
})
}
}
func LicensePurchaseHandler(database *sql.DB, heliusClient *helius.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
wallet, chain, ok := auth.WalletFromContext(r.Context())
if !ok {
jsonError(w, http.StatusUnauthorized, "wallet required")
return
}
var req LicensePurchaseRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonError(w, http.StatusBadRequest, "invalid request body")
return
}
// NFT claim: verify on-chain ownership
if req.ClaimedViaNFT {
if chain != "sol" {
jsonError(w, http.StatusBadRequest, "NFT claims require a Solana wallet")
return
}
// Check for existing active NFT claim (allow re-claim if revoked)
var nftClaimStatus string
claimErr := database.QueryRow(
"SELECT status FROM nft_license_verification WHERE wallet = ?", wallet,
).Scan(&nftClaimStatus)
if claimErr == nil && nftClaimStatus == "active" {
jsonError(w, http.StatusConflict, "you already have an active free license claim")
return
}
// Server-side verification via Helius
nftResult, err := heliusClient.CheckNFTs(wallet, db.TeamNFTCollection, db.CommunityNFTCollection)
if err != nil {
jsonError(w, http.StatusInternalServerError, fmt.Sprintf("failed to verify NFT ownership: %v", err))
return
}
if !nftResult.HasTeamNFT {
jsonError(w, http.StatusForbidden, "wallet does not hold a DeBros Team NFT")
return
}
} else {
if req.TxHash == "" {
jsonError(w, http.StatusBadRequest, "tx_hash is required")
return
}
if req.AmountPaid < db.LicensePrice {
jsonError(w, http.StatusBadRequest, fmt.Sprintf("license costs $%.0f", db.LicensePrice))
return
}
}
var sold int
database.QueryRow("SELECT COUNT(*) FROM license_purchases").Scan(&sold)
if sold >= db.TotalLicenses {
jsonError(w, http.StatusConflict, "all licenses have been sold")
return
}
licenseNumber := sold + 1
claimedInt := 0
if req.ClaimedViaNFT {
claimedInt = 1
req.AmountPaid = 0
req.PayCurrency = "nft_claim"
req.TxHash = fmt.Sprintf("nft-claim-%s-%d", wallet[:8], time.Now().Unix())
}
if !req.ClaimedViaNFT {
var existing int
database.QueryRow("SELECT COUNT(*) FROM license_purchases WHERE tx_hash = ?", req.TxHash).Scan(&existing)
if existing > 0 {
jsonError(w, http.StatusConflict, "transaction already recorded")
return
}
}
_, err := database.Exec(
"INSERT INTO license_purchases (wallet, chain, amount_paid, pay_currency, tx_hash, license_number, claimed_via_nft) VALUES (?, ?, ?, ?, ?, ?, ?)",
wallet, chain, req.AmountPaid, req.PayCurrency, req.TxHash, licenseNumber, claimedInt,
)
if err != nil {
jsonError(w, http.StatusInternalServerError, "failed to record license purchase")
return
}
detail := fmt.Sprintf("License #%d", licenseNumber)
if req.ClaimedViaNFT {
detail += " (DeBros NFT claim)"
// Register in verification table for periodic checks
database.Exec(`
INSERT INTO nft_license_verification (wallet, license_id, status)
VALUES (?, ?, 'active')
ON CONFLICT(wallet) DO UPDATE SET
license_id = ?,
status = 'active',
flagged_at = NULL,
warned_at = NULL,
revoked_at = NULL
`, wallet, licenseNumber, licenseNumber)
}
logActivity(database, "license_purchase", wallet, detail)
jsonResponse(w, http.StatusCreated, map[string]any{
"license_number": licenseNumber,
"claimed_via_nft": req.ClaimedViaNFT,
})
}
}

View File

@ -0,0 +1,28 @@
package handler
import (
"database/sql"
"net/http"
"github.com/debros/orama-website/invest-api/db"
)
func StatsHandler(database *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var stats db.StatsResponse
database.QueryRow("SELECT COALESCE(SUM(tokens_allocated), 0), COALESCE(SUM(amount_paid), 0) FROM token_purchases").
Scan(&stats.TokensSold, &stats.TokenRaised)
database.QueryRow("SELECT COUNT(*), COALESCE(SUM(amount_paid), 0) FROM license_purchases").
Scan(&stats.LicensesSold, &stats.LicenseRaised)
database.QueryRow("SELECT COUNT(*) FROM whitelist").Scan(&stats.WhitelistCount)
stats.TokensRemaining = db.TotalTokenAllocation - stats.TokensSold
stats.LicensesLeft = db.TotalLicenses - stats.LicensesSold
stats.TotalRaised = stats.TokenRaised + stats.LicenseRaised
jsonResponse(w, http.StatusOK, stats)
}
}

View File

@ -0,0 +1,41 @@
package handler
import (
"database/sql"
"fmt"
"net/http"
"github.com/debros/orama-website/invest-api/auth"
)
func WhitelistJoinHandler(database *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
wallet, chain, ok := auth.WalletFromContext(r.Context())
if !ok {
jsonError(w, http.StatusUnauthorized, "wallet required")
return
}
var existing int
database.QueryRow("SELECT COUNT(*) FROM whitelist WHERE wallet = ?", wallet).Scan(&existing)
if existing > 0 {
jsonError(w, http.StatusConflict, "wallet already on whitelist")
return
}
_, err := database.Exec("INSERT INTO whitelist (wallet, chain) VALUES (?, ?)", wallet, chain)
if err != nil {
jsonError(w, http.StatusInternalServerError, "failed to join whitelist")
return
}
var position int
database.QueryRow("SELECT COUNT(*) FROM whitelist").Scan(&position)
logActivity(database, "whitelist_join", wallet, fmt.Sprintf("Position #%d", position))
jsonResponse(w, http.StatusCreated, map[string]any{
"position": position,
})
}
}

View File

@ -0,0 +1,52 @@
package helius
import (
"sync"
"time"
)
type cacheEntry[T any] struct {
value T
expiresAt time.Time
}
type Cache[T any] struct {
mu sync.RWMutex
entries map[string]cacheEntry[T]
ttl time.Duration
}
func NewCache[T any](ttl time.Duration) *Cache[T] {
return &Cache[T]{
entries: make(map[string]cacheEntry[T]),
ttl: ttl,
}
}
func (c *Cache[T]) Get(key string) (T, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
entry, ok := c.entries[key]
if !ok || time.Now().After(entry.expiresAt) {
var zero T
return zero, false
}
return entry.value, true
}
func (c *Cache[T]) Set(key string, value T) {
c.mu.Lock()
defer c.mu.Unlock()
c.entries[key] = cacheEntry[T]{
value: value,
expiresAt: time.Now().Add(c.ttl),
}
}
func (c *Cache[T]) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.entries, key)
}

View File

@ -0,0 +1,208 @@
package helius
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type Client struct {
rpcURL string
apiKey string
http *http.Client
nftCache *Cache[NFTResult]
tokCache *Cache[float64]
}
type NFTResult struct {
HasTeamNFT bool
HasCommunityNFT bool
Count int
}
func NewClient(apiKey, rpcURL string) *Client {
return &Client{
rpcURL: rpcURL,
apiKey: apiKey,
http: &http.Client{Timeout: 15 * time.Second},
nftCache: NewCache[NFTResult](5 * time.Minute),
tokCache: NewCache[float64](5 * time.Minute),
}
}
// CheckNFTs checks if a wallet holds DeBros Team (100) or Community (700) NFTs.
func (c *Client) CheckNFTs(wallet, teamCollection, communityCollection string) (NFTResult, error) {
cacheKey := "nft:" + wallet
if cached, ok := c.nftCache.Get(cacheKey); ok {
return cached, nil
}
result := NFTResult{}
// Check team NFTs
teamCount, err := c.searchAssetsByCollection(wallet, teamCollection)
if err != nil {
return result, fmt.Errorf("failed to check team NFTs: %w", err)
}
// Check community NFTs
commCount, err := c.searchAssetsByCollection(wallet, communityCollection)
if err != nil {
return result, fmt.Errorf("failed to check community NFTs: %w", err)
}
result.HasTeamNFT = teamCount > 0
result.HasCommunityNFT = commCount > 0
result.Count = teamCount + commCount
c.nftCache.Set(cacheKey, result)
return result, nil
}
// GetTokenBalance returns the token balance for a specific mint.
func (c *Client) GetTokenBalance(wallet, mint string) (float64, error) {
cacheKey := "token:" + wallet + ":" + mint
if cached, ok := c.tokCache.Get(cacheKey); ok {
return cached, nil
}
body := map[string]any{
"jsonrpc": "2.0",
"id": "token-balance",
"method": "getTokenAccountsByOwner",
"params": []any{
wallet,
map[string]string{"mint": mint},
map[string]string{"encoding": "jsonParsed"},
},
}
resp, err := c.rpcCall(body)
if err != nil {
return 0, err
}
result, ok := resp["result"].(map[string]any)
if !ok {
return 0, nil
}
value, ok := result["value"].([]any)
if !ok || len(value) == 0 {
c.tokCache.Set(cacheKey, 0)
return 0, nil
}
// Parse the first token account
account, ok := value[0].(map[string]any)
if !ok {
return 0, nil
}
balance := extractUIAmount(account)
c.tokCache.Set(cacheKey, balance)
return balance, nil
}
// GetTokenBalanceFresh bypasses cache (used for claims).
func (c *Client) GetTokenBalanceFresh(wallet, mint string) (float64, error) {
cacheKey := "token:" + wallet + ":" + mint
c.tokCache.Delete(cacheKey)
return c.GetTokenBalance(wallet, mint)
}
func (c *Client) searchAssetsByCollection(owner, collectionAddr string) (int, error) {
body := map[string]any{
"jsonrpc": "2.0",
"id": "search-assets",
"method": "searchAssets",
"params": map[string]any{
"ownerAddress": owner,
"grouping": []any{"collection", collectionAddr},
"page": 1,
"limit": 1000,
},
}
resp, err := c.rpcCall(body)
if err != nil {
return 0, err
}
result, ok := resp["result"].(map[string]any)
if !ok {
return 0, nil
}
total, ok := result["total"].(float64)
if ok {
return int(total), nil
}
items, ok := result["items"].([]any)
if !ok {
return 0, nil
}
return len(items), nil
}
func (c *Client) rpcCall(body map[string]any) (map[string]any, error) {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
resp, err := c.http.Post(c.rpcURL, "application/json", bytes.NewReader(jsonBody))
if err != nil {
return nil, fmt.Errorf("helius request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("helius returned status %d: %s", resp.StatusCode, string(respBody))
}
var result map[string]any
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
if errObj, ok := result["error"]; ok {
return nil, fmt.Errorf("helius RPC error: %v", errObj)
}
return result, nil
}
func extractUIAmount(account map[string]any) float64 {
data, _ := account["account"].(map[string]any)
if data == nil {
return 0
}
dataInner, _ := data["data"].(map[string]any)
if dataInner == nil {
return 0
}
parsed, _ := dataInner["parsed"].(map[string]any)
if parsed == nil {
return 0
}
info, _ := parsed["info"].(map[string]any)
if info == nil {
return 0
}
tokenAmount, _ := info["tokenAmount"].(map[string]any)
if tokenAmount == nil {
return 0
}
uiAmount, _ := tokenAmount["uiAmount"].(float64)
return uiAmount
}

View File

@ -0,0 +1,78 @@
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/debros/orama-website/invest-api/config"
"github.com/debros/orama-website/invest-api/db"
"github.com/debros/orama-website/invest-api/helius"
"github.com/debros/orama-website/invest-api/router"
"github.com/debros/orama-website/invest-api/verifier"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatalf("config error: %v", err)
}
database, err := db.Open(cfg.DBPath)
if err != nil {
log.Fatalf("database error: %v", err)
}
defer database.Close()
if err := db.Migrate(database); err != nil {
log.Fatalf("migration error: %v", err)
}
if err := db.Seed(database); err != nil {
log.Fatalf("seed error: %v", err)
}
heliusClient := helius.NewClient(cfg.HeliusAPIKey, cfg.HeliusRPCURL)
handler := router.New(database, cfg.JWTSecret, heliusClient)
// Start background verifier
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
v := verifier.New(database, heliusClient)
go v.Run(ctx)
// HTTP server with graceful shutdown
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: handler,
}
go func() {
log.Printf("invest-api listening on :%s", cfg.Port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// Wait for shutdown signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down...")
cancel() // Stop verifier
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatalf("shutdown error: %v", err)
}
log.Println("invest-api stopped")
}

View File

@ -0,0 +1,41 @@
package middleware
import (
"net/http"
"strings"
"github.com/debros/orama-website/invest-api/auth"
)
// RequireAuth validates JWT from Authorization header.
// Falls back to X-Wallet-Address/X-Wallet-Chain headers for backward compatibility.
func RequireAuth(jwtSecret string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Try JWT first
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := auth.ParseToken(tokenStr, jwtSecret)
if err != nil {
http.Error(w, `{"error":"invalid or expired token"}`, http.StatusUnauthorized)
return
}
ctx := auth.WithWallet(r.Context(), claims.Wallet, claims.Chain)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
// Fallback: legacy header-based auth (remove after frontend migration)
wallet := strings.TrimSpace(r.Header.Get("X-Wallet-Address"))
chain := strings.ToLower(strings.TrimSpace(r.Header.Get("X-Wallet-Chain")))
if wallet != "" && (chain == "sol" || chain == "evm") {
ctx := auth.WithWallet(r.Context(), wallet, chain)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
http.Error(w, `{"error":"authentication required"}`, http.StatusUnauthorized)
})
}
}

View File

@ -0,0 +1,23 @@
package middleware
import "net/http"
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin == "" {
origin = "*"
}
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Wallet-Address, X-Wallet-Chain")
w.Header().Set("Access-Control-Max-Age", "86400")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}

View File

@ -0,0 +1,44 @@
package router
import (
"database/sql"
"net/http"
"github.com/debros/orama-website/invest-api/auth"
"github.com/debros/orama-website/invest-api/handler"
"github.com/debros/orama-website/invest-api/helius"
mw "github.com/debros/orama-website/invest-api/middleware"
)
func New(database *sql.DB, jwtSecret string, heliusClient *helius.Client) http.Handler {
mux := http.NewServeMux()
requireAuth := mw.RequireAuth(jwtSecret)
// Public endpoints
mux.HandleFunc("GET /api/stats", handler.StatsHandler(database))
mux.HandleFunc("GET /api/activity", handler.ActivityHandler(database))
mux.HandleFunc("GET /api/price", handler.PriceHandler())
// Auth
mux.HandleFunc("POST /api/auth/login", auth.LoginHandler(database, jwtSecret))
// Authenticated endpoints
authed := http.NewServeMux()
authed.HandleFunc("GET /api/me", handler.MeHandler(database))
authed.HandleFunc("POST /api/purchase/token", handler.TokenPurchaseHandler(database))
authed.HandleFunc("POST /api/purchase/license", handler.LicensePurchaseHandler(database, heliusClient))
authed.HandleFunc("POST /api/whitelist/join", handler.WhitelistJoinHandler(database))
authed.HandleFunc("GET /api/nft/status", handler.NftStatusHandler(heliusClient))
authed.HandleFunc("GET /api/anchat/balance", handler.AnchatBalanceHandler(database, heliusClient))
authed.HandleFunc("POST /api/anchat/claim", handler.AnchatClaimHandler(database, heliusClient))
// Wire authenticated routes through auth middleware
mux.Handle("GET /api/me", requireAuth(authed))
mux.Handle("POST /api/purchase/", requireAuth(authed))
mux.Handle("POST /api/whitelist/", requireAuth(authed))
mux.Handle("GET /api/nft/", requireAuth(authed))
mux.Handle("GET /api/anchat/", requireAuth(authed))
mux.Handle("POST /api/anchat/", requireAuth(authed))
return mw.CORS(mux)
}

10
website/invest-api/start.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
set -e
cd "$(dirname "$0")"
echo "Building invest-api..."
go build -o invest-api .
echo "Starting invest-api on port ${PORT:-8090}..."
./invest-api

View File

@ -0,0 +1,210 @@
package verifier
import (
"context"
"database/sql"
"log"
"time"
dbpkg "github.com/debros/orama-website/invest-api/db"
"github.com/debros/orama-website/invest-api/helius"
)
const (
SweepInterval = 30 * time.Minute
GracePeriod = 5 * time.Minute
)
// Verifier periodically checks that claim holders still hold their assets.
// State machine: active → flagged → warned → revoked (each transition requires
// the asset to still be missing after GracePeriod).
type Verifier struct {
db *sql.DB
helius *helius.Client
}
func New(db *sql.DB, heliusClient *helius.Client) *Verifier {
return &Verifier{db: db, helius: heliusClient}
}
// Run starts the background sweep loop. Blocks until ctx is cancelled.
func (v *Verifier) Run(ctx context.Context) {
log.Println("[verifier] starting background verification (interval:", SweepInterval, ")")
// Run once on startup to catch anything missed during downtime
v.sweep()
ticker := time.NewTicker(SweepInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
log.Println("[verifier] shutting down")
return
case <-ticker.C:
v.sweep()
}
}
}
func (v *Verifier) sweep() {
log.Println("[verifier] starting sweep...")
v.verifyAnchatClaims()
v.verifyNftLicenses()
log.Println("[verifier] sweep complete")
}
// ── $ANCHAT claim verification ──
func (v *Verifier) verifyAnchatClaims() {
rows, err := v.db.Query(
"SELECT id, wallet, status, flagged_at, warned_at FROM anchat_claims WHERE status != 'revoked'",
)
if err != nil {
log.Printf("[verifier] failed to query anchat_claims: %v", err)
return
}
defer rows.Close()
for rows.Next() {
var id int
var wallet, status string
var flaggedAt, warnedAt sql.NullString
if err := rows.Scan(&id, &wallet, &status, &flaggedAt, &warnedAt); err != nil {
log.Printf("[verifier] scan error: %v", err)
continue
}
balance, err := v.helius.GetTokenBalanceFresh(wallet, dbpkg.AnchatMint)
if err != nil {
log.Printf("[verifier] helius error for %s: %v", truncate(wallet), err)
continue // Don't change state on RPC errors
}
holdsAsset := balance >= dbpkg.AnchatMinBalance
v.updateState(id, "anchat_claims", wallet, status, holdsAsset, flaggedAt, warnedAt, "anchat_claim")
}
}
// ── NFT license verification ──
func (v *Verifier) verifyNftLicenses() {
rows, err := v.db.Query(
"SELECT id, wallet, status, flagged_at, warned_at FROM nft_license_verification WHERE status != 'revoked'",
)
if err != nil {
log.Printf("[verifier] failed to query nft_license_verification: %v", err)
return
}
defer rows.Close()
for rows.Next() {
var id int
var wallet, status string
var flaggedAt, warnedAt sql.NullString
if err := rows.Scan(&id, &wallet, &status, &flaggedAt, &warnedAt); err != nil {
log.Printf("[verifier] scan error: %v", err)
continue
}
nftResult, err := v.helius.CheckNFTs(wallet, dbpkg.TeamNFTCollection, dbpkg.CommunityNFTCollection)
if err != nil {
log.Printf("[verifier] helius NFT error for %s: %v", truncate(wallet), err)
continue
}
holdsAsset := nftResult.HasTeamNFT
v.updateState(id, "nft_license_verification", wallet, status, holdsAsset, flaggedAt, warnedAt, "nft_license")
}
}
// ── State machine logic ──
func (v *Verifier) updateState(
id int,
table, wallet, currentStatus string,
holdsAsset bool,
flaggedAt, warnedAt sql.NullString,
claimType string,
) {
now := time.Now()
if holdsAsset {
// Asset is back — reset to active
if currentStatus != "active" {
v.db.Exec(
"UPDATE "+table+" SET status = 'active', flagged_at = NULL, warned_at = NULL, revoked_at = NULL WHERE id = ?",
id,
)
log.Printf("[verifier] %s %s: RESTORED to active (asset found again)", table, truncate(wallet))
v.logActivity(claimType+"_restored", wallet, "Asset detected, claim restored")
}
return
}
// Asset NOT found — advance state machine
switch currentStatus {
case "active":
// First miss → flag
v.db.Exec("UPDATE "+table+" SET status = 'flagged', flagged_at = ? WHERE id = ?", now.Format(time.RFC3339), id)
log.Printf("[verifier] %s %s: FLAGGED (asset not found, check 1/3)", table, truncate(wallet))
case "flagged":
// Second miss — check grace period
if !flaggedAt.Valid {
return
}
flagTime, err := time.Parse(time.RFC3339, flaggedAt.String)
if err != nil {
return
}
if now.Sub(flagTime) < GracePeriod {
return // Too soon, wait
}
v.db.Exec("UPDATE "+table+" SET status = 'warned', warned_at = ? WHERE id = ?", now.Format(time.RFC3339), id)
log.Printf("[verifier] %s %s: WARNED (asset not found, check 2/3)", table, truncate(wallet))
case "warned":
// Third miss — check grace period from warned_at
if !warnedAt.Valid {
return
}
warnTime, err := time.Parse(time.RFC3339, warnedAt.String)
if err != nil {
return
}
if now.Sub(warnTime) < GracePeriod {
return
}
// REVOKE
v.db.Exec("UPDATE "+table+" SET status = 'revoked', revoked_at = ? WHERE id = ?", now.Format(time.RFC3339), id)
log.Printf("[verifier] %s %s: REVOKED (asset not found, check 3/3)", table, truncate(wallet))
v.logActivity(claimType+"_revoked", wallet, "Claim revoked — asset no longer held")
// For NFT licenses: also mark the license_purchase as revoked
if table == "nft_license_verification" {
v.db.Exec(
"UPDATE license_purchases SET claimed_via_nft = -1 WHERE wallet = ? AND claimed_via_nft = 1",
wallet,
)
}
}
}
func (v *Verifier) logActivity(eventType, wallet, detail string) {
truncated := truncate(wallet)
v.db.Exec(
"INSERT INTO activity_log (event_type, wallet, detail) VALUES (?, ?, ?)",
eventType, truncated, detail,
)
}
func truncate(wallet string) string {
if len(wallet) > 10 {
return wallet[:6] + "..." + wallet[len(wallet)-4:]
}
return wallet
}

56
website/package.json Normal file
View File

@ -0,0 +1,56 @@
{
"name": "orama-website",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@gsap/react": "^2.1.2",
"@mdx-js/react": "^3.1.1",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@solana/wallet-adapter-base": "^0.9.27",
"@solana/wallet-adapter-react": "^0.15.39",
"@solana/wallet-adapter-react-ui": "^0.9.39",
"@solana/wallet-adapter-wallets": "^0.19.37",
"@solana/web3.js": "^1.98.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"ethers": "^6.16.0",
"gsap": "^3.14.2",
"lightweight-charts": "^5.1.0",
"lucide-react": "^0.511.0",
"mermaid": "^11.12.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.6.1",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"shiki": "^4.0.0",
"tailwind-merge": "^3.3.0",
"three": "^0.183.2"
},
"devDependencies": {
"@mdx-js/rollup": "^3.1.1",
"@tailwindcss/vite": "^4.1.8",
"@types/mdx": "^2.0.13",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/three": "^0.183.1",
"@vitejs/plugin-react": "^4.5.2",
"tailwindcss": "^4.1.8",
"typescript": "^5.8.3",
"vite": "^7.0.0"
}
}

13940
website/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild

BIN
website/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>Thats an error.</ins>
<p>The requested URL <code>/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuFuYAZ9hiA.woff</code> was not found on this server. <ins>Thats all we know.</ins>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
website/public/icons/x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

BIN
website/public/images/anchat.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,103 @@
import type { SVGProps } from "react";
export function ReactLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1"
width="24"
height="24"
{...props}
>
{/* Center dot */}
<circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none" />
{/* Orbit 1 — horizontal */}
<ellipse cx="12" cy="12" rx="10" ry="4" />
{/* Orbit 2 — tilted right */}
<ellipse cx="12" cy="12" rx="10" ry="4" transform="rotate(60 12 12)" />
{/* Orbit 3 — tilted left */}
<ellipse cx="12" cy="12" rx="10" ry="4" transform="rotate(120 12 12)" />
</svg>
);
}
export function NextjsLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
width="24"
height="24"
{...props}
>
{/* "N" lettermark */}
<path d="M5 4v16h2.5V8.5L18.2 21.6l1.8-1.2V4h-2.5v11.5L6.8 2.4 5 4z" />
</svg>
);
}
export function GoLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
width="24"
height="24"
{...props}
>
{/* Stylized "Go" text */}
<text
x="12"
y="16"
textAnchor="middle"
fontSize="14"
fontWeight="700"
fontFamily="sans-serif"
fill="currentColor"
>
Go
</text>
</svg>
);
}
export function NodejsLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.2"
strokeLinejoin="round"
width="24"
height="24"
{...props}
>
{/* Hexagon shape */}
<path d="M12 2l8.66 5v10L12 22l-8.66-5V7L12 2z" />
{/* Inner "N" mark */}
<path d="M9 15V9l3 4 3-4v6" strokeLinecap="round" />
</svg>
);
}
export function WasmLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
width="24"
height="24"
{...props}
>
{/* WASM diamond/gear shape */}
<path d="M12 1L22 7v10l-10 6L2 17V7l10-6z" fill="none" stroke="currentColor" strokeWidth="1.2" />
{/* "W" inside */}
<path d="M7 9l1.5 6 1.5-4 1.5 4 1.5-6" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
{/* Small dot accent */}
<circle cx="17" cy="12" r="1.2" fill="currentColor" stroke="none" />
</svg>
);
}

View File

@ -0,0 +1,269 @@
import { useRef, useMemo } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { Float } from "@react-three/drei";
import * as THREE from "three";
const NODE_COUNT = 24;
const INNER_RADIUS = 0.6;
const OUTER_RADIUS = 1.6;
const PARTICLE_COUNT = 80;
/* ─── Dot ring (reusable) ─── */
function DotRing({ radius, count, color, opacity, dotSize }: {
radius: number; count: number; color: string; opacity: number; dotSize: number;
}) {
const dots = useMemo(() =>
Array.from({ length: count }, (_, i) => {
const angle = (i / count) * Math.PI * 2;
return [Math.cos(angle) * radius, Math.sin(angle) * radius] as [number, number];
}), [radius, count]);
return (
<group rotation={[-Math.PI / 2, 0, 0]}>
{dots.map(([x, y], i) => (
<mesh key={i} position={[x, y, 0]}>
<circleGeometry args={[dotSize, 8]} />
<meshBasicMaterial color={color} transparent opacity={opacity} side={THREE.DoubleSide} />
</mesh>
))}
</group>
);
}
/* ─── Core nodes — pulsing spheres in a scattered cluster ─── */
function CoreNodes() {
const meshRefs = useRef<(THREE.Mesh | null)[]>([]);
const nodes = useMemo(() =>
Array.from({ length: NODE_COUNT }, (_, i) => {
const angle = (i / NODE_COUNT) * Math.PI * 2 + (Math.random() - 0.5) * 0.3;
const r = INNER_RADIUS + Math.random() * (OUTER_RADIUS - INNER_RADIUS);
const y = (Math.random() - 0.5) * 0.6;
return {
x: Math.cos(angle) * r,
y,
z: Math.sin(angle) * r,
phase: i * 0.4 + Math.random() * 2,
size: 0.015 + Math.random() * 0.02,
};
}), []);
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
meshRefs.current.forEach((mesh, i) => {
if (!mesh) return;
const node = nodes[i];
const wave = Math.sin(t * 1.2 + node.phase);
const mat = mesh.material as THREE.MeshBasicMaterial;
mat.opacity = 0.2 + 0.5 * Math.max(0, wave);
const s = 1 + 0.4 * Math.max(0, wave);
mesh.scale.setScalar(s);
});
});
return (
<group>
{nodes.map((node, i) => (
<mesh
key={i}
ref={(el) => { meshRefs.current[i] = el; }}
position={[node.x, node.y, node.z]}
>
<sphereGeometry args={[node.size, 8, 8]} />
<meshBasicMaterial color="#d4d4d8" transparent opacity={0.3} />
</mesh>
))}
</group>
);
}
/* ─── Connection lines between nearby nodes ─── */
function Connections() {
const lineRefs = useRef<(THREE.Line | null)[]>([]);
const { nodes, pairs } = useMemo(() => {
const ns = Array.from({ length: NODE_COUNT }, (_, i) => {
const angle = (i / NODE_COUNT) * Math.PI * 2 + (Math.random() - 0.5) * 0.3;
const r = INNER_RADIUS + Math.random() * (OUTER_RADIUS - INNER_RADIUS);
const y = (Math.random() - 0.5) * 0.6;
return new THREE.Vector3(Math.cos(angle) * r, y, Math.sin(angle) * r);
});
const ps: [number, number][] = [];
for (let i = 0; i < ns.length; i++) {
for (let j = i + 1; j < ns.length; j++) {
if (ns[i].distanceTo(ns[j]) < 0.9) {
ps.push([i, j]);
}
}
}
return { nodes: ns, pairs: ps };
}, []);
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
lineRefs.current.forEach((line, idx) => {
if (!line) return;
const mat = line.material as THREE.LineBasicMaterial;
const wave = Math.sin(t * 0.8 + idx * 0.3);
mat.opacity = 0.03 + 0.06 * Math.max(0, wave);
});
});
return (
<group>
{pairs.map(([a, b], i) => {
const geo = new THREE.BufferGeometry().setFromPoints([nodes[a], nodes[b]]);
return (
<primitive
key={i}
ref={(el: THREE.Line | null) => { lineRefs.current[i] = el; }}
object={new THREE.Line(geo, new THREE.LineBasicMaterial({ color: "#a1a1aa", transparent: true, opacity: 0.05 }))}
/>
);
})}
</group>
);
}
/* ─── Expanding pulse rings from center ─── */
function PulseRings() {
const meshRefs = useRef<(THREE.Mesh | null)[]>([]);
const RING_COUNT = 3;
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
meshRefs.current.forEach((mesh, i) => {
if (!mesh) return;
const phase = (t * 0.3 + i * (1 / RING_COUNT)) % 1;
const scale = 0.2 + phase * 2;
mesh.scale.set(scale, scale, 1);
const mat = mesh.material as THREE.MeshBasicMaterial;
mat.opacity = 0.08 * (1 - phase);
});
});
return (
<group rotation={[-Math.PI / 2, 0, 0]}>
{Array.from({ length: RING_COUNT }, (_, i) => (
<mesh key={i} ref={(el) => { meshRefs.current[i] = el; }}>
<ringGeometry args={[0.98, 1, 64]} />
<meshBasicMaterial color="#d4d4d8" transparent opacity={0.05} side={THREE.DoubleSide} />
</mesh>
))}
</group>
);
}
/* ─── Floating ambient particles ─── */
function AmbientParticles() {
const ref = useRef<THREE.Points>(null);
const positions = useMemo(() => {
const arr = new Float32Array(PARTICLE_COUNT * 3);
for (let i = 0; i < PARTICLE_COUNT; i++) {
const angle = Math.random() * Math.PI * 2;
const r = 0.3 + Math.random() * 2.2;
arr[i * 3] = Math.cos(angle) * r;
arr[i * 3 + 1] = (Math.random() - 0.5) * 1.5;
arr[i * 3 + 2] = Math.sin(angle) * r;
}
return arr;
}, []);
useFrame(({ clock }) => {
if (!ref.current) return;
const arr = ref.current.geometry.attributes.position.array as Float32Array;
const t = clock.getElapsedTime();
for (let i = 0; i < PARTICLE_COUNT; i++) {
arr[i * 3 + 1] += Math.sin(t * 0.5 + i) * 0.0003;
}
ref.current.geometry.attributes.position.needsUpdate = true;
ref.current.rotation.y = t * 0.02;
});
return (
<points ref={ref}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
args={[positions, 3]}
/>
</bufferGeometry>
<pointsMaterial color="#a1a1aa" size={0.008} transparent opacity={0.15} sizeAttenuation />
</points>
);
}
/* ─── Center shield icon (icosahedron wireframe) ─── */
function CenterShield() {
const meshRef = useRef<THREE.Mesh>(null);
const glowRef = useRef<THREE.Mesh>(null);
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
if (meshRef.current) {
meshRef.current.rotation.y = t * 0.15;
meshRef.current.rotation.x = Math.sin(t * 0.1) * 0.1;
}
if (glowRef.current) {
const mat = glowRef.current.material as THREE.MeshBasicMaterial;
mat.opacity = 0.03 + 0.02 * Math.sin(t * 1.5);
const s = 1 + 0.05 * Math.sin(t * 1.5);
glowRef.current.scale.setScalar(s);
}
});
return (
<group>
<mesh ref={meshRef}>
<icosahedronGeometry args={[0.18, 1]} />
<meshBasicMaterial color="#d4d4d8" wireframe transparent opacity={0.25} />
</mesh>
<mesh ref={glowRef}>
<sphereGeometry args={[0.22, 16, 16]} />
<meshBasicMaterial color="#d4d4d8" transparent opacity={0.03} />
</mesh>
</group>
);
}
/* ─── Full scene ─── */
function AboutNetwork() {
const groupRef = useRef<THREE.Group>(null);
useFrame(({ clock }) => {
if (groupRef.current) {
groupRef.current.rotation.y = clock.getElapsedTime() * 0.03;
}
});
return (
<Float speed={0.5} rotationIntensity={0.01} floatIntensity={0.04}>
<group ref={groupRef}>
<DotRing radius={OUTER_RADIUS} count={80} color="#a1a1aa" opacity={0.03} dotSize={0.004} />
<DotRing radius={INNER_RADIUS} count={40} color="#d4d4d8" opacity={0.04} dotSize={0.003} />
<CoreNodes />
<Connections />
<PulseRings />
<AmbientParticles />
<CenterShield />
</group>
</Float>
);
}
export function AboutHeroScene() {
return (
<div className="w-full h-[550px] -mt-[400px]">
<Canvas
camera={{ position: [0, 2, 2.5], fov: 42 }}
dpr={[1, 2]}
gl={{ antialias: true, alpha: true, toneMapping: THREE.ACESFilmicToneMapping, toneMappingExposure: 1 }}
style={{ background: "transparent" }}
>
<ambientLight intensity={0.1} />
<AboutNetwork />
</Canvas>
</div>
);
}

View File

@ -0,0 +1,69 @@
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { Terminal } from "../ui/terminal";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { AnimateIn } from "../ui/animate-in";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
export function CliInstall() {
return (
<>
<Section>
<AnimateIn>
<div className="flex flex-col gap-8">
<SectionHeader
title="Install the CLI"
subtitle="Get started in seconds. Available on macOS, Linux, and from source."
/>
<Tabs defaultValue="brew">
<TabsList>
<TabsTrigger value="brew">macOS (Homebrew)</TabsTrigger>
<TabsTrigger value="apt">Linux (Debian/Ubuntu)</TabsTrigger>
<TabsTrigger value="source">From Source</TabsTrigger>
</TabsList>
<TabsContent value="brew">
<Terminal
lines={[
{ prefix: "$", text: "brew install DeBrosOfficial/tap/orama" },
{ prefix: "\u2192", text: "Downloading orama..." },
{ prefix: "\u2713", text: "orama installed successfully" },
{ text: "" },
{ prefix: "$", text: "orama --version" },
{ prefix: "\u2192", text: "orama v2.0.0" },
]}
/>
</TabsContent>
<TabsContent value="apt">
<Terminal
lines={[
{ prefix: "$", text: "curl -sL https://github.com/DeBrosOfficial/network/releases/latest/download/orama_linux_amd64.deb -o orama.deb" },
{ prefix: "$", text: "sudo dpkg -i orama.deb" },
{ prefix: "\u2192", text: "Selecting previously unselected package orama." },
{ prefix: "\u2713", text: "orama installed successfully" },
]}
/>
</TabsContent>
<TabsContent value="source">
<Terminal
lines={[
{ prefix: "$", text: "go install github.com/DeBrosOfficial/network/cmd/cli@latest" },
{ prefix: "\u2192", text: "Downloading modules..." },
{ prefix: "\u2713", text: "orama installed to $GOPATH/bin" },
]}
/>
</TabsContent>
</Tabs>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,361 @@
import { useRef, useMemo } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { Float } from "@react-three/drei";
import * as THREE from "three";
const NODE_COUNT = 32;
const GRID_SPACING = 1.1;
const EDGE_MAX_DIST = 1.6;
const PACKET_COUNT = 8;
interface GridNode {
position: THREE.Vector3;
phase: number;
baseScale: number;
}
interface Edge {
from: number;
to: number;
}
interface Packet {
edgeIndex: number;
progress: number;
speed: number;
direction: 1 | -1;
}
/* Generate nodes on a gently curved grid */
function generateGridNodes(): GridNode[] {
const nodes: GridNode[] = [];
const cols = 6;
const rows = 5;
for (let row = 0; row < rows; row++) {
// Offset every other row for a hex-like feel
const colsInRow = row % 2 === 0 ? cols : cols - 1;
const offsetX = row % 2 === 0 ? 0 : GRID_SPACING * 0.5;
for (let col = 0; col < colsInRow; col++) {
if (nodes.length >= NODE_COUNT) break;
const x =
(col - (colsInRow - 1) / 2) * GRID_SPACING + offsetX;
const z = (row - (rows - 1) / 2) * (GRID_SPACING * 0.85);
// Gentle curve — bowl shape
const dist = Math.sqrt(x * x + z * z);
const y = dist * dist * 0.03;
// Slight random jitter
const jx = (Math.random() - 0.5) * 0.15;
const jz = (Math.random() - 0.5) * 0.15;
nodes.push({
position: new THREE.Vector3(x + jx, y, z + jz),
phase: Math.random() * Math.PI * 2,
baseScale: 0.03 + Math.random() * 0.015,
});
}
}
return nodes;
}
/* Generate edges between nearby nodes */
function generateEdges(nodes: GridNode[]): Edge[] {
const edges: Edge[] = [];
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
if (
nodes[i].position.distanceTo(nodes[j].position) < EDGE_MAX_DIST
) {
edges.push({ from: i, to: j });
}
}
}
return edges;
}
/* ─── Grid node (sphere with pulse) ─── */
function GridNodes({ nodes }: { nodes: GridNode[] }) {
const meshRefs = useRef<(THREE.Mesh | null)[]>([]);
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
meshRefs.current.forEach((mesh, i) => {
if (!mesh) return;
const node = nodes[i];
const pulse = Math.sin(t * 1.5 + node.phase);
const mat = mesh.material as THREE.MeshBasicMaterial;
mat.opacity = 0.25 + 0.35 * Math.max(0, pulse);
const s =
node.baseScale * (1 + 0.4 * Math.max(0, pulse));
mesh.scale.setScalar(s / node.baseScale);
});
});
return (
<group>
{nodes.map((node, i) => (
<mesh
key={i}
ref={(el) => {
meshRefs.current[i] = el;
}}
position={node.position}
>
<sphereGeometry args={[node.baseScale, 10, 10]} />
<meshBasicMaterial
color="#d4d4d8"
transparent
opacity={0.3}
/>
</mesh>
))}
</group>
);
}
/* ─── Connection lines between nodes ─── */
function ConnectionLines({
nodes,
edges,
}: {
nodes: GridNode[];
edges: Edge[];
}) {
const lines = useMemo(() => {
return edges.map((edge) => {
const geo = new THREE.BufferGeometry().setFromPoints([
nodes[edge.from].position,
nodes[edge.to].position,
]);
const mat = new THREE.LineBasicMaterial({
color: "#a1a1aa",
transparent: true,
opacity: 0.06,
});
return new THREE.Line(geo, mat);
});
}, [nodes, edges]);
return (
<group>
{lines.map((line, i) => (
<primitive key={i} object={line} />
))}
</group>
);
}
/* ─── Light packets traveling along edges ─── */
function Packets({
nodes,
edges,
}: {
nodes: GridNode[];
edges: Edge[];
}) {
const meshRefs = useRef<(THREE.Mesh | null)[]>([]);
const glowRefs = useRef<(THREE.Mesh | null)[]>([]);
const packetsRef = useRef<Packet[]>([]);
// Initialize packets
if (packetsRef.current.length === 0 && edges.length > 0) {
packetsRef.current = Array.from(
{ length: PACKET_COUNT },
() => ({
edgeIndex: Math.floor(Math.random() * edges.length),
progress: Math.random(),
speed: 0.003 + Math.random() * 0.004,
direction: (Math.random() > 0.5 ? 1 : -1) as 1 | -1,
}),
);
}
useFrame(() => {
packetsRef.current.forEach((packet, i) => {
const mesh = meshRefs.current[i];
const glow = glowRefs.current[i];
if (!mesh || !glow) return;
// Advance
packet.progress += packet.speed * packet.direction;
// When reaching end, jump to a connected edge
if (packet.progress > 1 || packet.progress < 0) {
const currentEdge = edges[packet.edgeIndex];
const endNode =
packet.direction === 1
? currentEdge.to
: currentEdge.from;
// Find edges connected to end node
const connected = edges
.map((e, idx) => ({ e, idx }))
.filter(
({ e, idx }) =>
idx !== packet.edgeIndex &&
(e.from === endNode || e.to === endNode),
);
if (connected.length > 0) {
const next =
connected[Math.floor(Math.random() * connected.length)];
packet.edgeIndex = next.idx;
packet.direction =
next.e.from === endNode ? 1 : -1;
packet.progress = packet.direction === 1 ? 0 : 1;
} else {
packet.direction *= -1 as 1 | -1;
packet.progress = THREE.MathUtils.clamp(
packet.progress,
0,
1,
);
}
}
// Position along edge
const edge = edges[packet.edgeIndex];
const from = nodes[edge.from].position;
const to = nodes[edge.to].position;
const pos = new THREE.Vector3().lerpVectors(
from,
to,
packet.progress,
);
mesh.position.copy(pos);
glow.position.copy(pos);
});
});
return (
<group>
{Array.from({ length: PACKET_COUNT }, (_, i) => (
<group key={i}>
{/* Core */}
<mesh
ref={(el) => {
meshRefs.current[i] = el;
}}
>
<sphereGeometry args={[0.02, 8, 8]} />
<meshBasicMaterial
color="#ffffff"
transparent
opacity={0.7}
/>
</mesh>
{/* Glow */}
<mesh
ref={(el) => {
glowRefs.current[i] = el;
}}
>
<sphereGeometry args={[0.06, 8, 8]} />
<meshBasicMaterial
color="#d4d4d8"
transparent
opacity={0.08}
/>
</mesh>
</group>
))}
</group>
);
}
/* ─── Deploy burst — periodic flare on a random node ─── */
function DeployBursts({ nodes }: { nodes: GridNode[] }) {
const ringRef = useRef<THREE.Mesh>(null);
const burstNodeRef = useRef(0);
const lastBurstRef = useRef(0);
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
// Trigger a burst every ~5 seconds
if (t - lastBurstRef.current > 5) {
burstNodeRef.current = Math.floor(
Math.random() * nodes.length,
);
lastBurstRef.current = t;
}
if (ringRef.current) {
const elapsed = t - lastBurstRef.current;
const node = nodes[burstNodeRef.current];
ringRef.current.position.copy(node.position);
if (elapsed < 1.5) {
const scale = 1 + elapsed * 2;
ringRef.current.scale.setScalar(scale);
const mat = ringRef.current
.material as THREE.MeshBasicMaterial;
mat.opacity = 0.12 * (1 - elapsed / 1.5);
} else {
const mat = ringRef.current
.material as THREE.MeshBasicMaterial;
mat.opacity = 0;
}
}
});
return (
<mesh ref={ringRef} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[0.08, 0.1, 24]} />
<meshBasicMaterial
color="#d4d4d8"
transparent
opacity={0}
side={THREE.DoubleSide}
/>
</mesh>
);
}
/* ─── Full scene ─── */
function ComputeMesh() {
const groupRef = useRef<THREE.Group>(null);
const nodes = useMemo(() => generateGridNodes(), []);
const edges = useMemo(() => generateEdges(nodes), [nodes]);
// Static orientation — no spinning
return (
<Float speed={0.5} rotationIntensity={0.01} floatIntensity={0.04}>
<group ref={groupRef}>
<GridNodes nodes={nodes} />
<ConnectionLines nodes={nodes} edges={edges} />
<Packets nodes={nodes} edges={edges} />
<DeployBursts nodes={nodes} />
</group>
</Float>
);
}
export function ComputeMeshScene() {
return (
<div className="w-full h-[550px] -mt-[400px]">
<Canvas
camera={{ position: [0, 3, 3.5], fov: 38 }}
dpr={[1, 2]}
gl={{
antialias: true,
alpha: true,
toneMapping: THREE.ACESFilmicToneMapping,
toneMappingExposure: 1,
}}
style={{ background: "transparent" }}
>
<ambientLight intensity={0.1} />
<ComputeMesh />
</Canvas>
</div>
);
}

View File

@ -0,0 +1,245 @@
import { useRef, useMemo } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { Float } from "@react-three/drei";
import * as THREE from "three";
const VALIDATOR_COUNT = 12;
const ANGEL_COUNT = 8;
const RING_RADIUS = 1.4;
const ANGEL_RADIUS = 0.9;
/* ─── Thin ring made of dots ─── */
function DotRing({ radius, count, color, opacity, dotSize }: {
radius: number; count: number; color: string; opacity: number; dotSize: number;
}) {
const dots = useMemo(() => {
return Array.from({ length: count }, (_, i) => {
const angle = (i / count) * Math.PI * 2;
return [Math.cos(angle) * radius, Math.sin(angle) * radius] as [number, number];
});
}, [radius, count]);
return (
<group rotation={[-Math.PI / 2, 0, 0]}>
{dots.map(([x, y], i) => (
<mesh key={i} position={[x, y, 0]}>
<circleGeometry args={[dotSize, 8]} />
<meshBasicMaterial color={color} transparent opacity={opacity} side={THREE.DoubleSide} />
</mesh>
))}
</group>
);
}
/* ─── Validator nodes — small glowing dots on the outer ring ─── */
function Validators() {
const groupRef = useRef<THREE.Group>(null);
const meshRefs = useRef<(THREE.Mesh | null)[]>([]);
const positions = useMemo(() => {
return Array.from({ length: VALIDATOR_COUNT }, (_, i) => {
const angle = (i / VALIDATOR_COUNT) * Math.PI * 2;
return {
x: Math.cos(angle) * RING_RADIUS,
z: Math.sin(angle) * RING_RADIUS,
phase: i * 0.5,
};
});
}, []);
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
meshRefs.current.forEach((mesh, i) => {
if (!mesh) return;
const mat = mesh.material as THREE.MeshBasicMaterial;
// Staggered pulse wave around the ring
const wave = Math.sin(t * 1.5 - positions[i].phase);
mat.opacity = 0.3 + 0.4 * Math.max(0, wave);
const s = 1 + 0.3 * Math.max(0, wave);
mesh.scale.setScalar(s);
});
});
return (
<group ref={groupRef}>
{positions.map((pos, i) => (
<mesh
key={i}
ref={(el) => { meshRefs.current[i] = el; }}
position={[pos.x, 0, pos.z]}
>
<sphereGeometry args={[0.03, 12, 12]} />
<meshBasicMaterial color="#d4d4d8" transparent opacity={0.4} />
</mesh>
))}
</group>
);
}
/* ─── Angel nodes — teal particles orbiting inner ring ─── */
function Angels() {
const groupRef = useRef<THREE.Group>(null);
const meshRefs = useRef<(THREE.Mesh | null)[]>([]);
const glowRefs = useRef<(THREE.Mesh | null)[]>([]);
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
meshRefs.current.forEach((mesh, i) => {
if (!mesh) return;
const angle = (i / ANGEL_COUNT) * Math.PI * 2 + t * 0.4;
const wobbleY = Math.sin(t * 2.5 + i * 1.2) * 0.08;
mesh.position.x = Math.cos(angle) * ANGEL_RADIUS;
mesh.position.z = Math.sin(angle) * ANGEL_RADIUS;
mesh.position.y = wobbleY;
const mat = mesh.material as THREE.MeshBasicMaterial;
const pulse = Math.sin(t * 2 + i * 0.8);
mat.opacity = 0.5 + 0.3 * Math.max(0, pulse);
});
glowRefs.current.forEach((mesh, i) => {
if (!mesh) return;
const angle = (i / ANGEL_COUNT) * Math.PI * 2 + t * 0.4;
const wobbleY = Math.sin(t * 2.5 + i * 1.2) * 0.08;
mesh.position.x = Math.cos(angle) * ANGEL_RADIUS;
mesh.position.z = Math.sin(angle) * ANGEL_RADIUS;
mesh.position.y = wobbleY;
const mat = mesh.material as THREE.MeshBasicMaterial;
const pulse = Math.sin(t * 2 + i * 0.8);
mat.opacity = 0.06 + 0.08 * Math.max(0, pulse);
});
});
return (
<group ref={groupRef}>
{Array.from({ length: ANGEL_COUNT }, (_, i) => (
<group key={i}>
<mesh ref={(el) => { meshRefs.current[i] = el; }}>
<sphereGeometry args={[0.02, 8, 8]} />
<meshBasicMaterial color="#00d4aa" transparent opacity={0.5} />
</mesh>
<mesh ref={(el) => { glowRefs.current[i] = el; }}>
<sphereGeometry args={[0.06, 8, 8]} />
<meshBasicMaterial color="#00d4aa" transparent opacity={0.06} />
</mesh>
</group>
))}
</group>
);
}
/* ─── Center block — small, elegant, rotating ─── */
function CenterBlock() {
const meshRef = useRef<THREE.Mesh>(null);
const pulseRef = useRef<THREE.Mesh>(null);
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
if (meshRef.current) {
meshRef.current.rotation.y = t * 0.6;
meshRef.current.rotation.z = t * 0.3;
}
// Pulse ring expands outward periodically
if (pulseRef.current) {
const cycle = t % 3;
const expanding = cycle < 1;
const scale = expanding ? 1 + cycle * 2 : 3;
pulseRef.current.scale.setScalar(scale);
const mat = pulseRef.current.material as THREE.MeshBasicMaterial;
mat.opacity = expanding ? 0.08 * (1 - cycle) : 0;
}
});
return (
<group>
<mesh ref={meshRef}>
<octahedronGeometry args={[0.1, 0]} />
<meshBasicMaterial color="#d4d4d8" transparent opacity={0.6} wireframe />
</mesh>
{/* Inner solid */}
<mesh rotation={[0, 0.5, 0.5]}>
<octahedronGeometry args={[0.05, 0]} />
<meshBasicMaterial color="#d4d4d8" transparent opacity={0.3} />
</mesh>
{/* Expanding pulse ring */}
<mesh ref={pulseRef} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[0.12, 0.13, 32]} />
<meshBasicMaterial color="#d4d4d8" transparent opacity={0} side={THREE.DoubleSide} />
</mesh>
</group>
);
}
/* ─── Connection lines from validators to center ─── */
function ConnectionLines() {
const lines = useMemo(() => {
return Array.from({ length: VALIDATOR_COUNT }, (_, i) => {
const angle = (i / VALIDATOR_COUNT) * Math.PI * 2;
const x = Math.cos(angle) * RING_RADIUS;
const z = Math.sin(angle) * RING_RADIUS;
const geo = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(x, 0, z),
]);
const mat = new THREE.LineBasicMaterial({ color: "#a1a1aa", transparent: true, opacity: 0.04 });
return new THREE.Line(geo, mat);
});
}, []);
return (
<group>
{lines.map((line, i) => (
<primitive key={i} object={line} />
))}
</group>
);
}
/* ─── Full Scene ─── */
function ConsensusNetwork() {
const groupRef = useRef<THREE.Group>(null);
useFrame(({ clock }) => {
if (groupRef.current) {
groupRef.current.rotation.y = clock.getElapsedTime() * 0.05;
}
});
return (
<Float speed={0.6} rotationIntensity={0.01} floatIntensity={0.05}>
<group ref={groupRef}>
{/* Outer validator ring (dots) */}
<DotRing radius={RING_RADIUS} count={60} color="#a1a1aa" opacity={0.04} dotSize={0.005} />
{/* Inner angel ring (dots) */}
<DotRing radius={ANGEL_RADIUS} count={40} color="#00d4aa" opacity={0.03} dotSize={0.004} />
<ConnectionLines />
<Validators />
<Angels />
<CenterBlock />
</group>
</Float>
);
}
export function ConsensusScene() {
return (
<div className="w-full h-[550px] -mt-[400px]">
<Canvas
camera={{ position: [0, 2, 2], fov: 45 }}
dpr={[1, 2]}
gl={{ antialias: true, alpha: true, toneMapping: THREE.ACESFilmicToneMapping, toneMappingExposure: 1 }}
style={{ background: "transparent" }}
>
<ambientLight intensity={0.1} />
<ConsensusNetwork />
</Canvas>
</div>
);
}

View File

@ -0,0 +1,89 @@
import { ExternalLink } from "lucide-react";
import { Link } from "react-router";
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { DashedPanel } from "../ui/dashed-panel";
import { Button } from "../ui/button";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { AnimateIn } from "../ui/animate-in";
const repos = [
{
name: "Core Network",
url: "https://github.com/DeBrosOfficial/network",
},
{
name: "TypeScript SDK",
url: "https://github.com/DeBrosOfficial/network-ts-sdk",
},
{
name: "RootWallet",
url: "https://github.com/DeBrosOfficial/rootwallet",
},
{
name: "Orama Vault",
url: "https://github.com/DeBrosOfficial/orama-vault",
},
];
export function ContribOpenSource() {
return (
<>
<Section>
<AnimateIn>
<div className="flex flex-col gap-8">
<SectionHeader title="Pick an issue. Ship a fix." />
<DashedPanel withCorners withBackground>
<div className="flex flex-col gap-6">
<p className="text-muted leading-relaxed">
We pair-program with new contributors. Every PR gets reviewed.
No question is too basic. The codebase has comprehensive
documentation, strict code style guides, and a test suite with
CI/CD.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{repos.map((repo) => (
<Button
key={repo.name}
asChild
variant="ghost"
className="justify-start"
>
<a
href={repo.url}
target="_blank"
rel="noopener noreferrer"
>
{repo.name}
<ExternalLink className="w-3.5 h-3.5 ml-2" />
</a>
</Button>
))}
</div>
<div className="flex flex-wrap gap-3">
<Button asChild variant="ghost">
<Link to="/docs/contributor/dev-setup">
Development Setup Guide
</Link>
</Button>
<Button asChild variant="ghost">
<Link to="/docs/contributor/code-style">
Code Style Guide
</Link>
</Button>
</div>
</div>
</DashedPanel>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,80 @@
import {
Terminal,
Globe,
Database,
Zap,
Globe2,
HardDrive,
Lock,
KeyRound,
Code,
Cpu,
Shield,
Network,
} from "lucide-react";
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { DashedPanel } from "../ui/dashed-panel";
import { Badge } from "../ui/badge";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { AnimateIn } from "../ui/animate-in";
const services = [
{ icon: Terminal, title: "CLI", description: "Node management, deployment, monitoring", tech: "Go" },
{ icon: Globe, title: "Gateway", description: "API gateway, routing, auth, rate limiting", tech: "Go" },
{ icon: Database, title: "RQLite", description: "Distributed SQL with Raft consensus", tech: "Go" },
{ icon: Zap, title: "Olric", description: "In-memory distributed cache", tech: "Go" },
{ icon: Globe2, title: "CoreDNS", description: "Distributed DNS, custom domains", tech: "Go" },
{ icon: HardDrive, title: "IPFS", description: "Content-addressed file storage", tech: "Go" },
{ icon: Lock, title: "Vault", description: "Secrets management, Shamir's SSS", tech: "Zig" },
{ icon: KeyRound, title: "RootWallet", description: "Multi-chain wallet authentication", tech: "TypeScript" },
{ icon: Code, title: "TypeScript SDK", description: "Client library for all services", tech: "TypeScript" },
{ icon: Cpu, title: "WASM Runtime", description: "Serverless function execution", tech: "Go" },
{ icon: Shield, title: "WireGuard", description: "Encrypted overlay network mesh", tech: "Go" },
{ icon: Network, title: "Cluster", description: "Node discovery, health, Hetzner API", tech: "Go" },
];
export function ContribServices() {
return (
<>
<Section id="services">
<AnimateIn>
<div className="flex flex-col gap-8">
<SectionHeader
title="What you'll work on."
subtitle="Orama is a distributed system made of 12+ independent services. Pick what excites you."
/>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-0">
{services.map((service) => {
const Icon = service.icon;
return (
<DashedPanel key={service.title} className="p-4 sm:p-5">
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Icon className="w-4 h-4 text-accent" />
<span className="font-display font-semibold text-fg text-sm">
{service.title}
</span>
</div>
<p className="text-xs text-muted leading-relaxed">
{service.description}
</p>
<Badge variant="outline" className="w-fit text-[10px]">
{service.tech}
</Badge>
</div>
</DashedPanel>
);
})}
</div>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,92 @@
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { DashedPanel } from "../ui/dashed-panel";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { AnimateIn } from "../ui/animate-in";
const stacks = [
{
language: "Go",
reason: "Performance, concurrency, and a rich systems ecosystem. Powers the gateway, CLI, and all core services.",
items: [
"API Gateway (net/http)",
"RQLite integration (Raft)",
"Olric cache (consistent hashing)",
"WireGuard mesh controller",
"CLI tooling (cobra)",
"WASM runtime (wazero)",
],
},
{
language: "Zig",
reason: "Manual memory control and zero-overhead cryptography. Powers the Vault guardian daemon.",
items: [
"Vault guardian daemon",
"Shamir's SSS (GF(2^8))",
"AES-256-GCM encryption",
"HMAC-SHA256 authentication",
"Binary TCP protocol",
"File-per-user storage",
],
},
{
language: "TypeScript",
reason: "Isomorphic code for browser and Node.js. Powers the SDK, RootWallet, and all frontend tooling.",
items: [
"Network SDK (browser + Node)",
"RootWallet SDK",
"Website (React + Vite)",
"React hooks library",
"CLI companion tools",
],
},
];
export function ContribStack() {
return (
<>
<Section>
<AnimateIn>
<div className="flex flex-col gap-8">
<SectionHeader
title="Tech Stack"
subtitle="Three languages, each chosen for what it does best."
/>
<div className="grid grid-cols-1 md:grid-cols-3 gap-0">
{stacks.map((stack) => (
<DashedPanel key={stack.language} className="p-6">
<div className="flex flex-col gap-4">
<h3 className="font-display font-bold text-fg text-lg">
{stack.language}
</h3>
<p className="text-xs text-muted leading-relaxed">
{stack.reason}
</p>
<ul className="flex flex-col gap-2">
{stack.items.map((item) => (
<li
key={item}
className="text-sm text-muted font-mono flex items-center gap-2"
>
<span className="text-accent text-xs"></span>
{item}
</li>
))}
</ul>
</div>
</DashedPanel>
))}
</div>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,88 @@
import { Link } from "react-router";
import { ExternalLink } from "lucide-react";
import type { ReactNode } from "react";
import type { Persona } from "../../types/persona";
import { Section } from "../layout/section";
import { DashedPanel } from "../ui/dashed-panel";
import { Button } from "../ui/button";
import { AnimateIn } from "../ui/animate-in";
import { StatusDot } from "../ui/status-dot";
import { Redacted } from "../ui/redacted";
const ctaContent: Record<
Persona,
{
heading: string;
description: ReactNode;
buttonText: string;
to: string;
external?: boolean;
}
> = {
developer: {
heading: "Start building in 60 seconds.",
description:
"Free tier. No credit card. No email. Connect your wallet and deploy.",
buttonText: "Start Building",
to: "/dashboard",
},
operator: {
heading: "Start your node today.",
description:
<>Minimal hardware. Maximum rewards. Join <Redacted /> operators powering the decentralized cloud.</>,
buttonText: "Read Setup Guide",
to: "/docs/operator/getting-started",
},
contributor: {
heading: "Your first PR is waiting.",
description:
"Open source. Active development. Real impact. Pick an issue and start contributing.",
buttonText: "View on GitHub",
to: "https://github.com/DeBrosOfficial",
external: true,
},
};
export function CtaSection({ persona }: { persona: Persona }) {
const content = ctaContent[persona];
return (
<Section padding="wide">
<AnimateIn>
<DashedPanel withCorners withBackground>
<div className="flex flex-col items-center text-center gap-6 py-8">
<div className="flex items-center justify-center gap-2 mb-2">
<StatusDot status="active" />
<span className="text-xs font-mono text-muted tracking-wider uppercase">50+ Nodes Online</span>
</div>
<h2 className="font-display font-bold text-2xl lg:text-3xl text-fg">
{content.heading}
</h2>
<p className="text-muted max-w-lg leading-relaxed">
{content.description}
</p>
{content.external ? (
<Button asChild size="lg">
<a
href={content.to}
target="_blank"
rel="noopener noreferrer"
>
{content.buttonText}
<ExternalLink className="w-3.5 h-3.5 ml-2" />
</a>
</Button>
) : (
<Button asChild size="lg">
<Link to={content.to}>{content.buttonText}</Link>
</Button>
)}
</div>
</DashedPanel>
</AnimateIn>
</Section>
);
}

View File

@ -0,0 +1,131 @@
import { cn } from "../../lib/utils";
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { DashedPanel } from "../ui/dashed-panel";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { AnimateIn } from "../ui/animate-in";
const comparisons = [
{
aspect: "Data ownership",
legacy: "Stored on their servers",
orama: "Encrypted on distributed nodes you choose",
},
{
aspect: "Vendor lock-in",
legacy: "Deep. Migration is painful.",
orama: "None. Fully open source. Self-hostable.",
},
{
aspect: "Billing",
legacy: "Complex. Surprise charges.",
orama: "Free tier + Pay in $ORAMA. Pay in crypto.",
},
{
aspect: "Privacy",
legacy: "They can read your data.",
orama: "E2E encrypted. Orama Proxy routing.",
},
{
aspect: "Setup time",
legacy: "Hours — VPCs, IAM, YAML configs",
orama: "Minutes. One SDK. One CLI command.",
},
{
aspect: "Authentication",
legacy: "Email, password, OAuth, IAM roles",
orama: "Wallet-based. No passwords. No PII.",
},
];
export function DevComparison() {
return (
<>
<Section>
<AnimateIn>
<div className="flex flex-col gap-8">
<SectionHeader
title="What changes when you leave the cloud."
subtitle="Traditional clouds rent you infrastructure you never own. Orama gives you the same capabilities on infrastructure owned by the community."
/>
{/* Desktop table layout */}
<DashedPanel className="hidden sm:block" withCorners>
{/* Header row */}
<div className="grid grid-cols-[1fr_1fr_1fr] border-b border-dashed border-border">
<div className="p-4 sm:p-5" />
<div className="p-4 sm:p-5 border-l border-dashed border-border">
<span className="font-mono text-xs tracking-wider uppercase text-muted">
Traditional Cloud
</span>
</div>
<div className="p-4 sm:p-5 border-l border-dashed border-border">
<span className="font-mono text-xs tracking-wider uppercase text-accent">
Orama Network
</span>
</div>
</div>
{/* Data rows */}
{comparisons.map((row, i) => (
<div
key={row.aspect}
className={cn(
"grid grid-cols-[1fr_1fr_1fr]",
i < comparisons.length - 1 &&
"border-b border-dashed border-border",
)}
>
<div className="p-4 sm:p-5">
<span className="font-display font-semibold text-fg text-sm">
{row.aspect}
</span>
</div>
<div className="p-4 sm:p-5 border-l border-dashed border-border">
<span className="text-sm text-muted line-through decoration-muted/40">
{row.legacy}
</span>
</div>
<div className="p-4 sm:p-5 border-l border-dashed border-border">
<span className="text-sm text-fg">{row.orama}</span>
</div>
</div>
))}
</DashedPanel>
{/* Mobile card layout */}
<div className="flex flex-col gap-4 sm:hidden">
{comparisons.map((row) => (
<DashedPanel key={row.aspect} className="p-4">
<span className="font-display font-semibold text-fg text-sm block mb-3">
{row.aspect}
</span>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-0.5">
<span className="font-mono text-[10px] tracking-wider uppercase text-muted/60">
Traditional
</span>
<span className="text-sm text-muted line-through decoration-muted/40">
{row.legacy}
</span>
</div>
<div className="flex flex-col gap-0.5">
<span className="font-mono text-[10px] tracking-wider uppercase text-accent">
Orama
</span>
<span className="text-sm text-fg">{row.orama}</span>
</div>
</div>
</DashedPanel>
))}
</div>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,133 @@
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { Terminal } from "../ui/terminal";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
import { AnimateIn } from "../ui/animate-in";
import {
ReactLogo,
NextjsLogo,
GoLogo,
NodejsLogo,
WasmLogo,
} from "../icons/tech-logos";
import type { TerminalLine } from "../ui/terminal";
interface DeployTab {
id: string;
label: string;
lines: TerminalLine[];
}
const deployTabs: DeployTab[] = [
{
id: "static",
label: "React / Static",
lines: [
{ prefix: "$", text: "orama deploy static ./dist --name my-app" },
{ prefix: "\u2192", text: "Uploading to IPFS... done" },
{ prefix: "\u2192", text: "Pinned to 3 nodes" },
{ prefix: "\u2713", text: "Live at https://my-app.orama.network" },
],
},
{
id: "nextjs",
label: "Next.js SSR",
lines: [
{ prefix: "$", text: "orama deploy nextjs . --name my-next --ssr" },
{ prefix: "\u2192", text: "Building standalone output..." },
{ prefix: "\u2192", text: "Deploying to 3 nodes" },
{ prefix: "\u2713", text: "SSR running at https://my-next.orama.network" },
],
},
{
id: "go",
label: "Go API",
lines: [
{ prefix: "$", text: "orama deploy go ./cmd/api --name my-api" },
{ prefix: "\u2192", text: "Cross-compiling linux/amd64..." },
{ prefix: "\u2192", text: "Health check /health verified" },
{ prefix: "\u2713", text: "API live at https://api.orama.network" },
],
},
{
id: "node",
label: "Node.js",
lines: [
{ prefix: "$", text: "orama deploy nodejs . --name my-server" },
{ prefix: "\u2192", text: "Detecting start command..." },
{ prefix: "\u2192", text: "Deploying to 3 nodes" },
{ prefix: "\u2713", text: "Server running at https://srv.orama.network" },
],
},
{
id: "wasm",
label: "WASM Function",
lines: [
{ prefix: "$", text: "orama function deploy --name resize" },
{ prefix: "\u2192", text: "Compiled to WebAssembly" },
{ prefix: "\u2192", text: "Deployed network-wide" },
{ prefix: "\u2713", text: "Invoke via SDK or HTTP trigger" },
],
},
];
export function DevDeploy() {
return (
<>
<Section id="deploy">
<AnimateIn>
<div className="flex flex-col gap-8">
<SectionHeader title="Deploy anything. One command." />
<div className="flex flex-wrap items-center gap-8">
{[
{ Logo: ReactLogo, name: "React" },
{ Logo: NextjsLogo, name: "Next.js" },
{ Logo: GoLogo, name: "Go" },
{ Logo: NodejsLogo, name: "Node.js" },
{ Logo: WasmLogo, name: "WASM" },
].map(({ Logo, name }) => (
<div key={name} className="flex flex-col items-center gap-2">
<Logo className="w-8 h-8 text-muted" />
<span className="text-xs font-mono text-muted">{name}</span>
</div>
))}
</div>
<p className="text-muted text-base leading-relaxed max-w-2xl">
Static sites, Next.js with SSR, Go APIs, Node.js servers, WASM
functions. Deploy from your terminal. No infrastructure to manage. No
YAML to write.
</p>
<Tabs defaultValue="static">
<TabsList className="flex-wrap">
{deployTabs.map((tab) => (
<TabsTrigger key={tab.id} value={tab.id}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{deployTabs.map((tab) => (
<TabsContent key={tab.id} value={tab.id}>
<Terminal lines={tab.lines} />
</TabsContent>
))}
</Tabs>
<p className="text-sm text-muted font-mono">
Every deploy lands on distributed nodes. Automatic TLS. Health
checks. Custom domains. Zero downtime.
</p>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,70 @@
import { Globe, Lock, Link2 } from "lucide-react";
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { Terminal } from "../ui/terminal";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { AnimateIn } from "../ui/animate-in";
export function DevDns() {
return (
<>
<Section>
<AnimateIn>
<div className="flex flex-col gap-8">
<SectionHeader
title="Your domain. Zero configuration."
subtitle="Point your nameservers to Orama and every deployment gets a domain automatically. No Cloudflare. No DNS propagation headaches."
/>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12">
{/* Left — features */}
<div className="flex flex-col gap-6">
<div className="flex items-start gap-3">
<Globe className="w-5 h-5 text-accent shrink-0 mt-0.5" />
<div>
<p className="font-display font-semibold text-fg text-sm">Orama Nameservers</p>
<p className="text-sm text-muted">
Point to ns1.orama.network and ns2.orama.network. Your app gets a subdomain instantly.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Lock className="w-5 h-5 text-accent shrink-0 mt-0.5" />
<div>
<p className="font-display font-semibold text-fg text-sm">Automatic TLS</p>
<p className="text-sm text-muted">
Every domain gets a valid certificate. No manual cert management. No renewal headaches.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Link2 className="w-5 h-5 text-accent shrink-0 mt-0.5" />
<div>
<p className="font-display font-semibold text-fg text-sm">Custom Domains</p>
<p className="text-sm text-muted">
Add any domain via CLI or SDK. CNAME to your Orama app and it just works.
</p>
</div>
</div>
</div>
{/* Right — terminal */}
<Terminal
lines={[
{ prefix: "$", text: "orama domain add myapp.com" },
{ prefix: "\u2192", text: "CNAME myapp.com \u2192 my-app.orama.network" },
{ prefix: "\u2192", text: "TLS certificate provisioned" },
{ prefix: "\u2713", text: "Domain active \u2014 https://myapp.com" },
]}
/>
</div>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,107 @@
import {
Database,
HardDrive,
Cpu,
Archive,
Radio,
Video,
Globe,
Lock,
} from "lucide-react";
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { FeatureCard } from "../ui/feature-card";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { AnimateIn } from "../ui/animate-in";
const features = [
{
icon: <Database className="w-5 h-5" />,
title: "Network SQL",
subtitle: "replaces RDS / Aurora",
description:
"Distributed SQL with Raft consensus. ACID transactions. Automatic failover across nodes.",
},
{
icon: <HardDrive className="w-5 h-5" />,
title: "Network Cache",
subtitle: "replaces ElastiCache",
description:
"In-memory key-value store with TTL, replication, and namespace isolation via Olric.",
},
{
icon: <Cpu className="w-5 h-5" />,
title: "Network Functions",
subtitle: "replaces Lambda",
description:
"Serverless WebAssembly. Write in Go, compile to WASM, deploy network-wide.",
},
{
icon: <Archive className="w-5 h-5" />,
title: "Network Storage",
subtitle: "replaces S3 / R2",
description:
"Content-addressed storage on IPFS. Upload, pin, and retrieve from any node.",
},
{
icon: <Radio className="w-5 h-5" />,
title: "Network PubSub",
subtitle: "replaces SNS / SQS",
description:
"Real-time topic-based messaging with WebSocket native support.",
},
{
icon: <Video className="w-5 h-5" />,
title: "Network WebRTC",
subtitle: "replaces Twilio / Daily",
description:
"SFU + TURN servers for video, audio, and data channels.",
},
{
icon: <Globe className="w-5 h-5" />,
title: "Network DNS",
subtitle: "replaces Route53",
description:
"CoreDNS distributed across the mesh. Custom domains built in.",
},
{
icon: <Lock className="w-5 h-5" />,
title: "Network Vault",
subtitle: "replaces Secrets Manager",
description:
"Shamir's Secret Sharing. Secrets split across nodes. No single point of compromise.",
},
];
export function DevFeatures() {
return (
<>
<Section>
<AnimateIn>
<div className="flex flex-col gap-8">
<SectionHeader
title="A complete cloud. Zero infrastructure."
subtitle="No databases to provision. No cache to configure. Import the SDK and you have everything."
/>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-0">
{features.map((feature) => (
<FeatureCard
key={feature.title}
icon={feature.icon}
title={feature.title}
subtitle={feature.subtitle}
description={feature.description}
/>
))}
</div>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,108 @@
import { Link } from "react-router";
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { SyntaxCodeBlock } from "../ui/syntax-code-block";
import { Badge } from "../ui/badge";
import { DashedPanel } from "../ui/dashed-panel";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { Button } from "../ui/button";
import { ExternalLink } from "lucide-react";
import { AnimateIn } from "../ui/animate-in";
const sdkCode = `import { OramaClient } from '@debros/network-ts-sdk'
const client = new OramaClient({
gateway: 'https://orama-testnet.network',
apiKey: process.env.ORAMA_API_KEY
})
// SQL Database (like MySQL, powered by RQLite)
const users = await client.db.query(
'SELECT * FROM users WHERE active = true'
)
// Key-Value Cache (like Redis, powered by Olric)
await client.kv.set('session:abc', { theme: 'dark' }, { ttl: 3600 })
// Real-Time Messaging
client.pubsub.subscribe('chat:lobby', (msg) => {
console.log(msg)
})
// File Storage (IPFS)
const cid = await client.storage.upload(myFile)
// Serverless Functions (WASM)
await client.functions.invoke('resize-image', { cid, width: 800 })`;
const setupSteps = [
"Connect with Root Wallet",
"Get your API key from the dashboard",
"Import the SDK and start building",
];
export function DevQuickstart() {
return (
<>
<Section>
<AnimateIn>
<div className="flex flex-col gap-8">
<SectionHeader
title="One SDK. Every service."
subtitle="Login with your wallet. Get your API key. That's your entire setup. No databases to provision, no servers to configure."
/>
{/* Setup steps */}
<DashedPanel withBackground className="max-w-2xl mx-auto w-full">
<div className="flex flex-col gap-3 p-4 sm:p-6">
{setupSteps.map((step, i) => (
<div key={step} className="flex items-center gap-3">
<Badge variant="outline" className="shrink-0 font-mono">
{i + 1}
</Badge>
<p className="text-sm text-fg">{step}</p>
</div>
))}
</div>
</DashedPanel>
{/* Code block */}
<SyntaxCodeBlock code={sdkCode} label="FIG.02 — ORAMACLIENT USAGE" />
{/* Explainer */}
<p className="text-sm text-muted max-w-2xl mx-auto text-center">
RQLite is our distributed SQL database like MySQL but with Raft consensus and automatic failover.
Olric is our distributed cache like Redis but built into every node. They're already running on the network. You just use them.
</p>
{/* Links */}
<div className="flex flex-wrap items-center justify-center gap-4">
<p className="text-sm text-muted">
Works with React, Next.js, Node.js, and any JavaScript runtime.
</p>
<div className="flex items-center gap-3">
<Button asChild variant="link" size="sm">
<Link to="/docs/developer/sdk">SDK Docs</Link>
</Button>
<Button asChild variant="link" size="sm">
<a
href="https://github.com/DeBrosOfficial/network-ts-sdk"
target="_blank"
rel="noopener noreferrer"
>
GitHub
<ExternalLink className="w-3 h-3 ml-1" />
</a>
</Button>
</div>
</div>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,127 @@
import { Link } from "react-router";
import { BookOpen, ArrowRight, FileText, Terminal, Code } from "lucide-react";
import type { Persona } from "../../types/persona";
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { AnimateIn } from "../ui/animate-in";
interface DocLink {
title: string;
description: string;
href: string;
icon: React.ReactNode;
}
const docsContent: Record<Persona, { subtitle: string; links: DocLink[] }> = {
developer: {
subtitle: "Guides and references to help you deploy, manage, and scale on Orama.",
links: [
{
title: "Getting Started",
description: "Deploy your first app in under a minute",
href: "/docs",
icon: <BookOpen className="w-4 h-4" />,
},
{
title: "CLI Reference",
description: "Full command reference for the Orama CLI",
href: "/docs",
icon: <Terminal className="w-4 h-4" />,
},
{
title: "SDK & APIs",
description: "Integrate Orama services into your application",
href: "/docs",
icon: <Code className="w-4 h-4" />,
},
],
},
operator: {
subtitle: "Everything you need to set up, configure, and maintain your Orama node.",
links: [
{
title: "Operator Setup Guide",
description: "Install prerequisites and connect your node",
href: "/docs/operator/getting-started",
icon: <Terminal className="w-4 h-4" />,
},
{
title: "Node Configuration",
description: "Hardware requirements, ports, and environment setup",
href: "/docs/operator/getting-started",
icon: <FileText className="w-4 h-4" />,
},
{
title: "Monitoring & Rewards",
description: "Track uptime, performance, and earnings",
href: "/docs/operator/getting-started",
icon: <BookOpen className="w-4 h-4" />,
},
],
},
contributor: {
subtitle: "Architecture docs, contribution guidelines, and the full technical stack.",
links: [
{
title: "Architecture Overview",
description: "How the gateway, services, and mesh work together",
href: "/docs",
icon: <BookOpen className="w-4 h-4" />,
},
{
title: "Contributing Guide",
description: "Setup your dev environment and submit your first PR",
href: "/docs",
icon: <FileText className="w-4 h-4" />,
},
{
title: "API Reference",
description: "Internal APIs, endpoints, and data models",
href: "/docs",
icon: <Code className="w-4 h-4" />,
},
],
},
};
export function DocsSection({ persona }: { persona: Persona }) {
const content = docsContent[persona];
return (
<>
<Section>
<AnimateIn>
<div className="flex flex-col gap-6">
<SectionHeader title="Documentation" subtitle={content.subtitle} />
<div className="grid grid-cols-1 md:grid-cols-3 gap-0">
{content.links.map((doc) => (
<Link
key={doc.title}
to={doc.href}
className="group border border-dashed border-border p-5 flex items-start gap-4 transition-all duration-200 hover:bg-surface-2/50 hover:border-border/80"
>
<div className="text-muted group-hover:text-accent transition-colors shrink-0 mt-0.5">
{doc.icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="font-display font-semibold text-fg text-sm">{doc.title}</h3>
<ArrowRight className="w-3 h-3 text-muted opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all" />
</div>
<p className="text-xs text-muted mt-1 leading-relaxed">{doc.description}</p>
</div>
</Link>
))}
</div>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,81 @@
import { useState } from "react";
import { Section } from "../layout/section";
import { DashedPanel } from "../ui/dashed-panel";
import { Button } from "../ui/button";
import { AnimateIn } from "../ui/animate-in";
import { ArrowRight } from "lucide-react";
export function EmailCapture({
heading = "Get early access & updates.",
description = "Be the first to know when public node onboarding, the token launch, and mainnet go live.",
}: {
heading?: string;
description?: string;
}) {
const [email, setEmail] = useState("");
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState("");
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
if (!email || !email.includes("@") || !email.includes(".")) {
setError("Please enter a valid email address.");
return;
}
// TODO: Replace with actual API endpoint
console.log("Email captured:", email);
setSubmitted(true);
}
return (
<Section>
<AnimateIn>
<DashedPanel withCorners withBackground>
<div className="flex flex-col items-center text-center gap-5 py-6">
<h2 className="font-display font-bold text-2xl lg:text-3xl text-fg">
{heading}
</h2>
<p className="text-muted max-w-lg leading-relaxed">
{description}
</p>
{submitted ? (
<div className="flex items-center gap-2 text-accent-2 font-mono text-sm tracking-wider uppercase">
<span className="w-2 h-2 rounded-full bg-accent-2 animate-pulse-dot" />
You're on the list
</div>
) : (
<form
onSubmit={handleSubmit}
className="flex flex-col sm:flex-row items-center gap-3 w-full max-w-md"
>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
className="w-full flex-1 px-4 py-3 bg-surface-2 border border-border text-fg text-sm font-mono placeholder:text-muted/50 focus:outline-none focus:border-accent/50 transition-colors rounded-sm"
/>
<Button type="submit" size="default" className="shrink-0 w-full sm:w-auto">
Subscribe
<ArrowRight className="w-3.5 h-3.5 ml-2" />
</Button>
</form>
)}
{error && (
<p className="text-red-400 text-xs font-mono">{error}</p>
)}
<p className="text-muted/50 text-xs font-mono">
No spam. Unsubscribe anytime.
</p>
</div>
</DashedPanel>
</AnimateIn>
</Section>
);
}

View File

@ -0,0 +1,354 @@
import { useRef, useMemo } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { Float } from "@react-three/drei";
import * as THREE from "three";
const PARTICLE_COUNT = 60;
const ORAMA_RING_RADIUS = 1.6;
const PROXY_RING_RADIUS = 1.2;
/* ─── Wireframe vault (dodecahedron) ─── */
function Vault() {
const meshRef = useRef<THREE.Mesh>(null);
const innerRef = useRef<THREE.Mesh>(null);
const pulseRef = useRef<THREE.Mesh>(null);
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
if (meshRef.current) {
meshRef.current.rotation.y = t * 0.15;
meshRef.current.rotation.x = Math.sin(t * 0.1) * 0.1;
}
// Inner core pulses
if (innerRef.current) {
const pulse = 0.4 + 0.15 * Math.sin(t * 1.5);
const mat = innerRef.current.material as THREE.MeshBasicMaterial;
mat.opacity = pulse;
const s = 1 + 0.08 * Math.sin(t * 1.5);
innerRef.current.scale.setScalar(s);
}
// Expanding pulse ring
if (pulseRef.current) {
const cycle = t % 4;
const expanding = cycle < 1.5;
const scale = expanding ? 1 + cycle * 1.5 : 3.25;
pulseRef.current.scale.setScalar(scale);
const mat = pulseRef.current.material as THREE.MeshBasicMaterial;
mat.opacity = expanding ? 0.06 * (1 - cycle / 1.5) : 0;
}
});
return (
<group>
{/* Outer wireframe vault */}
<mesh ref={meshRef}>
<dodecahedronGeometry args={[0.5, 0]} />
<meshBasicMaterial
color="#d4d4d8"
transparent
opacity={0.25}
wireframe
/>
</mesh>
{/* Inner glowing core */}
<mesh ref={innerRef}>
<icosahedronGeometry args={[0.15, 1]} />
<meshBasicMaterial color="#d4d4d8" transparent opacity={0.4} />
</mesh>
{/* Pulse ring */}
<mesh ref={pulseRef} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[0.5, 0.52, 48]} />
<meshBasicMaterial
color="#d4d4d8"
transparent
opacity={0}
side={THREE.DoubleSide}
/>
</mesh>
</group>
);
}
/* ─── Orbital ring made of dots ─── */
function OrbitalRing({
radius,
count,
color,
opacity,
dotSize,
tilt,
}: {
radius: number;
count: number;
color: string;
opacity: number;
dotSize: number;
tilt: [number, number, number];
}) {
const dots = useMemo(() => {
return Array.from({ length: count }, (_, i) => {
const angle = (i / count) * Math.PI * 2;
return [
Math.cos(angle) * radius,
Math.sin(angle) * radius,
] as [number, number];
});
}, [radius, count]);
return (
<group rotation={tilt}>
{dots.map(([x, y], i) => (
<mesh key={i} position={[x, 0, y]}>
<sphereGeometry args={[dotSize, 6, 6]} />
<meshBasicMaterial
color={color}
transparent
opacity={opacity}
/>
</mesh>
))}
</group>
);
}
/* ─── Orbiting token particles ($ORAMA ring) ─── */
function OramaOrbit() {
const groupRef = useRef<THREE.Group>(null);
const meshRefs = useRef<(THREE.Mesh | null)[]>([]);
const count = 10;
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
meshRefs.current.forEach((mesh, i) => {
if (!mesh) return;
const angle = (i / count) * Math.PI * 2 + t * 0.3;
mesh.position.x = Math.cos(angle) * ORAMA_RING_RADIUS;
mesh.position.z = Math.sin(angle) * ORAMA_RING_RADIUS;
mesh.position.y = Math.sin(t * 2 + i * 0.8) * 0.05;
const mat = mesh.material as THREE.MeshBasicMaterial;
const pulse = Math.sin(t * 1.5 - i * 0.6);
mat.opacity = 0.3 + 0.4 * Math.max(0, pulse);
const s = 1 + 0.3 * Math.max(0, pulse);
mesh.scale.setScalar(s);
});
});
return (
<group ref={groupRef}>
{Array.from({ length: count }, (_, i) => (
<mesh
key={i}
ref={(el) => {
meshRefs.current[i] = el;
}}
>
<sphereGeometry args={[0.025, 10, 10]} />
<meshBasicMaterial color="#d4d4d8" transparent opacity={0.4} />
</mesh>
))}
</group>
);
}
/* ─── Orbiting token particles (Proxy ring — teal) ─── */
function ProxyOrbit() {
const groupRef = useRef<THREE.Group>(null);
const meshRefs = useRef<(THREE.Mesh | null)[]>([]);
const count = 7;
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
meshRefs.current.forEach((mesh, i) => {
if (!mesh) return;
// Counter-rotate for visual contrast
const angle = (i / count) * Math.PI * 2 - t * 0.4;
mesh.position.x = Math.cos(angle) * PROXY_RING_RADIUS;
mesh.position.z = Math.sin(angle) * PROXY_RING_RADIUS;
mesh.position.y = Math.sin(t * 2.5 + i * 1.2) * 0.06;
const mat = mesh.material as THREE.MeshBasicMaterial;
const pulse = Math.sin(t * 2 + i * 0.9);
mat.opacity = 0.4 + 0.3 * Math.max(0, pulse);
});
});
return (
<group ref={groupRef}>
{Array.from({ length: count }, (_, i) => (
<mesh
key={i}
ref={(el) => {
meshRefs.current[i] = el;
}}
>
<sphereGeometry args={[0.018, 8, 8]} />
<meshBasicMaterial color="#00d4aa" transparent opacity={0.5} />
</mesh>
))}
</group>
);
}
/* ─── Inflowing capital particles ─── */
function InflowParticles() {
const ref = useRef<THREE.Points>(null);
const { positions, velocities } = useMemo(() => {
const pos = new Float32Array(PARTICLE_COUNT * 3);
const vel = new Float32Array(PARTICLE_COUNT * 3);
for (let i = 0; i < PARTICLE_COUNT; i++) {
// Start at random positions on a large sphere
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const r = 3 + Math.random() * 2;
pos[i * 3] = r * Math.sin(phi) * Math.cos(theta);
pos[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
pos[i * 3 + 2] = r * Math.cos(phi);
// Velocity toward center
vel[i * 3] = -pos[i * 3] * 0.008;
vel[i * 3 + 1] = -pos[i * 3 + 1] * 0.008;
vel[i * 3 + 2] = -pos[i * 3 + 2] * 0.008;
}
return { positions: pos, velocities: vel };
}, []);
useFrame(({ clock }) => {
if (!ref.current) return;
const posAttr = ref.current.geometry.attributes
.position as THREE.BufferAttribute;
const arr = posAttr.array as Float32Array;
const t = clock.getElapsedTime();
for (let i = 0; i < PARTICLE_COUNT; i++) {
const ix = i * 3;
arr[ix] += velocities[ix];
arr[ix + 1] += velocities[ix + 1];
arr[ix + 2] += velocities[ix + 2];
// Reset when close to center
const dist = Math.sqrt(
arr[ix] ** 2 + arr[ix + 1] ** 2 + arr[ix + 2] ** 2,
);
if (dist < 0.3) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const r = 3 + Math.random() * 2;
arr[ix] = r * Math.sin(phi) * Math.cos(theta);
arr[ix + 1] = r * Math.sin(phi) * Math.sin(theta);
arr[ix + 2] = r * Math.cos(phi);
velocities[ix] = -arr[ix] * (0.006 + Math.random() * 0.004);
velocities[ix + 1] = -arr[ix + 1] * (0.006 + Math.random() * 0.004);
velocities[ix + 2] = -arr[ix + 2] * (0.006 + Math.random() * 0.004);
}
// Add slight spiral motion
const spiral = 0.002;
const cx = arr[ix];
const cz = arr[ix + 2];
arr[ix] += -cz * spiral;
arr[ix + 2] += cx * spiral;
}
posAttr.needsUpdate = true;
// Slow overall rotation
ref.current.rotation.y = t * 0.02;
});
return (
<points ref={ref}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
args={[positions, 3]}
/>
</bufferGeometry>
<pointsMaterial
size={0.015}
color="#d4d4d8"
transparent
opacity={0.2}
sizeAttenuation
/>
</points>
);
}
/* ─── Full scene composition ─── */
function GrowthVaultNetwork() {
const groupRef = useRef<THREE.Group>(null);
useFrame(({ clock }) => {
if (groupRef.current) {
groupRef.current.rotation.y = clock.getElapsedTime() * 0.03;
}
});
return (
<Float speed={0.5} rotationIntensity={0.01} floatIntensity={0.04}>
<group ref={groupRef}>
{/* Dot rings for orbit paths */}
<OrbitalRing
radius={ORAMA_RING_RADIUS}
count={80}
color="#a1a1aa"
opacity={0.04}
dotSize={0.004}
tilt={[0, 0, 0]}
/>
<OrbitalRing
radius={PROXY_RING_RADIUS}
count={50}
color="#00d4aa"
opacity={0.03}
dotSize={0.003}
tilt={[0.3, 0, 0.2]}
/>
{/* Vault core */}
<Vault />
{/* Orbiting tokens */}
<OramaOrbit />
<group rotation={[0.3, 0, 0.2]}>
<ProxyOrbit />
</group>
{/* Capital flowing in */}
<InflowParticles />
</group>
</Float>
);
}
export function GrowthVaultScene() {
return (
<div className="w-full h-[550px] -mt-[400px]">
<Canvas
camera={{ position: [0, 2, 3], fov: 40 }}
dpr={[1, 2]}
gl={{
antialias: true,
alpha: true,
toneMapping: THREE.ACESFilmicToneMapping,
toneMappingExposure: 1,
}}
style={{ background: "transparent" }}
>
<ambientLight intensity={0.1} />
<GrowthVaultNetwork />
</Canvas>
</div>
);
}

View File

@ -0,0 +1,204 @@
import { Link } from "react-router";
import type { Persona } from "../../types/persona";
import { Section } from "../layout/section";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { ExternalLink } from "lucide-react";
const heroContent: Record<
Persona,
{
label: string;
titleLine1: string;
titleLine2: string;
description: string;
primaryCta: { text: string; to: string; external?: boolean };
secondaryCta: { text: string; to: string };
comingSoon?: boolean;
badges: string[];
}
> = {
developer: {
label: "",
titleLine1: "Ship faster. Pay less.",
titleLine2: "Own everything.",
description:
"Deploy your React app, API, or database in one command. No AWS console. No YAML. No surprise bills. Just push and ship.",
primaryCta: { text: "Start Building", to: "/dashboard" },
secondaryCta: { text: "See Documentation", to: "/docs" },
badges: ["Free Tier (No Credit Card)", "Zero Telemetry", "Decentralized Infrastructure"],
},
operator: {
label: "",
titleLine1: "Earn by powering",
titleLine2: "the decentralized cloud.",
description:
"Run an Orama node on any VPS. Earn $ORAMA tokens for every request you serve. Join the infrastructure that replaces AWS.",
primaryCta: { text: "Become an Operator", to: "/dashboard" },
secondaryCta: { text: "See Documentation", to: "/docs" },
comingSoon: true,
badges: ["$ORAMA Rewards", "Deploy on Any VPS", "100+ Operators"],
},
contributor: {
label: "",
titleLine1: "Become a Contributor",
titleLine2: "on Orama Network.",
description:
"Help build the decentralized cloud. From the gateway to the CLI, every service is open source and needs sharp engineers.",
primaryCta: {
text: "View on GitHub",
to: "https://github.com/DeBrosOfficial",
external: true,
},
secondaryCta: { text: "See Documentation", to: "/docs" },
badges: ["Open Source", "Go + TypeScript", "Active Community"],
},
};
function HeroCrosshairGrid() {
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
<svg
className="absolute inset-0 w-full h-full opacity-[0.04]"
xmlns="http://www.w3.org/2000/svg"
>
{/* Vertical center line */}
<line
x1="50%" y1="0" x2="50%" y2="100%"
stroke="currentColor"
strokeWidth="1"
strokeDasharray="1000"
className="text-fg animate-draw-line"
style={{ strokeDashoffset: 1000 }}
/>
{/* Horizontal center line */}
<line
x1="0" y1="50%" x2="100%" y2="50%"
stroke="currentColor"
strokeWidth="1"
strokeDasharray="1000"
className="text-fg animate-draw-line"
style={{ animationDelay: "0.3s", strokeDashoffset: 1000 }}
/>
{/* Center crosshair circle */}
<circle
cx="50%" cy="50%" r="40"
fill="none"
stroke="currentColor"
strokeWidth="0.5"
className="text-accent animate-crosshair-fade"
style={{ animationDelay: "0.8s", opacity: 0, animationFillMode: "forwards" }}
/>
{/* Outer ring */}
<circle
cx="50%" cy="50%" r="120"
fill="none"
stroke="currentColor"
strokeWidth="0.5"
strokeDasharray="6 6"
className="text-accent/50 animate-crosshair-fade"
style={{ animationDelay: "1.2s", opacity: 0, animationFillMode: "forwards" }}
/>
</svg>
{/* Corner crosshairs */}
<div className="absolute top-[15%] left-[15%] w-8 h-8 animate-crosshair-fade" style={{ animationDelay: "1.5s", opacity: 0, animationFillMode: "forwards" }}>
<div className="absolute top-1/2 left-0 w-full h-px bg-accent/20" />
<div className="absolute left-1/2 top-0 h-full w-px bg-accent/20" />
</div>
<div className="absolute top-[15%] right-[15%] w-8 h-8 animate-crosshair-fade" style={{ animationDelay: "1.7s", opacity: 0, animationFillMode: "forwards" }}>
<div className="absolute top-1/2 left-0 w-full h-px bg-accent/20" />
<div className="absolute left-1/2 top-0 h-full w-px bg-accent/20" />
</div>
<div className="absolute bottom-[15%] left-[15%] w-8 h-8 animate-crosshair-fade" style={{ animationDelay: "1.9s", opacity: 0, animationFillMode: "forwards" }}>
<div className="absolute top-1/2 left-0 w-full h-px bg-accent/20" />
<div className="absolute left-1/2 top-0 h-full w-px bg-accent/20" />
</div>
<div className="absolute bottom-[15%] right-[15%] w-8 h-8 animate-crosshair-fade" style={{ animationDelay: "2.1s", opacity: 0, animationFillMode: "forwards" }}>
<div className="absolute top-1/2 left-0 w-full h-px bg-accent/20" />
<div className="absolute left-1/2 top-0 h-full w-px bg-accent/20" />
</div>
</div>
);
}
export function LandingHero({ persona }: { persona: Persona }) {
const content = heroContent[persona];
return (
<>
<Section padding="wide">
<div className="relative flex flex-col items-center text-center min-h-[70vh] pt-[12vh] gap-6 max-w-3xl mx-auto">
<HeroCrosshairGrid />
<Badge variant="default" className="w-fit">
{content.label}
</Badge>
{content.comingSoon && (
<Badge variant="status" className="w-fit">
COMING SOON
</Badge>
)}
<h1 className="font-display font-bold text-4xl lg:text-5xl leading-tight text-fg">
{content.titleLine1}
<br />
<span className="text-accent">{content.titleLine2}</span>
</h1>
<div className="flex flex-wrap gap-2 justify-center">
{content.badges.map((badge) => (
<Badge key={badge} variant="outline">
{badge}
</Badge>
))}
</div>
<p className="text-muted text-lg leading-relaxed max-w-xl">
{content.description}
</p>
<div className="flex flex-wrap items-center gap-3 justify-center pt-2">
{content.primaryCta.external ? (
<Button asChild size="lg">
<a
href={content.primaryCta.to}
target="_blank"
rel="noopener noreferrer"
>
{content.primaryCta.text}
<ExternalLink className="w-3.5 h-3.5 ml-2" />
</a>
</Button>
) : (
<Button asChild size="lg">
<Link to={content.primaryCta.to}>
{content.primaryCta.text}
</Link>
</Button>
)}
{content.secondaryCta.to.startsWith("#") ? (
<Button asChild variant="ghost" size="lg">
<a href={content.secondaryCta.to}>
{content.secondaryCta.text}
</a>
</Button>
) : (
<Button asChild variant="ghost" size="lg">
<Link to={content.secondaryCta.to}>
{content.secondaryCta.text}
</Link>
</Button>
)}
</div>
</div>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,114 @@
import { useState } from "react";
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { DashedPanel } from "../ui/dashed-panel";
import { Button } from "../ui/button";
import { AnimateIn } from "../ui/animate-in";
import { ArrowRight } from "lucide-react";
const WEB3FORMS_ACCESS_KEY = "YOUR_ACCESS_KEY_HERE";
const inputClass =
"w-full px-4 py-3 bg-surface-2 border border-border text-fg text-sm font-mono placeholder:text-muted/50 focus:outline-none focus:border-accent/50 transition-colors rounded-sm";
export function InvestorForm() {
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError("");
setLoading(true);
const formData = new FormData(e.currentTarget);
formData.append("access_key", WEB3FORMS_ACCESS_KEY);
formData.append("subject", "New Investor Inquiry — Orama Network");
formData.append("from_name", "Orama Network Website");
try {
const res = await fetch("https://api.web3forms.com/submit", {
method: "POST",
body: formData,
});
const data = await res.json();
if (data.success) {
setSubmitted(true);
} else {
setError("Something went wrong. Please try again.");
}
} catch {
setError("Network error. Please try again.");
} finally {
setLoading(false);
}
}
return (
<Section>
<AnimateIn>
<SectionHeader
title="Get in Touch"
subtitle="Interested in backing Orama Network? We'd love to hear from you."
/>
<div className="mt-8">
<DashedPanel withCorners withBackground>
{submitted ? (
<div className="flex flex-col items-center text-center gap-4 py-8">
<div className="flex items-center gap-2 text-accent-2 font-mono text-sm tracking-wider uppercase">
<span className="w-2 h-2 rounded-full bg-accent-2 animate-pulse-dot" />
Message Received
</div>
<p className="text-muted max-w-md">
Thanks for reaching out. We'll get back to you shortly.
</p>
</div>
) : (
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<input type="hidden" name="to" value="dev@debros.io" />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<input
type="text"
name="name"
placeholder="Name *"
required
className={inputClass}
/>
<input
type="email"
name="email"
placeholder="Email *"
required
className={inputClass}
/>
</div>
<input
type="text"
name="role"
placeholder="Role (e.g. VC Partner, Angel, Builder)"
className={inputClass}
/>
<textarea
name="message"
placeholder="Tell us about your interest in Orama Network..."
rows={4}
className={`${inputClass} resize-none`}
/>
{error && (
<p className="text-red-400 text-xs font-mono">{error}</p>
)}
<Button type="submit" size="lg" className="w-full sm:w-auto self-end" disabled={loading}>
{loading ? "Sending..." : "Send Message"}
{!loading && <ArrowRight className="w-3.5 h-3.5 ml-2" />}
</Button>
</form>
)}
</DashedPanel>
</div>
</AnimateIn>
</Section>
);
}

View File

@ -0,0 +1,303 @@
import { useRef, useMemo, useState, useEffect } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { Float } from "@react-three/drei";
import * as THREE from "three";
/*
Step 0/1: Nodes appear and connect
Step 1/2: Deploy packets flow through the network
Step 2/3: User device connects, app loads
*/
interface NodeData {
position: THREE.Vector3;
baseScale: number;
phase: number;
}
interface EdgeData {
from: number;
to: number;
}
/* Generate deterministic node positions on a sphere */
function generateNodes(count: number, radius: number): NodeData[] {
const nodes: NodeData[] = [];
const goldenRatio = (1 + Math.sqrt(5)) / 2;
for (let i = 0; i < count; i++) {
const theta = (2 * Math.PI * i) / goldenRatio;
const phi = Math.acos(1 - (2 * (i + 0.5)) / count);
const x = radius * Math.sin(phi) * Math.cos(theta);
const y = radius * Math.sin(phi) * Math.sin(theta);
const z = radius * Math.cos(phi);
nodes.push({
position: new THREE.Vector3(x, y, z),
baseScale: 0.03 + Math.random() * 0.03,
phase: Math.random() * Math.PI * 2,
});
}
return nodes;
}
/* Generate edges between nearby nodes */
function generateEdges(nodes: NodeData[], maxDist: number): EdgeData[] {
const edges: EdgeData[] = [];
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
if (nodes[i].position.distanceTo(nodes[j].position) < maxDist) {
edges.push({ from: i, to: j });
}
}
}
return edges;
}
const NODE_COUNT = 40;
const SPHERE_RADIUS = 2.8;
const EDGE_MAX_DIST = 2.2;
/* ─── Glowing Node ─── */
function GlowNode({
node,
active,
highlight,
}: {
node: NodeData;
active: boolean;
highlight: boolean;
}) {
const meshRef = useRef<THREE.Mesh>(null);
useFrame(({ clock }) => {
if (!meshRef.current) return;
const t = clock.getElapsedTime();
const pulse = 1 + 0.3 * Math.sin(t * 2 + node.phase);
const targetScale = active ? node.baseScale * pulse : 0;
const current = meshRef.current.scale.x;
const next = THREE.MathUtils.lerp(current, targetScale, 0.05);
meshRef.current.scale.setScalar(next);
});
return (
<mesh ref={meshRef} position={node.position} scale={0}>
<sphereGeometry args={[1, 12, 12]} />
<meshBasicMaterial
color={highlight ? "#00d4aa" : "#4169E1"}
transparent
opacity={0.9}
/>
</mesh>
);
}
/* ─── Network Edges ─── */
function NetworkEdges({
nodes,
edges,
active,
}: {
nodes: NodeData[];
edges: EdgeData[];
active: boolean;
}) {
const linesRef = useRef<THREE.Group>(null);
const opacityRef = useRef(0);
useFrame(() => {
const target = active ? 0.15 : 0;
opacityRef.current = THREE.MathUtils.lerp(opacityRef.current, target, 0.03);
if (!linesRef.current) return;
linesRef.current.children.forEach((child) => {
const line = child as THREE.Line;
const mat = line.material as THREE.LineBasicMaterial;
mat.opacity = opacityRef.current;
});
});
const lineObjects = useMemo(() => {
return edges.map((edge) => {
const geo = new THREE.BufferGeometry().setFromPoints([
nodes[edge.from].position,
nodes[edge.to].position,
]);
const mat = new THREE.LineBasicMaterial({
color: "#4169E1",
transparent: true,
opacity: 0,
});
return new THREE.Line(geo, mat);
});
}, [nodes, edges]);
return (
<group ref={linesRef}>
{lineObjects.map((lineObj, i) => (
<primitive key={i} object={lineObj} />
))}
</group>
);
}
/* ─── Deploy Packets (Step 2) ─── */
function DeployPackets({
nodes,
edges,
active,
}: {
nodes: NodeData[];
edges: EdgeData[];
active: boolean;
}) {
const groupRef = useRef<THREE.Group>(null);
const packetCount = 12;
// Each packet travels along a random edge
const packetData = useMemo(() => {
return Array.from({ length: packetCount }, (_, i) => {
const edge = edges[i % edges.length];
return {
from: nodes[edge.from].position,
to: nodes[edge.to].position,
speed: 0.3 + Math.random() * 0.4,
offset: Math.random(),
};
});
}, [nodes, edges]);
useFrame(({ clock }) => {
if (!groupRef.current) return;
const t = clock.getElapsedTime();
groupRef.current.children.forEach((child, i) => {
const mesh = child as THREE.Mesh;
const data = packetData[i];
const progress = ((t * data.speed + data.offset) % 1);
mesh.position.lerpVectors(data.from, data.to, progress);
const targetScale = active ? 0.035 : 0;
const current = mesh.scale.x;
mesh.scale.setScalar(THREE.MathUtils.lerp(current, targetScale, 0.05));
});
});
return (
<group ref={groupRef}>
{packetData.map((_, i) => (
<mesh key={i} scale={0}>
<sphereGeometry args={[1, 8, 8]} />
<meshBasicMaterial color="#00d4aa" transparent opacity={0.8} />
</mesh>
))}
</group>
);
}
/* ─── User Device (Step 3) ─── */
function UserDevice({ active }: { active: boolean }) {
const groupRef = useRef<THREE.Group>(null);
const beamRef = useRef<THREE.Mesh>(null);
useFrame(({ clock }) => {
if (!groupRef.current) return;
const targetScale = active ? 1 : 0;
const current = groupRef.current.scale.x;
const next = THREE.MathUtils.lerp(current, targetScale, 0.04);
groupRef.current.scale.setScalar(next);
if (beamRef.current) {
const t = clock.getElapsedTime();
const mat = beamRef.current.material as THREE.MeshBasicMaterial;
mat.opacity = active ? 0.1 + 0.05 * Math.sin(t * 3) : 0;
}
});
return (
<group ref={groupRef} position={[0, -3.5, 1.5]} scale={0}>
<Float speed={2} rotationIntensity={0} floatIntensity={0.3}>
{/* Phone shape */}
<mesh>
<boxGeometry args={[0.35, 0.6, 0.03]} />
<meshBasicMaterial color="#ffffff" transparent opacity={0.15} />
</mesh>
{/* Screen */}
<mesh position={[0, 0, 0.02]}>
<planeGeometry args={[0.28, 0.48]} />
<meshBasicMaterial color="#00d4aa" transparent opacity={0.3} />
</mesh>
</Float>
{/* Connection beam to network */}
<mesh ref={beamRef} position={[0, 1.5, 0]}>
<cylinderGeometry args={[0.005, 0.05, 3, 8]} />
<meshBasicMaterial color="#00d4aa" transparent opacity={0} />
</mesh>
</group>
);
}
/* ─── Scene ─── */
function NetworkScene({ step }: { step: number }) {
const groupRef = useRef<THREE.Group>(null);
const nodes = useMemo(() => generateNodes(NODE_COUNT, SPHERE_RADIUS), []);
const edges = useMemo(() => generateEdges(nodes, EDGE_MAX_DIST), [nodes]);
useFrame(({ clock }) => {
if (!groupRef.current) return;
groupRef.current.rotation.y = clock.getElapsedTime() * 0.05;
});
return (
<group ref={groupRef}>
{/* Edges */}
<NetworkEdges nodes={nodes} edges={edges} active={step >= 0} />
{/* Nodes */}
{nodes.map((node, i) => (
<GlowNode
key={i}
node={node}
active={step >= 0}
highlight={step >= 2 && i % 5 === 0}
/>
))}
{/* Deploy packets (step 2) */}
<DeployPackets nodes={nodes} edges={edges} active={step >= 1} />
{/* User device (step 3) */}
<UserDevice active={step >= 2} />
</group>
);
}
/* ─── Exported Component ─── */
export function NetworkVisualization({ step }: { step: number }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<div className="w-full aspect-square max-w-[500px] mx-auto bg-surface-2/30 rounded-lg" />
);
}
return (
<div className="w-full aspect-square max-w-[500px] mx-auto">
<Canvas
camera={{ position: [0, 0, 7], fov: 50 }}
dpr={[1, 1.5]}
gl={{ antialias: true, alpha: true }}
style={{ background: "transparent" }}
>
<ambientLight intensity={0.5} />
<NetworkScene step={step} />
</Canvas>
</div>
);
}

View File

@ -0,0 +1,62 @@
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { SpecTable } from "../ui/spec-table";
import { DashedPanel } from "../ui/dashed-panel";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { Badge } from "../ui/badge";
import { AnimateIn } from "../ui/animate-in";
const rewardSpec = [
{ label: "Reward token", value: "$ORAMA — uptime, bandwidth, compute" },
{ label: "Privacy relay", value: "Orama Proxy on every node" },
{ label: "Routing", value: "Onion-routed traffic for all requests" },
{ label: "Payout", value: "Continuous, based on contribution metrics" },
];
export function OpsAnyone() {
return (
<>
<Section>
<AnimateIn>
<div className="flex flex-col gap-8">
<SectionHeader title="Privacy Built In with Orama Proxy" />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12">
{/* Left — text */}
<div className="flex flex-col gap-4">
<Badge variant="accent" className="w-fit">Privacy Layer</Badge>
<p className="text-muted leading-relaxed">
Every Orama node runs the Orama Proxy privacy relay. As an operator,
you earn $ORAMA rewards while providing onion-routed privacy for all network traffic.
</p>
<SpecTable rows={rewardSpec} />
</div>
{/* Right — visual */}
<DashedPanel withCorners withBackground>
<div className="flex flex-col items-center justify-center gap-6 p-8">
<div className="w-20 h-20 rounded-full border border-dashed border-border flex items-center justify-center">
<span className="text-3xl font-bold font-mono text-fg">OP</span>
</div>
<div className="flex items-center gap-6">
<div className="text-center">
<div className="text-2xl font-bold text-fg font-display">$ORAMA</div>
<div className="text-xs font-mono text-muted mt-1">Network rewards</div>
</div>
</div>
<div className="w-full border-t border-dashed border-border" />
<p className="text-sm text-muted text-center">
One node. Privacy built in. Earn $ORAMA from privacy infrastructure.
</p>
</div>
</DashedPanel>
</div>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,113 @@
import { Section } from "../layout/section";
import { DashedPanel } from "../ui/dashed-panel";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { AnimateIn } from "../ui/animate-in";
export function OpsOramaOne() {
return (
<>
<Section>
<AnimateIn>
<div className="flex flex-col gap-12">
{/* Header */}
<div className="flex flex-col items-center text-center gap-4">
<Badge variant="status">COMING SOON</Badge>
<h2 className="font-display text-3xl lg:text-4xl font-bold text-fg tracking-tight">
Orama One
</h2>
<p className="text-lg text-accent font-mono tracking-wider">
Plug in. Connect. Earn.
</p>
</div>
{/* Device showcase */}
<DashedPanel withCorners className="w-full overflow-hidden">
<div className="relative flex items-center justify-center py-16 sm:py-24">
{/* Ambient glow */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="w-64 h-64 sm:w-96 sm:h-96 rounded-full bg-accent/[0.06] blur-[80px]" />
</div>
{/* Device silhouette */}
<div className="relative w-72 sm:w-96 h-32 sm:h-40">
{/* Main body */}
<div className="absolute inset-0 bg-bg rounded-xl border border-border/60 shadow-[0_0_60px_rgba(65,105,225,0.1),0_0_120px_rgba(65,105,225,0.05)]" />
{/* Top edge highlight */}
<div className="absolute top-0 left-4 right-4 h-px bg-gradient-to-r from-transparent via-accent/30 to-transparent" />
{/* LED indicators */}
<div className="absolute top-5 left-6 flex gap-3">
<div className="w-2 h-2 rounded-full bg-border/60" />
<div className="w-2 h-2 rounded-full bg-border/60" />
<div className="w-2 h-2 rounded-full bg-accent/50 animate-pulse" />
</div>
{/* Ventilation lines */}
<div className="absolute top-5 right-6 flex flex-col gap-1.5">
<div className="w-8 h-px bg-border/40" />
<div className="w-8 h-px bg-border/40" />
<div className="w-8 h-px bg-border/40" />
</div>
{/* Center wordmark */}
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-sm sm:text-base font-mono text-muted/20 tracking-[0.4em] uppercase">
Orama One
</span>
</div>
{/* Bottom ports */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-4">
<div className="w-6 h-2 rounded-sm border border-border/40" />
<div className="w-6 h-2 rounded-sm border border-border/40" />
<div className="w-3 h-2 rounded-full border border-border/40" />
</div>
{/* Base stand */}
<div className="absolute -bottom-3 left-1/2 -translate-x-1/2 w-48 sm:w-64 h-1 bg-border/20 rounded-full" />
</div>
</div>
</DashedPanel>
{/* Specs */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-4xl mx-auto w-full">
<DashedPanel className="p-4 text-center">
<span className="text-xs font-mono text-muted block mb-1">FORM FACTOR</span>
<span className="text-sm text-fg">Compact. Silent. Always-on.</span>
</DashedPanel>
<DashedPanel className="p-4 text-center">
<span className="text-xs font-mono text-muted block mb-1">CONNECTIVITY</span>
<span className="text-sm text-fg">Ethernet + WiFi + WireGuard</span>
</DashedPanel>
<DashedPanel className="p-4 text-center">
<span className="text-xs font-mono text-muted block mb-1">SETUP</span>
<span className="text-sm text-fg">Plug in and start earning</span>
</DashedPanel>
</div>
{/* Description + CTA */}
<div className="flex flex-col items-center text-center gap-6 max-w-2xl mx-auto">
<p className="text-muted leading-relaxed">
A pre-built hardware node. No VPS. No terminal. No configuration.
Just plug it in, connect to the network, and start earning $ORAMA.
</p>
<Button variant="ghost" size="lg">
Notify Me When Available
</Button>
<p className="text-xs font-mono text-muted">
Expected Q2 2026
</p>
</div>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,86 @@
import { Link } from "react-router";
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { Terminal } from "../ui/terminal";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { AnimateIn } from "../ui/animate-in";
export function OpsSetup() {
return (
<>
<Section id="setup">
<AnimateIn>
<div className="flex flex-col gap-8">
<SectionHeader
title="From zero to earning in 5 minutes."
subtitle="Get your node running with just a VPS and an invite token."
/>
<div className="flex flex-col gap-8 max-w-3xl mx-auto w-full">
{/* Step 1 */}
<div className="flex items-start gap-4">
<Badge variant="outline" className="shrink-0 mt-1 font-mono">1</Badge>
<div>
<h3 className="font-display font-semibold text-fg text-sm mb-1">Get a VPS</h3>
<p className="text-sm text-muted">
Any Linux VPS with 4GB RAM, 2 cores, 40GB disk. Hetzner, DigitalOcean, Vultr any provider works.
</p>
</div>
</div>
{/* Step 2 */}
<div className="flex items-start gap-4">
<Badge variant="outline" className="shrink-0 mt-1 font-mono">2</Badge>
<div className="flex-1">
<h3 className="font-display font-semibold text-fg text-sm mb-2">Install the Orama node</h3>
<Terminal
lines={[
{ prefix: "$", text: "brew install DeBrosOfficial/tap/orama" },
{ prefix: "\u2192", text: "Installing Orama CLI... done" },
{ text: "" },
{ prefix: "$", text: "sudo orama node install --vps-ip <IP> --domain <domain> --token <invite>" },
{ prefix: "\u2192", text: "Downloading services... done" },
{ prefix: "\u2192", text: "Configuring WireGuard mesh..." },
{ prefix: "\u2192", text: "Starting RQLite, Olric, Gateway..." },
{ prefix: "\u2713", text: "Node is live and connected to the mesh" },
]}
/>
</div>
</div>
{/* Step 3 */}
<div className="flex items-start gap-4">
<Badge variant="outline" className="shrink-0 mt-1 font-mono">3</Badge>
<div className="flex-1">
<h3 className="font-display font-semibold text-fg text-sm mb-2">Verify and start earning</h3>
<Terminal
lines={[
{ prefix: "$", text: "orama node status" },
{ prefix: "\u2192", text: "Node: online" },
{ prefix: "\u2192", text: "Cluster: connected (3/3 peers)" },
{ prefix: "\u2713", text: "Rewards: accumulating" },
]}
/>
</div>
</div>
</div>
<div className="flex justify-center">
<Button asChild variant="ghost">
<Link to="/docs/operator/node-setup">
Read the full setup guide &rarr;
</Link>
</Button>
</div>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

View File

@ -0,0 +1,156 @@
import { Link } from "react-router";
import { Coins, Vote, CreditCard, Server, ArrowRight } from "lucide-react";
import { Section } from "../layout/section";
import { SectionHeader } from "../ui/section-header";
import { MetricCard } from "../ui/metric-card";
import { DashedPanel } from "../ui/dashed-panel";
import { CrosshairDivider } from "../ui/crosshair-divider";
import { AnimateIn } from "../ui/animate-in";
import { Button } from "../ui/button";
import { Badge } from "../ui/badge";
const TIER_ACCENT: Record<string, string> = {
Base: "#888",
Enhanced: "#4169E1",
Governor: "#a855f7",
};
const rewardTiers = [
{
icon: <Server className="w-5 h-5" />,
tier: "Base",
stake: "***",
multiplier: "***",
description: "Standard rewards for running a node with minimum stake",
},
{
icon: <Coins className="w-5 h-5" />,
tier: "Enhanced",
stake: "***",
multiplier: "***",
description: "Higher stake unlocks enhanced reward multiplier",
},
{
icon: <Vote className="w-5 h-5" />,
tier: "Governor",
stake: "***",
multiplier: "***",
description: "Top-tier rewards plus governance voting power",
},
];
export function OpsTokenomics() {
return (
<>
<Section id="tokenomics">
<AnimateIn>
<div className="flex flex-col gap-8">
<SectionHeader
title="Reward Structure"
subtitle="Earn $ORAMA for every request you serve. Higher stake, higher rewards."
/>
{/* Key metrics */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 max-w-4xl mx-auto w-full">
<DashedPanel className="p-4">
<MetricCard label="Rewards" value="$ORAMA" />
</DashedPanel>
<DashedPanel className="p-4">
<MetricCard label="Payout" value="Daily" />
</DashedPanel>
<DashedPanel className="p-4">
<MetricCard label="Based On" value="Uptime + Traffic" />
</DashedPanel>
<DashedPanel className="p-4">
<MetricCard label="Operators" value="Unlimited" />
</DashedPanel>
</div>
{/* Reward tiers */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{rewardTiers.map((tier) => {
const accent = TIER_ACCENT[tier.tier] ?? "#888";
return (
<div
key={tier.tier}
className="group relative border border-dashed border-border p-6 flex flex-col gap-5 transition-all duration-300 hover:border-border/80"
style={{ borderLeftColor: accent, borderLeftWidth: 2, borderLeftStyle: "solid" }}
>
{/* Subtle gradient hover overlay */}
<div
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"
style={{
background: `linear-gradient(135deg, ${accent}08 0%, transparent 60%)`,
}}
/>
<div className="relative flex items-center justify-between">
<div className="flex items-center gap-3">
<div style={{ color: accent }}>{tier.icon}</div>
<div className="flex flex-col">
<span className="font-display font-semibold text-fg">
{tier.tier}
</span>
<span className="text-xs text-muted font-mono">Tier</span>
</div>
</div>
<Badge variant="outline">{tier.tier}</Badge>
</div>
{/* Multiplier — large and prominent */}
<div className="relative flex items-baseline gap-2">
<span
className="font-mono text-4xl font-bold tracking-tight"
style={{ color: accent }}
>
<span className="redacted-inline">{tier.multiplier}</span>
</span>
<span className="text-xs text-muted font-mono uppercase tracking-wider">
Multiplier
</span>
</div>
<div className="relative flex items-baseline gap-1">
<span className="font-mono text-lg font-bold text-fg">
<span className="redacted-inline">{tier.stake}</span>
</span>
<span className="text-xs text-muted font-mono">$ORAMA staked</span>
</div>
<p className="relative text-sm text-muted leading-relaxed">
{tier.description}
</p>
</div>
);
})}
</div>
{/* Utility summary + CTA */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 border border-dashed border-border p-5">
<div className="flex flex-wrap gap-6 text-sm text-muted">
<div className="flex items-center gap-2">
<CreditCard className="w-4 h-4 text-accent" />
<span>Pay in BTC or $ORAMA</span>
</div>
<div className="flex items-center gap-2">
<Vote className="w-4 h-4 text-accent" />
<span>Governance voting with stake</span>
</div>
</div>
<Button asChild variant="ghost" size="sm">
<Link to="/token" className="flex items-center gap-1.5">
Full Tokenomics
<ArrowRight className="w-3.5 h-3.5" />
</Link>
</Button>
</div>
</div>
</AnimateIn>
</Section>
<Section padding="none">
<CrosshairDivider />
</Section>
</>
);
}

Some files were not shown because too many files have changed in this diff Show More