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
20 changes: 20 additions & 0 deletions src/api/models/MinersDashboard.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { directionalRate, rateUnit } from '../../utils/format';

// Each hub↔spoke leg is its own crown/pool: the forward SOL→spoke quotes plus
// their SOL-hub reverses. Mirrors das-allways' Direction / allways.constants.
export type Direction = 'SOL-BTC' | 'BTC-SOL' | 'SOL-TAO' | 'TAO-SOL';
Expand Down Expand Up @@ -37,6 +39,24 @@ export const decomposeDirection = (
};
};

// Directional presentation of a canonical stored rate for `dir` — "to per 1
// from", what the user receives per 1 sent. Stored rates (quotes, swaps,
// crown, rate history) are ALWAYS canonical "spoke per 1 SOL"; reverse legs
// invert here, at the presentation boundary.
export const directionalRateFor = (
dir: Direction,
rate: string | number | null | undefined,
): number | null => {
const { from, to } = decomposeDirection(dir);
return directionalRate(from, to, rate);
};

// "BTC/SOL" — compact unit for directionalRateFor's output (to per 1 from).
export const rateUnitFor = (dir: Direction): string => {
const { from, to } = decomposeDirection(dir);
return rateUnit(from, to);
};

export type CurrentCrown = {
uid: number | null;
hotkey: string | null;
Expand Down
25 changes: 16 additions & 9 deletions src/components/agents/RateQuoteHelper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ALL_DIRECTIONS,
decomposeDirection,
directionLabel,
directionalRateFor,
useMiners,
type Direction,
} from '../../api';
Expand All @@ -25,10 +26,10 @@ import { formatRate, trimTrailingZeros } from '../../utils/format';
interface BestQuote {
uid: number | null;
hotkey: string;
// The miner's quote for the chosen leg, "dest per 1 source" (forward =
// m.rate, reverse = m.counterRate — see Miner model docs).
// The miner's STORED quote for the chosen leg — canonical "spoke per 1 SOL"
// (forward = m.rate, reverse = m.counterRate — see Miner model docs).
rawRate: string;
// Same value, as a number: destSym per 1 sourceSym.
// Directional "to per 1 from" of the chosen direction (reverse inverts).
effectiveRate: number;
out: string;
}
Expand All @@ -48,9 +49,10 @@ const computeBest = (
): BestQuote | null => {
// A miner serves one hub↔spoke pair; canonical order pins SOL as source, so
// its spoke is the non-SOL leg. The forward leg (SOL→spoke) is quoted in
// m.rate, the reverse (spoke→SOL) in m.counterRate — both "dest per 1
// source", so more output per unit in is always the better deal (highest
// first). Filter case-insensitively so an API casing change can't zero this.
// m.rate, the reverse (spoke→SOL) in m.counterRate — both stored CANONICAL
// ("spoke per 1 SOL"). Convert to the directional "to per 1 from" so more
// output per unit in is always the better deal (highest first). Filter
// case-insensitively so an API casing change can't zero this.
const { spoke, leg } = decomposeDirection(direction);
const candidates = miners
.filter((m) => m.isActive)
Expand All @@ -61,7 +63,7 @@ const computeBest = (
if (minerSpoke !== spoke) return null;
const r = leg === 'reverse' ? m.counterRate : m.rate;
if (!r) return null;
const parsed = parseFloat(r);
const parsed = directionalRateFor(direction, r) ?? 0;
if (!isFinite(parsed) || parsed <= 0) return null;
return { uid: m.uid, hotkey: m.hotkey, rawRate: r, parsed };
})
Expand Down Expand Up @@ -173,9 +175,14 @@ const RateQuoteHelper: React.FC = () => {

// The miner row is canonical (sourceChain=sol, destChain=spoke); the leg
// picks which quote to read (forward = .rate, reverse = .counterRate). Both
// are "dest per 1 source", so the best deal is always the highest.
// stored values are canonical "spoke per 1 SOL", so the best FORWARD quote
// is the highest stored value and the best REVERSE quote the lowest.
const rateField = leg === 'reverse' ? '.counterRate' : '.rate';
const curlCmd = `curl -s https://api.all-ways.io/miners | jq '.[] | select(.isActive and (.sourceChain | ascii_downcase) == "sol" and (.destChain | ascii_downcase) == "${spoke}") | {uid, rate: ${rateField}, hotkey}' | jq -s 'sort_by(-(.rate | tonumber))[0]'`;
const jqSort =
leg === 'reverse'
? 'sort_by(.rate | tonumber)'
: 'sort_by(-(.rate | tonumber))';
const curlCmd = `curl -s https://api.all-ways.io/miners | jq '.[] | select(.isActive and (.sourceChain | ascii_downcase) == "sol" and (.destChain | ascii_downcase) == "${spoke}") | {uid, rate: ${rateField}, hotkey} | select((.rate // "0" | tonumber) > 0)' | jq -s '${jqSort}[0]'`;

return (
<HoverCard
Expand Down
3 changes: 2 additions & 1 deletion src/components/dashboard/AllwaysMarketRate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ const AllwaysMarketRate: React.FC<{
<Box sx={{ maxWidth: 280 }}>
Executed rate of recently completed swaps (points) with an EMA
(line) over the most recent window, plus a dashed line at the
live crown rate. X-axis is time, y-axis is rate in SOL.
live crown rate. X-axis is time; y-axis is the directional rate
— what you receive per 1 you send for the selected direction.
</Box>
}
arrow
Expand Down
48 changes: 32 additions & 16 deletions src/components/dashboard/MarketRateChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { Box, Typography, useTheme, type Theme } from '@mui/material';
import { useAllSwaps, useCurrentCrown } from '../../api';
import {
directionLabel,
directionalRateFor,
rateUnitFor,
type Direction,
} from '../../api/models/MinersDashboard';
import { FONTS } from '../../theme';
Expand Down Expand Up @@ -43,14 +45,15 @@ const accentFor = (theme: Theme, index: number) =>

const labelFor = directionLabel;

// The market-rate chart. With one direction it shows that direction's scatter +
// EMA (with a gradient area fill), live crown reference, and per-timestamp
// volume with a max-volume marker. With two directions it overlays both on a
// single shared price scale — the vertical gap between the two EMA lines IS the
// directional spread — drawing each in its own accent (BTC orange / primary
// blue) over a shared time x-axis and a shared SOL-volume sub-chart. Both modes
// run through one option builder; the few visual differences are gated on the
// number of directions.
// The market-rate chart. All rates render DIRECTIONALLY ("to per 1 from" —
// completedPoints converts the canonical stored values; the crown reference is
// converted here). With one direction it shows that direction's scatter + EMA
// (with a gradient area fill), live crown reference, and per-timestamp volume
// with a max-volume marker. With two directions it overlays both on a single
// shared price scale — only meaningful for same-orientation directions (e.g.
// SOL→BTC vs SOL→TAO); a forward and its reverse now live on reciprocal
// scales. Both modes run through one option builder; the few visual
// differences are gated on the number of directions.
const MarketRateChart: React.FC<{
directions: Direction[];
fill?: boolean;
Expand Down Expand Up @@ -110,7 +113,8 @@ const MarketRateChart: React.FC<{
const prepared = series.map((s, i) => ({
...s,
accent: accentFor(theme, i),
crownRate: crown?.[s.dir]?.rate ?? null,
// Directional, matching the (already-converted) scatter/EMA scale.
crownRate: directionalRateFor(s.dir, crown?.[s.dir]?.rate),
}));

// One shared price range across every direction's rates + EMAs + crowns, so
Expand All @@ -128,8 +132,8 @@ const MarketRateChart: React.FC<{
// Adaptive y-axis precision: a wide span (e.g. once a far-off crown rate is
// included) reads fine as integers, but a tight band would collapse every
// tick to the same rounded value — so show decimals when the span is small.
// Below 0.01 a fixed 2dp collapsed a whole BTC-scale axis (~0.0021 SOL) to
// "0.00", so scale the decimals to the span's magnitude instead.
// Below 0.01 a fixed 2dp collapsed a whole SOL→BTC axis (~0.0021 BTC/SOL)
// to "0.00", so scale the decimals to the span's magnitude instead.
const ySpan = yRange ? yRange.max - yRange.min : 0;
const yDecimals =
ySpan <= 0
Expand Down Expand Up @@ -185,7 +189,7 @@ const MarketRateChart: React.FC<{
// Dashed reference line at a direction's live crown rate so the chart shows
// where "now" sits versus recent fills. Keep the label inside the frame:
// when the crown sits in the top half, render below it, else above.
const crownMarkLine = (rate: number | null, color: string) =>
const crownMarkLine = (rate: number | null, unit: string, color: string) =>
rate != null
? {
silent: true,
Expand All @@ -200,7 +204,7 @@ const MarketRateChart: React.FC<{
color,
fontFamily: FONTS.mono,
fontSize: 9,
formatter: `crown ${formatRate(rate)} SOL`,
formatter: `crown ${formatRate(rate)} ${unit}`,
},
}
: undefined;
Expand Down Expand Up @@ -244,7 +248,11 @@ const MarketRateChart: React.FC<{
},
}),
z: 3,
markLine: crownMarkLine(s.crownRate, single ? crownColor : s.accent),
markLine: crownMarkLine(
s.crownRate,
rateUnitFor(s.dir),
single ? crownColor : s.accent,
),
},
]);

Expand Down Expand Up @@ -290,13 +298,21 @@ const MarketRateChart: React.FC<{
}[],
) => {
const t = params[0]?.axisValue;
// Rate series carry their direction's unit; single mode names them
// 'Rate'/'EMA', so fall back to the lone direction's unit.
const fallbackUnit = rateUnitFor(prepared[0].dir);
const unitByName = new Map(
prepared.map((s) => [labelFor(s.dir), rateUnitFor(s.dir)]),
);
const lines = params
.map((p) => {
const v = Array.isArray(p.value) ? p.value[1] : p.value;
const isVolume = p.seriesName === 'Volume';
const unit = isVolume ? 'SOL vol' : 'SOL';
const unit = isVolume
? 'SOL vol'
: (unitByName.get(p.seriesName) ?? fallbackUnit);
// Volume is an amount (2dp reads fine); a rate needs sig figs
// or a BTC leg (~0.0021 SOL) shows as "0.00".
// or SOL→BTC (~0.0021 BTC/SOL) shows as "0.00".
const shown = isVolume
? Number(v).toFixed(2)
: formatRate(Number(v));
Expand Down
43 changes: 29 additions & 14 deletions src/components/dashboard/MinerRatesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ import {
import { FONTS } from '../../theme';
import CopyableAddress from '../CopyableAddress';
import { MinerRatesTableSkeleton } from './Skeletons';
import { formatRate } from '../../utils/format';
import {
HUB_CHAIN,
directionalRate,
formatRate,
rateUnit,
} from '../../utils/format';

type SortKey = 'uid' | 'rate' | 'collateral' | 'status';
type SortDir = 'asc' | 'desc';
Expand Down Expand Up @@ -62,11 +67,23 @@ const parseRate = (raw: string | null): number => {
const statusRank = (m: Miner) =>
!m.isActive ? 3 : m.hasActiveSwap ? 2 : m.isReserved ? 1 : 0;

// The quoted rate for the active leg. Both rates are "dest per 1 source", so a
// higher value is always more output per unit in — best-first is desc for every
// direction. forward = SOL→spoke (m.rate); reverse = spoke→SOL (m.counterRate).
const directionRate = (m: Miner, filter: DirectionFilter): number =>
filter === 'reverse' ? parseRate(m.counterRate) : parseRate(m.rate);
// The leg's (from, to) chains: forward = SOL→spoke (m.rate), reverse =
// spoke→SOL (m.counterRate). Both STORED values are canonical "spoke per 1
// SOL" (see api/models/Miners.ts) — never per-direction.
const legChains = (m: Miner, filter: DirectionFilter): [string, string] => {
const spoke = minerSpoke(m) ?? '';
return filter === 'reverse' ? [spoke, HUB_CHAIN] : [HUB_CHAIN, spoke];
};

// The DIRECTIONAL rate for the active leg ("to per 1 from" — what the user
// receives per 1 sent; the reverse leg inverts the canonical stored value).
// Higher is always more output per unit in — best-first is desc for every
// direction.
const directionRate = (m: Miner, filter: DirectionFilter): number => {
const [from, to] = legChains(m, filter);
const raw = filter === 'reverse' ? m.counterRate : m.rate;
return directionalRate(from, to, raw) ?? 0;
};

const getSortValue = (
m: Miner,
Expand Down Expand Up @@ -159,9 +176,9 @@ const MinerRatesTable: React.FC<{ syncDirection?: Direction }> = ({
const [statusFilter, setStatusFilter] = useState<StatusFilter>('active');

// When the EMA chart's direction flips, re-default the table to the most
// advantageous rate first. Both legs quote "dest per source", so more output
// is always better → highest first (desc) for every direction. A manual
// re-sort persists until the next flip.
// advantageous rate first. Rates sort DIRECTIONALLY ("to per 1 from"), so
// more output is always better → highest first (desc) for every direction.
// A manual re-sort persists until the next flip.
useEffect(() => {
setSortKey('rate');
setSortDir('desc');
Expand Down Expand Up @@ -211,21 +228,19 @@ const MinerRatesTable: React.FC<{ syncDirection?: Direction }> = ({
const hasSearch = search.trim().length > 0;

const renderRate = (m: Miner) => {
const v =
directionFilter === 'reverse'
? parseRate(m.counterRate)
: parseRate(m.rate);
const v = directionRate(m, directionFilter);
if (v <= 0)
return (
<Box component="span" sx={{ color: disabled }}>
{'—'}
</Box>
);
const [from, to] = legChains(m, directionFilter);
return (
<Box component="span">
{formatRate(v)}
<Box component="span" sx={{ color: 'text.secondary', ml: 0.5 }}>
SOL
{rateUnit(from, to)}
</Box>
</Box>
);
Expand Down
19 changes: 12 additions & 7 deletions src/components/dashboard/OrderbookDepth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { useMiners, type Miner } from '../../api';
import {
decomposeDirection,
directionLabel,
directionalRateFor,
rateUnitFor,
type Direction,
} from '../../api/models/MinersDashboard';
import { FONTS } from '../../theme';
Expand All @@ -35,8 +37,9 @@ const minerSpoke = (m: Miner): string | null => {
// Depth of market for the page's active direction: hittable collateral grouped
// by quoted rate, best rate first, with a cumulative running total. Follows the
// Active Rates table's direction semantics — forward = SOL→spoke (m.rate),
// reverse = spoke→SOL (m.counterRate); both quote "dest per 1 source", so
// higher is always the better rate.
// reverse = spoke→SOL (m.counterRate); stored values are canonical and are
// converted to the DIRECTIONAL "to per 1 from" here, so higher is always the
// better rate.
const OrderbookDepth: React.FC<{
direction: Direction;
embedded?: boolean;
Expand Down Expand Up @@ -80,16 +83,16 @@ const OrderbookDepth: React.FC<{
const capacitySol = parseInt(m.collateral, 10) / 1e9;
if (!Number.isFinite(capacitySol) || capacitySol <= 0) return;
const raw = leg === 'reverse' ? m.counterRate : m.rate;
const r = raw ? parseFloat(raw) : 0;
const r = directionalRateFor(direction, raw) ?? 0;
if (!Number.isFinite(r) || r <= 0) return;
// formatRate is the price LEVEL key — it groups miners, sorts the book,
// and is rendered verbatim (2dp would collapse BTC-scale quotes to 0.00).
const key = formatRate(r);
groups[key] = (groups[key] || 0) + capacitySol;
});

// Both legs quote "dest per 1 source" — more output per unit in is always
// better, so best-first is highest-first for every direction.
// Levels are directional "to per 1 from" — more output per unit in is
// always better, so best-first is highest-first for every direction.
const rates = Object.keys(groups).sort(
(a, b) => parseFloat(b) - parseFloat(a),
);
Expand All @@ -100,7 +103,7 @@ const OrderbookDepth: React.FC<{
cum += capacity;
return { rate: key, capacity, cumCapacity: cum };
});
}, [miners, spoke, leg]);
}, [miners, spoke, leg, direction]);

const maxCum = useMemo(
() =>
Expand Down Expand Up @@ -189,7 +192,9 @@ const OrderbookDepth: React.FC<{
<Table size="small" stickyHeader sx={{ tableLayout: 'fixed' }}>
<TableHead>
<TableRow>
<TableCell sx={{ ...headerSx, width: '34%' }}>Rate</TableCell>
<TableCell sx={{ ...headerSx, width: '34%' }}>
Rate ({rateUnitFor(direction)})
</TableCell>
<TableCell sx={{ ...headerSx, width: '36%' }} align="right">
Capacity (SOL)
</TableCell>
Expand Down
11 changes: 6 additions & 5 deletions src/components/dashboard/RatesTicker.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { Box, Stack } from '@mui/material';
import { useAllSwaps, useCurrentCrown } from '../../api';
import { ALL_DIRECTIONS } from '../../api/models/MinersDashboard';
import { ALL_DIRECTIONS, rateUnitFor } from '../../api/models/MinersDashboard';
import { FONTS } from '../../theme';
import { BlockIndicator } from '../index';
import Ticker from '../Ticker';
Expand All @@ -12,9 +12,10 @@ import { formatRate } from '../../utils/format';
const DIRECTIONS = ALL_DIRECTIONS;

// Mirrors the miners-page StickyNetworkHeader eyebrow — the "updated <ago>"
// indicator plus the current crown holder and its live rate per direction
// (uid N @ rate SOL), then the smoothed EMA market rate. No last-refresh /
// health segment. Both share CrownDirectionSegment; only the EMA suffix is ours.
// indicator plus the current crown holder and its live directional rate per
// direction (uid N @ rate TO/FROM), then the smoothed EMA market rate. No
// last-refresh / health segment. Both share CrownDirectionSegment; only the
// EMA suffix is ours.
const RatesTicker: React.FC = () => {
const { data: crown } = useCurrentCrown();
const { data: swaps } = useAllSwaps({ limit: 600 });
Expand Down Expand Up @@ -86,7 +87,7 @@ const RatesTicker: React.FC = () => {
fontWeight: 400,
}}
>
SOL
{rateUnitFor(dir)}
</Box>
</Box>
</>
Expand Down
Loading
Loading