|
6 | 6 | import CryptoAddressWidget from '$lib/components/CryptoAddressWidget.svelte'; |
7 | 7 | import TextInput from '$lib/components/TextInput.svelte'; |
8 | 8 | import Timestamp from '$lib/components/Timestamp.svelte'; |
9 | | - import DataTable from '$lib/components/datatable/DataTable.svelte'; |
10 | 9 | import TopVaultsOptIn from './TopVaultsOptIn.svelte'; |
11 | 10 | import ChainCell from './ChainCell.svelte'; |
12 | 11 | import VaultCell from './VaultCell.svelte'; |
13 | | - import MultiValCell, { multiValCompareFn } from './MultiValCell.svelte'; |
| 12 | + import MultiValCell from './MultiValCell.svelte'; |
14 | 13 | import FeesCell from './FeesCell.svelte'; |
15 | 14 | import DepositEventsCell from './DepositEventsCell.svelte'; |
16 | 15 | import RiskCell from './RiskCell.svelte'; |
17 | | - import { createTable } from 'svelte-headless-table'; |
18 | | - import { addSortBy, addHiddenColumns, addTableFilter } from 'svelte-headless-table/plugins'; |
19 | | - import { createRender } from '$lib/components/datatable/utils'; |
20 | | - import { readable } from 'svelte/store'; |
21 | 16 | import { formatDollar, formatNumber, formatPercent, formatValue } from '$lib/helpers/formatters'; |
22 | 17 |
|
23 | 18 | interface Props { |
|
30 | 25 | const formatReturn = (v: number | null) => formatPercent(v, 2); |
31 | 26 | const formatTvl = (v: number | null) => formatDollar(v, 2); |
32 | 27 |
|
33 | | - const vaultsStore = readable(topVaults.vaults); |
34 | | -
|
35 | | - const table = createTable(vaultsStore, { |
36 | | - hide: addHiddenColumns({ initialHiddenColumnIds: chain ? ['chain'] : [] }), |
37 | | - sort: addSortBy({ |
38 | | - initialSortKeys: [{ id: 'one_month_return_ann', order: 'desc' }], |
39 | | - toggleOrder: ['desc', 'asc'] |
40 | | - }), |
41 | | - filter: addTableFilter({ |
42 | | - fn: ({ filterValue, value }) => value.toLocaleLowerCase().includes(filterValue.toLocaleLowerCase()) |
43 | | - }) |
44 | | - }); |
45 | | -
|
46 | | - const tableColumns = table.createColumns([ |
47 | | - table.display({ |
48 | | - id: 'index', |
49 | | - header: '', |
50 | | - cell: () => '' // populated with row index via `rowNumber` CSS counter |
51 | | - }), |
52 | | - table.column({ |
53 | | - id: 'chain', |
54 | | - header: '', |
55 | | - accessor: ({ chain_id }) => { |
56 | | - const chain = getChain(chain_id); |
57 | | - const label = chain?.name ?? `Chain ${chain_id}`; |
58 | | - return { chain_id, chain, label }; |
59 | | - }, |
60 | | - cell: ({ value }) => createRender(ChainCell, value), |
61 | | - plugins: { |
62 | | - sort: { |
63 | | - getSortValue: ({ label }) => label, |
64 | | - invert: true |
65 | | - }, |
66 | | - filter: { |
67 | | - getFilterValue: ({ chain_id, chain }) => `${chain_id} ${chain?.name}` |
68 | | - } |
69 | | - } |
70 | | - }), |
71 | | - table.column({ |
72 | | - id: 'vault', |
73 | | - header: 'Vault', |
74 | | - accessor: ({ name, protocol }) => ({ name, protocol }), |
75 | | - cell: ({ value }) => createRender(VaultCell, value), |
76 | | - plugins: { |
77 | | - sort: { |
78 | | - getSortValue: ({ name }) => name.trim().toLowerCase(), |
79 | | - invert: true |
80 | | - }, |
81 | | - filter: { |
82 | | - getFilterValue: ({ name, protocol }) => `${name} ${protocol}` |
83 | | - } |
84 | | - } |
85 | | - }), |
86 | | - table.column({ |
87 | | - id: 'one_month_return_ann', |
88 | | - accessor: (vault) => [vault.one_month_cagr_net, vault.one_month_cagr], |
89 | | - header: '1M return ann.<br/>(net/​gross)', |
90 | | - cell: ({ value }) => createRender(MultiValCell, { values: value, formatter: formatReturn }), |
91 | | - plugins: { |
92 | | - sort: { compareFn: multiValCompareFn }, |
93 | | - filter: { exclude: true } |
94 | | - } |
95 | | - }), |
96 | | - table.column({ |
97 | | - id: 'three_months_return_ann', |
98 | | - accessor: (vault) => [vault.three_months_cagr_net, vault.three_months_cagr], |
99 | | - header: '3M return ann.<br/>(net/​gross)', |
100 | | - cell: ({ value }) => createRender(MultiValCell, { values: value, formatter: formatReturn }), |
101 | | - plugins: { |
102 | | - sort: { compareFn: multiValCompareFn }, |
103 | | - filter: { exclude: true } |
104 | | - } |
105 | | - }), |
106 | | - table.column({ |
107 | | - id: 'lifetime_return_ann', |
108 | | - accessor: (vault) => [vault.cagr_net, vault.cagr], |
109 | | - header: 'Lifetime return ann.<br/>(net/​gross)', |
110 | | - cell: ({ value }) => createRender(MultiValCell, { values: value, formatter: formatReturn }), |
111 | | - plugins: { |
112 | | - sort: { compareFn: multiValCompareFn }, |
113 | | - filter: { exclude: true } |
114 | | - } |
115 | | - }), |
116 | | - table.column({ |
117 | | - id: 'lifetime_return_abs', |
118 | | - accessor: (vault) => [vault.lifetime_return_net, vault.lifetime_return], |
119 | | - header: 'Lifetime return abs.<br/>(net/​gross)', |
120 | | - cell: ({ value }) => createRender(MultiValCell, { values: value, formatter: formatReturn }), |
121 | | - plugins: { |
122 | | - sort: { compareFn: multiValCompareFn }, |
123 | | - filter: { exclude: true } |
124 | | - } |
125 | | - }), |
126 | | - table.column({ |
127 | | - accessor: 'three_months_sharpe', |
128 | | - header: '3M Sharpe', |
129 | | - cell: ({ value }) => formatNumber(value, 1), |
130 | | - plugins: { filter: { exclude: true } } |
131 | | - }), |
132 | | - table.column({ |
133 | | - accessor: 'three_months_volatility', |
134 | | - header: '3M vola­tility', |
135 | | - cell: ({ value }) => formatPercent(value), |
136 | | - plugins: { filter: { exclude: true } } |
137 | | - }), |
138 | | - table.column({ |
139 | | - accessor: 'denomination', |
140 | | - header: 'Denom­ination', |
141 | | - cell: ({ value }) => formatValue(value), |
142 | | - plugins: { sort: { invert: true } } |
143 | | - }), |
144 | | - table.column({ |
145 | | - id: 'tvl', |
146 | | - accessor: (vault) => [vault.current_nav, vault.peak_nav], |
147 | | - header: 'TVL USD<br/>(current/​peak)', |
148 | | - cell: ({ value }) => createRender(MultiValCell, { values: value, formatter: formatTvl }), |
149 | | - plugins: { |
150 | | - sort: { compareFn: multiValCompareFn }, |
151 | | - filter: { exclude: true } |
152 | | - } |
153 | | - }), |
154 | | - table.column({ |
155 | | - id: 'age', |
156 | | - accessor: 'years', |
157 | | - header: 'Age (years)', |
158 | | - cell: ({ value }) => formatNumber(value, 1), |
159 | | - plugins: { filter: { exclude: true } } |
160 | | - }), |
161 | | - // `last_deposit` is missing from latest data schema so dropping from table for now |
162 | | - // table.column({ |
163 | | - // header: 'Last deposit', |
164 | | - // accessor: 'last_deposit', |
165 | | - // cell: ({ value }) => createRender(LastDepositCell, { last_deposit: value }), |
166 | | - // plugins: { filter: { exclude: true } } |
167 | | - // }), |
168 | | - table.column({ |
169 | | - id: 'fees', |
170 | | - header: 'Fees<br>(mgmt/​perf)', |
171 | | - accessor: ({ mgmt_fee, perf_fee }) => ({ mgmt_fee, perf_fee }), |
172 | | - cell: ({ value }) => createRender(FeesCell, value), |
173 | | - plugins: { |
174 | | - sort: { |
175 | | - // sort by perf_fee then mgmt_fee (scaled down as tie-break), nulls last |
176 | | - getSortValue: ({ perf_fee, mgmt_fee }) => (perf_fee ?? 1) + (mgmt_fee ?? 1) / 100, |
177 | | - invert: true |
178 | | - }, |
179 | | - filter: { exclude: true } |
180 | | - } |
181 | | - }), |
182 | | - table.column({ |
183 | | - accessor: 'event_count', |
184 | | - header: 'Deposit events', |
185 | | - cell: ({ value }) => createRender(DepositEventsCell, { value }), |
186 | | - plugins: { filter: { exclude: true } } |
187 | | - }), |
188 | | - table.column({ |
189 | | - accessor: ({ risk, risk_numeric }) => ({ risk, risk_numeric }), |
190 | | - header: 'Protocol technical risk', |
191 | | - cell: ({ value }) => createRender(RiskCell, value), |
192 | | - plugins: { |
193 | | - sort: { |
194 | | - getSortValue: (v) => v.risk_numeric ?? Infinity, |
195 | | - invert: true |
196 | | - } |
197 | | - } |
198 | | - }), |
199 | | - table.column({ |
200 | | - id: 'address', |
201 | | - header: 'Vault address', |
202 | | - accessor: ({ address, chain_id }) => ({ address, chain_id }), |
203 | | - cell: ({ value: { address, chain_id } }) => |
204 | | - createRender(CryptoAddressWidget, { |
205 | | - class: 'vault-address', |
206 | | - size: 'sm', |
207 | | - address, |
208 | | - href: getExplorerUrl(getChain(chain_id), address) |
209 | | - }), |
210 | | - plugins: { |
211 | | - sort: { disable: true }, |
212 | | - filter: { |
213 | | - getFilterValue: ({ address }) => address |
214 | | - } |
215 | | - } |
216 | | - }) |
217 | | - ]); |
218 | | -
|
219 | | - const tableViewModel = table.createViewModel(tableColumns); |
220 | | - const { pluginStates } = tableViewModel; |
221 | | - const filterValue = pluginStates.filter.filterValue; |
| 28 | + let filterValue = $state(''); |
222 | 29 | </script> |
223 | 30 |
|
224 | 31 | <div class="top-vaults"> |
|
232 | 39 | <span>{topVaults.vaults.length} {chain?.name ?? 'total'} vaults</span> |
233 | 40 | <span>Updated <Timestamp date={topVaults.generated_at} relative /></span> |
234 | 41 | </div> |
235 | | - <TextInput bind:value={$filterValue} type="search" placeholder="Search vaults" /> |
| 42 | + <TextInput bind:value={filterValue} type="search" placeholder="Search vaults" /> |
236 | 43 | </div> |
237 | 44 |
|
238 | 45 | <div class="table-wrapper"> |
239 | | - <DataTable class="top-vaults-table" {tableViewModel} /> |
| 46 | + <table class="top-vaults-table"> |
| 47 | + <thead> |
| 48 | + <tr> |
| 49 | + <th class="index"></th> |
| 50 | + <th class="chain"></th> |
| 51 | + <th class="vault">Vault</th> |
| 52 | + <th class="one_month_return_ann">1M return ann.<br />(net/​gross)</th> |
| 53 | + <th class="three_months_return_ann">3M return ann.<br />(net/​gross)</th> |
| 54 | + <th class="lifetime_return_ann">Lifetime return ann.<br />(net/​gross)</th> |
| 55 | + <th class="lifetime_return_abs">Lifetime return abs.<br />(net/​gross)</th> |
| 56 | + <th class="three_months_sharpe">3m Sharpe</th> |
| 57 | + <th class="three_months_volatility">3M Vola-tility</th> |
| 58 | + <th class="denomination">Denom-ination</th> |
| 59 | + <th class="tvl">TVL USD<br />(current/​peak)</th> |
| 60 | + <th class="age">Age (Years)</th> |
| 61 | + <th class="fees">Fees<br />(mgmt/​perf)</th> |
| 62 | + <th class="event_count">Deposit Events</th> |
| 63 | + <th class="risk">Protocol Technical Risk</th> |
| 64 | + <th class="address">Vault Address</th> |
| 65 | + </tr> |
| 66 | + </thead> |
| 67 | + <tbody> |
| 68 | + {#each topVaults.vaults as vault (vault.id)} |
| 69 | + {@const chain = getChain(vault.chain_id)} |
| 70 | + <tr> |
| 71 | + <!-- index cell is populated with row index via `rowNumber` CSS counter --> |
| 72 | + <td class="index"></td> |
| 73 | + <td class="chain"> |
| 74 | + <ChainCell {chain} label={chain?.name ?? `Chain ${vault.chain_id}`} /> |
| 75 | + </td> |
| 76 | + <td class="vault"> |
| 77 | + <VaultCell name={vault.name} protocol={vault.protocol} /> |
| 78 | + </td> |
| 79 | + <td class="one_month_return_ann"> |
| 80 | + <MultiValCell values={[vault.one_month_cagr_net, vault.one_month_cagr]} formatter={formatReturn} /> |
| 81 | + </td> |
| 82 | + <td class="three_months_return_ann"> |
| 83 | + <MultiValCell |
| 84 | + values={[vault.three_months_cagr_net, vault.three_months_cagr]} |
| 85 | + formatter={formatReturn} |
| 86 | + /> |
| 87 | + </td> |
| 88 | + <td class="lifetime_return_ann"> |
| 89 | + <MultiValCell values={[vault.cagr_net, vault.cagr]} formatter={formatReturn} /> |
| 90 | + </td> |
| 91 | + <td class="lifetime_return_abs"> |
| 92 | + <MultiValCell values={[vault.lifetime_return_net, vault.lifetime_return]} formatter={formatReturn} /> |
| 93 | + </td> |
| 94 | + <td class="three_months_sharpe"> |
| 95 | + {formatNumber(vault.three_months_sharpe, 1)} |
| 96 | + </td> |
| 97 | + <td class="three_months_volatility"> |
| 98 | + {formatPercent(vault.three_months_volatility, 1)} |
| 99 | + </td> |
| 100 | + <td class="denomination"> |
| 101 | + {formatValue(vault.denomination)} |
| 102 | + </td> |
| 103 | + <td class="tvl"> |
| 104 | + <MultiValCell values={[vault.current_nav, vault.peak_nav]} formatter={formatTvl} /> |
| 105 | + </td> |
| 106 | + <td class="age"> |
| 107 | + {formatNumber(vault.years, 1)} |
| 108 | + </td> |
| 109 | + <td class="fees"> |
| 110 | + <FeesCell mgmt_fee={vault.mgmt_fee} perf_fee={vault.perf_fee} /> |
| 111 | + </td> |
| 112 | + <td class="event_count"> |
| 113 | + <DepositEventsCell value={vault.event_count} /> |
| 114 | + </td> |
| 115 | + <td class="risk"> |
| 116 | + <RiskCell risk={vault.risk} risk_numeric={vault.risk_numeric} /> |
| 117 | + </td> |
| 118 | + <td class="address"> |
| 119 | + <CryptoAddressWidget |
| 120 | + class="vault-address" |
| 121 | + size="sm" |
| 122 | + address={vault.address} |
| 123 | + href={getExplorerUrl(chain, vault.address)} |
| 124 | + /> |
| 125 | + </td> |
| 126 | + </tr> |
| 127 | + {/each} |
| 128 | + </tbody> |
| 129 | + </table> |
| 130 | + |
| 131 | + <!-- <DataTable class="top-vaults-table" {tableViewModel} /> --> |
240 | 132 | </div> |
241 | 133 | {/if} |
242 | 134 | </div> |
|
0 commit comments