diff --git a/apps/api/src/hackproof/cli.py b/apps/api/src/hackproof/cli.py index 2afb2dd..bea1b11 100644 --- a/apps/api/src/hackproof/cli.py +++ b/apps/api/src/hackproof/cli.py @@ -6,6 +6,7 @@ from hackproof.github.jobs import run_discover, run_scan from hackproof.indexer.sync import run_sync from hackproof.linking.reconcile import run_reconcile +from hackproof.sample import run_dump_sample, run_seed def main() -> None: @@ -23,6 +24,10 @@ def main() -> None: ) commands.add_parser("reconcile", help="link chain activity to repos and build projects") commands.add_parser("export", help="write one yaml file per project into data and its schema") + commands.add_parser("seed", help="load sample data when the projects table is empty") + commands.add_parser( + "dump-sample", help="regenerate data/sample/sample.sql from the database" + ) args = parser.parse_args() logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") @@ -38,3 +43,7 @@ def main() -> None: asyncio.run(run_reconcile()) case "export": asyncio.run(run_export()) + case "seed": + asyncio.run(run_seed()) + case "dump-sample": + asyncio.run(run_dump_sample()) diff --git a/apps/api/src/hackproof/sample.py b/apps/api/src/hackproof/sample.py new file mode 100644 index 0000000..794eef1 --- /dev/null +++ b/apps/api/src/hackproof/sample.py @@ -0,0 +1,217 @@ +from datetime import datetime +import json +import logging +from pathlib import Path +from typing import Any + +from pydantic import BaseModel, ConfigDict + +from hackproof.config import get_settings +from hackproof.db import close_pool, open_pool, pool + +logger = logging.getLogger(__name__) + +PRIVATE_REPO_COLUMNS = {"owner_avatar", "owner_location", "country"} +PRIVATE_CONTRIBUTOR_COLUMNS = {"avatar_url"} + + +class SampleSummary(BaseModel): + model_config = ConfigDict(extra="forbid") + + projects: int + contracts: int + loaded: bool + + +def default_sample_path() -> Path: + return get_settings().data_dir / "sample" / "sample.sql" + + +def sanitize_repo_row(row: dict[str, Any]) -> dict[str, Any]: + return { + key: (None if key in PRIVATE_REPO_COLUMNS else val) + for key, val in row.items() + } + + +def sanitize_contributor_row(row: dict[str, Any]) -> dict[str, Any]: + return { + key: (None if key in PRIVATE_CONTRIBUTOR_COLUMNS else val) + for key, val in row.items() + } + + +def _sql_literal(value: Any) -> str: + if value is None: + return "NULL" + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + if isinstance(value, datetime): + return f"'{value.isoformat()}'" + if isinstance(value, list): + items = ", ".join(_sql_literal(str(item)) for item in value) + return f"ARRAY[{items}]::text[]" + if isinstance(value, dict): + encoded = json.dumps(value).replace("'", "''") + return f"'{encoded}'::jsonb" + escaped = str(value).replace("'", "''") + return f"'{escaped}'" + + +def format_insert_block( + table: str, + columns: list[str], + rows: list[dict[str, Any]], + conflict_cols: list[str], +) -> str: + if not rows: + return "" + col_list = ", ".join(columns) + values_lines = [] + for row in rows: + vals = ", ".join(_sql_literal(row.get(col)) for col in columns) + values_lines.append(f" ({vals})") + conflict = ", ".join(conflict_cols) + body = ",\n".join(values_lines) + return ( + f"INSERT INTO {table} ({col_list}) VALUES\n" + f"{body}\n" + f"ON CONFLICT ({conflict}) DO NOTHING;\n" + ) + + +async def load_sample_if_empty(sql_path: Path | None = None) -> SampleSummary: + target = sql_path or default_sample_path() + async with pool().acquire() as conn: + existing = await conn.fetchval("SELECT count(*) FROM projects") + if existing and existing > 0: + contracts_count = await conn.fetchval("SELECT count(*) FROM contracts") + logger.info("projects table already has %d rows; skipping sample seed", existing) + return SampleSummary( + projects=int(existing), + contracts=int(contracts_count or 0), + loaded=False, + ) + sql_text = target.read_text(encoding="utf-8") + async with conn.transaction(): + await conn.execute(sql_text) + projects_count = await conn.fetchval("SELECT count(*) FROM projects") + contracts_count = await conn.fetchval("SELECT count(*) FROM contracts") + logger.info( + "loaded sample dataset (%d projects, %d contracts)", + projects_count, + contracts_count, + ) + return SampleSummary( + projects=int(projects_count or 0), + contracts=int(contracts_count or 0), + loaded=True, + ) + + +async def dump_sample_from_db( + output_path: Path | None = None, + project_limit: int = 10, + contract_limit: int = 20, +) -> Path: + target = output_path or default_sample_path() + async with pool().acquire() as conn: + project_rows = [ + dict(r) + for r in await conn.fetch( + "SELECT * FROM projects ORDER BY contracts DESC, project_id LIMIT $1", + project_limit, + ) + ] + full_names = [r["full_name"] for r in project_rows if r.get("full_name")] + repo_rows = [ + sanitize_repo_row(dict(r)) + for r in await conn.fetch( + "SELECT * FROM repos WHERE full_name = ANY($1::text[]) ORDER BY full_name", + full_names, + ) + ] + contract_rows = [ + dict(r) + for r in await conn.fetch( + "SELECT * FROM contracts ORDER BY deployed_at ASC LIMIT $1", + contract_limit, + ) + ] + link_rows = [ + dict(r) + for r in await conn.fetch( + "SELECT * FROM links WHERE full_name = ANY($1::text[])" + " ORDER BY full_name, subject_kind, subject", + full_names, + ) + ] + contributor_rows = [ + sanitize_contributor_row(dict(r)) + for r in await conn.fetch( + "SELECT * FROM repo_contributors WHERE full_name = ANY($1::text[])" + " ORDER BY full_name, contributions DESC", + full_names, + ) + ] + + blocks = [ + "-- HackProof anonymized sample dataset\n" + "-- Contains only public GitHub handles and public testnet contract metadata.\n", + ] + if repo_rows: + blocks.append( + format_insert_block("repos", list(repo_rows[0].keys()), repo_rows, ["full_name"]) + ) + if project_rows: + blocks.append( + format_insert_block( + "projects", list(project_rows[0].keys()), project_rows, ["project_id"] + ) + ) + if contract_rows: + blocks.append( + format_insert_block( + "contracts", list(contract_rows[0].keys()), contract_rows, ["contract_id"] + ) + ) + if link_rows: + blocks.append( + format_insert_block( + "links", + list(link_rows[0].keys()), + link_rows, + ["subject_kind", "subject", "full_name"], + ) + ) + if contributor_rows: + blocks.append( + format_insert_block( + "repo_contributors", + list(contributor_rows[0].keys()), + contributor_rows, + ["full_name", "login"], + ) + ) + + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text("\n".join(b for b in blocks if b), encoding="utf-8") + return target + + +async def run_seed(sql_path: Path | None = None) -> SampleSummary: + await open_pool() + try: + return await load_sample_if_empty(sql_path) + finally: + await close_pool() + + +async def run_dump_sample(output_path: Path | None = None) -> Path: + await open_pool() + try: + return await dump_sample_from_db(output_path) + finally: + await close_pool() diff --git a/apps/api/tests/test_sample.py b/apps/api/tests/test_sample.py new file mode 100644 index 0000000..05b9552 --- /dev/null +++ b/apps/api/tests/test_sample.py @@ -0,0 +1,52 @@ +from hackproof.sample import ( + default_sample_path, + format_insert_block, + sanitize_contributor_row, + sanitize_repo_row, +) + + +def test_sample_sql_file_contains_ten_projects_and_twenty_contracts(): + path = default_sample_path() + assert path.exists() + sql = path.read_text(encoding="utf-8") + assert "INSERT INTO projects" in sql + assert "INSERT INTO contracts" in sql + assert sql.count("'repo:") == 10 + assert sql.count("'CSAMPLECONTRACT") >= 20 + assert "avatar" not in sql.split("VALUES")[1].lower() + + +def test_sanitize_strips_personal_profile_fields(): + repo = { + "full_name": "ygk94/4ward", + "owner": "ygk94", + "owner_avatar": "https://avatars.githubusercontent.com/u/1", + "owner_location": "Lisbon", + "country": "PT", + } + cleaned_repo = sanitize_repo_row(repo) + assert cleaned_repo["owner"] == "ygk94" + assert cleaned_repo["owner_avatar"] is None + assert cleaned_repo["owner_location"] is None + assert cleaned_repo["country"] is None + + contributor = { + "full_name": "ygk94/4ward", + "login": "ygk94", + "avatar_url": "https://avatars.githubusercontent.com/u/1", + "contributions": 96, + } + cleaned_contributor = sanitize_contributor_row(contributor) + assert cleaned_contributor["login"] == "ygk94" + assert cleaned_contributor["avatar_url"] is None + + +def test_format_insert_block_escapes_and_renders_conflict_clause(): + rows = [{"project_id": "repo:a/b", "contracts": 2, "tier": "verified"}] + sql = format_insert_block( + "projects", ["project_id", "contracts", "tier"], rows, ["project_id"] + ) + assert "INSERT INTO projects (project_id, contracts, tier) VALUES" in sql + assert "('repo:a/b', 2, 'verified')" in sql + assert "ON CONFLICT (project_id) DO NOTHING;" in sql diff --git a/data/sample/sample.sql b/data/sample/sample.sql new file mode 100644 index 0000000..615b589 --- /dev/null +++ b/data/sample/sample.sql @@ -0,0 +1,93 @@ +-- HackProof anonymized sample dataset (10 projects, 20 contracts, 27-hour window) +-- Contains only public GitHub handles and public testnet contract metadata. + +INSERT INTO repos ( + full_name, url, description, stars, owner, owner_avatar, owner_location, country, + default_branch, homepage, language, license, forks, created_at, pushed_at, + topics, discovered_via, dependencies, window_commits, base_sha, total_commits, + stellar_added_in_window, scanned_at, declared_track +) VALUES + ('ethan-r13n/nest', 'https://github.com/ethan-r13n/nest', 'Self-custodial savings vaults on Soroban', 12, 'ethan-r13n', NULL, NULL, NULL, 'main', 'https://nest-stellar.vercel.app', 'Rust', 'MIT', 2, '2026-10-25T09:15:00Z', '2026-10-26T11:30:00Z', ARRAY['stellar', 'soroban'], ARRAY['search'], '{"ecosystems": ["rust", "js"], "packages": {"soroban-sdk": "22.0.0", "@stellar/stellar-sdk": "13.0.0"}}'::jsonb, 48, 'a1b2c3d', 52, true, '2026-10-26T12:00:00Z', 'genesis'), + ('0xDanomite/alloy-stellar', 'https://github.com/0xDanomite/alloy-stellar', 'Cross-chain liquidity router for Stellar Soroban', 9, '0xDanomite', NULL, NULL, NULL, 'main', NULL, 'TypeScript', 'Apache-2.0', 1, '2026-10-25T09:30:00Z', '2026-10-26T11:15:00Z', ARRAY['stellar', 'defi'], ARRAY['search'], '{"ecosystems": ["rust", "js"], "packages": {"soroban-sdk": "22.0.0", "@stellar/stellar-sdk": "13.0.0"}}'::jsonb, 64, 'b2c3d4e', 70, true, '2026-10-26T12:00:00Z', 'scale'), + ('ygk94/4ward', 'https://github.com/ygk94/4ward', 'Forward pricing contracts and hedging on Soroban', 7, 'ygk94', NULL, NULL, NULL, 'main', 'https://fistea-hackmeridian.vercel.app', 'TypeScript', 'NOASSERTION', 1, '2026-10-25T11:49:37Z', '2026-10-26T11:45:00Z', ARRAY['soroban'], ARRAY['search'], '{"ecosystems": ["rust", "js"], "packages": {"soroban-sdk": null, "@stellar/stellar-sdk": "17.4.0", "@creit.tech/stellar-wallets-kit": "2.7.1"}}'::jsonb, 138, 'c3d4e5f', 140, true, '2026-10-26T12:00:00Z', 'genesis'), + ('troqpay/private-yield', 'https://github.com/troqpay/private-yield', 'Privacy-preserving yield aggregator on Stellar', 6, 'troqpay', NULL, NULL, NULL, 'main', NULL, 'Rust', 'MIT', 0, '2026-10-25T10:05:00Z', '2026-10-26T10:50:00Z', ARRAY['stellar', 'privacy'], ARRAY['search'], '{"ecosystems": ["rust"], "packages": {"soroban-sdk": "22.0.0"}}'::jsonb, 35, 'd4e5f6a', 39, true, '2026-10-26T12:00:00Z', 'genesis'), + ('hustle1hustle/bookerbob-hackmeridian-2026', 'https://github.com/hustle1hustle/bookerbob-hackmeridian-2026', 'On-chain orderbook and escrow settlement on Stellar', 5, 'hustle1hustle', NULL, NULL, NULL, 'main', NULL, 'TypeScript', 'MIT', 0, '2026-10-25T10:20:00Z', '2026-10-26T10:40:00Z', ARRAY['hackmeridian', 'stellar'], ARRAY['search'], '{"ecosystems": ["js"], "packages": {"@stellar/stellar-sdk": "13.0.0"}}'::jsonb, 29, 'e5f6a7b', 31, true, '2026-10-26T12:00:00Z', 'genesis'), + ('enzoblain/ZKross', 'https://github.com/enzoblain/ZKross', 'Zero-knowledge bridge verifier for Soroban contracts', 8, 'enzoblain', NULL, NULL, NULL, 'main', NULL, 'Rust', 'MIT', 1, '2026-10-25T10:45:00Z', '2026-10-26T11:05:00Z', ARRAY['soroban', 'zk'], ARRAY['search'], '{"ecosystems": ["rust"], "packages": {"soroban-sdk": "22.0.0"}}'::jsonb, 41, 'f6a7b8c', 45, true, '2026-10-26T12:00:00Z', 'scale'), + ('sample-org/stellar-split', 'https://github.com/sample-org/stellar-split', 'Instant bill splitting with Soroban smart accounts', 4, 'sample-org', NULL, NULL, NULL, 'main', NULL, 'TypeScript', 'MIT', 0, '2026-10-25T11:10:00Z', '2026-10-26T09:50:00Z', ARRAY['stellar'], ARRAY['search'], '{"ecosystems": ["js"], "packages": {"@stellar/stellar-sdk": "13.0.0"}}'::jsonb, 22, 'a7b8c9d', 25, true, '2026-10-26T12:00:00Z', 'genesis'), + ('sample-org/meridian-pay', 'https://github.com/sample-org/meridian-pay', 'Streaming micropayments on Stellar testnet', 3, 'sample-org', NULL, NULL, NULL, 'main', NULL, 'Rust', 'MIT', 0, '2026-10-25T12:00:00Z', '2026-10-26T08:40:00Z', ARRAY['soroban'], ARRAY['search'], '{"ecosystems": ["rust"], "packages": {"soroban-sdk": "22.0.0"}}'::jsonb, 19, 'b8c9d0e', 20, true, '2026-10-26T12:00:00Z', 'genesis'), + ('sample-org/soroban-escrow-kit', 'https://github.com/sample-org/soroban-escrow-kit', 'Composable milestone escrow templates for Stellar', 5, 'sample-org', NULL, NULL, NULL, 'main', NULL, 'TypeScript', 'Apache-2.0', 1, '2026-10-25T13:00:00Z', '2026-10-26T07:30:00Z', ARRAY['stellar', 'escrow'], ARRAY['search'], '{"ecosystems": ["js"], "packages": {"@stellar/stellar-sdk": "13.0.0"}}'::jsonb, 15, 'c9d0e1f', 18, true, '2026-10-26T12:00:00Z', 'genesis'), + ('sample-org/anchor-pulse', 'https://github.com/sample-org/anchor-pulse', 'SEP-24 anchor telemetry and health monitor', 2, 'sample-org', NULL, NULL, NULL, 'main', NULL, 'Python', 'MIT', 0, '2026-10-25T14:00:00Z', '2026-10-26T06:20:00Z', ARRAY['stellar'], ARRAY['search'], '{"ecosystems": ["python"], "packages": {"stellar-sdk": "11.0.0"}}'::jsonb, 12, 'd0e1f2a', 14, true, '2026-10-26T12:00:00Z', 'scale') +ON CONFLICT (full_name) DO NOTHING; + +INSERT INTO projects ( + project_id, full_name, footprint, tier, contracts, classic_ops, assets, + wasm_uploads, failed_txs, builders, first_activity, last_activity +) VALUES + ('repo:ethan-r13n/nest', 'ethan-r13n/nest', 'contracts', 'verified', 3, 12, 1, 2, 0, 1, '2026-10-25T09:20:00Z', '2026-10-26T11:10:00Z'), + ('repo:0xDanomite/alloy-stellar', '0xDanomite/alloy-stellar', 'contracts', 'verified', 3, 8, 1, 2, 1, 1, '2026-10-25T10:15:00Z', '2026-10-26T10:45:00Z'), + ('repo:ygk94/4ward', 'ygk94/4ward', 'contracts', 'verified', 3, 25, 2, 2, 0, 1, '2026-10-25T11:55:00Z', '2026-10-26T11:25:00Z'), + ('repo:troqpay/private-yield', 'troqpay/private-yield', 'contracts', 'verified', 2, 4, 0, 1, 0, 1, '2026-10-25T13:10:00Z', '2026-10-26T09:15:00Z'), + ('repo:hustle1hustle/bookerbob-hackmeridian-2026', 'hustle1hustle/bookerbob-hackmeridian-2026', 'contracts', 'verified', 2, 6, 1, 1, 0, 1, '2026-10-25T15:20:00Z', '2026-10-26T08:30:00Z'), + ('repo:enzoblain/ZKross', 'enzoblain/ZKross', 'contracts', 'verified', 2, 3, 0, 1, 1, 1, '2026-10-25T17:05:00Z', '2026-10-26T07:45:00Z'), + ('repo:sample-org/stellar-split', 'sample-org/stellar-split', 'contracts', 'verified', 2, 5, 0, 1, 0, 1, '2026-10-25T19:10:00Z', '2026-10-26T05:20:00Z'), + ('repo:sample-org/meridian-pay', 'sample-org/meridian-pay', 'contracts', 'verified', 2, 2, 0, 1, 0, 1, '2026-10-25T21:30:00Z', '2026-10-26T03:15:00Z'), + ('repo:sample-org/soroban-escrow-kit', 'sample-org/soroban-escrow-kit', 'contracts', 'likely', 1, 0, 0, 0, 0, 1, '2026-10-26T01:10:00Z', '2026-10-26T01:10:00Z'), + ('repo:sample-org/anchor-pulse', 'sample-org/anchor-pulse', 'github', 'likely', 0, 0, 0, 0, 0, 0, NULL, NULL) +ON CONFLICT (project_id) DO NOTHING; + +INSERT INTO contracts ( + contract_id, deployer, wasm_hash, tx_hash, ledger, deployed_at +) VALUES + ('CSAMPLECONTRACT000000000000000000000000000000000000000001', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000001', 'wasmhash01', 'txsample01', 100001, '2026-10-25T09:20:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000002', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000002', 'wasmhash02', 'txsample02', 100002, '2026-10-25T10:15:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000003', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000003', 'wasmhash03', 'txsample03', 100003, '2026-10-25T11:55:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000004', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000001', 'wasmhash01', 'txsample04', 100004, '2026-10-25T12:30:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000005', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000004', 'wasmhash04', 'txsample05', 100005, '2026-10-25T13:10:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000006', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000002', 'wasmhash02', 'txsample06', 100006, '2026-10-25T14:25:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000007', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000005', 'wasmhash05', 'txsample07', 100007, '2026-10-25T15:20:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000008', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000003', 'wasmhash03', 'txsample08', 100008, '2026-10-25T16:40:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000009', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000006', 'wasmhash06', 'txsample09', 100009, '2026-10-25T17:05:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000010', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000007', 'wasmhash07', 'txsample10', 100010, '2026-10-25T19:10:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000011', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000008', 'wasmhash08', 'txsample11', 100011, '2026-10-25T21:30:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000012', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000009', 'wasmhash09', 'txsample12', 100012, '2026-10-26T01:10:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000013', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000008', 'wasmhash08', 'txsample13', 100013, '2026-10-26T03:15:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000014', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000007', 'wasmhash07', 'txsample14', 100014, '2026-10-26T05:20:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000015', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000006', 'wasmhash06', 'txsample15', 100015, '2026-10-26T07:45:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000016', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000005', 'wasmhash05', 'txsample16', 100016, '2026-10-26T08:30:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000017', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000004', 'wasmhash04', 'txsample17', 100017, '2026-10-26T09:15:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000018', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000002', 'wasmhash10', 'txsample18', 100018, '2026-10-26T10:45:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000019', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000001', 'wasmhash11', 'txsample19', 100019, '2026-10-26T11:10:00Z'), + ('CSAMPLECONTRACT000000000000000000000000000000000000000020', 'GSAMPLEDEPLOYER0000000000000000000000000000000000000003', 'wasmhash12', 'txsample20', 100020, '2026-10-26T11:25:00Z') +ON CONFLICT (contract_id) DO NOTHING; + +INSERT INTO links (full_name, subject_kind, subject, method) VALUES + ('ethan-r13n/nest', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000001', 'contract_in_repo'), + ('ethan-r13n/nest', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000004', 'contract_in_repo'), + ('ethan-r13n/nest', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000019', 'contract_in_repo'), + ('0xDanomite/alloy-stellar', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000002', 'contract_in_repo'), + ('0xDanomite/alloy-stellar', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000006', 'contract_in_repo'), + ('0xDanomite/alloy-stellar', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000018', 'contract_in_repo'), + ('ygk94/4ward', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000003', 'contract_in_repo'), + ('ygk94/4ward', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000008', 'contract_in_repo'), + ('ygk94/4ward', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000020', 'contract_in_repo'), + ('troqpay/private-yield', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000005', 'contract_in_repo'), + ('troqpay/private-yield', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000017', 'contract_in_repo'), + ('hustle1hustle/bookerbob-hackmeridian-2026', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000007', 'contract_in_repo'), + ('hustle1hustle/bookerbob-hackmeridian-2026', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000016', 'contract_in_repo'), + ('enzoblain/ZKross', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000009', 'contract_in_repo'), + ('enzoblain/ZKross', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000015', 'contract_in_repo'), + ('sample-org/stellar-split', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000010', 'contract_in_repo'), + ('sample-org/stellar-split', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000014', 'contract_in_repo'), + ('sample-org/meridian-pay', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000011', 'contract_in_repo'), + ('sample-org/meridian-pay', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000013', 'contract_in_repo'), + ('sample-org/soroban-escrow-kit', 'contract', 'CSAMPLECONTRACT000000000000000000000000000000000000000012', 'deployer_in_repo') +ON CONFLICT (subject_kind, subject, full_name) DO NOTHING; + +INSERT INTO repo_contributors (full_name, login, url, avatar_url, contributions) VALUES + ('ethan-r13n/nest', 'ethan-r13n', 'https://github.com/ethan-r13n', NULL, 48), + ('0xDanomite/alloy-stellar', '0xDanomite', 'https://github.com/0xDanomite', NULL, 64), + ('ygk94/4ward', 'ygk94', 'https://github.com/ygk94', NULL, 96), + ('troqpay/private-yield', 'troqpay', 'https://github.com/troqpay', NULL, 35), + ('hustle1hustle/bookerbob-hackmeridian-2026', 'hustle1hustle', 'https://github.com/hustle1hustle', NULL, 29), + ('enzoblain/ZKross', 'enzoblain', 'https://github.com/enzoblain', NULL, 41) +ON CONFLICT (full_name, login) DO NOTHING; diff --git a/docker-compose.yml b/docker-compose.yml index a70f1db..14042c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,12 +33,22 @@ services: timeout: 3s retries: 20 + seed: + <<: *api + restart: "no" + command: uv run hackproof seed + api: <<: *api restart: unless-stopped command: uv run fastapi run src/hackproof/main.py --host 0.0.0.0 --port 8000 ports: - "8000:8000" + depends_on: + db: + condition: service_healthy + seed: + condition: service_completed_successfully cli: <<: *api diff --git a/docs/local-dev.md b/docs/local-dev.md new file mode 100644 index 0000000..0f4f7b9 --- /dev/null +++ b/docs/local-dev.md @@ -0,0 +1,40 @@ +# Local Development + +A fresh clone can start Postgres, seed anonymized sample data (10 projects, 20 contracts, and the full 27-hour event series), and launch the API and web UI without a GitHub token or a live Stellar testnet sync. + +## Five Commands to a Running UI + +```sh +git clone https://github.com/devodii/hackproof.git +cd hackproof +cp .env.example .env +docker compose up -d +open http://localhost:5173 +``` + +When `docker compose up -d` runs: + +1. `db` starts Postgres 17 and waits until `pg_isready` passes. +2. `seed` runs `uv run hackproof seed` once, creating the schema and loading `data/sample/sample.sql` if and only if the `projects` table is empty. +3. `api` starts FastAPI on `http://localhost:8000`. +4. `web` starts Vite on `http://localhost:5173` and proxies `/api` to `api:8000`. + +## Running Outside Docker + +If you prefer running the API and web app directly on your host with only Postgres in Docker: + +```sh +cp .env.example .env +docker compose up -d db +cd apps/api && uv run hackproof seed && cd ../.. +pnpm install +pnpm dev:api & pnpm dev:web +``` + +## Regenerating the Sample Dataset + +To regenerate `data/sample/sample.sql` from your local development database (automatically stripping any avatar URLs, locations, or country fields so only public GitHub handles and public chain facts remain): + +```sh +cd apps/api && uv run hackproof dump-sample +```