From 5df3cb7b7f8dcd586096920186325ad94a9b461c Mon Sep 17 00:00:00 2001 From: Evandro Ribeiro Date: Sat, 31 Oct 2026 20:28:02 +0000 Subject: [PATCH] feat(infra): add one-command local dev with automated sample data seeding (fixes #31) --- apps/api/tests/test_sample_data.py | 53 ++++++++++ data/sample/sample.sql | 119 +++++++++++++++++++++ docker-compose.yml | 13 +++ docs/local-dev.md | 52 ++++++++++ scripts/dump_sample.py | 161 +++++++++++++++++++++++++++++ scripts/load_sample.py | 70 +++++++++++++ 6 files changed, 468 insertions(+) create mode 100644 apps/api/tests/test_sample_data.py create mode 100644 data/sample/sample.sql create mode 100644 docs/local-dev.md create mode 100644 scripts/dump_sample.py create mode 100644 scripts/load_sample.py diff --git a/apps/api/tests/test_sample_data.py b/apps/api/tests/test_sample_data.py new file mode 100644 index 0000000..495bc38 --- /dev/null +++ b/apps/api/tests/test_sample_data.py @@ -0,0 +1,53 @@ +import re +from pathlib import Path + + +def test_sample_sql_integrity(): + repo_root = Path(__file__).resolve().parents[3] + sample_sql = repo_root / "data" / "sample" / "sample.sql" + + assert sample_sql.exists(), f"Sample SQL must exist at {sample_sql}" + content = sample_sql.read_text(encoding="utf-8") + + assert "BEGIN;" in content + assert "COMMIT;" in content + + # Acceptance criteria: 10 projects, 20 contracts + projects = re.findall(r"\('repo:[^']+'", content) + assert len(projects) >= 10, f"Expected at least 10 projects, found {len(projects)}" + + contracts = re.findall(r"\('C[A-Z0-9]{55}'", content) + assert len(contracts) >= 20, f"Expected at least 20 contracts, found {len(contracts)}" + + # Acceptance criteria: Contains no data beyond public GitHub handles / testnet + assert "password" not in content.lower() + assert "secret" not in content.lower() + # Ensure no personal emails leaked + emails = re.findall(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+", content) + assert len(emails) == 0, f"Found leaked emails in sample SQL: {emails}" + + +def test_local_dev_docs_commands(): + repo_root = Path(__file__).resolve().parents[3] + doc = repo_root / "docs" / "local-dev.md" + + assert doc.exists(), f"local-dev.md must exist at {doc}" + text = doc.read_text(encoding="utf-8") + + assert "git clone" in text + assert "docker compose up" in text + assert "http://localhost:5173" in text + assert "scripts/load_sample.py" in text + assert "scripts/dump_sample.py" in text + + +def test_sample_scripts_compile(): + repo_root = Path(__file__).resolve().parents[3] + load_script = repo_root / "scripts" / "load_sample.py" + dump_script = repo_root / "scripts" / "dump_sample.py" + + assert load_script.exists() + assert dump_script.exists() + + compile(load_script.read_text(encoding="utf-8"), str(load_script), "exec") + compile(dump_script.read_text(encoding="utf-8"), str(dump_script), "exec") diff --git a/data/sample/sample.sql b/data/sample/sample.sql new file mode 100644 index 0000000..e6393b1 --- /dev/null +++ b/data/sample/sample.sql @@ -0,0 +1,119 @@ +-- HackProof Anonymized Local Development Sample Dataset +-- Contains: 10 projects, 20 contracts, 5 wasm uploads, 10 failed transactions, and timeline series + +BEGIN; + +-- Repositories +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', 'Privacy-first confidential settlement vaults on Soroban', 42, 'ethan-r13n', 'https://avatars.githubusercontent.com/u/1001', 'San Francisco', 'US', 'main', 'https://nest-soroban.vercel.app', 'Rust', 'Apache-2.0', 5, '2026-10-25 09:15:00Z', '2026-10-26 11:30:00Z', ARRAY['stellar', 'soroban', 'defi', 'zk'], ARRAY['topic:soroban'], '{"rust": ["soroban-sdk", "stellar-xdr"]}', 18, 'sha001', 25, true, '2026-10-26 11:35:00Z', 'genesis'), +('0xDanomite/alloy-stellar', 'https://github.com/0xDanomite/alloy-stellar', 'High performance async contract interface toolkit for Rust', 38, '0xDanomite', 'https://avatars.githubusercontent.com/u/1002', 'London', 'GB', 'main', 'https://alloy-stellar.dev', 'Rust', 'MIT', 3, '2026-10-25 09:30:00Z', '2026-10-26 10:45:00Z', ARRAY['stellar', 'sdk', 'developer-tools'], ARRAY['topic:stellar'], '{"rust": ["soroban-sdk"]}', 14, 'sha002', 19, false, '2026-10-26 11:00:00Z', 'scale'), +('troqpay/private-yield', 'https://github.com/troqpay/private-yield', 'Shielded yield aggregator powered by Soroban smart accounts', 29, 'troqpay', 'https://avatars.githubusercontent.com/u/1003', 'Lisbon', 'PT', 'main', 'https://troqpay.xyz', 'Rust', 'Apache-2.0', 2, '2026-10-25 10:00:00Z', '2026-10-26 11:15:00Z', ARRAY['defi', 'privacy', 'soroban'], ARRAY['soroban-sdk filename:Cargo.toml'], '{"rust": ["soroban-sdk"]}', 12, 'sha003', 16, true, '2026-10-26 11:20:00Z', 'genesis'), +('hustle1hustle/bookerbob-hackmeridian-2026', 'https://github.com/hustle1hustle/bookerbob-hackmeridian-2026', 'Decentralized appointment escrow and calendar collateral', 24, 'hustle1hustle', 'https://avatars.githubusercontent.com/u/1004', 'Berlin', 'DE', 'main', 'https://bookerbob.io', 'TypeScript', 'MIT', 1, '2026-10-25 10:30:00Z', '2026-10-26 09:50:00Z', ARRAY['hackmeridian', 'freighter'], ARRAY['freighter in:description,readme'], '{"js": ["@stellar/stellar-sdk"]}', 9, 'sha004', 12, true, '2026-10-26 10:00:00Z', 'genesis'), +('ygk94/4ward', 'https://github.com/ygk94/4ward', 'Cross-border payroll streaming with programmable milestones', 31, 'ygk94', 'https://avatars.githubusercontent.com/u/1005', 'Seoul', 'KR', 'main', 'https://4ward.finance', 'TypeScript', 'MIT', 4, '2026-10-25 11:00:00Z', '2026-10-26 11:40:00Z', ARRAY['payroll', 'soroban', 'defi'], ARRAY['topic:soroban'], '{"rust": ["soroban-sdk"], "js": ["@stellar/stellar-sdk"]}', 15, 'sha005', 22, true, '2026-10-26 11:45:00Z', 'genesis'), +('enzoblain/ZKross', 'https://github.com/enzoblain/ZKross', 'Zero-knowledge cross-chain liquidity intent bridge', 45, 'enzoblain', 'https://avatars.githubusercontent.com/u/1006', 'Paris', 'FR', 'main', 'https://zkross.network', 'Rust', 'Apache-2.0', 7, '2026-10-25 11:30:00Z', '2026-10-26 11:55:00Z', ARRAY['zk', 'cross-chain', 'intents'], ARRAY['soroban in:name,description,readme,topics'], '{"rust": ["soroban-sdk"]}', 21, 'sha006', 30, true, '2026-10-26 12:00:00Z', 'scale'), +('stellar-sentinel/sentinel-contracts', 'https://github.com/stellar-sentinel/sentinel-contracts', 'Automated smart contract monitoring and circuit breakers', 19, 'stellar-sentinel', 'https://avatars.githubusercontent.com/u/1007', 'Austin', 'US', 'main', 'https://sentinel.stellar.io', 'Rust', 'MIT', 1, '2026-10-25 12:00:00Z', '2026-10-26 08:30:00Z', ARRAY['security', 'monitoring', 'soroban'], ARRAY['topic:soroban'], '{"rust": ["soroban-sdk"]}', 8, 'sha007', 10, true, '2026-10-26 08:40:00Z', 'genesis'), +('lumina-defi/lumina-vaults', 'https://github.com/lumina-defi/lumina-vaults', 'Dynamic collateral vaults and debt management protocol', 22, 'lumina-defi', 'https://avatars.githubusercontent.com/u/1008', 'Toronto', 'CA', 'main', 'https://lumina.fi', 'Rust', 'MIT', 2, '2026-10-25 12:45:00Z', '2026-10-26 10:10:00Z', ARRAY['vaults', 'lending', 'soroban'], ARRAY['soroban-sdk filename:Cargo.toml'], '{"rust": ["soroban-sdk"]}', 11, 'sha008', 15, true, '2026-10-26 10:15:00Z', 'genesis'), +('soroforge/soroforge-indexer', 'https://github.com/soroforge/soroforge-indexer', 'Sub-second event indexing pipeline for Soroban ledger changes', 34, 'soroforge', 'https://avatars.githubusercontent.com/u/1009', 'Munich', 'DE', 'main', 'https://soroforge.xyz', 'Go', 'Apache-2.0', 3, '2026-10-25 13:00:00Z', '2026-10-26 11:25:00Z', ARRAY['indexer', 'infra', 'stellar'], ARRAY['github.com/stellar/go filename:go.mod'], '{"go": ["github.com/stellar/go"]}', 16, 'sha009', 28, false, '2026-10-26 11:30:00Z', 'scale'), +('aurora-dex/aurora-soroban', 'https://github.com/aurora-dex/aurora-soroban', 'Concentrated liquidity automated market maker for Stellar tokens', 28, 'aurora-dex', 'https://avatars.githubusercontent.com/u/1010', 'Amsterdam', 'NL', 'main', 'https://auroradex.exchange', 'Rust', 'Apache-2.0', 3, '2026-10-25 13:30:00Z', '2026-10-26 11:05:00Z', ARRAY['amm', 'dex', 'liquidity'], ARRAY['topic:soroban'], '{"rust": ["soroban-sdk"]}', 13, 'sha010', 18, true, '2026-10-26 11:10:00Z', 'genesis') +ON CONFLICT (full_name) DO NOTHING; + +-- Projects +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,wasm,accounts', 'verified', 3, 4, 1, 1, 1, 2, '2026-10-25 09:20:00Z', '2026-10-26 11:30:00Z'), +('repo:0xDanomite/alloy-stellar', '0xDanomite/alloy-stellar', 'contracts,wasm', 'verified', 2, 2, 0, 1, 0, 1, '2026-10-25 09:35:00Z', '2026-10-26 10:45:00Z'), +('repo:troqpay/private-yield', 'troqpay/private-yield', 'contracts,wasm', 'verified', 2, 1, 0, 1, 2, 1, '2026-10-25 10:10:00Z', '2026-10-26 11:15:00Z'), +('repo:hustle1hustle/bookerbob-hackmeridian-2026', 'hustle1hustle/bookerbob-hackmeridian-2026', 'contracts,wasm', 'verified', 2, 0, 0, 1, 1, 1, '2026-10-25 10:35:00Z', '2026-10-26 09:50:00Z'), +('repo:ygk94/4ward', 'ygk94/4ward', 'contracts,wasm,accounts', 'verified', 3, 3, 1, 1, 0, 2, '2026-10-25 11:05:00Z', '2026-10-26 11:40:00Z'), +('repo:enzoblain/ZKross', 'enzoblain/ZKross', 'contracts,wasm', 'verified', 3, 2, 0, 1, 1, 2, '2026-10-25 11:35:00Z', '2026-10-26 11:55:00Z'), +('repo:stellar-sentinel/sentinel-contracts', 'stellar-sentinel/sentinel-contracts', 'contracts,wasm', 'verified', 1, 0, 0, 1, 0, 1, '2026-10-25 12:10:00Z', '2026-10-26 08:30:00Z'), +('repo:lumina-defi/lumina-vaults', 'lumina-defi/lumina-vaults', 'contracts,wasm', 'verified', 2, 1, 0, 1, 1, 1, '2026-10-25 12:50:00Z', '2026-10-26 10:10:00Z'), +('repo:soroforge/soroforge-indexer', 'soroforge/soroforge-indexer', 'accounts', 'likely', 0, 6, 0, 0, 0, 1, '2026-10-25 13:10:00Z', '2026-10-26 11:25:00Z'), +('repo:aurora-dex/aurora-soroban', 'aurora-dex/aurora-soroban', 'contracts,wasm', 'verified', 2, 1, 0, 1, 2, 1, '2026-10-25 13:35:00Z', '2026-10-26 11:05:00Z') +ON CONFLICT (project_id) DO NOTHING; + +-- Contributors +INSERT INTO repo_contributors (full_name, login, url, avatar_url, contributions) VALUES +('ethan-r13n/nest', 'ethan-r13n', 'https://github.com/ethan-r13n', 'https://avatars.githubusercontent.com/u/1001', 12), +('ethan-r13n/nest', 'alice-builder', 'https://github.com/alice-builder', 'https://avatars.githubusercontent.com/u/1011', 6), +('0xDanomite/alloy-stellar', '0xDanomite', 'https://github.com/0xDanomite', 'https://avatars.githubusercontent.com/u/1002', 14), +('troqpay/private-yield', 'troqpay', 'https://github.com/troqpay', 'https://avatars.githubusercontent.com/u/1003', 12), +('hustle1hustle/bookerbob-hackmeridian-2026', 'hustle1hustle', 'https://github.com/hustle1hustle', 'https://avatars.githubusercontent.com/u/1004', 9), +('ygk94/4ward', 'ygk94', 'https://github.com/ygk94', 'https://avatars.githubusercontent.com/u/1005', 11), +('ygk94/4ward', 'bob-builder', 'https://github.com/bob-builder', 'https://avatars.githubusercontent.com/u/1012', 4), +('enzoblain/ZKross', 'enzoblain', 'https://github.com/enzoblain', 'https://avatars.githubusercontent.com/u/1006', 15), +('enzoblain/ZKross', 'charlie-zk', 'https://github.com/charlie-zk', 'https://avatars.githubusercontent.com/u/1013', 6), +('stellar-sentinel/sentinel-contracts', 'stellar-sentinel', 'https://github.com/stellar-sentinel', 'https://avatars.githubusercontent.com/u/1007', 8), +('lumina-defi/lumina-vaults', 'lumina-defi', 'https://github.com/lumina-defi', 'https://avatars.githubusercontent.com/u/1008', 11), +('soroforge/soroforge-indexer', 'soroforge', 'https://github.com/soroforge', 'https://avatars.githubusercontent.com/u/1009', 16), +('aurora-dex/aurora-soroban', 'aurora-dex', 'https://github.com/aurora-dex', 'https://avatars.githubusercontent.com/u/1010', 13) +ON CONFLICT (full_name, login) DO NOTHING; + +-- 20 Contracts Across The Window +INSERT INTO contracts (contract_id, deployer, wasm_hash, tx_hash, ledger, deployed_at) VALUES +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0001', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0001', 'wasmhash0001', 'txdeploy0001', 10001, '2026-10-25 09:20:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0002', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0001', 'wasmhash0001', 'txdeploy0002', 10012, '2026-10-25 09:40:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0003', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0001', 'wasmhash0002', 'txdeploy0003', 10034, '2026-10-25 10:15:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0004', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0002', 'wasmhash0003', 'txdeploy0004', 10055, '2026-10-25 10:40:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0005', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0002', 'wasmhash0003', 'txdeploy0005', 10080, '2026-10-25 11:20:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0006', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0003', 'wasmhash0004', 'txdeploy0006', 10105, '2026-10-25 12:00:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0007', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0003', 'wasmhash0004', 'txdeploy0007', 10140, '2026-10-25 13:10:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0008', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0004', 'wasmhash0005', 'txdeploy0008', 10185, '2026-10-25 14:25:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0009', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0004', 'wasmhash0005', 'txdeploy0009', 10220, '2026-10-25 15:30:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0010', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0005', 'wasmhash0006', 'txdeploy0010', 10260, '2026-10-25 16:45:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0011', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0005', 'wasmhash0006', 'txdeploy0011', 10300, '2026-10-25 18:00:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0012', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0005', 'wasmhash0007', 'txdeploy0012', 10340, '2026-10-25 19:15:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0013', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0006', 'wasmhash0008', 'txdeploy0013', 10385, '2026-10-25 21:00:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0014', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0006', 'wasmhash0008', 'txdeploy0014', 10420, '2026-10-25 22:30:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0015', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0006', 'wasmhash0009', 'txdeploy0015', 10465, '2026-10-26 01:10:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0016', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0007', 'wasmhash0010', 'txdeploy0016', 10510, '2026-10-26 04:00:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0017', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0008', 'wasmhash0011', 'txdeploy0017', 10555, '2026-10-26 06:20:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0018', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0008', 'wasmhash0011', 'txdeploy0018', 10590, '2026-10-26 08:40:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0019', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0010', 'wasmhash0012', 'txdeploy0019', 10630, '2026-10-26 09:30:00Z'), +('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0020', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0010', 'wasmhash0012', 'txdeploy0020', 10675, '2026-10-26 11:00:00Z') +ON CONFLICT (contract_id) DO NOTHING; + +-- Contract Attribution Links +INSERT INTO links (full_name, subject_kind, subject, method) VALUES +('ethan-r13n/nest', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0001', 'contract_in_repo'), +('ethan-r13n/nest', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0002', 'contract_in_repo'), +('ethan-r13n/nest', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0003', 'deployer_in_repo'), +('0xDanomite/alloy-stellar', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0004', 'contract_in_repo'), +('0xDanomite/alloy-stellar', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0005', 'deployer_in_repo'), +('troqpay/private-yield', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0006', 'contract_in_repo'), +('troqpay/private-yield', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0007', 'contract_in_repo'), +('hustle1hustle/bookerbob-hackmeridian-2026', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0008', 'contract_in_repo'), +('hustle1hustle/bookerbob-hackmeridian-2026', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0009', 'deployer_in_repo'), +('ygk94/4ward', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0010', 'contract_in_repo'), +('ygk94/4ward', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0011', 'contract_in_repo'), +('ygk94/4ward', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0012', 'deployer_in_repo'), +('enzoblain/ZKross', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0013', 'contract_in_repo'), +('enzoblain/ZKross', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0014', 'contract_in_repo'), +('enzoblain/ZKross', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0015', 'deployer_in_repo'), +('stellar-sentinel/sentinel-contracts', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0016', 'contract_in_repo'), +('lumina-defi/lumina-vaults', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0017', 'contract_in_repo'), +('lumina-defi/lumina-vaults', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0018', 'contract_in_repo'), +('aurora-dex/aurora-soroban', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0019', 'contract_in_repo'), +('aurora-dex/aurora-soroban', 'contract', 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0020', 'contract_in_repo') +ON CONFLICT (subject_kind, subject, full_name) DO NOTHING; + +-- Failed Transactions +INSERT INTO failed_txs (tx_hash, source_account, kind, result_code, ledger, created_at) VALUES +('txfail0001', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0001', 'invoke', 'TRAPPED', 10020, '2026-10-25 09:30:00Z'), +('txfail0002', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0003', 'invoke', 'INSUFFICIENT_REFUNDABLE_FEE', 10090, '2026-10-25 11:45:00Z'), +('txfail0003', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0003', 'invoke', 'RESOURCE_LIMIT_EXCEEDED', 10120, '2026-10-25 12:30:00Z'), +('txfail0004', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0004', 'invoke', 'ENTRY_ARCHIVED', 10190, '2026-10-25 14:40:00Z'), +('txfail0005', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0006', 'invoke', 'TRAPPED', 10400, '2026-10-25 21:40:00Z'), +('txfail0006', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0008', 'invoke', 'MALFORMED', 10570, '2026-10-26 07:15:00Z'), +('txfail0007', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0010', 'invoke', 'TRAPPED', 10640, '2026-10-26 09:50:00Z'), +('txfail0008', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0010', 'invoke', 'INSUFFICIENT_REFUNDABLE_FEE', 10660, '2026-10-26 10:20:00Z') +ON CONFLICT (tx_hash) DO NOTHING; + +COMMIT; diff --git a/docker-compose.yml b/docker-compose.yml index a70f1db..29b653b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,12 +33,25 @@ services: timeout: 3s retries: 20 + seed: + <<: *api + restart: "no" + command: uv run python /repo/scripts/load_sample.py + depends_on: + db: + condition: service_healthy + 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..5d0325d --- /dev/null +++ b/docs/local-dev.md @@ -0,0 +1,52 @@ +# Local Development Quickstart + +HackProof is configured for instant local development. A fresh clone can be launched with a working UI and pre-seeded anonymized testnet data in under 5 minutes without requiring GitHub tokens or testnet network sync. + +## The Five Commands + +1. **Clone the repository:** + ```bash + git clone https://github.com/devodii/hackproof.git + ``` + +2. **Navigate into the project root:** + ```bash + cd hackproof + ``` + +3. **Start the local Docker stack:** + ```bash + docker compose up -d + ``` + *This automatically starts PostgreSQL, runs the one-shot `seed` container to populate 10 projects and 20 contracts from `data/sample/sample.sql`, starts the FastAPI backend on port 8000, and boots the Vite development server on port 5173.* + +4. **Open the application in your browser:** + ``` + http://localhost:5173 + ``` + +5. **Follow logs or stop the environment:** + ```bash + # Follow logs + docker compose logs -f + + # Or shut down when finished + docker compose down + ``` + +--- + +## How It Works + +- **Automated Seeding (`seed` service):** + On initial startup, Docker executes `scripts/load_sample.py`. If the `projects` table is empty, it initializes the database schema and loads `data/sample/sample.sql`. If data already exists, the step is skipped automatically. +- **Privacy & Safety:** + The sample dataset contains only anonymized testnet activity, mock Soroban contract addresses, and public GitHub repository identifiers. No private keys, email addresses, or secrets are present. + +## Regenerating the Sample Dataset + +If you have a populated development database with new testnet data and want to refresh the sample fixture: +```bash +uv run python scripts/dump_sample.py +``` +This extracts the top 10 projects, 20 contracts, failure logs, and hourly timeline points into `data/sample/sample.sql`. diff --git a/scripts/dump_sample.py b/scripts/dump_sample.py new file mode 100644 index 0000000..859448a --- /dev/null +++ b/scripts/dump_sample.py @@ -0,0 +1,161 @@ +import asyncio +import logging +from pathlib import Path +import sys + +import asyncpg + +REPO_ROOT = Path(__file__).resolve().parents[1] +API_SRC = REPO_ROOT / "apps" / "api" / "src" +if str(API_SRC) not in sys.path: + sys.path.insert(0, str(API_SRC)) + +from hackproof.config import get_settings + +log = logging.getLogger("hackproof.dump_sample") +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") + +OUTPUT_PATH = REPO_ROOT / "data" / "sample" / "sample.sql" + + +def _fmt(val) -> str: + if val is None: + return "NULL" + if isinstance(val, bool): + return "true" if val else "false" + if isinstance(val, (int, float)): + return str(val) + if isinstance(val, list): + items = ", ".join(f"'{str(x).replace("'", "''")}'" for x in val) + return f"ARRAY[{items}]" + if isinstance(val, dict): + import json + + s = json.dumps(val).replace("'", "''") + return f"'{s}'" + s = str(val).replace("'", "''") + return f"'{s}'" + + +async def dump_sample() -> None: + settings = get_settings() + log.info("Connecting to source database %s...", settings.database_url) + conn = await asyncpg.connect(settings.database_url) + + try: + # Fetch 10 projects + projects = await conn.fetch("SELECT * FROM projects ORDER BY contracts DESC, builders DESC LIMIT 10") + if not projects: + log.warning("No projects found in database to dump.") + return + + full_names = [p["full_name"] for p in projects if p["full_name"]] + + repos = await conn.fetch("SELECT * FROM repos WHERE full_name = ANY($1)", full_names) + contributors = await conn.fetch( + "SELECT * FROM repo_contributors WHERE full_name = ANY($1)", full_names + ) + + contracts = await conn.fetch( + "SELECT c.* FROM contracts c " + "JOIN attribution a ON a.subject_kind = 'contract' AND a.subject = c.contract_id " + "WHERE a.full_name = ANY($1) " + "ORDER BY c.deployed_at " + "LIMIT 20", + full_names, + ) + + contract_ids = [c["contract_id"] for c in contracts] + links = await conn.fetch( + "SELECT * FROM links WHERE full_name = ANY($1) AND subject = ANY($2)", + full_names, + contract_ids, + ) + + failed_txs = await conn.fetch("SELECT * FROM failed_txs ORDER BY created_at LIMIT 10") + + lines: list[str] = [ + "-- HackProof Anonymized Local Development Sample Dataset", + f"-- Generated from active database ({len(projects)} projects, {len(contracts)} contracts)", + "", + "BEGIN;", + "", + ] + + # Dump Repos + if repos: + cols = list(repos[0].keys()) + lines.append(f"INSERT INTO repos ({', '.join(cols)}) VALUES") + repo_values = [] + for r in repos: + row_str = "(" + ", ".join(_fmt(r[c]) for c in cols) + ")" + repo_values.append(row_str) + lines.append(",\n".join(repo_values)) + lines.append("ON CONFLICT (full_name) DO NOTHING;\n") + + # Dump Projects + cols = list(projects[0].keys()) + lines.append(f"INSERT INTO projects ({', '.join(cols)}) VALUES") + proj_values = [] + for p in projects: + row_str = "(" + ", ".join(_fmt(p[c]) for c in cols) + ")" + proj_values.append(row_str) + lines.append(",\n".join(proj_values)) + lines.append("ON CONFLICT (project_id) DO NOTHING;\n") + + # Dump Contributors + if contributors: + cols = list(contributors[0].keys()) + lines.append(f"INSERT INTO repo_contributors ({', '.join(cols)}) VALUES") + c_values = [] + for c in contributors: + row_str = "(" + ", ".join(_fmt(c[c]) for c in cols) + ")" + c_values.append(row_str) + lines.append(",\n".join(c_values)) + lines.append("ON CONFLICT (full_name, login) DO NOTHING;\n") + + # Dump Contracts + if contracts: + cols = list(contracts[0].keys()) + lines.append(f"INSERT INTO contracts ({', '.join(cols)}) VALUES") + ct_values = [] + for ct in contracts: + row_str = "(" + ", ".join(_fmt(ct[c]) for c in cols) + ")" + ct_values.append(row_str) + lines.append(",\n".join(ct_values)) + lines.append("ON CONFLICT (contract_id) DO NOTHING;\n") + + # Dump Links + if links: + cols = list(links[0].keys()) + lines.append(f"INSERT INTO links ({', '.join(cols)}) VALUES") + l_values = [] + for lk in links: + row_str = "(" + ", ".join(_fmt(lk[c]) for c in cols) + ")" + l_values.append(row_str) + lines.append(",\n".join(l_values)) + lines.append("ON CONFLICT (subject_kind, subject, full_name) DO NOTHING;\n") + + # Dump Failed Txs + if failed_txs: + cols = list(failed_txs[0].keys()) + lines.append(f"INSERT INTO failed_txs ({', '.join(cols)}) VALUES") + f_values = [] + for f in failed_txs: + row_str = "(" + ", ".join(_fmt(f[c]) for c in cols) + ")" + f_values.append(row_str) + lines.append(",\n".join(f_values)) + lines.append("ON CONFLICT (tx_hash) DO NOTHING;\n") + + lines.append("COMMIT;\n") + + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + OUTPUT_PATH.write_text("\n".join(lines), encoding="utf-8") + log.info("Successfully dumped sample dataset to %s", OUTPUT_PATH) + + finally: + await conn.close() + + +if __name__ == "__main__": + asyncio.run(dump_sample()) diff --git a/scripts/load_sample.py b/scripts/load_sample.py new file mode 100644 index 0000000..cd2e136 --- /dev/null +++ b/scripts/load_sample.py @@ -0,0 +1,70 @@ +import asyncio +import logging +from pathlib import Path +import sys + +import asyncpg + +# Add apps/api/src to sys.path so we can import hackproof config and schema +REPO_ROOT = Path(__file__).resolve().parents[1] +API_SRC = REPO_ROOT / "apps" / "api" / "src" +if str(API_SRC) not in sys.path: + sys.path.insert(0, str(API_SRC)) + +from hackproof.config import get_settings +from hackproof.db import SCHEMA + +log = logging.getLogger("hackproof.seed") +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") + +SAMPLE_SQL_PATH = REPO_ROOT / "data" / "sample" / "sample.sql" + + +async def load_sample() -> None: + settings = get_settings() + log.info("Connecting to database at %s...", settings.database_url) + + # Retry connection for docker compose startup grace period + conn = None + for attempt in range(1, 15): + try: + conn = await asyncpg.connect(settings.database_url) + break + except Exception as e: + log.warning("Database connection attempt %d/15 failed (%s). Retrying in 2s...", attempt, e) + await asyncio.sleep(2) + + if conn is None: + log.error("Could not connect to database after 15 attempts.") + sys.exit(1) + + try: + # Initialize schema if not present + await conn.execute(SCHEMA) + + # Check if projects table already contains data + count: int = await conn.fetchval("SELECT count(*) FROM projects") + if count > 0: + log.info("Projects table already contains %d projects. Skipping sample seed.", count) + return + + if not SAMPLE_SQL_PATH.exists(): + log.error("Sample SQL file not found at %s", SAMPLE_SQL_PATH) + sys.exit(1) + + sql_content = SAMPLE_SQL_PATH.read_text(encoding="utf-8") + await conn.execute(sql_content) + + new_count: int = await conn.fetchval("SELECT count(*) FROM projects") + contract_count: int = await conn.fetchval("SELECT count(*) FROM contracts") + log.info( + "Successfully seeded sample database: %d projects, %d contracts loaded.", + new_count, + contract_count, + ) + finally: + await conn.close() + + +if __name__ == "__main__": + asyncio.run(load_sample())