Skip to content
Merged
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: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ k8s/*.env

# TLS private keys / origin certs (keep out of git)
certs/

# Maxmind
GeoIP.conf
leader-stream/*.mmdb
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ docker run -p 3000:3000 --env-file .env leader-stream
| `TRACK_LOOKAHEAD` | Slots to prefetch per tracked validator | 5000 |
| `STATIC_DIR` | Override static dir | `<repo>/leader-stream/public` |
| `NEXT_PUBLIC_LEADER_STREAM_URL` | Override SSE path injected into HTML | `/api/leader-stream` |
| `MAXMIND_DB_PATH` | Path to the MaxMind MMDB file to use for geolocation | `./GeoLite2-City.mmdb` |
| `MAXMIND_LICENSE_KEY` | Optional MaxMind license key for downloading GeoLite/GeoIP2 | none |
| `MAXMIND_DB_DOWNLOAD_URL` | Override URL for downloading the MMDB (expects raw file or tar.gz) | none |
| `MAXMIND_FALLBACK_URL` | Fallback URL for a free/test MaxMind database when no key is present | MaxMind test DB |
| `MAXMIND_EDITION_ID` | Edition ID when downloading via license key | `GeoLite2-City` |

See `.env.example` and `k8s/secret.env.example` for templates.

Expand All @@ -57,6 +62,7 @@ Static docs at `/docs.html` (source: `leader-stream/public/docs.html`). Key endp
- `GET /api/next-leaders?limit=1000`
- `GET /api/current-slot`
- `GET /api/leader-stream?track=<validator>` (SSE)
- `GET /map` globe view of upcoming leaders with geolocation (uses `/api/leader-path`)

## Deployment (Kubernetes)
`k8s/` uses Kustomize. Replace image `ghcr.io/trustless-engineering/leader-stream:${GIT_SHA}` and supply your own overlays/secrets:
Expand Down
8 changes: 8 additions & 0 deletions k8s/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ spec:
# Required only if the GHCR repo is private.
imagePullSecrets:
- name: ghcr-pull-secret
volumes:
- name: geoip-data
emptyDir: {}
containers:
- name: leader-stream
# Replace in CI/CD (e.g. envsubst) with the current git SHA.
Expand All @@ -31,9 +34,14 @@ spec:
env:
- name: PORT
value: "3000"
- name: MAXMIND_DB_PATH
value: "/var/lib/leader-stream/geoip/GeoLite2-City.mmdb"
# Optional override for Solana RPC
# - name: SOLANA_RPC_URL
# value: "https://your-solana-rpc"
volumeMounts:
- name: geoip-data
mountPath: /var/lib/leader-stream/geoip
readinessProbe:
httpGet:
path: /health
Expand Down
2 changes: 1 addition & 1 deletion k8s/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ resources:
- service.yaml
- ingress-public.yaml

namespace: leader-stream
namespace: leaderlist

generatorOptions:
disableNameSuffixHash: true
Expand Down
118 changes: 118 additions & 0 deletions leader-stream/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions leader-stream/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ anyhow = "1"
async-stream = "0.3"
axum = { version = "0.7", features = ["macros"] }
bytes = "1"
flate2 = "1"
futures-util = "0.3"
maxminddb = "0.24"
reqwest = { version = "0.11", features = ["json", "rustls-tls", "stream"] }
rustls = { version = "0.23", default-features = false, features = ["ring"] }
serde_json = "1"
Expand All @@ -23,6 +25,7 @@ tokio-tungstenite = { version = "0.23", features = ["rustls-tls-native-roots"] }
tower-http = { version = "0.5", features = ["fs"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tar = "0.4"
url = "2"
dotenvy = "0.15"

Expand Down
23 changes: 19 additions & 4 deletions leader-stream/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ pub(crate) struct Config {
pub(crate) ws_ping_interval: Duration,
pub(crate) leader_lookahead: usize,
pub(crate) track_lookahead: usize,
pub(crate) maxmind_db_path: String,
pub(crate) maxmind_license_key: Option<String>,
pub(crate) maxmind_edition_id: String,
pub(crate) maxmind_db_download_url: Option<String>,
pub(crate) maxmind_fallback_url: Option<String>,
}

impl Config {
Expand All @@ -32,10 +37,7 @@ impl Config {
.clone()
.unwrap_or_else(|| DEFAULT_RPC_URL.to_string());
if using_default_rpc {
warn!(
"SOLANA_RPC_URL not set; defaulting to {}",
DEFAULT_RPC_URL
);
warn!("SOLANA_RPC_URL not set; defaulting to {}", DEFAULT_RPC_URL);
}
let rpc_x_token = read_env_first(&["SOLANA_RPC_X_TOKEN"]);
let ws_override = read_env_first(&["SOLANA_WSS_URL", "SOLANA_WS_URL"]);
Expand Down Expand Up @@ -94,6 +96,14 @@ impl Config {
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(DEFAULT_TRACK_LOOKAHEAD);

let maxmind_db_path =
env::var("MAXMIND_DB_PATH").unwrap_or_else(|_| "./GeoLite2-City.mmdb".to_string());
let maxmind_license_key = read_env_first(&["MAXMIND_LICENSE_KEY", "GEOIP_LICENSE_KEY"]);
let maxmind_edition_id =
env::var("MAXMIND_EDITION_ID").unwrap_or_else(|_| "GeoLite2-City".to_string());
let maxmind_db_download_url = read_env_first(&["MAXMIND_DB_DOWNLOAD_URL"]);
let maxmind_fallback_url = read_env_first(&["MAXMIND_FALLBACK_URL"]);

Ok(Self {
rpc_url,
rpc_x_token,
Expand All @@ -106,6 +116,11 @@ impl Config {
ws_ping_interval,
leader_lookahead,
track_lookahead,
maxmind_db_path,
maxmind_license_key,
maxmind_edition_id,
maxmind_db_download_url,
maxmind_fallback_url,
})
}
}
Expand Down
32 changes: 32 additions & 0 deletions leader-stream/src/docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ <h1>
<a class="api-link" href="#next-leaders">/api/next-leaders</a>
<a class="api-link" href="#current-slot">/api/current-slot</a>
<a class="api-link" href="#leader-stream">/api/leader-stream</a>
<a class="api-link" href="#leader-path">/api/leader-path</a>
</div>
</aside>
</header>
Expand Down Expand Up @@ -158,6 +159,37 @@ <h2>GET /api/leader-stream</h2>
</div>
</div>
</section>

<section class="panel docs-panel" id="leader-path">
<div class="panel-head">
<div>
<h2>GET /api/leader-path</h2>
<p class="sub">Returns upcoming leaders plus MaxMind geolocation data for the map view.</p>
</div>
</div>
<div class="docs-grid">
<div class="docs-block">
<span class="docs-label">Query params</span>
<ul class="docs-list">
<li><span class="docs-key">limit</span> Optional integer. Defaults to 1000, clamps between 1 and 5000. Invalid values fall back to the default.</li>
</ul>
</div>
<div class="docs-block">
<span class="docs-label">Response fields</span>
<ul class="docs-list">
<li><span class="docs-key">currentSlot</span> Latest slot used as the starting point.</li>
<li><span class="docs-key">limit</span> The resolved limit after clamping.</li>
<li><span class="docs-key">slotMs</span> Estimated milliseconds per slot.</li>
<li><span class="docs-key">path</span> Array of rows with <span class="docs-key">slot</span>, <span class="docs-key">leader</span>, <span class="docs-key">ip</span>, <span class="docs-key">port</span>, and geolocation fields <span class="docs-key">latitude</span>, <span class="docs-key">longitude</span>, <span class="docs-key">city</span>, <span class="docs-key">country</span>.</li>
<li><span class="docs-key">ts</span> Server timestamp (ms since epoch).</li>
</ul>
</div>
<div class="docs-block docs-span">
<span class="docs-label">Example</span>
<pre class="docs-code"><code class="docs-example" data-command="curl" data-endpoint="/api/leader-path?limit=250"></code></pre>
</div>
</div>
</section>
</main>
<script>
(function () {
Expand Down
Loading