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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/api/src/hackproof/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,11 @@
total_commits INTEGER,
stellar_added_in_window BOOLEAN,
scanned_at TIMESTAMPTZ,
declared_track TEXT
declared_track TEXT,
tooling JSONB NOT NULL DEFAULT '{}'
);
ALTER TABLE repos ADD COLUMN IF NOT EXISTS declared_track TEXT;
ALTER TABLE repos ADD COLUMN IF NOT EXISTS tooling JSONB NOT NULL DEFAULT '{}';

CREATE TABLE IF NOT EXISTS repo_contributors (
full_name TEXT NOT NULL,
Expand Down
81 changes: 81 additions & 0 deletions apps/api/src/hackproof/github/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import re
import tomllib
from pathlib import PurePosixPath
from typing import Any

from hackproof.github.models import DependencyFingerprint

Expand All @@ -16,6 +17,29 @@
REQUIREMENT = re.compile(r"^([A-Za-z0-9_.-]+)\s*(?:[=~<>!]=?\s*([\w.*-]+))?")
GO_REQUIRE = re.compile(r"^\s*(?:require\s+)?(\S+)\s+(v[\w.+-]+)", re.MULTILINE)

OPENZEPPELIN_PACKAGES = {
"stellar-contracts",
"openzeppelin-stellar-contracts",
"openzeppelin_stellar_contracts",
"@openzeppelin/contracts",
"@openzeppelin/stellar-contracts",
}

WALLETS_KIT_PACKAGES = {
"@creit.tech/stellar-wallets-kit",
"stellar-wallets-kit",
}

FREIGHTER_PACKAGES = {
"@stellar/freighter-api",
"freighter-api",
}

PASSKEY_PACKAGES = {
"passkey-kit",
"@stellar/passkey-kit",
}


def _cargo(text: str) -> dict[str, str | None]:
data = tomllib.loads(text)
Expand Down Expand Up @@ -82,3 +106,60 @@ def fingerprint(files: dict[str, str], wanted: dict[str, list[str]]) -> Dependen
if ecosystem not in result.ecosystems:
result.ecosystems.append(ecosystem)
return result


def detect_openzeppelin(files: dict[str, str]) -> bool:
for path, text in files.items():
name = PurePosixPath(path).name
if name not in PARSERS:
continue
try:
declared = PARSERS[name](text)
if any(pkg in declared for pkg in OPENZEPPELIN_PACKAGES):
return True
except Exception:
continue
return False


def detect_framework(files: dict[str, str]) -> str:
for path, text in files.items():
if PurePosixPath(path).name == "package.json":
try:
data = json.loads(text)
deps: dict[str, Any] = {}
for section in ("dependencies", "devDependencies"):
deps.update(data.get(section, {}))
if "next" in deps:
return "Next.js"
if "vite" in deps:
return "Vite"
except Exception:
continue
return "Other"


def detect_ai_tooling(paths: list[str]) -> list[str]:
found = set()
for path in paths:
normalized = "/" + path.strip("/").lower()
parts = PurePosixPath(path).parts
name = parts[-1].lower() if parts else ""
if name == "claude.md":
found.add("CLAUDE.md")
elif name == "agents.md":
found.add("AGENTS.md")
elif name == ".cursorrules" or ".cursor/rules" in normalized or "cursorrules" in name:
found.add("Cursor rules")
elif "copilot-instructions" in normalized:
found.add("Copilot instructions")
return sorted(found)


def parse_major_version(spec: str | None) -> str:
if not spec:
return "Unknown"
m = re.search(r"(\d+)", spec)
if m:
return f"v{m.group(1)}"
return "Unknown"
3 changes: 2 additions & 1 deletion apps/api/src/hackproof/github/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Literal
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field

Expand Down Expand Up @@ -60,3 +60,4 @@ class RepoScan(BaseModel):
stellar_added_in_window: bool | None
integrations: list[Detection]
contributors: list[Contributor]
tooling: dict[str, Any] = Field(default_factory=dict)
18 changes: 16 additions & 2 deletions apps/api/src/hackproof/github/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@

from hackproof.github.client import GithubClient
from hackproof.github.demo import scan_demo
from hackproof.github.dependencies import MANIFESTS, fingerprint
from hackproof.github.dependencies import (
MANIFESTS,
detect_ai_tooling,
detect_framework,
detect_openzeppelin,
fingerprint,
)
from hackproof.github.files import fetch_texts
from hackproof.github.history import stellar_added_in_window
from hackproof.github.models import Candidate, Contributor, Ref, RepoScan
Expand Down Expand Up @@ -123,6 +129,12 @@ async def scan(self, repo: Candidate) -> RepoScan | None:
if dependencies.packages
else None
)
all_paths = [entry.get("path", "") for entry in tree.get("tree", [])]
tooling = {
"framework": detect_framework(files),
"ai": detect_ai_tooling(all_paths),
"openzeppelin": detect_openzeppelin(files),
}
return RepoScan(
full_name=repo.full_name,
declared_track=declared_track(files),
Expand All @@ -131,6 +143,7 @@ async def scan(self, repo: Candidate) -> RepoScan | None:
stellar_added_in_window=added,
integrations=detect(files, self._catalog, repo.owner),
contributors=contributors_from(people),
tooling=tooling,
)


Expand Down Expand Up @@ -178,11 +191,12 @@ async def store_scan(connection: asyncpg.Connection, scan: RepoScan) -> None:
await connection.execute(
"""
UPDATE repos SET dependencies = $2, stellar_added_in_window = $3,
declared_track = $4, scanned_at = now()
declared_track = $4, tooling = $5, scanned_at = now()
WHERE full_name = $1
""",
scan.full_name,
scan.dependencies.model_dump(),
scan.stellar_added_in_window,
scan.declared_track,
scan.tooling,
)
12 changes: 12 additions & 0 deletions apps/api/src/hackproof/hackathon.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Hackathon(BaseModel):
window: Window
awards: list[Award] = []
discovery: Discovery
hidden: list[str] = []


@lru_cache
Expand All @@ -46,3 +47,14 @@ def get_hackathon() -> Hackathon:

def places() -> dict[str, Place]:
return {award.project_id: award.place for award in get_hackathon().awards}


def hidden_ids() -> list[str]:
raw = get_hackathon().hidden
ids = set(raw)
for item in raw:
if item.startswith("repo:"):
ids.add(item[5:])
elif not item.startswith("acct:"):
ids.add(f"repo:{item}")
return sorted(ids)
161 changes: 160 additions & 1 deletion apps/api/src/hackproof/queries.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from collections import Counter
from datetime import datetime
from typing import Any, Literal

from hackproof.db import pool
from hackproof.hackathon import get_hackathon, places
from hackproof.hackathon import get_hackathon, hidden_ids, places
from hackproof.integrations.catalog import get_catalog
from hackproof.linking.methods import METHOD_RANK_SQL

Expand Down Expand Up @@ -256,3 +257,161 @@ async def fetch_integrations() -> list[dict[str, Any]]:
async def fetch_countries(limit: int = 12) -> list[dict[str, Any]]:
async with pool().acquire() as connection:
return [dict(row) for row in await connection.fetch(COUNTRIES_SQL, limit)]

TOOLING_SQL = """
SELECT p.project_id, p.full_name, r.dependencies, COALESCE(r.tooling, '{}'::jsonb) AS tooling,
ARRAY(SELECT i.partner FROM repo_integrations i
WHERE i.full_name = p.full_name) AS integrations
FROM projects p
JOIN repos r ON r.full_name = p.full_name
WHERE p.tier IN ('verified', 'likely')
AND ($1::text[] IS NULL OR NOT (p.project_id = ANY($1) OR p.full_name = ANY($1)))
"""


async def fetch_tooling() -> dict[str, Any]:
from hackproof.github.dependencies import parse_major_version

hidden = hidden_ids() or None
async with pool().acquire() as connection:
rows = await connection.fetch(TOOLING_SQL, hidden)

total = len(rows)

oz_count = 0
wallets_count = 0
freighter_count = 0
passkey_count = 0

soroban_versions: Counter[str] = Counter()
stellar_versions: Counter[str] = Counter()
framework_counts: Counter[str] = Counter()
ai_counts: Counter[str] = Counter()

for row in rows:
deps = row["dependencies"] or {}
if isinstance(deps, str):
import json
deps = json.loads(deps)
packages = deps.get("packages", {}) or {}
tooling = row["tooling"] or {}
if isinstance(tooling, str):
import json
tooling = json.loads(tooling)
integrations = set(row["integrations"] or [])

# 1. OpenZeppelin Stellar Contracts
has_oz = (
tooling.get("openzeppelin")
or any(
p in packages
for p in (
"stellar-contracts",
"openzeppelin-stellar-contracts",
"openzeppelin_stellar_contracts",
"@openzeppelin/contracts",
"@openzeppelin/stellar-contracts",
)
)
or "openzeppelin" in integrations
)
if has_oz:
oz_count += 1

# 2. Stellar Wallets Kit
has_wallets = (
"@creit.tech/stellar-wallets-kit" in packages
or "stellar-wallets-kit" in packages
or "stellar-wallets-kit" in integrations
)
if has_wallets:
wallets_count += 1

# 3. Freighter API
has_freighter = (
"@stellar/freighter-api" in packages
or "freighter" in integrations
or "freighter-api" in packages
)
if has_freighter:
freighter_count += 1

# 4. Passkey Kit
has_passkey = (
"passkey-kit" in packages
or "@stellar/passkey-kit" in packages
or "passkey" in integrations
)
if has_passkey:
passkey_count += 1

# 5. soroban-sdk major versions
if "soroban-sdk" in packages:
ver = parse_major_version(packages["soroban-sdk"])
soroban_versions[ver] += 1

# 6. stellar-sdk JS versions
sdk_spec = packages.get("@stellar/stellar-sdk") or packages.get("stellar-sdk")
if sdk_spec:
ver = parse_major_version(sdk_spec)
stellar_versions[ver] += 1

# 7. Frameworks
fw = tooling.get("framework")
if not fw or fw == "Other":
if "next" in packages:
fw = "Next.js"
elif "vite" in packages:
fw = "Vite"
else:
fw = "Other"
framework_counts[fw] += 1

# 8. AI tooling
for tool in tooling.get("ai", []):
ai_counts[tool] += 1

def make_items(counter: dict[str, int]) -> list[dict[str, Any]]:
items = []
for name, count in counter.items():
pct = round((count / total * 100), 1) if total > 0 else 0.0
items.append({"name": name, "count": count, "percentage": pct})
return sorted(items, key=lambda x: x["count"], reverse=True)

kits_items = make_items({
"Stellar Wallets Kit": wallets_count,
"Freighter API": freighter_count,
"OpenZeppelin Contracts": oz_count,
"Passkey Kit": passkey_count,
})

return {
"total_projects": total,
"categories": [
{
"category": "Ecosystem Kits & Wallets",
"description": "Adoption of official and partner Stellar libraries and kits",
"items": kits_items,
},
{
"category": "Soroban SDK Major Versions",
"description": "Declared soroban-sdk version in Cargo.toml contracts",
"items": make_items(dict(soroban_versions)),
},
{
"category": "Stellar SDK (JS) Versions",
"description": "Declared @stellar/stellar-sdk version in web applications",
"items": make_items(dict(stellar_versions)),
},
{
"category": "Frontend Frameworks",
"description": "Application architecture: Next.js vs Vite vs other",
"items": make_items(dict(framework_counts)),
},
{
"category": "AI Tooling & Rules",
"description": "Agent instructions, prompts and rules present in repository trees",
"items": make_items(dict(ai_counts)),
},
],
}
7 changes: 7 additions & 0 deletions apps/api/src/hackproof/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Stats,
Tier,
TimelinePoint,
ToolingStats,
WasmUpload,
)

Expand Down Expand Up @@ -47,6 +48,12 @@ async def countries() -> list[CountryCount]:
return [CountryCount(**row) for row in await queries.fetch_countries()]




@router.get("/tooling")
async def tooling() -> ToolingStats:
return ToolingStats(**await queries.fetch_tooling())

@router.get("/integrations")
async def integrations() -> list[Integration]:
return [Integration(**row) for row in await queries.fetch_integrations()]
Expand Down
Loading