-
Notifications
You must be signed in to change notification settings - Fork 334
feat(developer-hub): add Change Log page under Price Feeds #3674
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
8211d51
54e58ad
d4f4d1e
f94e824
27833c2
3d78ceb
0d09451
3ad2201
6101ada
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| name: Snapshot Pyth symbols for Change Log | ||
|
|
||
| on: | ||
| schedule: | ||
| # Daily at 00:30 UTC. Slight offset from midnight to avoid contention | ||
| # and to ensure any in-progress upstream writes have settled. | ||
| - cron: "30 0 * * *" | ||
| workflow_dispatch: {} | ||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
||
| jobs: | ||
| snapshot: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 1 | ||
|
|
||
| - uses: pnpm/action-setup@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version-file: "package.json" | ||
| cache: pnpm | ||
|
|
||
| - name: Install dependencies for developer-hub | ||
| working-directory: apps/developer-hub | ||
| run: pnpm install --frozen-lockfile --filter @pythnetwork/developer-hub... | ||
|
|
||
| - name: Capture today's snapshot and diff | ||
| working-directory: apps/developer-hub | ||
| run: pnpm snapshot:changelog | ||
|
|
||
| - name: Verify generator runs cleanly | ||
| working-directory: apps/developer-hub | ||
| run: pnpm generate:changelog | ||
|
|
||
| - name: Commit and push if changed | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
| git add apps/developer-hub/data/latest-snapshot.json \ | ||
| apps/developer-hub/data/changelog-diffs/ | ||
|
|
||
| if git diff --cached --quiet; then | ||
| echo "No baseline change and no new diff — nothing to commit." | ||
| exit 0 | ||
| fi | ||
|
|
||
| DATE=$(date -u +%Y-%m-%d) | ||
| git commit -m "chore(developer-hub): daily change-log diff ${DATE}" | ||
| git push | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,5 +3,8 @@ | |
| dist/ | ||
| next-env.d.ts | ||
|
|
||
| # Auto-generated by `pnpm generate:changelog` from data/changelog-snapshots/. | ||
| src/components/ChangeLog/generated-data.ts | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
|
|
||
| # Ai Migration | ||
| .ai/** | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| title: Change Log | ||
| description: Daily record of status transitions on Pyth price feeds — additions, activations, upcoming expirations, and removals. | ||
| slug: /price-feeds/changelog | ||
| --- | ||
|
|
||
| import { ChangeLog } from "../../../src/components/ChangeLog"; | ||
|
|
||
| <ChangeLog /> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| { | ||
| "date": "2026-05-08", | ||
| "label": "Friday", | ||
| "summary": { | ||
| "added": 6, | ||
| "went_live": 2, | ||
| "expiring": 0, | ||
| "removed": 0 | ||
| }, | ||
| "hero": "6 new feeds announced, 2 went live.", | ||
| "events": [ | ||
| { | ||
| "id": "Equity.US.IGE/USD", | ||
| "lazerId": 2795, | ||
| "asset": "Ishares North American Natural Resources Etf", | ||
| "assetType": "equity", | ||
| "date": "2026-05-08", | ||
| "quote": "USD", | ||
| "hermesId": "c0948c8763aafa254159835fac7d9cc1f65cb2c370791d7fa52493e0bfe974bd", | ||
| "changeType": "went_live" | ||
| }, | ||
| { | ||
| "id": "Equity.US.TER/USD", | ||
| "lazerId": 1420, | ||
| "asset": "Teradyne Inc", | ||
| "assetType": "equity", | ||
| "date": "2026-05-08", | ||
| "quote": "USD", | ||
| "hermesId": "58ab181e7512766728d2cc3581839bbb913e6cd24457ba422cbe2a33df64416e", | ||
| "changeType": "went_live" | ||
| }, | ||
| { | ||
| "id": "Crypto.AFSUI/SUI.RR", | ||
| "lazerId": 3245, | ||
| "asset": "Aftermath Staked Sui", | ||
| "assetType": "crypto-redemption-rate", | ||
| "date": "2026-05-08", | ||
| "quote": "SUI", | ||
| "changeType": "added" | ||
| }, | ||
| { | ||
| "id": "Crypto.HASUI/SUI.RR", | ||
| "lazerId": 3244, | ||
| "asset": "Haedal Staked Sui", | ||
| "assetType": "crypto-redemption-rate", | ||
| "date": "2026-05-08", | ||
| "quote": "SUI", | ||
| "changeType": "added" | ||
| }, | ||
| { | ||
| "id": "Crypto.EXUSDC/USDC.RR", | ||
| "lazerId": 3243, | ||
| "asset": "Exceed Usd Coin", | ||
| "assetType": "crypto-redemption-rate", | ||
| "date": "2026-05-08", | ||
| "quote": "USDC", | ||
| "changeType": "added" | ||
| }, | ||
| { | ||
| "id": "Crypto.LIMUSD/USDC.RR", | ||
| "lazerId": 3242, | ||
| "asset": "Liminal Us Dollar", | ||
| "assetType": "crypto-redemption-rate", | ||
| "date": "2026-05-08", | ||
| "quote": "USDC", | ||
| "changeType": "added" | ||
| }, | ||
| { | ||
| "id": "Crypto.HEMIBTC/BTC.RR", | ||
| "lazerId": 3241, | ||
| "asset": "Hemi Bitcoin", | ||
| "assetType": "crypto-redemption-rate", | ||
| "date": "2026-05-08", | ||
| "quote": "BTC", | ||
| "changeType": "added" | ||
| }, | ||
| { | ||
| "id": "Equity.US.DRAM/USD", | ||
| "lazerId": 3240, | ||
| "asset": "Roundhill Memory Etf", | ||
| "assetType": "equity", | ||
| "date": "2026-05-08", | ||
| "quote": "USD", | ||
| "changeType": "added" | ||
| } | ||
| ] | ||
| } |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| /** | ||
| * Shared types and helpers for the Change Log snapshot/diff/generate | ||
| * scripts. Kept outside `src/` so it can be imported by both | ||
| * `snapshot-and-diff.ts` (writes daily diff JSONs) and | ||
| * `generate-changelog.ts` (bundles diffs into the rendered TS module). | ||
| */ | ||
|
|
||
| export type Symbol = { | ||
| pyth_lazer_id: number; | ||
| name: string; | ||
| symbol: string; | ||
| description?: string | null; | ||
| asset_type: string; | ||
| state: string; | ||
| hermes_id?: string | null; | ||
| quote_currency?: string | null; | ||
| }; | ||
|
|
||
| export type ChangeType = "added" | "went_live" | "expiring_soon" | "removed"; | ||
|
|
||
| export type Entry = { | ||
| id: string; | ||
| lazerId: number; | ||
| asset: string; | ||
| assetType: string; | ||
| quote?: string; | ||
| hermesId?: string; | ||
| changeType: ChangeType; | ||
| date: string; | ||
| }; | ||
|
|
||
| export type DaySummary = { | ||
| added: number; | ||
| went_live: number; | ||
| expiring: number; | ||
| removed: number; | ||
| }; | ||
|
|
||
| export type Day = { | ||
| date: string; | ||
| label: string; | ||
| summary: DaySummary; | ||
| hero: string; | ||
| events: Entry[]; | ||
| }; | ||
|
|
||
| // ─── Helpers ───────────────────────────────────────────────────────────── | ||
|
|
||
| export const isDeprecated = (s: Symbol): boolean => { | ||
| const desc = (s.description ?? "").toUpperCase(); | ||
| return ( | ||
| desc.includes("DEPRECATED") || | ||
| s.asset_type.toUpperCase().includes("DEPRECATED") | ||
| ); | ||
| }; | ||
|
|
||
| export const toTitleCase = (raw: string): string => | ||
| raw.toLowerCase().replaceAll(/\b([a-z])/g, (_, c: string) => c.toUpperCase()); | ||
|
|
||
| export const extractAssetName = (s: Symbol): string => { | ||
| const desc = s.description?.replace(/^DEPRECATED FEED\s*-?\s*/i, "").trim(); | ||
| if (desc && desc.length > 0) { | ||
| const [base] = desc.split("/"); | ||
| return toTitleCase((base ?? "").trim()); | ||
| } | ||
| const sym = s.symbol.split(".").pop() ?? s.symbol; | ||
| return sym.split("/")[0] ?? sym; | ||
| }; | ||
|
|
||
| export const dayLabel = (iso: string): string => { | ||
| const d = new Date(`${iso}T00:00:00Z`); | ||
| return d.toLocaleDateString("en-US", { weekday: "long", timeZone: "UTC" }); | ||
| }; | ||
|
|
||
| export const synthesizeHero = (s: DaySummary): string => { | ||
| const parts: string[] = []; | ||
| if (s.added > 0) { | ||
| parts.push(`${s.added.toString()} new feed${s.added === 1 ? "" : "s"} announced`); | ||
| } | ||
| if (s.went_live > 0) { | ||
| parts.push(`${s.went_live.toString()} went live`); | ||
| } | ||
| if (s.removed > 0) { | ||
| parts.push(`${s.removed.toString()} deactivated or deprecated`); | ||
| } | ||
| if (parts.length === 0) { | ||
| return "A quiet day on Pyth. No status transitions on any price feed."; | ||
| } | ||
| return `${parts.join(", ")}.`; | ||
| }; | ||
|
|
||
| const baseEntry = (s: Symbol, date: string): Omit<Entry, "changeType"> => ({ | ||
| id: s.symbol, | ||
| lazerId: s.pyth_lazer_id, | ||
| asset: extractAssetName(s), | ||
| assetType: s.asset_type, | ||
| date, | ||
| ...(s.quote_currency == null ? {} : { quote: s.quote_currency }), | ||
| ...(s.hermes_id == null ? {} : { hermesId: s.hermes_id }), | ||
| }); | ||
|
|
||
| /** | ||
| * Diff one pair of snapshots into a single Day entry. Diff rules: | ||
| * | ||
| * - new pyth_lazer_id today (not in yesterday, not deprecated) → added | ||
| * - missing pyth_lazer_id today (was in yesterday) → removed | ||
| * - state coming_soon → stable → went_live | ||
| * - state stable → inactive → removed | ||
| * - description gains "DEPRECATED FEED" → removed | ||
| * | ||
| * `expiring_soon` is not synthesizable from the symbols API alone | ||
| * (no scheduled deactivation date is exposed). | ||
| */ | ||
| export const diffPair = ( | ||
| yesterday: Symbol[], | ||
| today: Symbol[], | ||
| date: string, | ||
| ): Day => { | ||
| const ymap = new Map<number, Symbol>( | ||
| yesterday.map((s) => [s.pyth_lazer_id, s]), | ||
| ); | ||
| const tmap = new Map<number, Symbol>( | ||
| today.map((s) => [s.pyth_lazer_id, s]), | ||
| ); | ||
|
|
||
| const events: Entry[] = []; | ||
|
|
||
| for (const s of today) { | ||
| if (!ymap.has(s.pyth_lazer_id) && !isDeprecated(s)) { | ||
| events.push({ ...baseEntry(s, date), changeType: "added" }); | ||
| } | ||
| } | ||
|
|
||
| for (const s of yesterday) { | ||
| if (!tmap.has(s.pyth_lazer_id)) { | ||
| events.push({ ...baseEntry(s, date), changeType: "removed" }); | ||
| } | ||
| } | ||
|
|
||
| for (const t of today) { | ||
| const y = ymap.get(t.pyth_lazer_id); | ||
| if (!y) continue; | ||
| const prev = y.state; | ||
| const next = t.state; | ||
| const yDep = isDeprecated(y); | ||
| const tDep = isDeprecated(t); | ||
|
|
||
| if (prev === "coming_soon" && next === "stable") { | ||
| events.push({ ...baseEntry(t, date), changeType: "went_live" }); | ||
| } else if (prev === "stable" && next === "inactive") { | ||
| events.push({ ...baseEntry(t, date), changeType: "removed" }); | ||
| } else if (!yDep && tDep) { | ||
| events.push({ ...baseEntry(t, date), changeType: "removed" }); | ||
| } | ||
| } | ||
|
|
||
| const order: Record<ChangeType, number> = { | ||
| removed: 0, | ||
| went_live: 1, | ||
| added: 2, | ||
| expiring_soon: 3, | ||
| }; | ||
| events.sort((a, b) => { | ||
| const ord = order[a.changeType] - order[b.changeType]; | ||
| return ord === 0 ? b.lazerId - a.lazerId : ord; | ||
| }); | ||
|
|
||
| const summary: DaySummary = { | ||
| added: events.filter((e) => e.changeType === "added").length, | ||
| went_live: events.filter((e) => e.changeType === "went_live").length, | ||
| expiring: 0, | ||
| removed: events.filter((e) => e.changeType === "removed").length, | ||
| }; | ||
|
|
||
| return { | ||
| date, | ||
| label: dayLabel(date), | ||
| summary, | ||
| hero: synthesizeHero(summary), | ||
| events, | ||
| }; | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.