zframes 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +2 -0
- package/dist/index.js +868 -170
- package/package.json +4 -3
- package/runtime/assets/d3-B3esOK0k.js +1 -0
- package/runtime/assets/editor-CY2XpG9x.js +68 -0
- package/runtime/assets/editor-DinyMxo6.css +1 -0
- package/runtime/assets/gridstack-BxxvPIFU.js +9 -0
- package/runtime/assets/gridstack-FIb_WQSt.css +1 -0
- package/runtime/assets/index-Bpmi3x1d.js +597 -0
- package/runtime/assets/index-CsheOEUr.css +1 -0
- package/runtime/assets/{index-A5M9erCx.js → index-DPlG_EDx.js} +1 -1
- package/runtime/assets/liveline-C5zt2WuS.js +1 -0
- package/runtime/assets/react-qoZPGuNy.js +49 -0
- package/runtime/index.html +6 -3
- package/runtime/assets/index-6CnH3oHt.js +0 -719
- package/runtime/assets/index-CgpsbUVX.css +0 -1
package/dist/index.js
CHANGED
|
@@ -101,7 +101,7 @@ var DashboardSpecSchema = z2.preprocess(
|
|
|
101
101
|
),
|
|
102
102
|
title: z2.string().describe("Dashboard name, like package.json's name."),
|
|
103
103
|
author: z2.string().optional().describe(
|
|
104
|
-
`Who made this dashboard \u2014 a free-form credit, like package.json's author ("
|
|
104
|
+
`Who made this dashboard \u2014 a free-form credit, like package.json's author ("You" or "You <you@example.com>"). Optional.`
|
|
105
105
|
),
|
|
106
106
|
grid: z2.object({
|
|
107
107
|
columns: z2.number().int().min(1).default(12),
|
|
@@ -154,13 +154,20 @@ var SOURCES = {
|
|
|
154
154
|
finra: {
|
|
155
155
|
name: "FINRA",
|
|
156
156
|
url: "https://www.finra.org/finra-data/browse-catalog/short-sale-volume-data"
|
|
157
|
-
}
|
|
157
|
+
},
|
|
158
|
+
ofr: {
|
|
159
|
+
name: "OFR",
|
|
160
|
+
url: "https://www.financialresearch.gov/financial-stress-index/"
|
|
161
|
+
},
|
|
162
|
+
mempool: { name: "mempool.space", url: "https://mempool.space" },
|
|
163
|
+
deribit: { name: "Deribit", url: "https://www.deribit.com" },
|
|
164
|
+
coinpaprika: { name: "Coinpaprika", url: "https://coinpaprika.com" }
|
|
158
165
|
};
|
|
159
166
|
var clockMeta = defineFrameMeta({
|
|
160
167
|
name: "clock",
|
|
161
168
|
iconUrl: widgetIcon("clock"),
|
|
162
169
|
layout: { w: 3, h: 2, minW: 2, minH: 1 },
|
|
163
|
-
description: "Digital clock showing the current time, ticking every second. Configurable IANA timezone (defaults to the viewer's local zone), 12/24-hour format, optional seconds and date, and a caption label. Drop several with different timezones for a trading-desk world clock. Needs no data provider.",
|
|
170
|
+
description: "Digital clock showing the current time, ticking every second. Configurable IANA timezone (defaults to the viewer's local zone), 12/24-hour format, optional seconds and date, the timezone abbreviation, and a caption label. Drop several with different timezones for a trading-desk world clock. Needs no data provider.",
|
|
164
171
|
capabilities: [],
|
|
165
172
|
schema: z3.object({
|
|
166
173
|
timezone: z3.string().default("").describe(
|
|
@@ -174,7 +181,10 @@ var clockMeta = defineFrameMeta({
|
|
|
174
181
|
showMillis: z3.boolean().default(false).describe(
|
|
175
182
|
"Show milliseconds (HH:MM:SS.mmm), updated smoothly each animation frame. Implies seconds."
|
|
176
183
|
),
|
|
177
|
-
showDate: z3.boolean().default(false).describe("Show the weekday and date under the time.")
|
|
184
|
+
showDate: z3.boolean().default(false).describe("Show the weekday and date under the time."),
|
|
185
|
+
showTimezone: z3.boolean().default(true).describe(
|
|
186
|
+
'Show the timezone abbreviation (e.g. "EST", "GMT+7", "UTC") in the caption. Combines with the label when set, e.g. "New York \xB7 EST".'
|
|
187
|
+
)
|
|
178
188
|
})
|
|
179
189
|
});
|
|
180
190
|
var marketHoursMeta = defineFrameMeta({
|
|
@@ -337,6 +347,58 @@ var inflationPulseMeta = defineFrameMeta({
|
|
|
337
347
|
months: z3.number().int().min(13).max(36).default(18).describe("How many monthly CPI observations to show in the trend.")
|
|
338
348
|
})
|
|
339
349
|
});
|
|
350
|
+
var financialStressMeta = defineFrameMeta({
|
|
351
|
+
name: "financial-stress",
|
|
352
|
+
iconUrl: widgetIcon("financial-stress"),
|
|
353
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
354
|
+
description: "The OFR Financial Stress Index \u2014 a daily, market-based gauge of systemic financial stress from the U.S. Office of Financial Research. One headline value where 0 is the long-run average (positive = elevated stress, negative = calmer than normal), an optional breakdown of the five contributing categories (credit, equity valuation, safe assets, funding, volatility), and a trend line. Keyless official data, updated each business day; needs the zframes runtime's data proxy (ships with `zframes serve` / `vite dev`). Not a price feed.",
|
|
355
|
+
capabilities: ["financial-stress"],
|
|
356
|
+
source: SOURCES.ofr,
|
|
357
|
+
schema: z3.object({
|
|
358
|
+
trendDays: z3.number().int().min(20).max(90).default(60).describe("How many recent daily readings to plot in the trend line."),
|
|
359
|
+
showCategories: z3.boolean().default(true).describe(
|
|
360
|
+
"Show the five category contributions (credit, equity valuation, safe assets, funding, volatility) under the headline."
|
|
361
|
+
)
|
|
362
|
+
})
|
|
363
|
+
});
|
|
364
|
+
var nationalDebtMeta = defineFrameMeta({
|
|
365
|
+
name: "national-debt",
|
|
366
|
+
iconUrl: widgetIcon("national-debt"),
|
|
367
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
368
|
+
description: "U.S. total public debt outstanding from the Treasury's keyless 'Debt to the Penny' dataset \u2014 the headline total in trillions, the change over the chosen window, an optional split into debt held by the public vs intragovernmental holdings, and a trend line. Official data updated each business day; CORS-safe (no proxy needed). Macro context, not a live price feed.",
|
|
369
|
+
capabilities: ["national-debt"],
|
|
370
|
+
source: SOURCES.treasury,
|
|
371
|
+
schema: z3.object({
|
|
372
|
+
trendDays: z3.number().int().min(30).max(365).default(180).describe(
|
|
373
|
+
"How many business days of history to load for the trend and the change figure."
|
|
374
|
+
),
|
|
375
|
+
showSplit: z3.boolean().default(true).describe(
|
|
376
|
+
"Show the debt-held-by-the-public vs intragovernmental-holdings split."
|
|
377
|
+
)
|
|
378
|
+
})
|
|
379
|
+
});
|
|
380
|
+
var laborMarketMeta = defineFrameMeta({
|
|
381
|
+
name: "labor-market",
|
|
382
|
+
iconUrl: widgetIcon("labor-market"),
|
|
383
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
384
|
+
description: "U.S. labor-market snapshot from the BLS keyless public API: the headline unemployment rate, the latest monthly change in nonfarm payrolls (jobs added or lost), the total payroll level, and an unemployment-rate trend line. Monthly macro context for stock dashboards; updates on the BLS jobs-report schedule, not a live feed.",
|
|
385
|
+
capabilities: ["macro-series"],
|
|
386
|
+
source: SOURCES.bls,
|
|
387
|
+
schema: z3.object({
|
|
388
|
+
months: z3.number().int().min(13).max(36).default(18).describe("How many monthly observations to show in the trend.")
|
|
389
|
+
})
|
|
390
|
+
});
|
|
391
|
+
var treasuryAuctionsMeta = defineFrameMeta({
|
|
392
|
+
name: "treasury-auctions",
|
|
393
|
+
iconUrl: widgetIcon("treasury-auctions"),
|
|
394
|
+
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
395
|
+
description: "Recent completed U.S. Treasury auctions from the keyless Fiscal Data API \u2014 each row shows the security (bill/note/bond + term), the high awarded yield, and the bid-to-cover ratio (demand: total bids \xF7 amount accepted; higher = stronger). Newest first. Official market-plumbing data, CORS-safe; updates as auctions settle, not a live price feed.",
|
|
396
|
+
capabilities: ["treasury-auctions"],
|
|
397
|
+
source: SOURCES.treasury,
|
|
398
|
+
schema: z3.object({
|
|
399
|
+
count: z3.number().int().min(3).max(20).default(8).describe("How many recent auctions to list (newest first).")
|
|
400
|
+
})
|
|
401
|
+
});
|
|
340
402
|
var filingsFeedMeta = defineFrameMeta({
|
|
341
403
|
name: "filings-feed",
|
|
342
404
|
iconUrl: widgetIcon("filings-feed"),
|
|
@@ -416,12 +478,12 @@ var fundingHeatmapMeta = defineFrameMeta({
|
|
|
416
478
|
name: "funding-heatmap",
|
|
417
479
|
iconUrl: widgetIcon("funding-heatmap"),
|
|
418
480
|
layout: { w: 6, h: 3, minW: 4, minH: 3 },
|
|
419
|
-
description: "Heatmap of perp funding rates \u2014 symbols as rows,
|
|
481
|
+
description: "Heatmap of perp funding rates \u2014 symbols as rows, daily average over the last 7 days as columns, green positive / red negative. Spots persistent funding regimes at a glance.",
|
|
420
482
|
capabilities: ["funding-history"],
|
|
421
483
|
source: SOURCES.hyperliquid,
|
|
422
484
|
schema: z3.object({
|
|
423
|
-
symbols: z3.array(z3.string()).min(
|
|
424
|
-
'Hyperliquid symbols as heatmap rows, e.g. ["xyz:TSLA", "xyz:NVDA", "xyz:AAPL"].'
|
|
485
|
+
symbols: z3.array(z3.string()).min(2).max(8).describe(
|
|
486
|
+
'Hyperliquid symbols as heatmap rows \u2014 at least 2, since the heatmap compares funding across symbols, e.g. ["xyz:TSLA", "xyz:NVDA", "xyz:AAPL"].'
|
|
425
487
|
)
|
|
426
488
|
})
|
|
427
489
|
});
|
|
@@ -512,7 +574,383 @@ var allocationMeta = defineFrameMeta({
|
|
|
512
574
|
).min(2).max(8).describe("The holdings to chart. 2 to 8 positions.")
|
|
513
575
|
})
|
|
514
576
|
});
|
|
577
|
+
var newsFeedMeta = defineFrameMeta({
|
|
578
|
+
name: "news-feed",
|
|
579
|
+
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
580
|
+
description: 'Scrolling feed of the latest news headlines from a chosen outlet \u2014 each row is a clickable headline with its publish time, newest first. Free, keyless RSS sources: crypto press (CoinDesk, Cointelegraph, Decrypt), broad markets/macro (CNBC, Nasdaq), or \u2014 source "stocks" \u2014 per-company headlines (via Google News) scoped to the specific tickers in `symbols`. IMPORTANT: news feeds are CORS-blocked, so this frame reads them through the zframes runtime\'s data proxy (it ships with `zframes serve` / `vite dev`); on a fully static host with no runtime it shows an empty state.',
|
|
581
|
+
capabilities: ["news"],
|
|
582
|
+
schema: z3.object({
|
|
583
|
+
source: z3.enum([
|
|
584
|
+
"coindesk",
|
|
585
|
+
"cointelegraph",
|
|
586
|
+
"decrypt",
|
|
587
|
+
"cnbc",
|
|
588
|
+
"nasdaq",
|
|
589
|
+
"stocks"
|
|
590
|
+
]).default("coindesk").describe(
|
|
591
|
+
'Which feed to show. Crypto press: "coindesk", "cointelegraph", "decrypt". Markets/macro: "cnbc", "nasdaq". "stocks" = per-company headlines (Google News) for the tickers in `symbols`.'
|
|
592
|
+
),
|
|
593
|
+
symbols: z3.array(z3.string()).default([]).describe(
|
|
594
|
+
'Only used when source is "stocks": stock tickers to pull headlines for, e.g. ["TSLA","NVDA","AAPL"]. HIP-3 symbols ("xyz:TSLA") work too \u2014 the dex prefix is stripped. Ignored for the other sources.'
|
|
595
|
+
),
|
|
596
|
+
count: z3.number().int().min(3).max(20).default(8).describe("How many headlines to list (newest first).")
|
|
597
|
+
})
|
|
598
|
+
});
|
|
599
|
+
var dexVolumeTreemapMeta = defineFrameMeta({
|
|
600
|
+
name: "dex-volume-treemap",
|
|
601
|
+
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
602
|
+
description: "Treemap of decentralized-exchange (DEX) protocols sized by trailing-24h trading volume, tiles colored green/red by 1-day change. Data from DeFiLlama. One-glance read on where on-chain trading flow is concentrated right now.",
|
|
603
|
+
capabilities: ["dex-volume"],
|
|
604
|
+
source: SOURCES.defillama,
|
|
605
|
+
schema: z3.object({
|
|
606
|
+
topN: z3.number().int().min(3).max(30).default(12).describe("How many of the highest-volume DEX protocols to show.")
|
|
607
|
+
})
|
|
608
|
+
});
|
|
609
|
+
var dexVolumeChartMeta = defineFrameMeta({
|
|
610
|
+
name: "dex-volume-chart",
|
|
611
|
+
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
612
|
+
description: "Multi-series line chart of daily DEX trading volume for several protocols over a lookback window \u2014 compare how Uniswap, PancakeSwap, Aerodrome etc. trend against each other. Data from DeFiLlama (daily granularity).",
|
|
613
|
+
capabilities: ["dex-volume"],
|
|
614
|
+
source: SOURCES.defillama,
|
|
615
|
+
schema: z3.object({
|
|
616
|
+
protocols: z3.array(z3.string()).min(1).max(6).describe(
|
|
617
|
+
'DeFiLlama DEX protocol slugs (lowercase, hyphenated), e.g. ["uniswap", "pancakeswap", "aerodrome-slipstream"]. 1 to 6.'
|
|
618
|
+
),
|
|
619
|
+
lookback: z3.enum(["7D", "1M", "3M"]).default("1M").describe("History window for the chart.")
|
|
620
|
+
})
|
|
621
|
+
});
|
|
622
|
+
var protocolTvlTreemapMeta = defineFrameMeta({
|
|
623
|
+
name: "protocol-tvl-treemap",
|
|
624
|
+
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
625
|
+
description: "Treemap of DeFi protocols sized by current total value locked (TVL), tiles colored green/red by 1-day change. Data from DeFiLlama. Unlike tvl-treemap (which groups by blockchain), this ranks individual protocols (Lido, Aave, EigenLayer\u2026).",
|
|
626
|
+
capabilities: ["protocol-tvl"],
|
|
627
|
+
source: SOURCES.defillama,
|
|
628
|
+
schema: z3.object({
|
|
629
|
+
topN: z3.number().int().min(3).max(30).default(12).describe("How many of the largest protocols by TVL to show.")
|
|
630
|
+
})
|
|
631
|
+
});
|
|
632
|
+
var protocolTvlChartMeta = defineFrameMeta({
|
|
633
|
+
name: "protocol-tvl-chart",
|
|
634
|
+
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
635
|
+
description: "Multi-series line chart of total value locked (TVL) for several DeFi protocols over a lookback window. Data from DeFiLlama (daily granularity).",
|
|
636
|
+
capabilities: ["protocol-tvl"],
|
|
637
|
+
source: SOURCES.defillama,
|
|
638
|
+
schema: z3.object({
|
|
639
|
+
protocols: z3.array(z3.string()).min(1).max(6).describe(
|
|
640
|
+
'DeFiLlama protocol slugs (lowercase, hyphenated), e.g. ["lido", "aave", "eigenlayer"]. 1 to 6.'
|
|
641
|
+
),
|
|
642
|
+
lookback: z3.enum(["7D", "1M", "3M"]).default("1M").describe("History window for the chart.")
|
|
643
|
+
})
|
|
644
|
+
});
|
|
645
|
+
var protocolFeesTreemapMeta = defineFrameMeta({
|
|
646
|
+
name: "protocol-fees-treemap",
|
|
647
|
+
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
648
|
+
description: "Treemap of protocols sized by the fees they generated in the last 24h, tiles colored green/red by 1-day change. Data from DeFiLlama. Shows where on-chain users are actually paying for blockspace and services right now.",
|
|
649
|
+
capabilities: ["protocol-fees"],
|
|
650
|
+
source: SOURCES.defillama,
|
|
651
|
+
schema: z3.object({
|
|
652
|
+
topN: z3.number().int().min(3).max(30).default(12).describe("How many of the highest fee-earning protocols to show.")
|
|
653
|
+
})
|
|
654
|
+
});
|
|
655
|
+
var marketCapTreemapMeta = defineFrameMeta({
|
|
656
|
+
name: "market-cap-treemap",
|
|
657
|
+
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
658
|
+
description: "Treemap of the largest cryptocurrencies sized by market capitalisation, tiles colored green/red by 24h price change. Data from CoinGecko (free tier). A heat-map of the whole crypto market at a glance.",
|
|
659
|
+
capabilities: ["coin-markets"],
|
|
660
|
+
source: SOURCES.coingecko,
|
|
661
|
+
schema: z3.object({
|
|
662
|
+
topN: z3.number().int().min(5).max(50).default(20).describe(
|
|
663
|
+
"How many of the largest coins by market cap to show (up to 50)."
|
|
664
|
+
)
|
|
665
|
+
})
|
|
666
|
+
});
|
|
667
|
+
var openInterestMeta = defineFrameMeta({
|
|
668
|
+
name: "open-interest",
|
|
669
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
670
|
+
description: 'Live open interest across a watchlist of Hyperliquid perps \u2014 each symbol is a horizontal bar sized by USD notional, largest first, refreshed on a ~30s poll. Single-venue (Hyperliquid only), so read it as a relative gauge across your symbols, not a market-wide total. Stocks (HIP-3, e.g. "xyz:TSLA") and crypto both work.',
|
|
671
|
+
capabilities: ["open-interest"],
|
|
672
|
+
source: SOURCES.hyperliquid,
|
|
673
|
+
schema: z3.object({
|
|
674
|
+
symbols: z3.array(z3.string()).min(1).max(20).describe(
|
|
675
|
+
`Hyperliquid symbols to compare open interest for, e.g. ["BTC", "ETH", "xyz:TSLA"]. A "<dex>:*" wildcard (e.g. "xyz:*") pulls that dex's entire universe.`
|
|
676
|
+
)
|
|
677
|
+
})
|
|
678
|
+
});
|
|
679
|
+
var snakeMeta = defineFrameMeta({
|
|
680
|
+
name: "snake",
|
|
681
|
+
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
682
|
+
description: "Classic snake game on canvas \u2014 steer with the arrow keys (or swipe), eat dots to grow, avoid the walls and your own tail. High score persists locally. For when the market is flat. Needs no data provider.",
|
|
683
|
+
capabilities: [],
|
|
684
|
+
schema: z3.object({})
|
|
685
|
+
});
|
|
686
|
+
var flappyBirdMeta = defineFrameMeta({
|
|
687
|
+
name: "flappy-bird",
|
|
688
|
+
layout: { w: 4, h: 4, minW: 3, minH: 3 },
|
|
689
|
+
description: "Flappy-bird style game on canvas \u2014 tap or press SPACE to flap through the gaps between pipes. High score persists locally. Needs no data provider.",
|
|
690
|
+
capabilities: [],
|
|
691
|
+
schema: z3.object({})
|
|
692
|
+
});
|
|
693
|
+
var videoMeta = defineFrameMeta({
|
|
694
|
+
name: "video",
|
|
695
|
+
layout: { w: 4, h: 3, minW: 2, minH: 2 },
|
|
696
|
+
description: "Embeds a video from a YouTube or Vimeo link (or any direct embed URL) as an iframe \u2014 a livestream, a market-news clip, a focus playlist. Needs no data provider.",
|
|
697
|
+
capabilities: [],
|
|
698
|
+
schema: z3.object({
|
|
699
|
+
url: z3.string().min(1).describe(
|
|
700
|
+
"Video URL \u2014 a YouTube watch/share link, a Vimeo link, or a direct embeddable URL (https)."
|
|
701
|
+
),
|
|
702
|
+
title: z3.string().default("Video").describe("Accessible title for the embedded player (iframe title).")
|
|
703
|
+
})
|
|
704
|
+
});
|
|
705
|
+
var drawdyMeta = defineFrameMeta({
|
|
706
|
+
name: "drawdy",
|
|
707
|
+
layout: { w: 8, h: 6, minW: 2, minH: 2 },
|
|
708
|
+
description: "Embeds drawdy.io as an interactive whiteboard canvas. No configuration needed.",
|
|
709
|
+
capabilities: [],
|
|
710
|
+
schema: z3.object({})
|
|
711
|
+
});
|
|
712
|
+
var countdownMeta = defineFrameMeta({
|
|
713
|
+
name: "countdown",
|
|
714
|
+
layout: { w: 3, h: 2, minW: 2, minH: 1 },
|
|
715
|
+
description: "Live countdown to a target date and time \u2014 FOMC decisions, CPI prints, options expiry, earnings, a token unlock, the next market open. Counts down in days / hours / minutes / seconds, ticking every second, and flips to a 'reached' state once the moment passes. Needs no data provider.",
|
|
716
|
+
capabilities: [],
|
|
717
|
+
schema: z3.object({
|
|
718
|
+
target: z3.string().default("").describe(
|
|
719
|
+
`The moment to count down to, as an ISO 8601 string. Add a timezone for an unambiguous instant, e.g. "2026-07-30T18:00:00-04:00" or "2026-12-31T23:59:59Z"; a bare "2026-07-30T18:00" is read in the viewer's local timezone. Empty shows a "set a target" prompt.`
|
|
720
|
+
),
|
|
721
|
+
label: z3.string().default("").describe(
|
|
722
|
+
'Caption above the countdown, e.g. "FOMC Decision". Empty hides it.'
|
|
723
|
+
),
|
|
724
|
+
showTarget: z3.boolean().default(true).describe("Show the formatted target date and time under the countdown.")
|
|
725
|
+
})
|
|
726
|
+
});
|
|
727
|
+
var linkGridMeta = defineFrameMeta({
|
|
728
|
+
name: "link-grid",
|
|
729
|
+
layout: { w: 3, h: 2, minW: 2, minH: 1 },
|
|
730
|
+
description: "A grid of quick-launch tiles linking to your favourite sites \u2014 TradingView, exchanges, news, docs, your own dashboards. Each tile opens in a new tab and shows the destination site's favicon by default (fetched keyless from a public favicon service), with an optional per-link icon override and a first-letter fallback. Needs no data provider.",
|
|
731
|
+
capabilities: [],
|
|
732
|
+
schema: z3.object({
|
|
733
|
+
links: z3.array(
|
|
734
|
+
z3.object({
|
|
735
|
+
label: z3.string().min(1).describe('Tile caption, e.g. "TradingView".'),
|
|
736
|
+
url: z3.string().min(1).describe("Destination URL (https). Opens in a new tab."),
|
|
737
|
+
icon: z3.string().default("").describe(
|
|
738
|
+
`Optional icon override: an emoji (e.g. "\u{1F4C8}") or an https image URL. Empty uses the destination site's favicon, falling back to the label's first letter.`
|
|
739
|
+
)
|
|
740
|
+
})
|
|
741
|
+
).min(1).default([
|
|
742
|
+
{
|
|
743
|
+
label: "TradingView",
|
|
744
|
+
url: "https://www.tradingview.com",
|
|
745
|
+
icon: "\u{1F4C8}"
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
label: "Hyperliquid",
|
|
749
|
+
url: "https://app.hyperliquid.xyz",
|
|
750
|
+
icon: "\u26A1"
|
|
751
|
+
}
|
|
752
|
+
]).describe("The links to show as tiles. At least one."),
|
|
753
|
+
columns: z3.number().int().min(1).max(4).default(2).describe("How many tiles per row.")
|
|
754
|
+
})
|
|
755
|
+
});
|
|
756
|
+
var calculatorMeta = defineFrameMeta({
|
|
757
|
+
name: "calculator",
|
|
758
|
+
layout: { w: 3, h: 3, minW: 2, minH: 2 },
|
|
759
|
+
description: "Position-size & risk calculator. Enter account size, risk-per-trade %, entry and stop price; it computes the dollars at risk, the per-unit risk, the position size (units) that respects that risk budget, the resulting position value, and whether the setup is long or short. All math runs client-side \u2014 no data provider. Inputs are editable live; the configured values are the starting point.",
|
|
760
|
+
capabilities: [],
|
|
761
|
+
schema: z3.object({
|
|
762
|
+
account: z3.number().positive().default(1e4).describe("Account size used as the risk base, in the quote currency."),
|
|
763
|
+
riskPct: z3.number().positive().max(100).default(1).describe("Percent of the account risked on the trade, e.g. 1 = 1%."),
|
|
764
|
+
entry: z3.number().positive().default(100).describe("Entry price."),
|
|
765
|
+
stop: z3.number().positive().default(95).describe(
|
|
766
|
+
"Stop-loss price. Its distance from entry sets the per-unit risk; below entry = long, above = short."
|
|
767
|
+
),
|
|
768
|
+
currency: z3.string().default("$").describe("Currency symbol shown next to money values.")
|
|
769
|
+
})
|
|
770
|
+
});
|
|
771
|
+
var quoteMeta = defineFrameMeta({
|
|
772
|
+
name: "quote",
|
|
773
|
+
layout: { w: 4, h: 2, minW: 2, minH: 1 },
|
|
774
|
+
description: 'Displays a market or trading quote, centered \u2014 set one or rotate through several. A calm bit of wall-art for the dashboard: trading maxims, reminders of your own rules, mantras. Write any attribution into the text itself (e.g. "\u2026 \u2014 Buffett"). Needs no data provider.',
|
|
775
|
+
capabilities: [],
|
|
776
|
+
schema: z3.object({
|
|
777
|
+
quotes: z3.array(z3.string().min(1)).min(1).default([
|
|
778
|
+
"Be fearful when others are greedy, and greedy when others are fearful. \u2014 Warren Buffett",
|
|
779
|
+
"The trend is your friend until the end when it bends.",
|
|
780
|
+
"Plan the trade, trade the plan."
|
|
781
|
+
]).describe(
|
|
782
|
+
"One or more quotes. With more than one, the frame rotates through them."
|
|
783
|
+
),
|
|
784
|
+
intervalSec: z3.number().int().min(0).default(12).describe(
|
|
785
|
+
"Seconds between rotations when there are multiple quotes. 0 shows the first quote, fixed."
|
|
786
|
+
)
|
|
787
|
+
})
|
|
788
|
+
});
|
|
789
|
+
var dividerMeta = defineFrameMeta({
|
|
790
|
+
name: "divider",
|
|
791
|
+
layout: { w: 12, h: 1, minW: 1, minH: 1 },
|
|
792
|
+
description: "A plain rule that separates regions of the dashboard, with an optional centered label. Renders chrome-less (no card) \u2014 lighter than a heading. Use a horizontal divider full-width between stacked zones, or set orientation to vertical for a 1-column-wide column separator. Needs no data provider.",
|
|
793
|
+
capabilities: [],
|
|
794
|
+
chrome: "bare",
|
|
795
|
+
schema: z3.object({
|
|
796
|
+
label: z3.string().default("").describe(
|
|
797
|
+
"Optional text shown in the middle of the rule. Empty = a clean line."
|
|
798
|
+
),
|
|
799
|
+
orientation: z3.enum(["horizontal", "vertical"]).default("horizontal").describe(
|
|
800
|
+
"Horizontal rule (spans the width) or vertical rule (spans the height)."
|
|
801
|
+
),
|
|
802
|
+
style: z3.enum(["solid", "dashed", "dotted"]).default("solid").describe("Line style.")
|
|
803
|
+
})
|
|
804
|
+
});
|
|
805
|
+
var btcFeesMeta = defineFrameMeta({
|
|
806
|
+
name: "btc-fees",
|
|
807
|
+
iconUrl: widgetIcon("btc-fees"),
|
|
808
|
+
layout: { w: 3, h: 2, minW: 2, minH: 2 },
|
|
809
|
+
description: "Recommended Bitcoin on-chain fee rates (sat/vB) from mempool.space \u2014 the next-block ('fastest'), ~30-minute, ~1-hour, economy, and minimum tiers, as a compact gauge. Live mempool data, keyless; updates every ~30s.",
|
|
810
|
+
capabilities: ["btc-fees"],
|
|
811
|
+
source: SOURCES.mempool,
|
|
812
|
+
schema: z3.object({
|
|
813
|
+
tiers: z3.array(z3.enum(["fastest", "halfHour", "hour", "economy", "minimum"])).min(1).max(5).default(["fastest", "halfHour", "hour", "economy"]).describe(
|
|
814
|
+
'Which fee tiers to show, in order. "fastest" = next block, "halfHour"/"hour" = within ~30/60 min, "economy"/"minimum" = cheapest relayable.'
|
|
815
|
+
)
|
|
816
|
+
})
|
|
817
|
+
});
|
|
818
|
+
var btcMempoolMeta = defineFrameMeta({
|
|
819
|
+
name: "btc-mempool",
|
|
820
|
+
iconUrl: widgetIcon("btc-mempool"),
|
|
821
|
+
layout: { w: 5, h: 3, minW: 3, minH: 2 },
|
|
822
|
+
description: "Bitcoin mempool congestion at a glance \u2014 unconfirmed transaction count, total pending vsize, and a row of projected ('template') blocks the network will likely mine next, each labelled with its median fee rate (sat/vB) and tx count. Live mempool data from mempool.space, keyless.",
|
|
823
|
+
capabilities: ["btc-mempool"],
|
|
824
|
+
source: SOURCES.mempool,
|
|
825
|
+
schema: z3.object({
|
|
826
|
+
projectedBlocks: z3.number().int().min(1).max(8).default(5).describe(
|
|
827
|
+
"How many projected (yet-to-be-mined) blocks to show, next-to-mine first."
|
|
828
|
+
)
|
|
829
|
+
})
|
|
830
|
+
});
|
|
831
|
+
var btcBlocksMeta = defineFrameMeta({
|
|
832
|
+
name: "btc-blocks",
|
|
833
|
+
iconUrl: widgetIcon("btc-blocks"),
|
|
834
|
+
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
835
|
+
description: "Feed of the most recently mined Bitcoin blocks \u2014 each row shows the height, how long ago it was mined, transaction count, the mining pool that found it, total fees (BTC), and size. Live data from mempool.space, keyless; newest first.",
|
|
836
|
+
capabilities: ["btc-blocks"],
|
|
837
|
+
source: SOURCES.mempool,
|
|
838
|
+
schema: z3.object({
|
|
839
|
+
count: z3.number().int().min(3).max(15).default(8).describe("How many recent blocks to list (newest first).")
|
|
840
|
+
})
|
|
841
|
+
});
|
|
842
|
+
var btcHashrateMeta = defineFrameMeta({
|
|
843
|
+
name: "btc-hashrate",
|
|
844
|
+
iconUrl: widgetIcon("btc-hashrate"),
|
|
845
|
+
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
846
|
+
description: "Bitcoin network hashrate over time as a line chart, with the current hashrate (EH/s) and difficulty as headline figures. Shows the long-run security trend of the network. Data from mempool.space (daily granularity), keyless.",
|
|
847
|
+
capabilities: ["btc-hashrate"],
|
|
848
|
+
source: SOURCES.mempool,
|
|
849
|
+
schema: z3.object({
|
|
850
|
+
window: z3.enum(["1y", "2y", "3y"]).default("1y").describe("History window for the hashrate line.")
|
|
851
|
+
})
|
|
852
|
+
});
|
|
853
|
+
var btcDifficultyMeta = defineFrameMeta({
|
|
854
|
+
name: "btc-difficulty",
|
|
855
|
+
iconUrl: widgetIcon("btc-difficulty"),
|
|
856
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
857
|
+
description: "Countdown to the next Bitcoin difficulty adjustment \u2014 a progress bar through the current 2016-block epoch, the estimated change (+ = mining gets harder), blocks remaining, and the estimated retarget date. Also shows the previous adjustment. Data from mempool.space, keyless.",
|
|
858
|
+
capabilities: ["btc-difficulty"],
|
|
859
|
+
source: SOURCES.mempool,
|
|
860
|
+
schema: z3.object({
|
|
861
|
+
showPrevious: z3.boolean().default(true).describe(
|
|
862
|
+
"Also show the percentage change applied at the previous retarget."
|
|
863
|
+
)
|
|
864
|
+
})
|
|
865
|
+
});
|
|
866
|
+
var miningPoolsMeta = defineFrameMeta({
|
|
867
|
+
name: "mining-pools",
|
|
868
|
+
iconUrl: widgetIcon("mining-pools"),
|
|
869
|
+
layout: { w: 6, h: 4, minW: 3, minH: 3 },
|
|
870
|
+
description: "Treemap of Bitcoin mining-pool dominance over a window \u2014 each tile is a pool sized by the share of blocks it mined, so you can see how concentrated hashpower is right now (Foundry, AntPool, ViaBTC\u2026). Data from mempool.space, keyless.",
|
|
871
|
+
capabilities: ["mining-pools"],
|
|
872
|
+
source: SOURCES.mempool,
|
|
873
|
+
schema: z3.object({
|
|
874
|
+
window: z3.enum(["24h", "3d", "1w", "1m"]).default("1w").describe("Window over which to measure each pool's block share."),
|
|
875
|
+
topN: z3.number().int().min(3).max(25).default(12).describe(
|
|
876
|
+
"How many of the largest pools to show; the rest fold into 'Other'."
|
|
877
|
+
)
|
|
878
|
+
})
|
|
879
|
+
});
|
|
880
|
+
var lightningStatsMeta = defineFrameMeta({
|
|
881
|
+
name: "lightning-stats",
|
|
882
|
+
iconUrl: widgetIcon("lightning-stats"),
|
|
883
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
884
|
+
description: "Bitcoin Lightning Network snapshot \u2014 public node count, channel count, and total network capacity (BTC), with a day-over-day delta and the Tor/clearnet node split. Data from mempool.space, keyless; updates roughly daily.",
|
|
885
|
+
capabilities: ["lightning-stats"],
|
|
886
|
+
source: SOURCES.mempool,
|
|
887
|
+
schema: z3.object({
|
|
888
|
+
showSplit: z3.boolean().default(true).describe(
|
|
889
|
+
"Show the Tor vs clearnet node split under the headline stats."
|
|
890
|
+
)
|
|
891
|
+
})
|
|
892
|
+
});
|
|
893
|
+
var optionsPutCallMeta = defineFrameMeta({
|
|
894
|
+
name: "options-put-call",
|
|
895
|
+
iconUrl: widgetIcon("options-put-call"),
|
|
896
|
+
layout: { w: 4, h: 3, minW: 3, minH: 2 },
|
|
897
|
+
description: "Deribit options put/call ratio for BTC or ETH \u2014 the headline ratio (by open interest or 24h volume), a call-vs-put open-interest split bar, and the open-interest-weighted average implied volatility. A ratio above 1 means puts outweigh calls (defensive positioning). Keyless Deribit market data.",
|
|
898
|
+
capabilities: ["options-summary"],
|
|
899
|
+
source: SOURCES.deribit,
|
|
900
|
+
schema: z3.object({
|
|
901
|
+
currency: z3.enum(["BTC", "ETH"]).default("BTC").describe(
|
|
902
|
+
"Which Deribit options book to summarise \u2014 BTC or ETH (the only deeply liquid books)."
|
|
903
|
+
),
|
|
904
|
+
basis: z3.enum(["oi", "volume"]).default("oi").describe(
|
|
905
|
+
'Headline put/call ratio basis: "oi" = by open interest (positioning), "volume" = by 24h traded volume (flow). The other is shown smaller.'
|
|
906
|
+
)
|
|
907
|
+
})
|
|
908
|
+
});
|
|
909
|
+
var optionsIvMeta = defineFrameMeta({
|
|
910
|
+
name: "options-iv",
|
|
911
|
+
iconUrl: widgetIcon("options-iv"),
|
|
912
|
+
layout: { w: 6, h: 3, minW: 3, minH: 2 },
|
|
913
|
+
description: "Deribit DVOL implied-volatility index for BTC or ETH over time \u2014 the crypto equivalent of the VIX, as a line chart with the current reading and its change over the window. Rising DVOL = the market is pricing bigger expected swings. Keyless Deribit market data.",
|
|
914
|
+
capabilities: ["volatility-index"],
|
|
915
|
+
source: SOURCES.deribit,
|
|
916
|
+
schema: z3.object({
|
|
917
|
+
currency: z3.enum(["BTC", "ETH"]).default("BTC").describe("Which DVOL volatility index to plot \u2014 BTC or ETH."),
|
|
918
|
+
lookback: z3.enum(["7D", "1M", "3M"]).default("1M").describe("History window for the volatility-index line.")
|
|
919
|
+
})
|
|
920
|
+
});
|
|
921
|
+
var optionsOiStrikeMeta = defineFrameMeta({
|
|
922
|
+
name: "options-oi-strike",
|
|
923
|
+
iconUrl: widgetIcon("options-oi-strike"),
|
|
924
|
+
layout: { w: 6, h: 4, minW: 4, minH: 3 },
|
|
925
|
+
description: "Open interest by strike for the nearest Deribit options expiry (BTC or ETH) \u2014 a grouped histogram of call vs put open interest across strikes, with the current spot marked. Surfaces the strike 'walls' where positioning is concentrated. Keyless Deribit market data.",
|
|
926
|
+
capabilities: ["options-summary"],
|
|
927
|
+
source: SOURCES.deribit,
|
|
928
|
+
schema: z3.object({
|
|
929
|
+
currency: z3.enum(["BTC", "ETH"]).default("BTC").describe("Which Deribit options book \u2014 BTC or ETH."),
|
|
930
|
+
strikes: z3.number().int().min(6).max(30).default(14).describe(
|
|
931
|
+
"How many strikes nearest the current spot to show (centered on the underlying price)."
|
|
932
|
+
)
|
|
933
|
+
})
|
|
934
|
+
});
|
|
935
|
+
var coinMoversMeta = defineFrameMeta({
|
|
936
|
+
name: "coin-movers",
|
|
937
|
+
iconUrl: widgetIcon("coin-movers"),
|
|
938
|
+
layout: { w: 5, h: 4, minW: 3, minH: 3 },
|
|
939
|
+
description: "Broad-market crypto gainers and losers across the top ~300 coins by market cap, over a selectable window (1h / 24h / 7d / 30d) \u2014 the biggest movers side by side with price and % change. Unlike a top-coins heatmap, this surfaces mid- and small-caps that ripped or dumped, not just the megacaps. Keyless data from Coinpaprika. Crypto only.",
|
|
940
|
+
capabilities: ["coin-movers"],
|
|
941
|
+
source: SOURCES.coinpaprika,
|
|
942
|
+
schema: z3.object({
|
|
943
|
+
window: z3.enum(["1h", "24h", "7d", "30d"]).default("24h").describe(
|
|
944
|
+
"Which price-change window ranks the movers: 1h (intraday momentum), 24h (daily), 7d (weekly), 30d (monthly)."
|
|
945
|
+
),
|
|
946
|
+
count: z3.number().int().min(3).max(15).default(6).describe("How many gainers and how many losers to list (each side)."),
|
|
947
|
+
minRank: z3.number().int().min(20).max(300).default(150).describe(
|
|
948
|
+
"Only consider coins ranked at or above this market-cap rank \u2014 a liquidity floor that keeps illiquid micro-cap dust (which posts absurd % moves on no volume) out of the list. Lower = stricter (megacaps only); higher = includes more small-caps."
|
|
949
|
+
)
|
|
950
|
+
})
|
|
951
|
+
});
|
|
515
952
|
var frameMetas = [
|
|
953
|
+
newsFeedMeta,
|
|
516
954
|
allocationMeta,
|
|
517
955
|
bitcoinDominanceMeta,
|
|
518
956
|
clockMeta,
|
|
@@ -521,6 +959,10 @@ var frameMetas = [
|
|
|
521
959
|
fearGreedMeta,
|
|
522
960
|
filingsFeedMeta,
|
|
523
961
|
fundamentalsMeta,
|
|
962
|
+
financialStressMeta,
|
|
963
|
+
laborMarketMeta,
|
|
964
|
+
nationalDebtMeta,
|
|
965
|
+
treasuryAuctionsMeta,
|
|
524
966
|
fundingHeatmapMeta,
|
|
525
967
|
fundingRateChartMeta,
|
|
526
968
|
headingMeta,
|
|
@@ -536,7 +978,34 @@ var frameMetas = [
|
|
|
536
978
|
shortVolumeMeta,
|
|
537
979
|
topMoversMeta,
|
|
538
980
|
tvlTreemapMeta,
|
|
539
|
-
yieldCurveMeta
|
|
981
|
+
yieldCurveMeta,
|
|
982
|
+
dexVolumeTreemapMeta,
|
|
983
|
+
dexVolumeChartMeta,
|
|
984
|
+
protocolTvlTreemapMeta,
|
|
985
|
+
protocolTvlChartMeta,
|
|
986
|
+
protocolFeesTreemapMeta,
|
|
987
|
+
marketCapTreemapMeta,
|
|
988
|
+
openInterestMeta,
|
|
989
|
+
snakeMeta,
|
|
990
|
+
flappyBirdMeta,
|
|
991
|
+
videoMeta,
|
|
992
|
+
drawdyMeta,
|
|
993
|
+
countdownMeta,
|
|
994
|
+
linkGridMeta,
|
|
995
|
+
calculatorMeta,
|
|
996
|
+
quoteMeta,
|
|
997
|
+
dividerMeta,
|
|
998
|
+
btcFeesMeta,
|
|
999
|
+
btcMempoolMeta,
|
|
1000
|
+
btcBlocksMeta,
|
|
1001
|
+
btcHashrateMeta,
|
|
1002
|
+
btcDifficultyMeta,
|
|
1003
|
+
miningPoolsMeta,
|
|
1004
|
+
lightningStatsMeta,
|
|
1005
|
+
optionsPutCallMeta,
|
|
1006
|
+
optionsIvMeta,
|
|
1007
|
+
optionsOiStrikeMeta,
|
|
1008
|
+
coinMoversMeta
|
|
540
1009
|
];
|
|
541
1010
|
|
|
542
1011
|
// src/init.ts
|
|
@@ -582,7 +1051,7 @@ function skeleton(title, author) {
|
|
|
582
1051
|
background: {
|
|
583
1052
|
type: "unicorn",
|
|
584
1053
|
projectId: "YrTzGatwjK7EoFpCSfgZ",
|
|
585
|
-
opacity:
|
|
1054
|
+
opacity: 1
|
|
586
1055
|
},
|
|
587
1056
|
theme: { accentHue: 242, accentSat: 90 },
|
|
588
1057
|
appearance: {
|
|
@@ -643,17 +1112,76 @@ function init(args) {
|
|
|
643
1112
|
return 0;
|
|
644
1113
|
}
|
|
645
1114
|
|
|
1115
|
+
// src/lint.ts
|
|
1116
|
+
function lintSpec(spec) {
|
|
1117
|
+
const issues = [];
|
|
1118
|
+
const metaByName = new Map(frameMetas.map((meta) => [meta.name, meta]));
|
|
1119
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1120
|
+
for (const instance of spec.frames) {
|
|
1121
|
+
if (seenIds.has(instance.id))
|
|
1122
|
+
issues.push({
|
|
1123
|
+
frameId: instance.id,
|
|
1124
|
+
message: `duplicate frame id "${instance.id}"`
|
|
1125
|
+
});
|
|
1126
|
+
seenIds.add(instance.id);
|
|
1127
|
+
const meta = metaByName.get(instance.frame);
|
|
1128
|
+
if (!meta) {
|
|
1129
|
+
issues.push({
|
|
1130
|
+
frameId: instance.id,
|
|
1131
|
+
message: `unknown frame "${instance.frame}". available: ${[
|
|
1132
|
+
...metaByName.keys()
|
|
1133
|
+
].join(", ")}`
|
|
1134
|
+
});
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
const parsed = meta.schema.safeParse(instance.config);
|
|
1138
|
+
if (!parsed.success) {
|
|
1139
|
+
for (const issue of parsed.error.issues) {
|
|
1140
|
+
issues.push({
|
|
1141
|
+
frameId: instance.id,
|
|
1142
|
+
message: `config.${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
if (instance.position.x + instance.position.w > spec.grid.columns)
|
|
1147
|
+
issues.push({
|
|
1148
|
+
frameId: instance.id,
|
|
1149
|
+
message: `overflows the grid: x(${instance.position.x}) + w(${instance.position.w}) > ${spec.grid.columns} columns`
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
for (let i = 0; i < spec.frames.length; i++) {
|
|
1153
|
+
for (let j = i + 1; j < spec.frames.length; j++) {
|
|
1154
|
+
const a = spec.frames[i].position;
|
|
1155
|
+
const b = spec.frames[j].position;
|
|
1156
|
+
const overlap = a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
1157
|
+
if (overlap)
|
|
1158
|
+
issues.push({
|
|
1159
|
+
frameId: spec.frames[i].id,
|
|
1160
|
+
message: `overlaps frame "${spec.frames[j].id}"`
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return issues;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
646
1167
|
// src/serve.ts
|
|
647
|
-
import {
|
|
1168
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
648
1169
|
import { createServer } from "http";
|
|
649
|
-
import {
|
|
1170
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
650
1171
|
import { fileURLToPath } from "url";
|
|
1172
|
+
import sirv from "sirv";
|
|
651
1173
|
|
|
652
1174
|
// ../core/src/serve.ts
|
|
653
1175
|
import { readFile, writeFile } from "fs/promises";
|
|
1176
|
+
|
|
1177
|
+
// ../core/src/routes.ts
|
|
654
1178
|
var DASHBOARD_READ_ROUTE = "/__zframes/dashboard.json";
|
|
655
1179
|
var DASHBOARD_WRITE_ROUTE = "/__zframes/dashboard";
|
|
656
1180
|
var DASHBOARD_PROXY_ROUTE = "/__zframes/proxy";
|
|
1181
|
+
var AGENTS_LIST_ROUTE = "/__zframes/agents";
|
|
1182
|
+
var ASK_ROUTE = "/__zframes/ask";
|
|
1183
|
+
|
|
1184
|
+
// ../core/src/serve.ts
|
|
657
1185
|
var MAX_BODY_BYTES = 2e6;
|
|
658
1186
|
var PROXY_ALLOW_HOSTS = /* @__PURE__ */ new Set([
|
|
659
1187
|
"data.sec.gov",
|
|
@@ -666,7 +1194,15 @@ var PROXY_ALLOW_HOSTS = /* @__PURE__ */ new Set([
|
|
|
666
1194
|
"markets.newyorkfed.org",
|
|
667
1195
|
"api.fiscaldata.treasury.gov",
|
|
668
1196
|
"api.bls.gov",
|
|
669
|
-
"cdn.finra.org"
|
|
1197
|
+
"cdn.finra.org",
|
|
1198
|
+
// News-outlet RSS feeds (CORS-blocked, so the news-feed frame reads them
|
|
1199
|
+
// through here). Headlines + links only; no keys.
|
|
1200
|
+
"www.coindesk.com",
|
|
1201
|
+
"cointelegraph.com",
|
|
1202
|
+
"decrypt.co",
|
|
1203
|
+
"www.cnbc.com",
|
|
1204
|
+
"www.nasdaq.com",
|
|
1205
|
+
"news.google.com"
|
|
670
1206
|
]);
|
|
671
1207
|
var PROXY_MAX_BYTES = 16e6;
|
|
672
1208
|
var PROXY_TIMEOUT_MS = 2e4;
|
|
@@ -780,8 +1316,6 @@ import { access, readFile as readFile2 } from "fs/promises";
|
|
|
780
1316
|
import { constants } from "fs";
|
|
781
1317
|
import { tmpdir } from "os";
|
|
782
1318
|
import { dirname as dirname2, join as join2 } from "path";
|
|
783
|
-
var AGENTS_LIST_ROUTE = "/__zframes/agents";
|
|
784
|
-
var ASK_ROUTE = "/__zframes/ask";
|
|
785
1319
|
var MAX_BODY_BYTES2 = 64e3;
|
|
786
1320
|
var RUN_TIMEOUT_MS = 12e4;
|
|
787
1321
|
var RUNNERS = [
|
|
@@ -987,23 +1521,6 @@ function handleAsk(req, res, specFile) {
|
|
|
987
1521
|
|
|
988
1522
|
// src/serve.ts
|
|
989
1523
|
var DEFAULT_PORT = 37263;
|
|
990
|
-
var MIME = {
|
|
991
|
-
".html": "text/html; charset=utf-8",
|
|
992
|
-
".js": "text/javascript; charset=utf-8",
|
|
993
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
994
|
-
".css": "text/css; charset=utf-8",
|
|
995
|
-
".json": "application/json; charset=utf-8",
|
|
996
|
-
".map": "application/json; charset=utf-8",
|
|
997
|
-
".svg": "image/svg+xml",
|
|
998
|
-
".png": "image/png",
|
|
999
|
-
".jpg": "image/jpeg",
|
|
1000
|
-
".jpeg": "image/jpeg",
|
|
1001
|
-
".gif": "image/gif",
|
|
1002
|
-
".webp": "image/webp",
|
|
1003
|
-
".ico": "image/x-icon",
|
|
1004
|
-
".woff2": "font/woff2",
|
|
1005
|
-
".txt": "text/plain; charset=utf-8"
|
|
1006
|
-
};
|
|
1007
1524
|
function parseArgs2(args) {
|
|
1008
1525
|
let file = "dashboard.json";
|
|
1009
1526
|
let port = DEFAULT_PORT;
|
|
@@ -1029,31 +1546,6 @@ function parseArgs2(args) {
|
|
|
1029
1546
|
}
|
|
1030
1547
|
return { file, port, contact };
|
|
1031
1548
|
}
|
|
1032
|
-
function resolveWithin(rootDir, decodedPath) {
|
|
1033
|
-
const rel = decodedPath === "/" ? "/index.html" : decodedPath;
|
|
1034
|
-
const abs = resolve2(rootDir, `.${rel}`);
|
|
1035
|
-
if (abs !== rootDir && !abs.startsWith(rootDir + sep)) return null;
|
|
1036
|
-
return abs;
|
|
1037
|
-
}
|
|
1038
|
-
function sendFile(absPath, res) {
|
|
1039
|
-
res.statusCode = 200;
|
|
1040
|
-
res.setHeader(
|
|
1041
|
-
"content-type",
|
|
1042
|
-
MIME[extname(absPath).toLowerCase()] ?? "application/octet-stream"
|
|
1043
|
-
);
|
|
1044
|
-
createReadStream(absPath).pipe(res);
|
|
1045
|
-
}
|
|
1046
|
-
function tryStatic(rootDir, decodedPath, res) {
|
|
1047
|
-
const abs = resolveWithin(rootDir, decodedPath);
|
|
1048
|
-
if (!abs) return false;
|
|
1049
|
-
try {
|
|
1050
|
-
if (!statSync2(abs).isFile()) return false;
|
|
1051
|
-
} catch {
|
|
1052
|
-
return false;
|
|
1053
|
-
}
|
|
1054
|
-
sendFile(abs, res);
|
|
1055
|
-
return true;
|
|
1056
|
-
}
|
|
1057
1549
|
function serve(args) {
|
|
1058
1550
|
const parsed = parseArgs2(args);
|
|
1059
1551
|
if ("error" in parsed) {
|
|
@@ -1076,6 +1568,9 @@ function serve(args) {
|
|
|
1076
1568
|
console.error(" run `pnpm build:cli` to build it.");
|
|
1077
1569
|
return Promise.resolve(1);
|
|
1078
1570
|
}
|
|
1571
|
+
const serveBundle = sirv(bundleDir, { dev: true });
|
|
1572
|
+
const serveSiblings = sirv(userDir, { dev: true });
|
|
1573
|
+
const serveSpa = sirv(bundleDir, { dev: true, single: true });
|
|
1079
1574
|
return new Promise((done) => {
|
|
1080
1575
|
const server = createServer((req, res) => {
|
|
1081
1576
|
const rawPath = (req.url ?? "/").split("?")[0];
|
|
@@ -1129,9 +1624,18 @@ function serve(args) {
|
|
|
1129
1624
|
res.end();
|
|
1130
1625
|
return;
|
|
1131
1626
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1627
|
+
serveBundle(
|
|
1628
|
+
req,
|
|
1629
|
+
res,
|
|
1630
|
+
() => serveSiblings(
|
|
1631
|
+
req,
|
|
1632
|
+
res,
|
|
1633
|
+
() => serveSpa(req, res, () => {
|
|
1634
|
+
res.statusCode = 404;
|
|
1635
|
+
res.end();
|
|
1636
|
+
})
|
|
1637
|
+
)
|
|
1638
|
+
);
|
|
1135
1639
|
});
|
|
1136
1640
|
server.on("error", (err) => {
|
|
1137
1641
|
if (err.code === "EADDRINUSE") {
|
|
@@ -1144,7 +1648,7 @@ function serve(args) {
|
|
|
1144
1648
|
done(1);
|
|
1145
1649
|
});
|
|
1146
1650
|
server.listen(parsed.port, "127.0.0.1", () => {
|
|
1147
|
-
const url = `http://
|
|
1651
|
+
const url = `http://localhost:${parsed.port}`;
|
|
1148
1652
|
console.log(`\u26A1 zframes is live at ${url}`);
|
|
1149
1653
|
console.log(
|
|
1150
1654
|
` serving ${parsed.file} \u2014 live editing on; drag, resize, then Save writes back.`
|
|
@@ -1157,107 +1661,308 @@ function serve(args) {
|
|
|
1157
1661
|
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
1158
1662
|
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
1159
1663
|
|
|
1664
|
+
// ../core/src/cache.ts
|
|
1665
|
+
var TtlCache = class {
|
|
1666
|
+
entries = /* @__PURE__ */ new Map();
|
|
1667
|
+
inflight = /* @__PURE__ */ new Map();
|
|
1668
|
+
namespace;
|
|
1669
|
+
ttlMs;
|
|
1670
|
+
persist;
|
|
1671
|
+
staleOnError;
|
|
1672
|
+
revive;
|
|
1673
|
+
constructor(options) {
|
|
1674
|
+
this.namespace = options.namespace;
|
|
1675
|
+
this.ttlMs = options.ttlMs;
|
|
1676
|
+
this.persist = options.persist ?? false;
|
|
1677
|
+
this.staleOnError = options.staleOnError ?? true;
|
|
1678
|
+
this.revive = options.revive;
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Resolve the value for `key`, calling `load` only on a miss or a stale entry.
|
|
1682
|
+
* Serves a fresh cached value with no network call, coalesces concurrent loads
|
|
1683
|
+
* onto one promise, and (by default) serves the last good value if `load` throws.
|
|
1684
|
+
*/
|
|
1685
|
+
get(key, load) {
|
|
1686
|
+
const entry = this.read(key);
|
|
1687
|
+
if (entry && Date.now() - entry.at < this.ttlMs)
|
|
1688
|
+
return Promise.resolve(entry.value);
|
|
1689
|
+
const pending = this.inflight.get(key);
|
|
1690
|
+
if (pending) return pending;
|
|
1691
|
+
const promise = load().then((value) => {
|
|
1692
|
+
this.write(key, value);
|
|
1693
|
+
return value;
|
|
1694
|
+
}).catch((error) => {
|
|
1695
|
+
if (this.staleOnError && entry) return entry.value;
|
|
1696
|
+
throw error;
|
|
1697
|
+
}).finally(() => {
|
|
1698
|
+
this.inflight.delete(key);
|
|
1699
|
+
});
|
|
1700
|
+
this.inflight.set(key, promise);
|
|
1701
|
+
return promise;
|
|
1702
|
+
}
|
|
1703
|
+
/** Hydrate from memo, falling back to localStorage on a cold (persisted) read. */
|
|
1704
|
+
read(key) {
|
|
1705
|
+
const memo = this.entries.get(key);
|
|
1706
|
+
if (memo) return memo;
|
|
1707
|
+
if (!this.persist || typeof localStorage === "undefined") return null;
|
|
1708
|
+
try {
|
|
1709
|
+
const raw = localStorage.getItem(this.storageKey(key));
|
|
1710
|
+
if (!raw) return null;
|
|
1711
|
+
const parsed = JSON.parse(raw);
|
|
1712
|
+
if (typeof parsed?.at !== "number") return null;
|
|
1713
|
+
let value;
|
|
1714
|
+
if (this.revive) {
|
|
1715
|
+
const revived = this.revive(parsed.value);
|
|
1716
|
+
if (revived === null) return null;
|
|
1717
|
+
value = revived;
|
|
1718
|
+
} else {
|
|
1719
|
+
value = parsed.value;
|
|
1720
|
+
}
|
|
1721
|
+
const entry = { at: parsed.at, value };
|
|
1722
|
+
this.entries.set(key, entry);
|
|
1723
|
+
return entry;
|
|
1724
|
+
} catch {
|
|
1725
|
+
return null;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
write(key, value) {
|
|
1729
|
+
const entry = { at: Date.now(), value };
|
|
1730
|
+
this.entries.set(key, entry);
|
|
1731
|
+
if (!this.persist || typeof localStorage === "undefined") return;
|
|
1732
|
+
try {
|
|
1733
|
+
localStorage.setItem(this.storageKey(key), JSON.stringify(entry));
|
|
1734
|
+
} catch {
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
storageKey(key) {
|
|
1738
|
+
return `${this.namespace}:${key}`;
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1160
1742
|
// ../core/src/fetch.ts
|
|
1161
1743
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
1162
1744
|
var USER_AGENT = "zframes (+https://github.com/zentryhq/zframes)";
|
|
1163
|
-
|
|
1164
|
-
async function fetchJson(url, schema, { timeoutMs = DEFAULT_TIMEOUT_MS, init: init2, proxied } = {}) {
|
|
1745
|
+
async function request(url, { timeoutMs = DEFAULT_TIMEOUT_MS, init: init2, proxied }) {
|
|
1165
1746
|
const headers = new Headers(init2?.headers);
|
|
1166
1747
|
if (typeof document === "undefined" && !headers.has("User-Agent")) {
|
|
1167
1748
|
headers.set("User-Agent", USER_AGENT);
|
|
1168
1749
|
}
|
|
1169
|
-
const target = proxied && typeof document !== "undefined" ? `${
|
|
1750
|
+
const target = proxied && typeof document !== "undefined" ? `${DASHBOARD_PROXY_ROUTE}?url=${encodeURIComponent(url)}` : url;
|
|
1170
1751
|
const res = await fetch(target, {
|
|
1171
1752
|
...init2,
|
|
1172
1753
|
headers,
|
|
1173
1754
|
signal: init2?.signal ?? AbortSignal.timeout(timeoutMs)
|
|
1174
1755
|
});
|
|
1175
1756
|
if (!res.ok) throw new Error(`${url} failed: ${res.status}`);
|
|
1757
|
+
return res;
|
|
1758
|
+
}
|
|
1759
|
+
async function fetchJson(url, schema, opts = {}) {
|
|
1760
|
+
const res = await request(url, opts);
|
|
1176
1761
|
const body = await res.json();
|
|
1177
1762
|
return schema ? schema.parse(body) : body;
|
|
1178
1763
|
}
|
|
1179
1764
|
|
|
1180
1765
|
// ../provider-alternativeme/src/index.ts
|
|
1181
1766
|
var FNG_URL = "https://api.alternative.me/fng/";
|
|
1767
|
+
var sentimentCache = new TtlCache({
|
|
1768
|
+
namespace: "zframes:alternativeme:fng",
|
|
1769
|
+
ttlMs: 45 * 6e4,
|
|
1770
|
+
persist: true,
|
|
1771
|
+
revive: (value) => Array.isArray(value) ? value : null
|
|
1772
|
+
});
|
|
1182
1773
|
var AlternativeMeProvider = class {
|
|
1183
1774
|
name = "alternative.me";
|
|
1184
1775
|
capabilities = ["sentiment"];
|
|
1185
1776
|
async getFearGreed(limit = 30) {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1777
|
+
return sentimentCache.get(String(limit), async () => {
|
|
1778
|
+
const body = await fetchJson(`${FNG_URL}?limit=${limit}`);
|
|
1779
|
+
if (!Array.isArray(body?.data))
|
|
1780
|
+
throw new Error("alternative.me fng: unexpected response shape");
|
|
1781
|
+
return body.data.map((entry) => ({
|
|
1782
|
+
value: Number(entry.value),
|
|
1783
|
+
classification: entry.value_classification ?? "",
|
|
1784
|
+
time: Number(entry.timestamp) * 1e3
|
|
1785
|
+
})).filter(
|
|
1786
|
+
(point) => Number.isFinite(point.value) && Number.isFinite(point.time)
|
|
1787
|
+
);
|
|
1788
|
+
});
|
|
1196
1789
|
}
|
|
1197
1790
|
};
|
|
1198
1791
|
|
|
1199
1792
|
// ../provider-coingecko/src/index.ts
|
|
1200
1793
|
var GLOBAL_URL = "https://api.coingecko.com/api/v3/global";
|
|
1201
|
-
var
|
|
1202
|
-
var
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
} catch {
|
|
1215
|
-
}
|
|
1216
|
-
return null;
|
|
1217
|
-
}
|
|
1218
|
-
function writeCache(value) {
|
|
1219
|
-
memo = { at: Date.now(), value };
|
|
1220
|
-
try {
|
|
1221
|
-
if (typeof localStorage !== "undefined")
|
|
1222
|
-
localStorage.setItem(CACHE_KEY, JSON.stringify(memo));
|
|
1223
|
-
} catch {
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1794
|
+
var MARKETS_URL = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=50&page=1&sparkline=false&price_change_percentage=24h";
|
|
1795
|
+
var globalCache = new TtlCache({
|
|
1796
|
+
namespace: "zframes:coingecko:global",
|
|
1797
|
+
ttlMs: 12 * 6e4,
|
|
1798
|
+
persist: true
|
|
1799
|
+
});
|
|
1800
|
+
var marketsCache = new TtlCache({
|
|
1801
|
+
namespace: "zframes:coingecko:markets",
|
|
1802
|
+
ttlMs: 10 * 6e4,
|
|
1803
|
+
persist: true
|
|
1804
|
+
});
|
|
1226
1805
|
var CoinGeckoProvider = class {
|
|
1227
1806
|
name = "coingecko";
|
|
1228
|
-
capabilities = [
|
|
1807
|
+
capabilities = [
|
|
1808
|
+
"global-market",
|
|
1809
|
+
"coin-markets"
|
|
1810
|
+
];
|
|
1229
1811
|
async getGlobalMarket() {
|
|
1230
|
-
|
|
1231
|
-
if (cached && Date.now() - cached.at < TTL_MS) return cached.value;
|
|
1232
|
-
try {
|
|
1812
|
+
return globalCache.get("global", async () => {
|
|
1233
1813
|
const body = await fetchJson(GLOBAL_URL);
|
|
1234
1814
|
if (!body?.data?.total_market_cap || !body.data.market_cap_percentage)
|
|
1235
1815
|
throw new Error("coingecko global: unexpected response shape");
|
|
1236
1816
|
const change = Number(body.data.market_cap_change_percentage_24h_usd);
|
|
1237
|
-
|
|
1817
|
+
return {
|
|
1238
1818
|
totalMarketCapUsd: body.data.total_market_cap.usd ?? 0,
|
|
1239
1819
|
marketCapChangePct24h: Number.isFinite(change) ? change : 0,
|
|
1240
1820
|
dominance: body.data.market_cap_percentage
|
|
1241
1821
|
};
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
async getCoinMarkets() {
|
|
1825
|
+
return marketsCache.get("markets", async () => {
|
|
1826
|
+
const body = await fetchJson(MARKETS_URL);
|
|
1827
|
+
if (!Array.isArray(body))
|
|
1828
|
+
throw new Error("coingecko markets: unexpected response shape");
|
|
1829
|
+
return body.filter((c) => Number.isFinite(c.market_cap) && c.market_cap > 0).map((c) => ({
|
|
1830
|
+
symbol: (c.symbol ?? "").toUpperCase(),
|
|
1831
|
+
name: c.name,
|
|
1832
|
+
marketCapUsd: c.market_cap,
|
|
1833
|
+
changePct24h: Number.isFinite(c.price_change_percentage_24h) ? c.price_change_percentage_24h : void 0
|
|
1834
|
+
}));
|
|
1835
|
+
});
|
|
1248
1836
|
}
|
|
1249
1837
|
};
|
|
1250
1838
|
|
|
1251
1839
|
// ../provider-defillama/src/index.ts
|
|
1252
1840
|
var CHAINS_URL = "https://api.llama.fi/v2/chains";
|
|
1841
|
+
var DEXS_URL = "https://api.llama.fi/overview/dexs";
|
|
1842
|
+
var FEES_URL = "https://api.llama.fi/overview/fees";
|
|
1843
|
+
var PROTOCOLS_URL = "https://api.llama.fi/protocols";
|
|
1844
|
+
var SNAPSHOT_TTL_MS = 8 * 6e4;
|
|
1845
|
+
var HISTORY_TTL_MS = 30 * 6e4;
|
|
1846
|
+
var tvlCache = new TtlCache({
|
|
1847
|
+
namespace: "zframes:defillama:tvl",
|
|
1848
|
+
ttlMs: SNAPSHOT_TTL_MS
|
|
1849
|
+
});
|
|
1850
|
+
var dexVolumeCache = new TtlCache({
|
|
1851
|
+
namespace: "zframes:defillama:dex-volume",
|
|
1852
|
+
ttlMs: SNAPSHOT_TTL_MS
|
|
1853
|
+
});
|
|
1854
|
+
var protocolTvlCache = new TtlCache({
|
|
1855
|
+
namespace: "zframes:defillama:protocol-tvl",
|
|
1856
|
+
ttlMs: SNAPSHOT_TTL_MS
|
|
1857
|
+
});
|
|
1858
|
+
var protocolFeesCache = new TtlCache({
|
|
1859
|
+
namespace: "zframes:defillama:protocol-fees",
|
|
1860
|
+
ttlMs: SNAPSHOT_TTL_MS
|
|
1861
|
+
});
|
|
1862
|
+
var dexHistoryCache = new TtlCache({
|
|
1863
|
+
namespace: "zframes:defillama:dex-history",
|
|
1864
|
+
ttlMs: HISTORY_TTL_MS
|
|
1865
|
+
});
|
|
1866
|
+
var protocolHistoryCache = new TtlCache({
|
|
1867
|
+
namespace: "zframes:defillama:protocol-history",
|
|
1868
|
+
ttlMs: HISTORY_TTL_MS
|
|
1869
|
+
});
|
|
1870
|
+
var slugKey = (slugs) => [...slugs].sort().join(",");
|
|
1871
|
+
function toSeries(chart) {
|
|
1872
|
+
if (!Array.isArray(chart)) return [];
|
|
1873
|
+
return chart.map(([ts, value]) => ({ time: ts * 1e3, value: Number(value) })).filter((p) => Number.isFinite(p.time) && Number.isFinite(p.value));
|
|
1874
|
+
}
|
|
1875
|
+
var changeOf = (v) => Number.isFinite(v) ? v : void 0;
|
|
1253
1876
|
var DefiLlamaProvider = class {
|
|
1254
1877
|
name = "defillama";
|
|
1255
|
-
capabilities = [
|
|
1878
|
+
capabilities = [
|
|
1879
|
+
"tvl",
|
|
1880
|
+
"dex-volume",
|
|
1881
|
+
"protocol-tvl",
|
|
1882
|
+
"protocol-fees"
|
|
1883
|
+
];
|
|
1256
1884
|
async getTvlByChain() {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1885
|
+
return tvlCache.get("chains", async () => {
|
|
1886
|
+
const chains = await fetchJson(CHAINS_URL);
|
|
1887
|
+
if (!Array.isArray(chains))
|
|
1888
|
+
throw new Error("defillama chains: unexpected response shape");
|
|
1889
|
+
return chains.filter((chain) => Number.isFinite(chain.tvl) && chain.tvl > 0).sort((a, b) => b.tvl - a.tvl).map((chain) => ({ name: chain.name, tvl: chain.tvl }));
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
async getDexVolume() {
|
|
1893
|
+
return dexVolumeCache.get("overview", async () => {
|
|
1894
|
+
const body = await fetchJson(DEXS_URL);
|
|
1895
|
+
const protocols = body?.protocols;
|
|
1896
|
+
if (!Array.isArray(protocols))
|
|
1897
|
+
throw new Error("defillama dexs: unexpected response shape");
|
|
1898
|
+
return protocols.filter((p) => Number.isFinite(p.total24h) && (p.total24h ?? 0) > 0).map((p) => ({
|
|
1899
|
+
name: p.name,
|
|
1900
|
+
volume24h: p.total24h,
|
|
1901
|
+
changePct: changeOf(p.change_1d)
|
|
1902
|
+
})).sort((a, b) => b.volume24h - a.volume24h);
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
async getDexVolumeHistory(slugs) {
|
|
1906
|
+
return dexHistoryCache.get(slugKey(slugs), async () => {
|
|
1907
|
+
const pairs = await Promise.all(
|
|
1908
|
+
slugs.map(
|
|
1909
|
+
(slug) => fetchJson(
|
|
1910
|
+
`https://api.llama.fi/summary/dexs/${encodeURIComponent(
|
|
1911
|
+
slug
|
|
1912
|
+
)}?excludeTotalDataChartBreakdown=true`
|
|
1913
|
+
).then((body) => [slug, toSeries(body.totalDataChart)]).catch(() => [slug, []])
|
|
1914
|
+
)
|
|
1915
|
+
);
|
|
1916
|
+
return Object.fromEntries(pairs);
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
async getProtocolTvl() {
|
|
1920
|
+
return protocolTvlCache.get("overview", async () => {
|
|
1921
|
+
const protocols = await fetchJson(PROTOCOLS_URL);
|
|
1922
|
+
if (!Array.isArray(protocols))
|
|
1923
|
+
throw new Error("defillama protocols: unexpected response shape");
|
|
1924
|
+
return protocols.filter((p) => Number.isFinite(p.tvl) && (p.tvl ?? 0) > 0).map((p) => ({
|
|
1925
|
+
name: p.name,
|
|
1926
|
+
tvl: p.tvl,
|
|
1927
|
+
category: p.category,
|
|
1928
|
+
changePct: changeOf(p.change_1d)
|
|
1929
|
+
})).sort((a, b) => b.tvl - a.tvl);
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
async getProtocolTvlHistory(slugs) {
|
|
1933
|
+
return protocolHistoryCache.get(slugKey(slugs), async () => {
|
|
1934
|
+
const pairs = await Promise.all(
|
|
1935
|
+
slugs.map(
|
|
1936
|
+
(slug) => fetchJson(
|
|
1937
|
+
`https://api.llama.fi/protocol/${encodeURIComponent(slug)}`
|
|
1938
|
+
).then(
|
|
1939
|
+
(body) => [
|
|
1940
|
+
slug,
|
|
1941
|
+
(body.tvl ?? []).map((p) => ({
|
|
1942
|
+
time: p.date * 1e3,
|
|
1943
|
+
value: Number(p.totalLiquidityUSD)
|
|
1944
|
+
})).filter(
|
|
1945
|
+
(p) => Number.isFinite(p.time) && Number.isFinite(p.value)
|
|
1946
|
+
)
|
|
1947
|
+
]
|
|
1948
|
+
).catch(() => [slug, []])
|
|
1949
|
+
)
|
|
1950
|
+
);
|
|
1951
|
+
return Object.fromEntries(pairs);
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
async getProtocolFees() {
|
|
1955
|
+
return protocolFeesCache.get("overview", async () => {
|
|
1956
|
+
const body = await fetchJson(FEES_URL);
|
|
1957
|
+
const protocols = body?.protocols;
|
|
1958
|
+
if (!Array.isArray(protocols))
|
|
1959
|
+
throw new Error("defillama fees: unexpected response shape");
|
|
1960
|
+
return protocols.filter((p) => Number.isFinite(p.total24h) && (p.total24h ?? 0) > 0).map((p) => ({
|
|
1961
|
+
name: p.name,
|
|
1962
|
+
fees24h: p.total24h,
|
|
1963
|
+
changePct: changeOf(p.change_1d)
|
|
1964
|
+
})).sort((a, b) => b.fees24h - a.fees24h);
|
|
1965
|
+
});
|
|
1261
1966
|
}
|
|
1262
1967
|
};
|
|
1263
1968
|
|
|
@@ -1280,7 +1985,8 @@ var HyperliquidProvider = class {
|
|
|
1280
1985
|
"quote-stream",
|
|
1281
1986
|
"day-stats",
|
|
1282
1987
|
"funding-history",
|
|
1283
|
-
"ohlcv"
|
|
1988
|
+
"ohlcv",
|
|
1989
|
+
"open-interest"
|
|
1284
1990
|
];
|
|
1285
1991
|
ws = null;
|
|
1286
1992
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -1339,6 +2045,39 @@ var HyperliquidProvider = class {
|
|
|
1339
2045
|
);
|
|
1340
2046
|
return out;
|
|
1341
2047
|
}
|
|
2048
|
+
async getOpenInterest(symbols) {
|
|
2049
|
+
const wholeDexes = /* @__PURE__ */ new Set();
|
|
2050
|
+
const concrete = /* @__PURE__ */ new Set();
|
|
2051
|
+
if (!symbols) {
|
|
2052
|
+
wholeDexes.add("");
|
|
2053
|
+
} else {
|
|
2054
|
+
for (const s of symbols) {
|
|
2055
|
+
if (s.endsWith(":*")) wholeDexes.add(s.slice(0, -2));
|
|
2056
|
+
else concrete.add(s);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
const dexes = new Set(wholeDexes);
|
|
2060
|
+
for (const s of concrete) dexes.add(dexOf(s));
|
|
2061
|
+
const out = [];
|
|
2062
|
+
await Promise.all(
|
|
2063
|
+
[...dexes].map(async (dex) => {
|
|
2064
|
+
const body = { type: "metaAndAssetCtxs" };
|
|
2065
|
+
if (dex) body.dex = dex;
|
|
2066
|
+
const [meta, ctxs] = await info(body);
|
|
2067
|
+
const wholeDex = wholeDexes.has(dex);
|
|
2068
|
+
meta.universe.forEach((asset, i) => {
|
|
2069
|
+
if (!wholeDex && !concrete.has(asset.name)) return;
|
|
2070
|
+
const ctx = ctxs[i];
|
|
2071
|
+
if (!ctx) return;
|
|
2072
|
+
const markPx = Number(ctx.markPx);
|
|
2073
|
+
const oi = Number(ctx.openInterest);
|
|
2074
|
+
if (!Number.isFinite(markPx) || !Number.isFinite(oi)) return;
|
|
2075
|
+
out.push({ symbol: asset.name, openInterestUsd: oi * markPx });
|
|
2076
|
+
});
|
|
2077
|
+
})
|
|
2078
|
+
);
|
|
2079
|
+
return out.sort((a, b) => b.openInterestUsd - a.openInterestUsd);
|
|
2080
|
+
}
|
|
1342
2081
|
async getFundingHistory(symbols, startTimeMs) {
|
|
1343
2082
|
const results = await Promise.all(
|
|
1344
2083
|
symbols.map(async (coin) => {
|
|
@@ -1573,7 +2312,7 @@ usage:
|
|
|
1573
2312
|
frames) for the agent to fill in; --title <t>,
|
|
1574
2313
|
--author <a>, --force to overwrite
|
|
1575
2314
|
zframes serve [file] serve <dashboard.json> (default: ./dashboard.json)
|
|
1576
|
-
as a live, editable terminal at
|
|
2315
|
+
as a live, editable terminal at localhost:37263
|
|
1577
2316
|
(--port <n> to change); Save writes back to the file
|
|
1578
2317
|
zframes catalogue print the frame catalogue as JSON Schema
|
|
1579
2318
|
(this is what a generating agent reads)
|
|
@@ -1584,56 +2323,6 @@ usage:
|
|
|
1584
2323
|
stdout (the deterministic half of /zframes-brief)
|
|
1585
2324
|
zframes help this text
|
|
1586
2325
|
`;
|
|
1587
|
-
function lintSpec(spec) {
|
|
1588
|
-
const issues = [];
|
|
1589
|
-
const metaByName = new Map(frameMetas.map((meta) => [meta.name, meta]));
|
|
1590
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
1591
|
-
for (const instance of spec.frames) {
|
|
1592
|
-
if (seenIds.has(instance.id))
|
|
1593
|
-
issues.push({
|
|
1594
|
-
frameId: instance.id,
|
|
1595
|
-
message: `duplicate frame id "${instance.id}"`
|
|
1596
|
-
});
|
|
1597
|
-
seenIds.add(instance.id);
|
|
1598
|
-
const meta = metaByName.get(instance.frame);
|
|
1599
|
-
if (!meta) {
|
|
1600
|
-
issues.push({
|
|
1601
|
-
frameId: instance.id,
|
|
1602
|
-
message: `unknown frame "${instance.frame}". available: ${[
|
|
1603
|
-
...metaByName.keys()
|
|
1604
|
-
].join(", ")}`
|
|
1605
|
-
});
|
|
1606
|
-
continue;
|
|
1607
|
-
}
|
|
1608
|
-
const parsed = meta.schema.safeParse(instance.config);
|
|
1609
|
-
if (!parsed.success) {
|
|
1610
|
-
for (const issue of parsed.error.issues) {
|
|
1611
|
-
issues.push({
|
|
1612
|
-
frameId: instance.id,
|
|
1613
|
-
message: `config.${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
1614
|
-
});
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
if (instance.position.x + instance.position.w > spec.grid.columns)
|
|
1618
|
-
issues.push({
|
|
1619
|
-
frameId: instance.id,
|
|
1620
|
-
message: `overflows the grid: x(${instance.position.x}) + w(${instance.position.w}) > ${spec.grid.columns} columns`
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
for (let i = 0; i < spec.frames.length; i++) {
|
|
1624
|
-
for (let j = i + 1; j < spec.frames.length; j++) {
|
|
1625
|
-
const a = spec.frames[i].position;
|
|
1626
|
-
const b = spec.frames[j].position;
|
|
1627
|
-
const overlap = a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
1628
|
-
if (overlap)
|
|
1629
|
-
issues.push({
|
|
1630
|
-
frameId: spec.frames[i].id,
|
|
1631
|
-
message: `overlaps frame "${spec.frames[j].id}"`
|
|
1632
|
-
});
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
return issues;
|
|
1636
|
-
}
|
|
1637
2326
|
function lint(file) {
|
|
1638
2327
|
let raw;
|
|
1639
2328
|
try {
|
|
@@ -1698,7 +2387,16 @@ async function main() {
|
|
|
1698
2387
|
return 1;
|
|
1699
2388
|
}
|
|
1700
2389
|
}
|
|
1701
|
-
main().then(
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
2390
|
+
main().then(
|
|
2391
|
+
(code) => {
|
|
2392
|
+
if (process.stdout.write("")) {
|
|
2393
|
+
process.exit(code);
|
|
2394
|
+
} else {
|
|
2395
|
+
process.stdout.once("drain", () => process.exit(code));
|
|
2396
|
+
}
|
|
2397
|
+
},
|
|
2398
|
+
(error) => {
|
|
2399
|
+
console.error(error);
|
|
2400
|
+
process.exit(1);
|
|
2401
|
+
}
|
|
2402
|
+
);
|