Skip to content

Commit ea03ce1

Browse files
authored
Merge pull request #999 from tradingstrategy-ai/top-vaults-2
Top vaults pages part 2
2 parents bee49e8 + 89ed6e1 commit ea03ce1

12 files changed

Lines changed: 181 additions & 227 deletions

File tree

src/lib/helpers/public-api.ts

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
/**
2-
* Return appropriate error object for public API errors
3-
*
4-
* NOTE: Don't use this helper for APIs that may include sensitive data
5-
* in error message or stacktrace.
6-
*/
1+
// Helpers for working with Trading Strategy public APIs
72
import { type NumericRange, error } from '@sveltejs/kit';
83
import { backendUrl } from '$lib/config';
94

105
type Params = Record<string, string>;
116

127
const controllers: Record<string, AbortController> = {};
138

9+
/**
10+
* Fetch data from Trading Strategy backend endpoints
11+
*
12+
* @param fetch SvelteKit's fetch function
13+
* @param endpoint final path segment of endpoint, e.g.: 'chains', 'pairs'
14+
* @param params URL paramaters as a record of string values
15+
* @param abortPrevious set to true to abort pending requets to the same endpoint
16+
* @returns the deserialised JSON payload
17+
*/
1418
export async function fetchPublicApi(fetch: Fetch, endpoint: string, params: Params = {}, abortPrevious = false) {
1519
let signal: AbortSignal | undefined = undefined;
1620

@@ -34,6 +38,12 @@ export async function fetchPublicApi(fetch: Fetch, endpoint: string, params: Par
3438
}
3539
}
3640

41+
/**
42+
* Return appropriate error object for public API errors
43+
*
44+
* NOTE: Don't use this helper for APIs that may include sensitive data
45+
* in error message or stacktrace.
46+
*/
3747
export async function publicApiError(response: Response) {
3848
let status = response.status as NumericRange<400, 599>;
3949
if (status >= 500) status = 503;
@@ -45,3 +55,32 @@ export async function publicApiError(response: Response) {
4555

4656
return error(status, { message: response.statusText, stack });
4757
}
58+
59+
/**
60+
* Factory function to generate an error handler that can be used when fetching
61+
* optional data in order to fall back gracefully instead of throwing an error.
62+
*
63+
* Logs error to stderr and fails w/out re-throwing.
64+
*
65+
* @example
66+
* ```typescript
67+
* import { optionalDataError } from '$lib/helpers/public-api';
68+
*
69+
* export async function load({ fetch }) {
70+
* return {
71+
* impressiveNumbers: await fetchPublicApi(fetch, 'impressive-numbers').catch(optionalDataError('impressive-numbers')),
72+
* posts: await getPosts(fetch).catch(optionalDataError('blog posts'))
73+
* };
74+
* }
75+
* ```
76+
*
77+
* @param dataType the type of data being requested, e.g.: 'impressive-numbers'
78+
* @returns error handler function that can be passed to Promise.catch
79+
*/
80+
export function optionalDataError(dataType?: string) {
81+
const errorIntro = dataType ? `Request for ${dataType} failed` : 'Request failed';
82+
return (err: Error) => {
83+
console.error(`${errorIntro}; rendering page without data.`);
84+
console.error(err);
85+
};
86+
}

src/lib/top-vaults/TopVaultsRow.svelte

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
11
<script lang="ts">
22
import type { VaultInfo } from './schemas';
33
import { Timestamp } from '$lib/components';
4-
import { getChain } from '$lib/helpers/chain';
5-
import {
6-
formatPercent,
7-
formatNumber,
8-
formatDollar,
9-
formatValue,
10-
formatAmount,
11-
formatShortAddress
12-
} from '$lib/helpers/formatters';
4+
import { type ApiChain, getChain } from '$lib/helpers/chain';
5+
import { formatPercent, formatNumber, formatDollar, formatValue, formatAmount } from '$lib/helpers/formatters';
136
147
interface Props {
158
index: number;
169
vault: VaultInfo;
10+
chain?: ApiChain | undefined;
1711
}
1812
19-
const { index, vault }: Props = $props();
20-
const chain = getChain(vault.chain);
13+
const { index, vault, chain }: Props = $props();
2114
</script>
2215

2316
<tr>
@@ -30,13 +23,16 @@
3023
{/if}
3124
</div>
3225
</td>
33-
<td>
34-
{#if chain}
35-
<a href={`/trading-view/${chain.slug}`}>{chain.name}</a>
36-
{:else}
37-
Chain {vault.chain}
38-
{/if}
39-
</td>
26+
{#if !chain}
27+
{@const { name, slug } = getChain(vault.chain) ?? {}}
28+
<td>
29+
{#if slug}
30+
<a href={`/trading-view/${slug}`}>{name}</a>
31+
{:else}
32+
Chain {vault.chain}
33+
{/if}
34+
</td>
35+
{/if}
4036
<td align="right">{formatDollar(vault.current_tvl_usd, 2, 2)}</td>
4137
<td align="right">{formatPercent(vault['1m_return_ann'], 2)}</td>
4238
<td align="right">{formatPercent(vault['1m_return'], 2)}</td>

src/lib/top-vaults/TopVaultsTable.svelte

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
<script lang="ts">
2-
import type { TopVaults } from './client';
2+
import type { ApiChain } from '$lib/helpers/chain';
3+
import type { TopVaults } from './schemas';
34
import Alert from '$lib/components/Alert.svelte';
45
import Timestamp from '$lib/components/Timestamp.svelte';
56
import TopVaultsRow from './TopVaultsRow.svelte';
6-
import { formatDollar } from '$lib/helpers/formatters';
77
88
interface Props {
99
topVaults: TopVaults;
10+
chain?: ApiChain;
1011
}
1112
12-
const { topVaults }: Props = $props();
13+
const { topVaults, chain }: Props = $props();
1314
</script>
1415

1516
<div class="top-vaults-table">
16-
{#if !topVaults.rows.length}
17+
{#if !topVaults.vaults.length}
1718
<Alert title="Error">No vault data available.</Alert>
1819
{:else}
19-
<div class="totals">
20-
<span>Total current TVL {formatDollar(topVaults.current_tvl_usd, 2, 2)}</span>
21-
<span>Peak TVL {formatDollar(topVaults.peak_tvl_usd, 2, 2)}</span>
22-
<span>Total vaults {topVaults.rows.length}</span>
20+
<div class="table-meta">
21+
<span>{topVaults.vaults.length} {chain ? chain.chain_name : 'total'} vaults</span>
2322
<span>Updated <Timestamp date={topVaults.generated_at} relative /></span>
2423
</div>
2524

@@ -29,7 +28,9 @@
2928
<tr>
3029
<th></th>
3130
<th>Vault</th>
32-
<th>Chain</th>
31+
{#if !chain}
32+
<th>Chain</th>
33+
{/if}
3334
<th>Current TVL (USD)</th>
3435
<th>1M return (ann.)</th>
3536
<th>1M return</th>
@@ -50,8 +51,8 @@
5051
</tr>
5152
</thead>
5253
<tbody>
53-
{#each topVaults.rows as vault, idx (vault.id)}
54-
<TopVaultsRow {vault} index={idx + 1} />
54+
{#each topVaults.vaults as vault, idx (vault.id)}
55+
<TopVaultsRow {vault} index={idx + 1} {chain} />
5556
{/each}
5657
</tbody>
5758
</table>
@@ -64,13 +65,18 @@
6465
display: grid;
6566
gap: 1rem;
6667
67-
.totals {
68+
.table-meta {
6869
display: flex;
6970
flex-wrap: wrap;
70-
gap: 1rem;
71+
gap: 0.75rem;
7172
color: var(--c-text-extra-light);
7273
font: var(--f-ui-md-medium);
7374
margin-top: 1rem;
75+
76+
> :not(:last-child)::after {
77+
content: '|';
78+
margin-left: 0.75rem;
79+
}
7480
}
7581
7682
.table-wrapper {

src/lib/top-vaults/client.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,27 @@
11
import { publicApiError } from '$lib/helpers/public-api';
2-
import { topVaultsSchema } from './schemas';
2+
import { type TopVaults, topVaultsSchema } from './schemas';
33
import { getChain } from '$lib/helpers/chain';
44

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

77
export async function fetchTopVaults(
88
fetch: Fetch,
99
{ chainId, chainSlug }: { chainId?: number; chainSlug?: string } = {}
10-
) {
10+
): Promise<TopVaults> {
1111
const resp = await fetch(TOP_VAULTS_URL);
1212
if (!resp.ok) throw await publicApiError(resp);
1313

14-
const { vaults, generated_at } = topVaultsSchema.parse(await resp.json());
14+
// eslint-disable-next-line prefer-const
15+
let { vaults, generated_at } = topVaultsSchema.parse(await resp.json());
1516

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

1920
// filter by chain if provided
2021
chainId ??= getChain(chainSlug)?.id;
21-
const rows = chainId ? vaults.filter((vault) => vault.chain === chainId) : vaults;
22-
23-
// summary data
24-
let current_tvl_usd = 0;
25-
let peak_tvl_usd = 0;
26-
27-
for (const vault of vaults) {
28-
current_tvl_usd += vault.current_tvl_usd ?? 0;
29-
peak_tvl_usd += vault.peak_tvl_usd ?? 0;
22+
if (chainId) {
23+
vaults = vaults.filter((vault) => vault.chain === chainId);
3024
}
3125

32-
return {
33-
rows,
34-
generated_at,
35-
current_tvl_usd,
36-
peak_tvl_usd
37-
};
26+
return { vaults, generated_at };
3827
}
39-
40-
export type TopVaults = Awaited<ReturnType<typeof fetchTopVaults>>;

src/lib/top-vaults/schemas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ export const topVaultsSchema = z.object({
3232
generated_at: isoDateTime,
3333
vaults: vaultInfoSchema.array()
3434
});
35-
// See top-vaults/client for TopVaults type def
35+
export type TopVaults = z.infer<typeof topVaultsSchema>;

src/routes/+page.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
import { fetchPublicApi } from '$lib/helpers/public-api';
1+
import { fetchPublicApi, optionalDataError } from '$lib/helpers/public-api';
22
import { getPosts } from '$lib/blog/client';
33

4-
// handle API fetch errors gracefully (see `catch` below)
5-
function logError(err: Error) {
6-
console.error('Request failed; rendering page without data.');
7-
console.error(err);
8-
}
9-
104
export async function load({ fetch, setHeaders, data }) {
115
// Cache the landing data for 5 minutes at the Cloudflare so pages are
126
// served really fast if they get popular, and also for speed test
@@ -16,10 +10,10 @@ export async function load({ fetch, setHeaders, data }) {
1610

1711
return {
1812
strategies: data.strategies,
19-
chains: await fetchPublicApi(fetch, 'chains').catch(logError),
20-
impressiveNumbers: await fetchPublicApi(fetch, 'impressive-numbers').catch(logError),
13+
chains: await fetchPublicApi(fetch, 'chains').catch(optionalDataError('chains')),
14+
impressiveNumbers: await fetchPublicApi(fetch, 'impressive-numbers').catch(optionalDataError('impressive-numbers')),
2115
posts: await getPosts(fetch, { limit: 4 })
2216
.then((r) => r.posts)
23-
.catch(logError)
17+
.catch(optionalDataError('blog posts'))
2418
};
2519
}

src/routes/trading-view/+page.svelte

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import IconWallet from '~icons/local/wallet';
1919
2020
export let data;
21-
const { impressiveNumbers } = data;
21+
const { impressiveNumbers, topVaults } = data;
2222
</script>
2323

2424
<ContentCardsTemplate pageTitle="DEX trading view" pageDescription="DEX trading view">
@@ -34,7 +34,9 @@
3434
<IconBlockchain slot="icon" />
3535
<p>Trading Strategy provides powerful market data sets for on-chain trading on several blockchains.</p>
3636
{#if impressiveNumbers}
37-
<p>Currently indexing data from <strong>{formatAmount(impressiveNumbers.blockchains)} blockchains</strong></p>
37+
<p>
38+
Currently indexing data from <strong>{formatAmount(impressiveNumbers.blockchains)} blockchains</strong>.
39+
</p>
3840
{/if}
3941
<Button slot="cta" label="Explore blockchains" />
4042
</ContentCard>
@@ -46,7 +48,9 @@
4648
trades, across multiple DEXs.
4749
</p>
4850
{#if impressiveNumbers}
49-
<p>Currently indexing data from <strong>{formatAmount(impressiveNumbers.exchanges)} DEXes</strong></p>
51+
<p>
52+
Currently indexing data from <strong>{formatAmount(impressiveNumbers.exchanges)} DEXes</strong>.
53+
</p>
5054
{/if}
5155
<Button slot="cta" label="Browse DEXes" />
5256
</ContentCard>
@@ -58,7 +62,9 @@
5862
current datasets here.
5963
</p>
6064
{#if impressiveNumbers}
61-
<p>Currently indexing data from <strong>{formatAmount(impressiveNumbers.pairs)} trading pairs</strong></p>
65+
<p>
66+
Currently indexing data from <strong>{formatAmount(impressiveNumbers.pairs)} trading pairs</strong>.
67+
</p>
6268
{/if}
6369
<Button slot="cta" label="Browse trading pairs" />
6470
</ContentCard>
@@ -71,7 +77,7 @@
7177
{#if impressiveNumbers}
7278
<p>
7379
Currently indexing data from
74-
<strong>{formatAmount(impressiveNumbers.lending_reserves)} lending reserves</strong>
80+
<strong>{formatAmount(impressiveNumbers.lending_reserves)} lending reserves</strong>.
7581
</p>
7682
{/if}
7783
<Button slot="cta" label="Browse reserves" />
@@ -83,7 +89,13 @@
8389
Explore top-performing vaults across supported chains. Compare TVL, returns, Sharpe ratio, and historical
8490
performance metrics.
8591
</p>
86-
<Button slot="cta" label="See top vaults" />
92+
{#if topVaults?.vaults.length}
93+
<p>
94+
Currently displaying
95+
<strong>{formatAmount(topVaults?.vaults.length)} vaults</strong> with minimum $50k USD TVL.
96+
</p>
97+
{/if}
98+
<Button slot="cta" label="Compare vaults" />
8799
</ContentCard>
88100

89101
<ContentCard title="Advanced search" href="/search">
@@ -114,7 +126,7 @@
114126
</p>
115127
{#if impressiveNumbers}
116128
<p>
117-
Currently providing <strong>{formatByteUnits(impressiveNumbers.database_size)} of data</strong>
129+
Currently providing <strong>{formatByteUnits(impressiveNumbers.database_size)}</strong> of data.
118130
</p>
119131
{/if}
120132
<Button slot="cta" label="Download datasets" />

src/routes/trading-view/+page.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { fetchPublicApi } from '$lib/helpers/public-api';
1+
import { fetchPublicApi, optionalDataError } from '$lib/helpers/public-api';
2+
import { fetchTopVaults } from '$lib/top-vaults/client.js';
23

3-
export async function load({ fetch }) {
4-
try {
5-
const impressiveNumbers = await fetchPublicApi(fetch, 'impressive-numbers');
6-
return { impressiveNumbers };
7-
} catch (e) {
8-
console.error('Request failed; rendering page without data.');
9-
console.error(e);
10-
}
4+
export async function load({ fetch, setHeaders }) {
5+
setHeaders({
6+
'cache-control': 'public, max-age=300' // 5 minutes: 5 * 60 = 300
7+
});
8+
9+
return {
10+
impressiveNumbers: await fetchPublicApi(fetch, 'impressive-numbers').catch(optionalDataError('impressive-numbers')),
11+
topVaults: await fetchTopVaults(fetch).catch(optionalDataError('top-vaults'))
12+
};
1113
}

0 commit comments

Comments
 (0)