diff --git a/apps/api/src/hackproof/db.py b/apps/api/src/hackproof/db.py index f72d7b5..a42afb5 100644 --- a/apps/api/src/hackproof/db.py +++ b/apps/api/src/hackproof/db.py @@ -128,6 +128,16 @@ ); CREATE INDEX IF NOT EXISTS repo_integrations_partner ON repo_integrations (partner); +CREATE TABLE IF NOT EXISTS repo_tools ( + full_name TEXT NOT NULL, + tool TEXT NOT NULL, + method TEXT NOT NULL, + evidence TEXT NOT NULL, + path TEXT NOT NULL, + PRIMARY KEY (full_name, tool, method, evidence) +); +CREATE INDEX IF NOT EXISTS repo_tools_tool ON repo_tools (tool); + CREATE TABLE IF NOT EXISTS links ( full_name TEXT NOT NULL, subject_kind TEXT NOT NULL, diff --git a/apps/api/src/hackproof/export/build.py b/apps/api/src/hackproof/export/build.py index 961da67..d98a570 100644 --- a/apps/api/src/hackproof/export/build.py +++ b/apps/api/src/hackproof/export/build.py @@ -9,6 +9,7 @@ Chain, Contract, Contributor, + EcosystemTool, Evidence, HackathonExport, IntegrationEvidence, @@ -89,6 +90,13 @@ ORDER BY partner, method, evidence """ +TOOLS_SQL = """ +SELECT full_name, tool, method, evidence, path +FROM repo_tools +WHERE full_name = ANY($1) +ORDER BY tool, method, evidence +""" + def _evidence_url(kind: str, subject: str) -> str | None: match kind: @@ -109,18 +117,34 @@ def _group(rows: Iterable[asyncpg.Record], key: str = "full_name") -> dict[str, return grouped -def _partners( - catalog: Catalog, rows: list[asyncpg.Record], repo_url: str -) -> list[IntegrationPartner]: +def _found( + rows: list[asyncpg.Record], key: str, repo_url: str +) -> dict[str, list[IntegrationEvidence]]: found: dict[str, list[IntegrationEvidence]] = defaultdict(list) for row in rows: - found[row["partner"]].append( + found[row[key]].append( IntegrationEvidence( method=row["method"], detail=row["evidence"], repo_file_url=repo_file_url(repo_url, row["path"]), ) ) + return found + + +def _tools(catalog: Catalog, rows: list[asyncpg.Record], repo_url: str) -> list[EcosystemTool]: + found = _found(rows, "tool", repo_url) + return [ + EcosystemTool(slug=tool.slug, name=tool.name, website=tool.site, evidence=found[tool.slug]) + for tool in catalog.tools + if tool.slug in found + ] + + +def _partners( + catalog: Catalog, rows: list[asyncpg.Record], repo_url: str +) -> list[IntegrationPartner]: + found = _found(rows, "partner", repo_url) return [ IntegrationPartner( slug=partner.slug, @@ -148,6 +172,7 @@ async def build_export( wasm = _group(await connection.fetch(WASM_SQL, names, *span)) evidence = _group(await connection.fetch(EVIDENCE_SQL, names)) integrations = _group(await connection.fetch(INTEGRATIONS_SQL, names)) + tools = _group(await connection.fetch(TOOLS_SQL, names)) catalog = get_catalog() awards = {award.project_id: award.place for award in hackathon.awards} @@ -174,6 +199,7 @@ async def build_export( track=row["declared_track"] or "genesis", track_declared=row["declared_track"] is not None, integration_partners=_partners(catalog, integrations[row["full_name"]], row["url"]), + ecosystem_tools=_tools(catalog, tools[row["full_name"]], row["url"]), owner=Owner(login=row["owner"], avatar_url=row["owner_avatar"], country=row["country"]), contributors=[ Contributor( diff --git a/apps/api/src/hackproof/export/files.py b/apps/api/src/hackproof/export/files.py index bca1935..fb1f2d7 100644 --- a/apps/api/src/hackproof/export/files.py +++ b/apps/api/src/hackproof/export/files.py @@ -93,6 +93,9 @@ class ProjectFile(FileModel): default_factory=dict, description="Stellar package to declared version, null when unknown" ) integrations: list[FileIntegration] = Field(default_factory=list) + tools: list[FileIntegration] = Field( + default_factory=list, description="Stellar developer tools the repo builds with" + ) chain: FileChain contributors: list[FileContributor] @@ -177,6 +180,19 @@ def project_file(event: str, project: Project) -> ProjectFile: ) for partner in project.integration_partners ], + tools=[ + FileIntegration( + slug=tool.slug, + name=tool.name, + evidence=[ + FileIntegrationEvidence( + method=item.method, detail=item.detail, file_url=item.repo_file_url + ) + for item in tool.evidence + ], + ) + for tool in project.ecosystem_tools + ], chain=FileChain( contracts=chain.contract_count, wasm_uploads=chain.wasm_upload_count, diff --git a/apps/api/src/hackproof/export/models.py b/apps/api/src/hackproof/export/models.py index c8fb03f..92d6ce6 100644 --- a/apps/api/src/hackproof/export/models.py +++ b/apps/api/src/hackproof/export/models.py @@ -101,6 +101,13 @@ class IntegrationEvidence(Model): repo_file_url: str = Field(description="GitHub file where it was found") +class EcosystemTool(Model): + slug: str = Field(description="Stable tool id, listed in data/integrations.yaml") + name: str = Field(description="Tool name") + website: str = Field(description="Tool website") + evidence: list[IntegrationEvidence] = Field(description="Where the tool was found") + + class IntegrationPartner(Model): slug: str = Field(description="Stable partner id, listed in data/integrations.yaml") name: str = Field(description="Partner name") @@ -152,6 +159,9 @@ class Project(Model): integration_partners: list[IntegrationPartner] = Field( description="Stellar ecosystem partners the repo integrates, from the SCF integration list" ) + ecosystem_tools: list[EcosystemTool] = Field( + description="Stellar developer tools the repo builds with, such as Scaffold Stellar" + ) owner: Owner contributors: list[Contributor] chain: Chain diff --git a/apps/api/src/hackproof/github/models.py b/apps/api/src/hackproof/github/models.py index f5ec733..c05676a 100644 --- a/apps/api/src/hackproof/github/models.py +++ b/apps/api/src/hackproof/github/models.py @@ -59,4 +59,5 @@ class RepoScan(BaseModel): dependencies: DependencyFingerprint stellar_added_in_window: bool | None integrations: list[Detection] + tools: list[Detection] = Field(default_factory=list) contributors: list[Contributor] diff --git a/apps/api/src/hackproof/github/scan.py b/apps/api/src/hackproof/github/scan.py index 375ae74..2256ecd 100644 --- a/apps/api/src/hackproof/github/scan.py +++ b/apps/api/src/hackproof/github/scan.py @@ -16,7 +16,7 @@ from hackproof.github.track import declared_track from hackproof.hackathon import Discovery from hackproof.integrations.catalog import Catalog, get_catalog -from hackproof.integrations.detect import detect +from hackproof.integrations.detect import detect, detect_tools TEXT_SUFFIXES = { ".rs", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".toml", ".json", @@ -130,6 +130,7 @@ async def scan(self, repo: Candidate) -> RepoScan | None: dependencies=dependencies, stellar_added_in_window=added, integrations=detect(files, self._catalog, repo.owner), + tools=detect_tools(files, self._catalog, repo.owner), contributors=contributors_from(people), ) @@ -175,6 +176,11 @@ async def store_scan(connection: asyncpg.Connection, scan: RepoScan) -> None: "INSERT INTO repo_integrations VALUES ($1, $2, $3, $4, $5)", [(scan.full_name, i.slug, i.method, i.evidence, i.path) for i in scan.integrations], ) + await connection.execute("DELETE FROM repo_tools WHERE full_name = $1", scan.full_name) + await connection.executemany( + "INSERT INTO repo_tools VALUES ($1, $2, $3, $4, $5)", + [(scan.full_name, t.slug, t.method, t.evidence, t.path) for t in scan.tools], + ) await connection.execute( """ UPDATE repos SET dependencies = $2, stellar_added_in_window = $3, diff --git a/apps/api/src/hackproof/integrations/catalog.py b/apps/api/src/hackproof/integrations/catalog.py index c69d6d7..66050eb 100644 --- a/apps/api/src/hackproof/integrations/catalog.py +++ b/apps/api/src/hackproof/integrations/catalog.py @@ -6,18 +6,26 @@ from hackproof.config import get_settings -class Partner(BaseModel): +class Product(BaseModel): slug: str name: str - category: str site: str packages: dict[str, list[str]] = Field(default_factory=dict) endpoints: list[str] = Field(default_factory=list) github_owners: list[str] = Field(default_factory=list) +class Partner(Product): + category: str + + +class Tool(Product): + pass + + class Catalog(BaseModel): partners: list[Partner] + tools: list[Tool] = Field(default_factory=list) def partner(self, slug: str) -> Partner: return next(partner for partner in self.partners if partner.slug == slug) diff --git a/apps/api/src/hackproof/integrations/detect.py b/apps/api/src/hackproof/integrations/detect.py index 5b87228..8369313 100644 --- a/apps/api/src/hackproof/integrations/detect.py +++ b/apps/api/src/hackproof/integrations/detect.py @@ -1,8 +1,9 @@ import re +from collections.abc import Sequence from pathlib import PurePosixPath from hackproof.github.dependencies import ECOSYSTEM, PARSERS -from hackproof.integrations.catalog import Catalog, Partner +from hackproof.integrations.catalog import Catalog, Product from hackproof.integrations.models import Detection, Method PROSE_SUFFIXES = {".md", ".txt"} @@ -12,7 +13,7 @@ def _normalize(ecosystem: str, name: str) -> str: return name.lower().replace("_", "-") if ecosystem == "python" else name -def _declares(ecosystem: str, partner: Partner, declared: dict[str, str | None]) -> list[str]: +def _declares(ecosystem: str, partner: Product, declared: dict[str, str | None]) -> list[str]: wanted = [_normalize(ecosystem, name) for name in partner.packages.get(ecosystem, [])] found = [] for name in declared: @@ -30,9 +31,17 @@ def _endpoint(host: str) -> re.Pattern[str]: def detect(files: dict[str, str], catalog: Catalog, owner: str) -> list[Detection]: + return _detect(files, catalog.partners, owner) + + +def detect_tools(files: dict[str, str], catalog: Catalog, owner: str) -> list[Detection]: + return _detect(files, catalog.tools, owner) + + +def _detect(files: dict[str, str], products: Sequence[Product], owner: str) -> list[Detection]: partners = [ partner - for partner in catalog.partners + for partner in products if owner.lower() not in {name.lower() for name in partner.github_owners} ] endpoints = { diff --git a/apps/api/src/hackproof/queries.py b/apps/api/src/hackproof/queries.py index 9f9e1ab..92c98ff 100644 --- a/apps/api/src/hackproof/queries.py +++ b/apps/api/src/hackproof/queries.py @@ -15,6 +15,8 @@ r.created_at, r.topics, COALESCE(r.declared_track, 'genesis') AS track, ARRAY(SELECT i.partner FROM repo_integrations i WHERE i.full_name = p.full_name GROUP BY i.partner ORDER BY i.partner) AS integrations, + ARRAY(SELECT t.tool FROM repo_tools t WHERE t.full_name = p.full_name + GROUP BY t.tool ORDER BY t.tool) AS tools, count(*) OVER () AS total FROM projects p LEFT JOIN repos r ON r.full_name = p.full_name @@ -31,6 +33,9 @@ SELECT 1 FROM repo_integrations i WHERE i.full_name = p.full_name AND i.partner = ANY($8))) AND ($9::text[] IS NULL OR p.project_id = ANY($9)) + AND ($10::text[] IS NULL OR EXISTS ( + SELECT 1 FROM repo_tools t + WHERE t.full_name = p.full_name AND t.tool = ANY($10))) ORDER BY array_position( ARRAY['verified', 'likely', 'possible', 'unattributed'], p.tier), p.contracts DESC, p.classic_ops DESC, r.stars DESC NULLS LAST, p.last_activity DESC @@ -157,6 +162,20 @@ ORDER BY partner, method, evidence """ +TOOLS_SQL = """ +SELECT t.tool AS slug, count(DISTINCT t.full_name) AS projects +FROM repo_tools t +JOIN projects p ON p.full_name = t.full_name AND p.tier IN ('verified', 'likely') +GROUP BY t.tool +""" + +TOOL_USES_SQL = """ +SELECT tool AS slug, method, evidence, path +FROM repo_tools +WHERE full_name = $1 +ORDER BY tool, method, evidence +""" + COUNTRIES_SQL = """ SELECT u.country, count(DISTINCT c.login) AS contributors FROM repo_contributors c @@ -192,6 +211,7 @@ async def list_page( tiers: list[str] | None = None, track: str | None = None, integrations: list[str] | None = None, + tools: list[str] | None = None, winners: bool = False, limit: int = 50, offset: int = 0, @@ -199,7 +219,7 @@ async def list_page( awards = places() winner_ids = list(awards) if winners else None args = ( - (q, account, project_id, tiers, limit, offset, track, integrations, winner_ids) + (q, account, project_id, tiers, limit, offset, track, integrations, winner_ids, tools) if resource == "projects" else (*window(), q, account, project_id, limit, offset) ) @@ -241,6 +261,22 @@ async def fetch_integration_uses(full_name: str) -> list[dict[str, Any]]: return [{**row, "name": names[row["slug"]]} for row in map(dict, rows) if row["slug"] in names] +async def fetch_tool_uses(full_name: str) -> list[dict[str, Any]]: + async with pool().acquire() as connection: + rows = await connection.fetch(TOOL_USES_SQL, full_name) + names = {tool.slug: tool.name for tool in get_catalog().tools} + return [{**row, "name": names[row["slug"]]} for row in map(dict, rows) if row["slug"] in names] + + +async def fetch_tools() -> list[dict[str, Any]]: + async with pool().acquire() as connection: + counts = {row["slug"]: row["projects"] for row in await connection.fetch(TOOLS_SQL)} + return [ + {**tool.model_dump(include={"slug", "name", "site"}), "projects": counts.get(tool.slug, 0)} + for tool in get_catalog().tools + ] + + async def fetch_integrations() -> list[dict[str, Any]]: async with pool().acquire() as connection: counts = {row["slug"]: row["projects"] for row in await connection.fetch(INTEGRATIONS_SQL)} diff --git a/apps/api/src/hackproof/routes.py b/apps/api/src/hackproof/routes.py index 1c17422..84bd0c8 100644 --- a/apps/api/src/hackproof/routes.py +++ b/apps/api/src/hackproof/routes.py @@ -7,6 +7,7 @@ Builder, Contract, CountryCount, + EcosystemTool, FailedTx, Integration, Page, @@ -52,12 +53,18 @@ async def integrations() -> list[Integration]: return [Integration(**row) for row in await queries.fetch_integrations()] +@router.get("/tools") +async def tools() -> list[EcosystemTool]: + return [EcosystemTool(**row) for row in await queries.fetch_tools()] + + @router.get("/projects") async def projects( q: Search = None, tier: Tiers = None, track: Annotated[Literal["genesis", "scale"] | None, Query()] = None, integration: Annotated[list[str] | None, Query()] = None, + tool: Annotated[list[str] | None, Query()] = None, limit: Limit = 50, offset: Offset = 0, ) -> Page[Project]: @@ -67,6 +74,7 @@ async def projects( tiers=list(tier) if tier else None, track=track, integrations=integration, + tools=tool, limit=limit, offset=offset, ) @@ -106,6 +114,9 @@ async def project(project_id: str) -> ProjectDetail: if found[0]["full_name"] else [] ), + tool_uses=( + await queries.fetch_tool_uses(found[0]["full_name"]) if found[0]["full_name"] else [] + ), contributors=( await queries.fetch_contributors(found[0]["full_name"]) if found[0]["full_name"] else [] ), diff --git a/apps/api/src/hackproof/schemas.py b/apps/api/src/hackproof/schemas.py index f9918d4..764eb7a 100644 --- a/apps/api/src/hackproof/schemas.py +++ b/apps/api/src/hackproof/schemas.py @@ -54,6 +54,7 @@ class Project(BaseModel): topics: list[str] | None track: Literal["genesis", "scale"] | None = None integrations: list[str] = [] + tools: list[str] = [] award: Place | None = None @@ -114,6 +115,7 @@ class ProjectDetail(BaseModel): project: Project links: list[Link] integration_uses: list[IntegrationUse] + tool_uses: list[IntegrationUse] contributors: list[Contributor] contracts: list[Contract] wasm_uploads: list[WasmUpload] @@ -141,6 +143,13 @@ class Integration(BaseModel): projects: int +class EcosystemTool(BaseModel): + slug: str + name: str + site: str + projects: int + + class CountryCount(BaseModel): country: str contributors: int diff --git a/apps/api/tests/test_integrations.py b/apps/api/tests/test_integrations.py index ac2b5ef..efde5d4 100644 --- a/apps/api/tests/test_integrations.py +++ b/apps/api/tests/test_integrations.py @@ -1,5 +1,7 @@ -from hackproof.integrations.catalog import Catalog, Partner, get_catalog -from hackproof.integrations.detect import detect +import json + +from hackproof.integrations.catalog import Catalog, Partner, Tool, get_catalog +from hackproof.integrations.detect import detect, detect_tools from hackproof.integrations.models import Detection CATALOG = Catalog( @@ -97,3 +99,23 @@ def test_the_shipped_catalog_is_valid_and_unique(): assert len(slugs) == len(set(slugs)) >= 10 assert catalog.partner("trustless-work").packages["js"][0] == "@trustless-work/escrow" + + +def test_tools_are_found_apart_from_partners(): + catalog = Catalog( + partners=CATALOG.partners, + tools=[ + Tool( + slug="scaffold", + name="Scaffold", + site="https://scaffold.example", + packages={"js": ["@scaffold/app-lib"]}, + ) + ], + ) + files = { + "package.json": json.dumps({"dependencies": {"@scaffold/app-lib": "1", "@acme/sdk": "1"}}) + } + + assert {d.slug for d in detect_tools(files, catalog, "team")} == {"scaffold"} + assert {d.slug for d in detect(files, catalog, "team")} == {"acme"} diff --git a/apps/web/src/components/explorer/filter-panel.tsx b/apps/web/src/components/explorer/filter-panel.tsx index 1e96ecd..ed8385b 100644 --- a/apps/web/src/components/explorer/filter-panel.tsx +++ b/apps/web/src/components/explorer/filter-panel.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { countryName, flagEmoji, placeLabel, tierLabel, trackLabel } from '@/lib/format' -import type { Integration, Project } from '@/lib/types' +import type { EcosystemTool, Integration, Project } from '@/lib/types' import { countValues, type FilterKey, type Selection, selectedCount } from './filters' @@ -19,6 +19,7 @@ interface Option { interface FilterPanelProps { projects: Project[] integrations: Integration[] + tools: EcosystemTool[] selection: Selection onToggle: (key: FilterKey, value: string) => void onClear: () => void @@ -47,6 +48,7 @@ function options( key: FilterKey, projects: Project[], integrations: Integration[], + tools: EcosystemTool[], selection: Selection, ): Option[] { const counts = countValues(projects, selection, key) @@ -56,16 +58,18 @@ function options( ? FIXED[key].map(([value, label]) => [value, label]) : key === 'partner' ? integrations.map((item) => [item.slug, item.name, item.category]) - : [ - ...new Set( - projects.flatMap( - (project) => (key === 'language' ? project.language : project.country) ?? [], + : key === 'tool' + ? tools.map((item) => [item.slug, item.name]) + : [ + ...new Set( + projects.flatMap( + (project) => (key === 'language' ? project.language : project.country) ?? [], + ), ), - ), - ].map((value) => [ - value, - key === 'country' ? `${flagEmoji(value)} ${countryName(value)}` : value, - ]) + ].map((value) => [ + value, + key === 'country' ? `${flagEmoji(value)} ${countryName(value)}` : value, + ]) return labelled .map(([value, label, hint]) => ({ value, label, hint, count: counts.get(value) ?? 0 })) .filter((option) => option.count > 0 || chosen.has(option.value)) @@ -121,6 +125,7 @@ const GROUPS: { key: FilterKey; title: string; collapsible: boolean }[] = [ { key: 'tier', title: 'Evidence', collapsible: false }, { key: 'track', title: 'Track', collapsible: false }, { key: 'partner', title: 'Integrations', collapsible: true }, + { key: 'tool', title: 'Ecosystem tooling', collapsible: false }, { key: 'activity', title: 'On-chain activity', collapsible: false }, { key: 'language', title: 'Language', collapsible: true }, { key: 'country', title: 'Country', collapsible: true }, @@ -129,6 +134,7 @@ const GROUPS: { key: FilterKey; title: string; collapsible: boolean }[] = [ export function FilterPanel({ projects, integrations, + tools, selection, onToggle, onClear, @@ -149,7 +155,7 @@ export function FilterPanel({ onToggle(key, value)} collapsible={collapsible} diff --git a/apps/web/src/components/explorer/filters.ts b/apps/web/src/components/explorer/filters.ts index 5d97219..b70be3d 100644 --- a/apps/web/src/components/explorer/filters.ts +++ b/apps/web/src/components/explorer/filters.ts @@ -5,6 +5,7 @@ export const FILTER_KEYS = [ 'tier', 'track', 'partner', + 'tool', 'activity', 'language', 'country', @@ -17,6 +18,7 @@ const VALUES: Record string[]> = { tier: (project) => [project.tier], track: (project) => (project.track ? [project.track] : []), partner: (project) => project.integrations ?? [], + tool: (project) => project.tools ?? [], activity: (project) => [ ...(project.contracts > 0 ? ['contracts'] : []), ...(project.failed_txs > 0 ? ['failed'] : []), @@ -26,7 +28,16 @@ const VALUES: Record string[]> = { } export function readSelection(params: URLSearchParams): Selection { - return Object.fromEntries(FILTER_KEYS.map((key) => [key, params.getAll(key)])) as Selection + return Object.fromEntries( + FILTER_KEYS.map((key) => [ + key, + params + .getAll(key) + .flatMap((value) => value.split(',')) + .map((value) => (key === 'tool' ? value.replaceAll('_', '-') : value)) + .filter(Boolean), + ]), + ) as Selection } export function matches(project: Project, selection: Selection, skip?: FilterKey) { diff --git a/apps/web/src/components/explorer/project-card.tsx b/apps/web/src/components/explorer/project-card.tsx index 8bb2dbe..40eb52d 100644 --- a/apps/web/src/components/explorer/project-card.tsx +++ b/apps/web/src/components/explorer/project-card.tsx @@ -7,7 +7,7 @@ import { TierBadge } from '@/components/tier-badge' import { TrackBadge } from '@/components/track-badge' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardHeader } from '@/components/ui/card' -import { useIntegrations } from '@/hooks/use-projects' +import { useIntegrations, useTools } from '@/hooks/use-projects' import { flagEmoji, formatNumber, projectName } from '@/lib/format' import type { Project } from '@/lib/types' @@ -17,7 +17,11 @@ export function ProjectCard({ project }: { project: Project }) { const name = projectName(project.project_id) const topics = (project.topics ?? []).slice(0, VISIBLE_TOPICS) const { data: integrations = [] } = useIntegrations() - const partners = integrations.filter((item) => project.integrations?.includes(item.slug)) + const { data: tools = [] } = useTools() + const partners = [ + ...integrations.filter((item) => project.integrations?.includes(item.slug)), + ...tools.filter((item) => project.tools?.includes(item.slug)), + ] return ( diff --git a/apps/web/src/components/explorer/project-explorer.tsx b/apps/web/src/components/explorer/project-explorer.tsx index 36c3c85..8fb4f5f 100644 --- a/apps/web/src/components/explorer/project-explorer.tsx +++ b/apps/web/src/components/explorer/project-explorer.tsx @@ -11,7 +11,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/co import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { PAGE_SIZE } from '@/hooks/use-paged-list' import { usePaginationMode } from '@/hooks/use-pagination-mode' -import { useIntegrations, useProjects } from '@/hooks/use-projects' +import { useIntegrations, useProjects, useTools } from '@/hooks/use-projects' import { posthog } from '@/lib/analytics' import { formatNumber } from '@/lib/format' @@ -29,6 +29,7 @@ export function ProjectExplorer({ search }: { search: string }) { const navigate = useNavigate() const { data: projects, isPending, isError } = useProjects() const { data: integrations = [] } = useIntegrations() + const { data: tools = [] } = useTools() const view = params.get('view') === 'table' ? 'table' : 'cards' const selection = useMemo(() => readSelection(params), [params]) const active = selectedCount(selection) @@ -44,7 +45,7 @@ export function ProjectExplorer({ search }: { search: string }) { const toggle = (key: FilterKey, value: string) => update((next) => { - const chosen = next.getAll(key) + const chosen = readSelection(next)[key] next.delete(key) ;(chosen.includes(value) ? chosen.filter((item) => item !== value) @@ -107,6 +108,7 @@ export function ProjectExplorer({ search }: { search: string }) { apiGet('/tools'), + staleTime: Infinity, + }) +} + export function useIntegrations() { return useQuery({ queryKey: ['integrations'], diff --git a/apps/web/src/lib/types.ts b/apps/web/src/lib/types.ts index 372ebef..8c0f6c6 100644 --- a/apps/web/src/lib/types.ts +++ b/apps/web/src/lib/types.ts @@ -64,6 +64,14 @@ export interface Project { topics: string[] | null track: Track | null integrations: string[] + tools: string[] +} + +export interface EcosystemTool { + slug: string + name: string + site: string + projects: number } export interface Integration { @@ -131,6 +139,7 @@ export interface ProjectDetail { project: Project links: Link[] integration_uses: IntegrationUse[] + tool_uses: IntegrationUse[] contributors: Contributor[] contracts: Contract[] wasm_uploads: WasmUpload[] diff --git a/apps/web/src/pages/project.tsx b/apps/web/src/pages/project.tsx index 4036891..b3a9ef9 100644 --- a/apps/web/src/pages/project.tsx +++ b/apps/web/src/pages/project.tsx @@ -27,8 +27,16 @@ export function ProjectPage() { if (isPending) return if (error) return - const { project, links, integration_uses, contributors, contracts, wasm_uploads, failed_txs } = - data + const { + project, + links, + integration_uses, + tool_uses, + contributors, + contracts, + wasm_uploads, + failed_txs, + } = data const name = projectName(project.project_id) const direct = links.filter((link) => link.method !== 'footprint') const derived = links.length - direct.length @@ -136,7 +144,7 @@ export function ProjectPage() { )} - + {integration_uses.length > 0 && (
)} + {tool_uses.length > 0 && ( +
+ `${row.slug}:${row.method}:${row.evidence}`} + /> +
+ )} {contributors.length > 0 && (
diff --git a/data/2026/arqueo.yml b/data/2026/arqueo.yml index beb259f..540bfe3 100644 --- a/data/2026/arqueo.yml +++ b/data/2026/arqueo.yml @@ -24,6 +24,19 @@ integrations: - method: package detail: '@creit.tech/stellar-wallets-kit' file_url: https://github.com/Dabuca/arqueo/blob/HEAD/app-lib/package.json +tools: +- slug: stellar-scaffold + name: Scaffold Stellar + evidence: + - method: package + detail: '@stellar-scaffold/app-lib' + file_url: https://github.com/Dabuca/arqueo/blob/HEAD/app/package.json +- slug: stellar-registry + name: Stellar Registry + evidence: + - method: package + detail: stellar-registry + file_url: https://github.com/Dabuca/arqueo/blob/HEAD/Cargo.toml chain: contracts: 3 wasm_uploads: 2 diff --git a/data/2026/astros.yml b/data/2026/astros.yml index 7decb17..c6703b8 100644 --- a/data/2026/astros.yml +++ b/data/2026/astros.yml @@ -23,6 +23,13 @@ integrations: - method: package detail: '@creit.tech/stellar-wallets-kit' file_url: https://github.com/Eco-RV/AstrOS/blob/HEAD/app-lib/package.json +tools: +- slug: stellar-scaffold + name: Scaffold Stellar + evidence: + - method: package + detail: '@stellar-scaffold/app-lib' + file_url: https://github.com/Eco-RV/AstrOS/blob/HEAD/app/package.json chain: contracts: 0 wasm_uploads: 1 diff --git a/data/2026/drainmywallet.yml b/data/2026/drainmywallet.yml index 2fcf166..e28d224 100644 --- a/data/2026/drainmywallet.yml +++ b/data/2026/drainmywallet.yml @@ -23,6 +23,19 @@ integrations: - method: package detail: '@creit.tech/stellar-wallets-kit' file_url: https://github.com/bilfrux/DrainMyWallet/blob/HEAD/app-lib/package.json +tools: +- slug: stellar-scaffold + name: Scaffold Stellar + evidence: + - method: package + detail: '@stellar-scaffold/app-lib' + file_url: https://github.com/bilfrux/DrainMyWallet/blob/HEAD/app/package.json +- slug: stellar-registry + name: Stellar Registry + evidence: + - method: package + detail: stellar-registry + file_url: https://github.com/bilfrux/DrainMyWallet/blob/HEAD/Cargo.toml chain: contracts: 7 wasm_uploads: 13 diff --git a/data/2026/hackmeridian-stellar-hackathon.yml b/data/2026/hackmeridian-stellar-hackathon.yml index fd8503c..893db5b 100644 --- a/data/2026/hackmeridian-stellar-hackathon.yml +++ b/data/2026/hackmeridian-stellar-hackathon.yml @@ -25,6 +25,19 @@ integrations: - method: package detail: '@creit.tech/stellar-wallets-kit' file_url: https://github.com/MohamedDataX/HackMeridian-stellar-Hackathon/blob/HEAD/app-lib/package.json +tools: +- slug: stellar-scaffold + name: Scaffold Stellar + evidence: + - method: package + detail: '@stellar-scaffold/app-lib' + file_url: https://github.com/MohamedDataX/HackMeridian-stellar-Hackathon/blob/HEAD/app/package.json +- slug: stellar-registry + name: Stellar Registry + evidence: + - method: package + detail: stellar-registry + file_url: https://github.com/MohamedDataX/HackMeridian-stellar-Hackathon/blob/HEAD/Cargo.toml chain: contracts: 11 wasm_uploads: 7 diff --git a/data/2026/nicolecarolina-hackmeridian.yml b/data/2026/nicolecarolina-hackmeridian.yml index 99061db..baceffa 100644 --- a/data/2026/nicolecarolina-hackmeridian.yml +++ b/data/2026/nicolecarolina-hackmeridian.yml @@ -23,6 +23,19 @@ integrations: - method: package detail: '@creit.tech/stellar-wallets-kit' file_url: https://github.com/NicoleCarolina/HackMeridian/blob/HEAD/app-lib/package.json +tools: +- slug: stellar-scaffold + name: Scaffold Stellar + evidence: + - method: package + detail: '@stellar-scaffold/app-lib' + file_url: https://github.com/NicoleCarolina/HackMeridian/blob/HEAD/app/package.json +- slug: stellar-registry + name: Stellar Registry + evidence: + - method: package + detail: stellar-registry + file_url: https://github.com/NicoleCarolina/HackMeridian/blob/HEAD/Cargo.toml chain: contracts: 1 wasm_uploads: 1 diff --git a/data/2026/peaje-zbe.yml b/data/2026/peaje-zbe.yml index 25f327a..e95cf5c 100644 --- a/data/2026/peaje-zbe.yml +++ b/data/2026/peaje-zbe.yml @@ -23,6 +23,19 @@ integrations: - method: package detail: '@creit.tech/stellar-wallets-kit' file_url: https://github.com/marcocpp/peaje-zbe/blob/HEAD/app-lib/package.json +tools: +- slug: stellar-scaffold + name: Scaffold Stellar + evidence: + - method: package + detail: '@stellar-scaffold/app-lib' + file_url: https://github.com/marcocpp/peaje-zbe/blob/HEAD/app/package.json +- slug: stellar-registry + name: Stellar Registry + evidence: + - method: package + detail: stellar-registry + file_url: https://github.com/marcocpp/peaje-zbe/blob/HEAD/Cargo.toml chain: contracts: 10 wasm_uploads: 2 diff --git a/data/2026/scaffold-stellar-agentic.yml b/data/2026/scaffold-stellar-agentic.yml index 7d98e71..15b896c 100644 --- a/data/2026/scaffold-stellar-agentic.yml +++ b/data/2026/scaffold-stellar-agentic.yml @@ -49,6 +49,13 @@ integrations: - method: package detail: '@creit.tech/stellar-wallets-kit' file_url: https://github.com/bitfalt/scaffold-stellar-agentic/blob/HEAD/app-lib/package.json +tools: +- slug: stellar-scaffold + name: Scaffold Stellar + evidence: + - method: package + detail: '@stellar-scaffold/app-lib' + file_url: https://github.com/bitfalt/scaffold-stellar-agentic/blob/HEAD/app/package.json chain: contracts: 0 wasm_uploads: 1 diff --git a/data/2026/yield-bearing-rental-deposit.yml b/data/2026/yield-bearing-rental-deposit.yml index 468ab00..80445f6 100644 --- a/data/2026/yield-bearing-rental-deposit.yml +++ b/data/2026/yield-bearing-rental-deposit.yml @@ -25,6 +25,19 @@ integrations: - method: package detail: '@creit.tech/stellar-wallets-kit' file_url: https://github.com/ddortabencomo/yield-bearing-rental-deposit/blob/HEAD/app-lib/package.json +tools: +- slug: stellar-scaffold + name: Scaffold Stellar + evidence: + - method: package + detail: '@stellar-scaffold/app-lib' + file_url: https://github.com/ddortabencomo/yield-bearing-rental-deposit/blob/HEAD/app/package.json +- slug: stellar-registry + name: Stellar Registry + evidence: + - method: package + detail: stellar-registry + file_url: https://github.com/ddortabencomo/yield-bearing-rental-deposit/blob/HEAD/Cargo.toml chain: contracts: 18 wasm_uploads: 12 diff --git a/data/integrations.yaml b/data/integrations.yaml index 4c9dfb8..cfc6833 100644 --- a/data/integrations.yaml +++ b/data/integrations.yaml @@ -121,3 +121,27 @@ partners: packages: js: ["@axelar-network/axelarjs-sdk"] rust: [stellar-axelar-std] + +tools: + - slug: stellar-scaffold + name: Scaffold Stellar + site: https://github.com/theahaco/scaffold-stellar + packages: + rust: + [stellar-scaffold-cli, stellar-scaffold-macro, stellar-scaffold-ext-types, stellar-scaffold-reporter] + js: ["@stellar-scaffold/app-lib", "@stellar-scaffold/ui"] + github_owners: [theahaco, stellar-scaffold] + + - slug: stellar-registry + name: Stellar Registry + site: https://github.com/stellar-registry + packages: + rust: + [ + stellar-registry, + stellar-registry-build, + stellar-registry-cli, + stellar-registry-macro, + stellar-registry-name, + ] + github_owners: [stellar-registry, theahaco] diff --git a/data/schema-hackathon-project.yml b/data/schema-hackathon-project.yml index 3bbf571..62de02f 100644 --- a/data/schema-hackathon-project.yml +++ b/data/schema-hackathon-project.yml @@ -238,6 +238,12 @@ properties: $ref: '#/$defs/FileIntegration' title: Integrations type: array + tools: + description: Stellar developer tools the repo builds with + items: + $ref: '#/$defs/FileIntegration' + title: Tools + type: array chain: $ref: '#/$defs/FileChain' contributors: