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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Weblog of stuff

- Add TVL distribution charts for CORE3 and Xerberus risk-rated vaults (2026-07-30)
- Restore site-wide vault search with server-side vault JSON indexing, typeahead and responsive result listings, replacing the retired external integration (2026-07-30)
- Add CORE3 and Xerberus risk-rating vault lists, per-vault Xerberus report links, and a YouTube footer link with reordered social icons (2026-07-30)
- Rank strategy vaults by displayed annual return within green and red chart groups, and clarify the strategy listing description (2026-07-28)
Expand Down
81 changes: 79 additions & 2 deletions src/lib/top-vaults/RiskRatingsPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,26 @@ the score in a column beside each vault name.
import { formatDollar, formatPercent } from '$lib/helpers/formatters';
import { fetchAllVaultData, hasVaultCache } from './client-cache';
import { riskRatingProviders, type RiskRatingProvider } from './risk-rating-providers';
import { getRiskRatedVaults, getRiskRatingStatistics, type RiskRatingStatistics } from './risk-rating-statistics';
import {
getRiskRatedVaults,
getRiskRatingStatistics,
getRiskRatingTvlBands,
type RiskRatingStatistics,
type RiskRatingTvlBand
} from './risk-rating-statistics';
import type { TopVaults } from './schemas';
import TopVaultsPage from './TopVaultsPage.svelte';
import MarketSharePieChart from '../../routes/vaults/MarketSharePieChart.svelte';
import MarketShareWidgetBox from '../../routes/vaults/MarketShareWidgetBox.svelte';
import type { MarketSharePieSlice } from '../../routes/vaults/market-share-pie';

interface Props {
provider: RiskRatingProvider;
initialRatingStatistics?: RiskRatingStatistics;
initialRiskRatingTvlBands?: RiskRatingTvlBand[];
}

let { provider, initialRatingStatistics }: Props = $props();
let { provider, initialRatingStatistics, initialRiskRatingTvlBands }: Props = $props();
let topVaults = $state<TopVaults>();
let loading = $state(!hasVaultCache(page.data.generatedAt));
let providerDetails = $derived(riskRatingProviders[provider]);
Expand All @@ -44,6 +54,14 @@ the score in a column beside each vault name.
let ratingStatistics = $derived.by(() => {
return ratedTopVaults ? getRiskRatingStatistics(ratedTopVaults.vaults) : initialRatingStatistics;
});
let riskRatingTvlBands = $derived(
topVaults ? getRiskRatingTvlBands(topVaults, provider) : (initialRiskRatingTvlBands ?? [])
);
let riskRatingGroupLabel = $derived(provider === 'core3' ? 'CORE3 rating' : 'Risk bracket');
let riskRatingGroupLabelPlural = $derived(provider === 'core3' ? 'CORE3 ratings' : 'risk brackets');
let riskChartTitle = $derived(
`${formatDollar(ratingStatistics?.totalTvl ?? 0, 1, 1)} TVL by ${providerDetails.name} rated risk`
);
let ratingSummary = $derived.by(() => {
const { totalTvl, vaultCount, blockchainCount, averageMonthlyReturn } = ratingStatistics ?? {
totalTvl: 0,
Expand All @@ -62,6 +80,50 @@ the score in a column beside each vault name.
} ${(ratingStatistics?.vaultCount ?? 0) === 1 ? 'vault' : 'vaults'}`
);

const core3ToneColourValues = {
excellent: 'var(--c-success)',
good: 'color-mix(in srgb, var(--c-success), var(--c-warning))',
fair: 'var(--c-warning)',
poor: 'var(--c-error)'
};
const xerberusRiskColourValues = [
'var(--c-error)',
'hsl(18 92% 52%)',
'var(--c-warning)',
'hsl(82 70% 43%)',
'hsl(174 70% 40%)',
'var(--c-success)'
];

function resolveCssColour(colourValue: string): string {
const probe = document.createElement('span');
probe.style.color = colourValue;
document.body.append(probe);
const resolvedColour = getComputedStyle(probe).color;
probe.remove();
return resolvedColour;
}

function getCore3SliceColour(slice: MarketSharePieSlice): string | undefined {
const colourValue = core3ToneColourValues[slice.tone as keyof typeof core3ToneColourValues];
if (!colourValue) return undefined;
return resolveCssColour(colourValue);
}

function getXerberusSliceColour(slice: MarketSharePieSlice): string | undefined {
const bandIndex = Number(slice.slug?.replace('risk-', '')) - 1;
const colourValue = xerberusRiskColourValues[bandIndex];
return colourValue ? resolveCssColour(colourValue) : undefined;
}

function getRiskSliceColour(slice: MarketSharePieSlice): string | undefined {
return provider === 'core3' ? getCore3SliceColour(slice) : getXerberusSliceColour(slice);
}

function formatRiskChartTvl(slice: MarketSharePieSlice): string {
return formatDollar(slice.tvl, 1, 1);
}

$effect(() => {
fetchAllVaultData(page.data.generatedAt)
.then((data) => (topVaults = data))
Expand Down Expand Up @@ -118,6 +180,21 @@ the score in a column beside each vault name.
defaultSort="provider_risk_rating"
defaultDirection={providerDetails.defaultDirection}
>
{#snippet heroAside()}
<MarketShareWidgetBox title={riskChartTitle}>
<MarketSharePieChart
items={riskRatingTvlBands}
groupLabel={riskRatingGroupLabel}
groupLabelPlural={riskRatingGroupLabelPlural}
otherThreshold={0}
labelValueFormatter={formatRiskChartTvl}
getSliceColour={getRiskSliceColour}
centreImageUrl={providerDetails.logoUrl}
testId={`${provider}-risk-by-tvl-pie-chart`}
/>
</MarketShareWidgetBox>
{/snippet}

{#snippet subtitle()}
{#if provider === 'core3'}
<p>
Expand Down
22 changes: 15 additions & 7 deletions src/lib/top-vaults/TopVaultsPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Use `ratingProvider` to show a provider-specific risk rating column.
loading?: boolean;
/** Optional right-hand content for listing detail page overview panels */
detailAside?: Snippet;
/** Optional right-hand content rendered alongside the page heading. */
heroAside?: Snippet;
/** Optional left-hand description box rendered next to detailAside (used by
chain pages on its own, and by stablecoin pages above the about box) */
detailDescription?: Snippet;
Expand Down Expand Up @@ -112,6 +114,7 @@ Use `ratingProvider` to show a provider-specific risk rating column.
defaultDirection,
loading = false,
detailAside,
heroAside,
detailDescription,
beforeTable,
totalVaultCount,
Expand All @@ -125,6 +128,7 @@ Use `ratingProvider` to show a provider-specific risk rating column.
let renderDetailAsideInHero = $derived(
chain && detailAside && !detailDescription && !protocolMetadata && !stablecoinMetadata
);
let showHeroAside = $derived(renderDetailAsideInHero || heroAside);
</script>

<main class="top-vaults-page ds-3">
Expand All @@ -138,7 +142,7 @@ Use `ratingProvider` to show a provider-specific risk rating column.
</div>
{/if}
</div>
<div class:hero-layout={renderDetailAsideInHero}>
<div class:hero-layout={showHeroAside}>
<HeroBanner {subtitle}>
{#snippet title()}
<span class="page-title">
Expand Down Expand Up @@ -168,7 +172,11 @@ Use `ratingProvider` to show a provider-specific risk rating column.
{/snippet}
</HeroBanner>

{#if renderDetailAsideInHero && detailAside}
{#if heroAside}
<aside class="hero-aside">
{@render heroAside()}
</aside>
{:else if renderDetailAsideInHero && detailAside}
<aside class="hero-aside">
{@render detailAside()}
</aside>
Expand Down Expand Up @@ -401,6 +409,11 @@ Use `ratingProvider` to show a provider-specific risk rating column.
.detail-overview-single .detail-aside {
grid-column: auto;
}

.hero-layout {
grid-template-columns: 1fr;
align-items: stretch;
}
}

@media (--viewport-sm-down) {
Expand All @@ -413,11 +426,6 @@ Use `ratingProvider` to show a provider-specific risk rating column.
padding-block: var(--space-xs);
}

.hero-layout {
grid-template-columns: 1fr;
align-items: stretch;
}

.page-title {
display: flex;
flex-wrap: nowrap;
Expand Down
83 changes: 83 additions & 0 deletions src/lib/top-vaults/risk-rating-statistics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, expect, it } from 'vitest';
import { getRiskRatingTvlBands } from './risk-rating-statistics';
import { createTestVault } from './test-utils';

describe('getRiskRatingTvlBands', () => {
it('groups Xerberus-rated TVL into six score bands with the highest scores safest', () => {
const lowerScoreVault = createTestVault('Lower score vault', {
current_nav: 10,
xerberus: {
score: 0,
score_scale: '0_100_higher_is_better',
entity_type: 'pool',
entity_id: 'lower-score-vault',
name: 'Lower score vault',
protocol_slug: null,
report_url: 'https://app.xerberus.io/pool/dendrogram/lower-score-vault',
fetched_at: '2026-07-30T11:30:40Z'
}
});
const higherScoreVault = createTestVault('Higher score vault', {
current_nav: 90,
xerberus: {
score: 100,
score_scale: '0_100_higher_is_better',
entity_type: 'pool',
entity_id: 'higher-score-vault',
name: 'Higher score vault',
protocol_slug: null,
report_url: 'https://app.xerberus.io/pool/dendrogram/higher-score-vault',
fetched_at: '2026-07-30T11:30:40Z'
}
});

const bands = getRiskRatingTvlBands(
{
generated_at: '2026-07-30T11:30:40Z',
vaults: [lowerScoreVault, higherScoreVault],
core3_protocols: {},
curators: {}
},
'xerberus'
);

expect(bands.map(({ label }) => label)).toEqual(['0–16', '17–33', '34–49', '50–66', '67–83', '84–100']);
expect(bands[0]).toMatchObject({ tvl: 10, name: '0–16 · highest risk scores' });
expect(bands[5]).toMatchObject({ tvl: 90, name: '84–100 · safest scores' });
});

it('uses compact CORE3 letter ratings and their existing risk tones', () => {
const saferVault = createTestVault('Safer CORE3 vault', {
current_nav: 75,
core3: {
risk_score: 10,
risk_rating_label: 'AA',
market_cap: null,
core3_ranking: null,
data_coverage: null,
confidence: null
}
});
const riskierVault = createTestVault('Riskier CORE3 vault', {
current_nav: 25,
core3: {
risk_score: 90,
risk_rating_label: 'D',
market_cap: null,
core3_ranking: null,
data_coverage: null,
confidence: null
}
});

const bands = getRiskRatingTvlBands(
{ generated_at: '2026-07-30T11:30:40Z', vaults: [saferVault, riskierVault], core3_protocols: {}, curators: {} },
'core3'
);

expect(bands).toMatchObject([
{ label: 'AA', name: 'AA · lowest risk', tvl: 75, tone: 'excellent' },
{ label: 'D', name: 'D · highest risk', tvl: 25, tone: 'poor' }
]);
});
});
Loading
Loading