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
12 changes: 8 additions & 4 deletions src/lib/blog/client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import type { RequestHandler } from '@sveltejs/kit';
import type { BlogPostDetails, BlogPostIndex } from '$lib/schemas/blog';
import { error } from '@sveltejs/kit';
import { ghostConfig } from '$lib/config';
import { blogPostResponseSchema, blogPostIndexSchema, blogPostIndexItemSchema } from '$lib/schemas/blog';
import { type RequestHandler, error } from '@sveltejs/kit';
import {
type BlogPostDetails,
type BlogPostIndex,
blogPostResponseSchema,
blogPostIndexSchema,
blogPostIndexItemSchema
} from './schemas';

const { contentApiKey, apiUrl } = ghostConfig;

Expand Down
8 changes: 4 additions & 4 deletions src/lib/schemas/blog.ts → src/lib/blog/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from 'zod';
import { isoDateTime } from '$lib/schemas/utility';

const datetime = z.iso.datetime({ offset: true });
const url = z.url();
const positiveInteger = z.int().positive();

Expand All @@ -11,9 +11,9 @@ export const blogPostIndexItemSchema = z.object({
title: z.string(),
feature_image: url,
feature_image_alt: z.string().nullable(),
created_at: datetime,
updated_at: datetime,
published_at: datetime,
created_at: isoDateTime,
updated_at: isoDateTime,
published_at: isoDateTime,
excerpt: z.string()
});

Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/BlogPostTile.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Display a blog post tile - e.g., on main blog roll or home page preview
```
-->
<script lang="ts">
import type { BlogPostIndexItem } from '$lib/schemas/blog';
import type { BlogPostIndexItem } from '$lib/blog/schemas';
import Button from '$lib/components/Button.svelte';
import Timestamp from '$lib/components/Timestamp.svelte';

Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/BlogRoll.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Display a collection of blog posts is a responsive grid layout.
```
-->
<script lang="ts">
import type { BlogPostIndexItem } from '$lib/schemas/blog';
import type { BlogPostIndexItem } from '$lib/blog/schemas';
import BlogPostTile from '$lib/components/BlogPostTile.svelte';

type Props = {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/schemas/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ export type TimeBucket = z.infer<typeof timeBucketEnum>;

export const colorModeEnum = z.enum(['light', 'dark', 'system']);
export type ColorMode = z.infer<typeof colorModeEnum>;

export const isoDateTime = z.iso.datetime({ local: true, offset: true });
export type ISODateTime = z.infer<typeof isoDateTime>;
76 changes: 76 additions & 0 deletions src/lib/top-vaults/TopVaultsRow.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script lang="ts">
import type { VaultInfo } from './schemas';
import { Timestamp } from '$lib/components';
import { getChain } from '$lib/helpers/chain';
import {
formatPercent,
formatNumber,
formatDollar,
formatValue,
formatAmount,
formatShortAddress
} from '$lib/helpers/formatters';

interface Props {
index: number;
vault: VaultInfo;
}

const { index, vault }: Props = $props();
const chain = getChain(vault.chain);
</script>

<tr>
<td>{index}</td>
<td>
<div class="multiline">
<strong>{vault.name}</strong>
{#if vault.protocol}
<span class="protocol">{vault.protocol}</span>
{/if}
</div>
</td>
<td>
{#if chain}
<a href={`/trading-view/${chain.slug}`}>{chain.name}</a>
{:else}
Chain {vault.chain}
{/if}
</td>
<td align="right">{formatDollar(vault.current_tvl_usd, 2, 2)}</td>
<td align="right">{formatPercent(vault['1m_return_ann'], 2)}</td>
<td align="right">{formatPercent(vault['1m_return'], 2)}</td>
<td align="right">{formatPercent(vault['3m_return_ann'], 2)}</td>
<td align="right">{formatPercent(vault['3m_return'], 2)}</td>
<td align="right">{formatNumber(vault['3m_sharpe'])}</td>
<td align="right">{formatPercent(vault['3m_volatility'], 4)}</td>
<td align="right">{formatPercent(vault.lifetime_return_ann, 2)}</td>
<td align="right">{formatPercent(vault.lifetime_return, 2)}</td>
<td align="right">{formatNumber(vault.age_years, 2, 4)}</td>
<td align="center">{formatValue(vault.denomination)}</td>
<td align="right">{formatDollar(vault.peak_tvl_usd, 2, 2)}</td>
<td align="right">{formatAmount(vault.deposit_redeem_count)}</td>
<td align="right">{formatPercent(vault.management_fee, 1)}</td>
<td align="right">{formatPercent(vault.performance_fee, 1)}</td>
<td class="multiline">
<Timestamp date={vault.first_deposit} />
<Timestamp date={vault.last_deposit} />
</td>
<td>{vault.address}</td>
</tr>

<style>
.multiline {
display: grid;
gap: 0.125rem;
}

.protocol {
color: var(--c-text-light);
text-transform: uppercase;
}

a:hover {
text-decoration: underline;
}
</style>
124 changes: 124 additions & 0 deletions src/lib/top-vaults/TopVaultsTable.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<script lang="ts">
import type { TopVaults } from './client';
import Alert from '$lib/components/Alert.svelte';
import Timestamp from '$lib/components/Timestamp.svelte';
import TopVaultsRow from './TopVaultsRow.svelte';
import { formatDollar } from '$lib/helpers/formatters';

interface Props {
topVaults: TopVaults;
}

const { topVaults }: Props = $props();
</script>

<div class="top-vaults-table">
{#if !topVaults.rows.length}
<Alert title="Error">No vault data available.</Alert>
{:else}
<div class="totals">
<span>Total current TVL {formatDollar(topVaults.current_tvl_usd, 2, 2)}</span>
<span>Peak TVL {formatDollar(topVaults.peak_tvl_usd, 2, 2)}</span>
<span>Total vaults {topVaults.rows.length}</span>
<span>Updated <Timestamp date={topVaults.generated_at} relative /></span>
</div>

<div class="table-wrapper">
<table>
<thead>
<tr>
<th></th>
<th>Vault</th>
<th>Chain</th>
<th>Current TVL (USD)</th>
<th>1M return (ann.)</th>
<th>1M return</th>
<th>3M return (ann.)</th>
<th>3M return</th>
<th>3M Sharpe</th>
<th>3M volatility</th>
<th>Lifetime return (ann.)</th>
<th>Lifetime return</th>
<th>Age (years)</th>
<th>Denom-ination</th>
<th>Peak TVL (USD)</th>
<th>Deposits & Redeems</th>
<th>Mgmt fee</th>
<th>Perf fee</th>
<th>First/Last deposit</th>
<th>Vault address</th>
</tr>
</thead>
<tbody>
{#each topVaults.rows as vault, idx (vault.id)}
<TopVaultsRow {vault} index={idx + 1} />
{/each}
</tbody>
</table>
</div>
{/if}
</div>

<style>
.top-vaults-table {
display: grid;
gap: 1rem;

.totals {
display: flex;
flex-wrap: wrap;
gap: 1rem;
color: var(--c-text-extra-light);
font: var(--f-ui-md-medium);
margin-top: 1rem;
}

.table-wrapper {
width: 100%;
overflow-x: auto;
}

table {
width: 100%;
border-collapse: collapse;
min-width: 75rem;
background: var(--c-box-1);
color: inherit;
font: var(--f-mono-xs-regular);
line-height: 1;
letter-spacing: var(--f-text-md-spacing, normal);

@media (--viewport-sm-down) {
min-width: 64rem;
}

@media (--viewport-xs) {
font-size: 14px;
}

:global(:is(td, th)) {
padding: 0.25em 0.5em;
border-block: 1px solid var(--c-text-ultra-light);
vertical-align: top;
}

:global(td) {
/* Alternating column colors */
&:nth-child(even) {
background-color: var(--c-box-3);
}

&:nth-child(odd) {
background-color: var(--c-box-1);
}
}

th {
background: var(--c-box-3);
font-weight: 900;
text-transform: uppercase;
text-align: left;
}
}
}
</style>
40 changes: 40 additions & 0 deletions src/lib/top-vaults/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { publicApiError } from '$lib/helpers/public-api';
import { topVaultsSchema } from './schemas';
import { getChain } from '$lib/helpers/chain';

const TOP_VAULTS_URL = 'https://top-defi-vaults.tradingstrategy.ai/top_vaults_by_chain.json';

export async function fetchTopVaults(
fetch: Fetch,
{ chainId, chainSlug }: { chainId?: number; chainSlug?: string } = {}
) {
const resp = await fetch(TOP_VAULTS_URL);
if (!resp.ok) throw await publicApiError(resp);

const { vaults, generated_at } = topVaultsSchema.parse(await resp.json());

// sort by 1m return descending
vaults.sort((a, b) => b['1m_return'] - a['1m_return']);

// filter by chain if provided
chainId ??= getChain(chainSlug)?.id;
const rows = chainId ? vaults.filter((vault) => vault.chain === chainId) : vaults;

// summary data
let current_tvl_usd = 0;
let peak_tvl_usd = 0;

for (const vault of vaults) {
current_tvl_usd += vault.current_tvl_usd ?? 0;
peak_tvl_usd += vault.peak_tvl_usd ?? 0;
}

return {
rows,
generated_at,
current_tvl_usd,
peak_tvl_usd
};
}

export type TopVaults = Awaited<ReturnType<typeof fetchTopVaults>>;
35 changes: 35 additions & 0 deletions src/lib/top-vaults/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { z } from 'zod';
import { chainId, hexString } from '$lib/eth-defi/schemas/core';
import { isoDateTime } from '$lib/schemas/utility';

export const vaultInfoSchema = z.object({
name: z.string(),
lifetime_return: z.number(),
lifetime_return_ann: z.number(),
'3m_return': z.number().nullable(),
'3m_return_ann': z.number().nullable(),
'3m_sharpe': z.number().nullable(),
'1m_return': z.number(),
'1m_return_ann': z.number(),
'3m_volatility': z.number().nullable(),
denomination: z.string(),
chain: chainId,
peak_tvl_usd: z.number(),
current_tvl_usd: z.number(),
age_years: z.number(),
management_fee: z.number().nullable(),
performance_fee: z.number().nullable(),
deposit_redeem_count: z.int(),
protocol: z.string(),
id: z.string(),
first_deposit: isoDateTime,
last_deposit: isoDateTime,
address: hexString
});
export type VaultInfo = z.infer<typeof vaultInfoSchema>;

export const topVaultsSchema = z.object({
generated_at: isoDateTime,
vaults: vaultInfoSchema.array()
});
// See top-vaults/client for TopVaults type def
1 change: 0 additions & 1 deletion src/routes/blog/[slug=slug]/BlogPostContent.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@

font: var(--f-mono-xs-regular);
line-height: 1;
letter-spacing: var(--f-text-md-spacing, normal);

@media (--viewport-xs) {
font-size: 14px;
Expand Down
2 changes: 1 addition & 1 deletion src/routes/blog/rss.xml/+server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { BlogPostIndexItem } from '$lib/schemas/blog.js';
import type { BlogPostIndexItem } from '$lib/blog/schemas';
import { getPosts, maxAge } from '$lib/blog/client';
import { escapeHtml } from '$lib/helpers/html';

Expand Down
2 changes: 1 addition & 1 deletion src/routes/blog/sitemap.xml/+server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Generate a sitemap for blog posts
*/
import type { BlogPostIndexItem } from '$lib/schemas/blog.js';
import type { BlogPostIndexItem } from '$lib/blog/schemas';
import { SitemapStream } from 'sitemap';
import { Readable } from 'stream';
import { getPosts, maxAge } from '$lib/blog/client';
Expand Down
1 change: 1 addition & 0 deletions src/routes/sitemap-static.xml/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const pages = [
'trading-view/blockchains',
'trading-view/exchanges',
'trading-view/lending-reserves',
'trading-view/vaults',
'trading-view/top-list',
'trading-view/top-list/daily-up',
'trading-view/top-list/daily-down',
Expand Down
10 changes: 10 additions & 0 deletions src/routes/trading-view/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import IconPair from '~icons/local/pair';
import IconPython from '~icons/local/python';
import IconSearch from '~icons/local/search';
import IconWallet from '~icons/local/wallet';

export let data;
const { impressiveNumbers } = data;
Expand Down Expand Up @@ -76,6 +77,15 @@
<Button slot="cta" label="Browse reserves" />
</ContentCard>

<ContentCard title="Vaults" href="/trading-view/vaults">
<IconWallet slot="icon" />
<p>
Explore top-performing vaults across supported chains. Compare TVL, returns, Sharpe ratio, and historical
performance metrics.
</p>
<Button slot="cta" label="See top vaults" />
</ContentCard>

<ContentCard title="Advanced search" href="/search">
<IconSearch slot="icon" />
<p>
Expand Down
Loading
Loading