From c8b411b75d83dfd7c3599ad56e008f58439a0e2f Mon Sep 17 00:00:00 2001
From: Seranged <80223622+Seranged@users.noreply.github.com>
Date: Wed, 24 Jun 2026 12:58:47 +0100
Subject: [PATCH 01/10] feat: add manager profile pages
Add profile pages for label entities and link risk manager/curator displays into the new route.
---
.../entities/manager/ManagerEntityLink.vue | 86 +++++++
.../entities/vault/SecuritizeVaultItem.vue | 27 +--
components/entities/vault/VaultBorrowItem.vue | 27 +--
components/entities/vault/VaultEarnItem.vue | 34 +--
components/entities/vault/VaultItem.vue | 27 +--
.../overview/SecuritizeVaultOverview.vue | 14 +-
.../overview/VaultOverviewBlockGeneral.vue | 19 +-
.../earn/VaultOverviewEarnBlockGeneral.vue | 17 +-
composables/useEulerManagerProfile.ts | 84 +++++++
pages/managers/[slug].vue | 223 ++++++++++++++++++
tests/utils/manager-profile.test.ts | 58 +++++
utils/manager-profile.ts | 33 +++
12 files changed, 532 insertions(+), 117 deletions(-)
create mode 100644 components/entities/manager/ManagerEntityLink.vue
create mode 100644 composables/useEulerManagerProfile.ts
create mode 100644 pages/managers/[slug].vue
create mode 100644 tests/utils/manager-profile.test.ts
create mode 100644 utils/manager-profile.ts
diff --git a/components/entities/manager/ManagerEntityLink.vue b/components/entities/manager/ManagerEntityLink.vue
new file mode 100644
index 000000000..0cf37c124
--- /dev/null
+++ b/components/entities/manager/ManagerEntityLink.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+ {{ displayName }}
+ {{ displayName }}
+ {{ displayName }}
+
+
diff --git a/components/entities/vault/SecuritizeVaultItem.vue b/components/entities/vault/SecuritizeVaultItem.vue
index a5300b8b0..566c90fc2 100644
--- a/components/entities/vault/SecuritizeVaultItem.vue
+++ b/components/entities/vault/SecuritizeVaultItem.vue
@@ -4,7 +4,6 @@ import { formatAssetValue } from '~/utils/sdk-prices'
import { isVaultBlockedByCountry } from '~/composables/useGeoBlock'
import { useEulerProductOfVault, useEulerEntitiesOfVault } from '~/composables/useEulerLabels'
import { isVaultGovernanceLimited } from '~/utils/eulerLabelsUtils'
-import { getEulerLabelEntityLogo } from '~/entities/euler/labels'
import { withVaultIntrinsicApy, getVaultIntrinsicApy, getVaultIntrinsicApyInfo } from '~/utils/vault-intrinsic-apy'
import { formatNumber, formatCompactUsdValue } from '~/utils/string-utils'
import { VaultApyModal, UiModalPreviewTrigger } from '#components'
@@ -30,10 +29,6 @@ const entityName = computed(() => {
if (entities.length === 2) return `${entities[0].name} & ${entities[1].name}`
return `${entities[0].name} & others`
})
-const entityLogos = computed(() => {
- if (!entityName.value || entities.length === 0) return []
- return entities.map(e => getEulerLabelEntityLogo(e.logo))
-})
const displayName = computed(() => product.name || vault.shares.name)
const isGeoBlocked = computed(() => isVaultBlockedByCountry(vault.address))
@@ -198,18 +193,14 @@ watchEffect(async () => {
class="flex items-center gap-6"
:class="{ 'opacity-20': isGovernanceLimited }"
>
-
- {{ entityName }}
+ />
{
class="flex items-center gap-8"
:class="{ 'opacity-20': isGovernanceLimited }"
>
-
- {{ entityName }}
{
const entityDisplay = computed(() => {
const all = getUniqueEntitiesByVaults([pair.collateral, pair.borrow])
- if (all.length === 0) return { name: '', logos: [] }
+ if (all.length === 0) return { name: '', entities: [] }
const name = all.length === 1
? all[0].name
: all.length === 2
@@ -39,7 +38,7 @@ const entityDisplay = computed(() => {
: `${all[0].name} & others`
return {
name,
- logos: all.map(e => getEulerLabelEntityLogo(e.logo)),
+ entities: all,
}
})
@@ -465,18 +464,14 @@ const linkPath = computed(() => ({
class="flex items-center gap-6"
:class="{ 'opacity-20': isAnyGovernanceLimited }"
>
-
- {{ entityDisplay.name }}
+ />
({
class="flex items-center gap-8"
:class="{ 'opacity-20': isAnyGovernanceLimited }"
>
-
- {{ entityDisplay.name }}
{
if (entities.length === 2) return `${entities[0].name} & ${entities[1].name}`
return `${entities[0].name} & others`
})
-const entityLogos = computed(() => {
- if (!entityName.value || entities.length === 0) return []
- return entities.map(e => getEulerLabelEntityLogo(e.logo))
-})
const { getBalance, isLoading: isBalancesLoading } = useWallets()
const { settings } = useUserSettings()
const enableIntrinsicApy = computed(() => settings.value.enableIntrinsicApy)
@@ -327,18 +322,14 @@ const supplyApyModalData = computed(() => ({
v-else-if="entityName"
class="flex items-center gap-6"
>
-
- {{ entityName }}
+ />
({
/>
Unknown
-
-
- {{ entityName }}
-
+
{
if (entities.length === 2) return `${entities[0].name} & ${entities[1].name}`
return `${entities[0].name} & others`
})
-const entityLogos = computed(() => {
- if (!entityName.value || entities.length === 0) return []
- return entities.map(e => getEulerLabelEntityLogo(e.logo))
-})
const isEscrow = computed(() => getVaultCategory(vault.address) === 'escrow')
const isBorrowable = computed(() => isVaultBorrowable(vault))
const displayName = computed(() => {
@@ -299,18 +294,14 @@ watchEffect(async () => {
class="flex items-center gap-6"
:class="{ 'opacity-20': isGovernanceLimited }"
>
-
- {{ entityName }}
+ />
{
class="flex items-center gap-8"
:class="{ 'opacity-20': isGovernanceLimited }"
>
-
- {{ entityName }}
{
class="flex items-center gap-8"
:class="{ 'opacity-20': isGovernanceLimited }"
>
-
-
{{ entity.name }}
{
class="flex items-center gap-8"
:class="{ 'opacity-20': isGovernanceLimited }"
>
-
- {{ entity.name }}
- {{ entity.name }}
diff --git a/components/entities/vault/overview/earn/VaultOverviewEarnBlockGeneral.vue b/components/entities/vault/overview/earn/VaultOverviewEarnBlockGeneral.vue
index 9d1434ce0..06aa14552 100644
--- a/components/entities/vault/overview/earn/VaultOverviewEarnBlockGeneral.vue
+++ b/components/entities/vault/overview/earn/VaultOverviewEarnBlockGeneral.vue
@@ -4,7 +4,6 @@ import { getAddress } from 'viem'
import { formatAssetValue } from '~/utils/sdk-prices'
import { useEulerEntitiesOfEarnVault, useEulerProductOfVault } from '~/composables/useEulerLabels'
-import { getEulerLabelEntityLogo } from '~/entities/euler/labels'
import { isVaultBlockedByCountry } from '~/composables/useGeoBlock'
import { isEarnVaultDeprecated, getEarnVaultDeprecationReason, getEarnVaultDescription } from '~/utils/eulerLabelsUtils'
import { autoLink } from '~/utils/autoLink'
@@ -103,21 +102,7 @@ const feeDisplay = computed(() => {
:key="idx"
class="flex items-center gap-8"
>
-
-
{{ entity.name }}
-
{{ entity.name }}
+
) => {
+ const { entities, products, isReady: labelsReady } = useEulerLabels()
+ const {
+ isEVaultUpdating,
+ isEarnUpdating,
+ isSecuritizeUpdating,
+ isEscrowUpdating,
+ } = useVaults()
+ const {
+ getVerifiedEVaults,
+ getEarnVaults,
+ getSecuritizeVaults,
+ } = useVaultRegistry()
+ const showAllLabelEntries = useShowAllLabelEntries()
+
+ const entity = computed(() => entities[slug.value] ?? null)
+
+ const productEntries = computed(() =>
+ Object.entries(products)
+ .filter(([, product]) => isEulerLabelProductManagedBy(product, slug.value))
+ .map(([key, product]) => ({ key, product }))
+ .sort((a, b) => a.product.name.localeCompare(b.product.name)),
+ )
+
+ const managesEntity = (candidate: EVault | SecuritizeCollateralVault): boolean =>
+ getEntitiesByVault(candidate as EVault).some(manager =>
+ getEulerLabelEntitySlug(entities, manager) === slug.value,
+ )
+
+ const managesEarnEntity = (candidate: EulerEarn): boolean =>
+ getEntitiesByEarnVault(candidate).some(manager =>
+ getEulerLabelEntitySlug(entities, manager) === slug.value,
+ )
+
+ const evaults = computed(() =>
+ getVerifiedEVaults(showAllLabelEntries.value)
+ .filter(managesEntity)
+ .sort((a, b) => a.asset.symbol.localeCompare(b.asset.symbol)),
+ )
+
+ const securitizeVaults = computed(() =>
+ getSecuritizeVaults()
+ .filter(managesEntity)
+ .sort((a, b) => a.asset.symbol.localeCompare(b.asset.symbol)),
+ )
+
+ const earnVaults = computed(() =>
+ getEarnVaults()
+ .filter(managesEarnEntity)
+ .sort((a, b) => a.asset.symbol.localeCompare(b.asset.symbol)),
+ )
+
+ const vaultCount = computed(() =>
+ evaults.value.length + securitizeVaults.value.length + earnVaults.value.length,
+ )
+
+ const isLoading = computed(() =>
+ !labelsReady.value
+ || isEVaultUpdating.value
+ || isEarnUpdating.value
+ || isSecuritizeUpdating.value
+ || isEscrowUpdating.value,
+ )
+
+ return {
+ entity,
+ productEntries,
+ evaults,
+ securitizeVaults,
+ earnVaults,
+ vaultCount,
+ isLoading,
+ }
+}
diff --git a/pages/managers/[slug].vue b/pages/managers/[slug].vue
new file mode 100644
index 000000000..aa962c9df
--- /dev/null
+++ b/pages/managers/[slug].vue
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
+
+
+
+
+ Manager not found on this network.
+
+
+ Browse all markets
+
+
+
+
+
+
+
+ Manager profile
+
+
+
+
+
+
+
+
+ {{ entity.name }}
+
+
+
+
+
+ No profile description is available yet.
+
+
+
+
+
+
+
+ Products
+
+
+ {{ productEntries.length }}
+
+
+
+
+ Vaults
+
+
+ {{ vaultCount }}
+
+
+
+
+ Links
+
+
+ {{ socialLinks.length }}
+
+
+
+
+
+
+
+
+
+
+ Products managed
+
+
+
+
+
+ {{ entry.product.name }}
+
+
+ {{ entry.product.description }}
+
+
+
+
+
+
+
+ Vaults managed
+
+
+ No managed vaults are currently available on this network.
+
+
+
+ Lending vaults
+
+
+
+
+
+
+ Earn vaults
+
+
+
+
+
+
+
diff --git a/tests/utils/manager-profile.test.ts b/tests/utils/manager-profile.test.ts
new file mode 100644
index 000000000..108e9837c
--- /dev/null
+++ b/tests/utils/manager-profile.test.ts
@@ -0,0 +1,58 @@
+import { describe, expect, it } from 'vitest'
+import type { EulerLabelEntity, EulerLabelProduct } from '~/entities/euler/labels'
+import {
+ getEulerLabelEntityDisplayName,
+ getEulerLabelEntityKeys,
+ getEulerLabelEntitySlug,
+ getManagerProfilePath,
+ isEulerLabelProductManagedBy,
+} from '~/utils/manager-profile'
+
+const entity = (name: string, logo = `${name}.svg`): EulerLabelEntity => ({
+ name,
+ logo,
+ description: '',
+ url: `https://${name.toLowerCase()}.example`,
+ addresses: {},
+ social: {
+ twitter: '',
+ youtube: '',
+ discord: '',
+ telegram: '',
+ github: '',
+ },
+})
+
+describe('manager profile helpers', () => {
+ it('normalizes product entity keys', () => {
+ expect(getEulerLabelEntityKeys({ entity: 'k3' } as EulerLabelProduct)).toEqual(['k3'])
+ expect(getEulerLabelEntityKeys({ entity: ['k3', 're7'] } as EulerLabelProduct)).toEqual(['k3', 're7'])
+ expect(getEulerLabelEntityKeys({ entity: '' } as EulerLabelProduct)).toEqual([])
+ })
+
+ it('detects products managed by a slug', () => {
+ const product = { entity: ['k3', 're7'] } as EulerLabelProduct
+
+ expect(isEulerLabelProductManagedBy(product, 're7')).toBe(true)
+ expect(isEulerLabelProductManagedBy(product, 'mev-capital')).toBe(false)
+ })
+
+ it('resolves an entity slug by reference or stable fields', () => {
+ const k3 = entity('K3')
+ const entities = { k3, re7: entity('Re7') }
+
+ expect(getEulerLabelEntitySlug(entities, k3)).toBe('k3')
+ expect(getEulerLabelEntitySlug(entities, { ...k3 })).toBe('k3')
+ })
+
+ it('formats compact manager labels', () => {
+ expect(getEulerLabelEntityDisplayName([])).toBe('')
+ expect(getEulerLabelEntityDisplayName([entity('K3')])).toBe('K3')
+ expect(getEulerLabelEntityDisplayName([entity('K3'), entity('Re7')])).toBe('K3 & Re7')
+ expect(getEulerLabelEntityDisplayName([entity('K3'), entity('Re7'), entity('MEV Capital')])).toBe('K3 & others')
+ })
+
+ it('builds manager profile paths', () => {
+ expect(getManagerProfilePath('mev-capital')).toBe('/managers/mev-capital')
+ })
+})
diff --git a/utils/manager-profile.ts b/utils/manager-profile.ts
new file mode 100644
index 000000000..e0053d672
--- /dev/null
+++ b/utils/manager-profile.ts
@@ -0,0 +1,33 @@
+import type { EulerLabelEntity, EulerLabelProduct } from '~/entities/euler/labels'
+
+export const getEulerLabelEntityKeys = (product: EulerLabelProduct): string[] => {
+ if (Array.isArray(product.entity)) return product.entity
+ return product.entity ? [product.entity] : []
+}
+
+export const isEulerLabelProductManagedBy = (product: EulerLabelProduct, slug: string): boolean =>
+ getEulerLabelEntityKeys(product).includes(slug)
+
+export const getEulerLabelEntitySlug = (
+ entities: Record,
+ entity: EulerLabelEntity,
+): string => {
+ const exact = Object.entries(entities).find(([, value]) => value === entity)
+ if (exact) return exact[0]
+
+ const byStableFields = Object.entries(entities).find(([, value]) =>
+ value.name === entity.name
+ && value.logo === entity.logo
+ && value.url === entity.url,
+ )
+ return byStableFields?.[0] ?? ''
+}
+
+export const getEulerLabelEntityDisplayName = (entities: EulerLabelEntity[]): string => {
+ if (entities.length === 0) return ''
+ if (entities.length === 1) return entities[0].name
+ if (entities.length === 2) return `${entities[0].name} & ${entities[1].name}`
+ return `${entities[0].name} & others`
+}
+
+export const getManagerProfilePath = (slug: string): string => `/managers/${slug}`
From 8ae47ffa1cb5b751762404386626ae8c05c0c834 Mon Sep 17 00:00:00 2001
From: Seranged <80223622+Seranged@users.noreply.github.com>
Date: Wed, 24 Jun 2026 15:00:22 +0100
Subject: [PATCH 02/10] fix: make manager links keyboard accessible
Add keyboard semantics to the span-based manager profile affordance used inside vault cards.
---
components/entities/manager/ManagerEntityLink.vue | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/components/entities/manager/ManagerEntityLink.vue b/components/entities/manager/ManagerEntityLink.vue
index 0cf37c124..06fe48872 100644
--- a/components/entities/manager/ManagerEntityLink.vue
+++ b/components/entities/manager/ManagerEntityLink.vue
@@ -46,6 +46,13 @@ const goToManager = () => {
if (!isLinked.value) return
void navigateTo(to.value)
}
+
+const onSpanKeydown = (event: KeyboardEvent) => {
+ if (event.key !== 'Enter' && event.key !== ' ') return
+ event.preventDefault()
+ event.stopPropagation()
+ goToManager()
+}
@@ -58,12 +65,15 @@ const goToManager = () => {
/>
{{ displayName }}
Date: Wed, 24 Jun 2026 15:16:32 +0100
Subject: [PATCH 03/10] fix: use manager label metadata
Normalize manager profile links and surface labeled governance addresses plus product metadata from labels.
---
entities/euler/labels.ts | 1 +
pages/managers/[slug].vue | 135 ++++++++++++++++++++++------
tests/utils/manager-profile.test.ts | 51 +++++++++++
utils/manager-profile.ts | 83 +++++++++++++++++
4 files changed, 245 insertions(+), 25 deletions(-)
diff --git a/entities/euler/labels.ts b/entities/euler/labels.ts
index c52312b4c..97069c261 100644
--- a/entities/euler/labels.ts
+++ b/entities/euler/labels.ts
@@ -30,6 +30,7 @@ export type EulerLabelProduct = {
portfolioNotice?: string
entity: string[] | string
url: string
+ logo?: string
vaults: string[]
deprecatedVaults?: string[]
deprecationReason?: string
diff --git a/pages/managers/[slug].vue b/pages/managers/[slug].vue
index aa962c9df..e92616e6a 100644
--- a/pages/managers/[slug].vue
+++ b/pages/managers/[slug].vue
@@ -1,12 +1,20 @@
@@ -99,7 +103,7 @@ const hasVaults = computed(() =>
-
+
Products
@@ -124,6 +128,14 @@ const hasVaults = computed(() =>
{{ socialLinks.length }}
+
+
+ Addresses
+
+
+ {{ addressEntries.length }}
+
+
+
+
+ Governance addresses
+
+
+
+
+ {{ entry.label }}
+
+
+
+ {{ getShortAddress(entry.address) }}
+
+
+
+
+
+
+
-
-
- {{ entry.product.name }}
-
-
+
+
+
+ {{ entry.product.name }}
+
+
+ {{ entry.product.description }}
+
+
+
+
- {{ entry.product.description }}
-
-
+
+ {{ tag }}
+
+
+ Website
+
+
+
diff --git a/tests/utils/manager-profile.test.ts b/tests/utils/manager-profile.test.ts
index 108e9837c..97692a09d 100644
--- a/tests/utils/manager-profile.test.ts
+++ b/tests/utils/manager-profile.test.ts
@@ -4,7 +4,12 @@ import {
getEulerLabelEntityDisplayName,
getEulerLabelEntityKeys,
getEulerLabelEntitySlug,
+ getManagerProfileAddressEntries,
+ getManagerProfileExternalUrl,
getManagerProfilePath,
+ getManagerProfileSocialLinks,
+ getManagerProfileSocialUrl,
+ getShortAddress,
isEulerLabelProductManagedBy,
} from '~/utils/manager-profile'
@@ -55,4 +60,50 @@ describe('manager profile helpers', () => {
it('builds manager profile paths', () => {
expect(getManagerProfilePath('mev-capital')).toBe('/managers/mev-capital')
})
+
+ it('normalizes manager social handles into external URLs', () => {
+ expect(getManagerProfileSocialUrl('twitter', '@k3_capital')).toBe('https://x.com/k3_capital')
+ expect(getManagerProfileSocialUrl('github', 'euler-xyz')).toBe('https://github.com/euler-xyz')
+ expect(getManagerProfileSocialUrl('telegram', 'https://t.me/UsualCommunity')).toBe('https://t.me/UsualCommunity')
+ expect(getManagerProfileSocialUrl('discord', 'hhttps://discord.usual.money/')).toBe('https://discord.usual.money/')
+ expect(getManagerProfileExternalUrl('example.com')).toBe('https://example.com')
+ })
+
+ it('builds social links from entity metadata', () => {
+ expect(getManagerProfileSocialLinks({
+ ...entity('K3'),
+ url: 'k3.capital',
+ social: {
+ twitter: 'k3_capital',
+ youtube: '',
+ discord: '',
+ telegram: '',
+ github: 'k3-capital',
+ },
+ })).toEqual([
+ { label: 'Website', url: 'https://k3.capital' },
+ { label: 'X', url: 'https://x.com/k3_capital' },
+ { label: 'GitHub', url: 'https://github.com/k3-capital' },
+ ])
+ })
+
+ it('formats manager governance address entries', () => {
+ expect(getManagerProfileAddressEntries({
+ ...entity('K3'),
+ addresses: {
+ '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb': 'Timelock',
+ '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa': 'Multisig',
+ },
+ })).toEqual([
+ {
+ address: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ label: 'Multisig',
+ },
+ {
+ address: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
+ label: 'Timelock',
+ },
+ ])
+ expect(getShortAddress('0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')).toBe('0xaaaa...aaaa')
+ })
})
diff --git a/utils/manager-profile.ts b/utils/manager-profile.ts
index e0053d672..7d4c18ea1 100644
--- a/utils/manager-profile.ts
+++ b/utils/manager-profile.ts
@@ -1,5 +1,88 @@
import type { EulerLabelEntity, EulerLabelProduct } from '~/entities/euler/labels'
+export type ManagerProfileExternalLink = {
+ label: string
+ url: string
+}
+
+export type ManagerProfileAddressEntry = {
+ address: string
+ label: string
+}
+
+const withProtocol = (url: string): string => {
+ if (url.startsWith('hhttps://')) return `https://${url.slice('hhttps://'.length)}`
+ if (/^https?:\/\//i.test(url)) return url
+ return `https://${url}`
+}
+
+const cleanHandle = (value: string): string => value.trim().replace(/^@+/, '').replace(/^\/+/, '')
+
+export const getManagerProfileExternalUrl = (url: string): string => withProtocol(url.trim())
+
+export const getManagerProfileSocialUrl = (platform: string, value: string): string => {
+ const normalized = value.trim()
+ if (!normalized) return ''
+ if (/^h?https?:\/\//i.test(normalized)) return withProtocol(normalized)
+
+ const handle = cleanHandle(normalized)
+ if (!handle) return ''
+
+ switch (platform) {
+ case 'twitter':
+ return `https://x.com/${handle}`
+ case 'github':
+ return `https://github.com/${handle}`
+ case 'discord':
+ return `https://discord.gg/${handle}`
+ case 'telegram':
+ return `https://t.me/${handle}`
+ case 'youtube':
+ return `https://youtube.com/@${handle}`
+ default:
+ return withProtocol(handle)
+ }
+}
+
+export const getManagerProfileSocialLinks = (
+ entity: EulerLabelEntity,
+): ManagerProfileExternalLink[] => {
+ const social = (entity.social ?? {}) as Partial
+ const links: Array = [
+ entity.url ? { label: 'Website', url: withProtocol(entity.url) } : null,
+ social.twitter
+ ? { label: 'X', url: getManagerProfileSocialUrl('twitter', social.twitter) }
+ : null,
+ social.github
+ ? { label: 'GitHub', url: getManagerProfileSocialUrl('github', social.github) }
+ : null,
+ social.discord
+ ? { label: 'Discord', url: getManagerProfileSocialUrl('discord', social.discord) }
+ : null,
+ social.telegram
+ ? { label: 'Telegram', url: getManagerProfileSocialUrl('telegram', social.telegram) }
+ : null,
+ social.youtube
+ ? { label: 'YouTube', url: getManagerProfileSocialUrl('youtube', social.youtube) }
+ : null,
+ ]
+
+ return links.filter((link): link is ManagerProfileExternalLink => Boolean(link?.url))
+}
+
+export const getManagerProfileAddressEntries = (
+ entity: EulerLabelEntity,
+): ManagerProfileAddressEntry[] =>
+ Object.entries(entity.addresses)
+ .map(([address, label]) => ({
+ address,
+ label: label || 'Manager address',
+ }))
+ .sort((a, b) => a.label.localeCompare(b.label) || a.address.localeCompare(b.address))
+
+export const getShortAddress = (address: string): string =>
+ `${address.slice(0, 6)}...${address.slice(-4)}`
+
export const getEulerLabelEntityKeys = (product: EulerLabelProduct): string[] => {
if (Array.isArray(product.entity)) return product.entity
return product.entity ? [product.entity] : []
From 5e0b98f1a9b6db3e563c0af8bcc3aa99daab9522 Mon Sep 17 00:00:00 2001
From: Seranged <80223622+Seranged@users.noreply.github.com>
Date: Thu, 25 Jun 2026 11:48:39 +0100
Subject: [PATCH 04/10] fix: simplify manager profiles
Align manager profiles with the review notes by removing governance addresses and individual lending vault lists, using market cards, and keeping flexible profile links.
---
composables/useEulerManagerProfile.ts | 60 +++------
entities/euler/labels.ts | 8 +-
pages/managers/[slug].vue | 179 +++-----------------------
tests/utils/manager-profile.test.ts | 24 +---
utils/manager-profile.ts | 72 +++++------
5 files changed, 74 insertions(+), 269 deletions(-)
diff --git a/composables/useEulerManagerProfile.ts b/composables/useEulerManagerProfile.ts
index 1e4d2ef1d..324b3fef8 100644
--- a/composables/useEulerManagerProfile.ts
+++ b/composables/useEulerManagerProfile.ts
@@ -1,6 +1,7 @@
-import type { EulerEarn, EVault, SecuritizeCollateralVault } from '@eulerxyz/euler-v2-sdk'
+import type { EulerEarn } from '@eulerxyz/euler-v2-sdk'
+import type { MarketGroup } from '~/entities/lend-discovery'
import type { EulerLabelProduct } from '~/entities/euler/labels'
-import { getEntitiesByEarnVault, getEntitiesByVault } from '~/utils/eulerLabelsUtils'
+import { getEntitiesByEarnVault } from '~/utils/eulerLabelsUtils'
import { getEulerLabelEntitySlug, isEulerLabelProductManagedBy } from '~/utils/manager-profile'
export type ManagerProductEntry = {
@@ -10,18 +11,13 @@ export type ManagerProductEntry = {
export const useEulerManagerProfile = (slug: Ref) => {
const { entities, products, isReady: labelsReady } = useEulerLabels()
+ const { isEarnUpdating } = useVaults()
+ const { getEarnVaults } = useVaultRegistry()
const {
- isEVaultUpdating,
- isEarnUpdating,
- isSecuritizeUpdating,
- isEscrowUpdating,
- } = useVaults()
- const {
- getVerifiedEVaults,
- getEarnVaults,
- getSecuritizeVaults,
- } = useVaultRegistry()
- const showAllLabelEntries = useShowAllLabelEntries()
+ marketGroups,
+ isReady: marketGroupsReady,
+ isResolvingTVL,
+ } = useMarketGroups()
const entity = computed(() => entities[slug.value] ?? null)
@@ -32,53 +28,37 @@ export const useEulerManagerProfile = (slug: Ref) => {
.sort((a, b) => a.product.name.localeCompare(b.product.name)),
)
- const managesEntity = (candidate: EVault | SecuritizeCollateralVault): boolean =>
- getEntitiesByVault(candidate as EVault).some(manager =>
- getEulerLabelEntitySlug(entities, manager) === slug.value,
- )
+ const managedProductKeys = computed(() => new Set(productEntries.value.map(entry => entry.key)))
+
+ const managedMarkets = computed(() =>
+ marketGroups.value
+ .filter(group => group.source === 'product' && managedProductKeys.value.has(group.id))
+ .sort((a, b) => b.metrics.totalTVL - a.metrics.totalTVL || a.name.localeCompare(b.name)),
+ )
const managesEarnEntity = (candidate: EulerEarn): boolean =>
getEntitiesByEarnVault(candidate).some(manager =>
getEulerLabelEntitySlug(entities, manager) === slug.value,
)
- const evaults = computed(() =>
- getVerifiedEVaults(showAllLabelEntries.value)
- .filter(managesEntity)
- .sort((a, b) => a.asset.symbol.localeCompare(b.asset.symbol)),
- )
-
- const securitizeVaults = computed(() =>
- getSecuritizeVaults()
- .filter(managesEntity)
- .sort((a, b) => a.asset.symbol.localeCompare(b.asset.symbol)),
- )
-
const earnVaults = computed(() =>
getEarnVaults()
.filter(managesEarnEntity)
.sort((a, b) => a.asset.symbol.localeCompare(b.asset.symbol)),
)
- const vaultCount = computed(() =>
- evaults.value.length + securitizeVaults.value.length + earnVaults.value.length,
- )
-
const isLoading = computed(() =>
!labelsReady.value
- || isEVaultUpdating.value
- || isEarnUpdating.value
- || isSecuritizeUpdating.value
- || isEscrowUpdating.value,
+ || !marketGroupsReady.value
+ || isResolvingTVL.value
+ || isEarnUpdating.value,
)
return {
entity,
productEntries,
- evaults,
- securitizeVaults,
+ managedMarkets,
earnVaults,
- vaultCount,
isLoading,
}
}
diff --git a/entities/euler/labels.ts b/entities/euler/labels.ts
index 97069c261..54954f19b 100644
--- a/entities/euler/labels.ts
+++ b/entities/euler/labels.ts
@@ -4,13 +4,7 @@ export type EulerLabelEntity = {
description: string
url: string
addresses: Record
- social: {
- twitter: string
- youtube: string
- discord: string
- telegram: string
- github: string
- }
+ social: Record
}
export type EulerLabelVaultOverride = {
name?: string
diff --git a/pages/managers/[slug].vue b/pages/managers/[slug].vue
index e92616e6a..d43d4fb9a 100644
--- a/pages/managers/[slug].vue
+++ b/pages/managers/[slug].vue
@@ -1,12 +1,8 @@
@@ -103,7 +85,7 @@ const copyAddress = (address: string) => {
-
+
Products
@@ -114,10 +96,10 @@ const copyAddress = (address: string) => {
- Vaults
+ Markets
- {{ vaultCount }}
+ {{ managedMarkets.length }}
@@ -128,14 +110,6 @@ const copyAddress = (address: string) => {
{{ socialLinks.length }}
-
-
- Addresses
-
-
- {{ addressEntries.length }}
-
-
{
-
- Governance addresses
-
-
-
-
- {{ entry.label }}
-
-
-
- {{ getShortAddress(entry.address) }}
-
-
-
-
-
-
-
-
- Products managed
+ Markets
-
-
-
-
-
-
- {{ entry.product.name }}
-
-
- {{ entry.product.description }}
-
-
-
-
-
-
+
-
+
- Vaults managed
+ Earn vaults
-
- No managed vaults are currently available on this network.
-
-
-
- Lending vaults
-
-
-
-
-
-
- Earn vaults
-
-
-
+
diff --git a/tests/utils/manager-profile.test.ts b/tests/utils/manager-profile.test.ts
index 97692a09d..de4ea86f5 100644
--- a/tests/utils/manager-profile.test.ts
+++ b/tests/utils/manager-profile.test.ts
@@ -4,12 +4,10 @@ import {
getEulerLabelEntityDisplayName,
getEulerLabelEntityKeys,
getEulerLabelEntitySlug,
- getManagerProfileAddressEntries,
getManagerProfileExternalUrl,
getManagerProfilePath,
getManagerProfileSocialLinks,
getManagerProfileSocialUrl,
- getShortAddress,
isEulerLabelProductManagedBy,
} from '~/utils/manager-profile'
@@ -79,31 +77,13 @@ describe('manager profile helpers', () => {
discord: '',
telegram: '',
github: 'k3-capital',
+ legal: 'legal.k3.capital',
},
})).toEqual([
{ label: 'Website', url: 'https://k3.capital' },
{ label: 'X', url: 'https://x.com/k3_capital' },
{ label: 'GitHub', url: 'https://github.com/k3-capital' },
+ { label: 'Legal', url: 'https://legal.k3.capital' },
])
})
-
- it('formats manager governance address entries', () => {
- expect(getManagerProfileAddressEntries({
- ...entity('K3'),
- addresses: {
- '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb': 'Timelock',
- '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa': 'Multisig',
- },
- })).toEqual([
- {
- address: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
- label: 'Multisig',
- },
- {
- address: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
- label: 'Timelock',
- },
- ])
- expect(getShortAddress('0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')).toBe('0xaaaa...aaaa')
- })
})
diff --git a/utils/manager-profile.ts b/utils/manager-profile.ts
index 7d4c18ea1..1044128aa 100644
--- a/utils/manager-profile.ts
+++ b/utils/manager-profile.ts
@@ -5,11 +5,6 @@ export type ManagerProfileExternalLink = {
url: string
}
-export type ManagerProfileAddressEntry = {
- address: string
- label: string
-}
-
const withProtocol = (url: string): string => {
if (url.startsWith('hhttps://')) return `https://${url.slice('hhttps://'.length)}`
if (/^https?:\/\//i.test(url)) return url
@@ -20,6 +15,21 @@ const cleanHandle = (value: string): string => value.trim().replace(/^@+/, '').r
export const getManagerProfileExternalUrl = (url: string): string => withProtocol(url.trim())
+const SOCIAL_LINK_LABELS: Record
= {
+ twitter: 'X',
+ github: 'GitHub',
+ discord: 'Discord',
+ telegram: 'Telegram',
+ youtube: 'YouTube',
+}
+
+const getSocialLinkLabel = (platform: string): string =>
+ SOCIAL_LINK_LABELS[platform] ?? platform
+ .split(/[-_\s]+/)
+ .filter(Boolean)
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
+ .join(' ')
+
export const getManagerProfileSocialUrl = (platform: string, value: string): string => {
const normalized = value.trim()
if (!normalized) return ''
@@ -47,42 +57,28 @@ export const getManagerProfileSocialUrl = (platform: string, value: string): str
export const getManagerProfileSocialLinks = (
entity: EulerLabelEntity,
): ManagerProfileExternalLink[] => {
- const social = (entity.social ?? {}) as Partial
- const links: Array = [
+ const social = entity.social ?? {}
+ const links: ManagerProfileExternalLink[] = ([
entity.url ? { label: 'Website', url: withProtocol(entity.url) } : null,
- social.twitter
- ? { label: 'X', url: getManagerProfileSocialUrl('twitter', social.twitter) }
- : null,
- social.github
- ? { label: 'GitHub', url: getManagerProfileSocialUrl('github', social.github) }
- : null,
- social.discord
- ? { label: 'Discord', url: getManagerProfileSocialUrl('discord', social.discord) }
- : null,
- social.telegram
- ? { label: 'Telegram', url: getManagerProfileSocialUrl('telegram', social.telegram) }
- : null,
- social.youtube
- ? { label: 'YouTube', url: getManagerProfileSocialUrl('youtube', social.youtube) }
- : null,
- ]
-
- return links.filter((link): link is ManagerProfileExternalLink => Boolean(link?.url))
+ ...Object.entries(social).map(([platform, value]) =>
+ value
+ ? {
+ label: getSocialLinkLabel(platform),
+ url: getManagerProfileSocialUrl(platform, value),
+ }
+ : null,
+ ),
+ ] as Array).filter((link): link is ManagerProfileExternalLink => Boolean(link?.url))
+
+ const seen = new Set()
+ return links.filter((link) => {
+ const key = `${link.label}:${link.url}`
+ if (seen.has(key)) return false
+ seen.add(key)
+ return true
+ })
}
-export const getManagerProfileAddressEntries = (
- entity: EulerLabelEntity,
-): ManagerProfileAddressEntry[] =>
- Object.entries(entity.addresses)
- .map(([address, label]) => ({
- address,
- label: label || 'Manager address',
- }))
- .sort((a, b) => a.label.localeCompare(b.label) || a.address.localeCompare(b.address))
-
-export const getShortAddress = (address: string): string =>
- `${address.slice(0, 6)}...${address.slice(-4)}`
-
export const getEulerLabelEntityKeys = (product: EulerLabelProduct): string[] => {
if (Array.isArray(product.entity)) return product.entity
return product.entity ? [product.entity] : []
From 1143381da53d681ea14a3843970fe5aede626fea Mon Sep 17 00:00:00 2001
From: Seranged <80223622+Seranged@users.noreply.github.com>
Date: Thu, 25 Jun 2026 11:52:05 +0100
Subject: [PATCH 05/10] fix: remove manager links count
Drop the low-value links stat card now that profile links render directly below the summary.
---
pages/managers/[slug].vue | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/pages/managers/[slug].vue b/pages/managers/[slug].vue
index d43d4fb9a..7933fbc6e 100644
--- a/pages/managers/[slug].vue
+++ b/pages/managers/[slug].vue
@@ -85,7 +85,7 @@ const socialLinks = computed(() => entity.value ? getManagerProfileSocialLinks(e
-
+
Products
@@ -102,14 +102,6 @@ const socialLinks = computed(() => entity.value ? getManagerProfileSocialLinks(e
{{ managedMarkets.length }}
-
-
- Links
-
-
- {{ socialLinks.length }}
-
-
Date: Thu, 25 Jun 2026 11:54:28 +0100
Subject: [PATCH 06/10] fix: align manager profile header
Remove the extra manager-profile label and place the back button with the manager identity header.
---
pages/managers/[slug].vue | 15 ++++++---------
1 file changed, 6 insertions(+), 9 deletions(-)
diff --git a/pages/managers/[slug].vue b/pages/managers/[slug].vue
index 7933fbc6e..41cda9a43 100644
--- a/pages/managers/[slug].vue
+++ b/pages/managers/[slug].vue
@@ -51,19 +51,16 @@ const socialLinks = computed(() => entity.value ? getManagerProfileSocialLinks(e
-
-
-
- Manager profile
-
-
-
-
+
+
From e964bbec5598cda218a33afb129b140f0cae3208 Mon Sep 17 00:00:00 2001
From: Seranged <80223622+Seranged@users.noreply.github.com>
Date: Thu, 25 Jun 2026 12:00:02 +0100
Subject: [PATCH 07/10] fix: position manager back button
---
pages/managers/[slug].vue | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/pages/managers/[slug].vue b/pages/managers/[slug].vue
index 41cda9a43..8f0617787 100644
--- a/pages/managers/[slug].vue
+++ b/pages/managers/[slug].vue
@@ -51,10 +51,14 @@ const socialLinks = computed(() => entity.value ? getManagerProfileSocialLinks(e
-
+
+
Date: Thu, 25 Jun 2026 12:05:41 +0100
Subject: [PATCH 08/10] fix: center manager back button
---
pages/managers/[slug].vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pages/managers/[slug].vue b/pages/managers/[slug].vue
index 8f0617787..d46a94f75 100644
--- a/pages/managers/[slug].vue
+++ b/pages/managers/[slug].vue
@@ -53,7 +53,7 @@ const socialLinks = computed(() => entity.value ? getManagerProfileSocialLinks(e
From 9bf6ef3c9fea90ef836d415e622d9a27764292a9 Mon Sep 17 00:00:00 2001
From: Seranged <80223622+Seranged@users.noreply.github.com>
Date: Thu, 25 Jun 2026 12:11:33 +0100
Subject: [PATCH 09/10] fix: simplify manager profile summary
---
pages/managers/[slug].vue | 69 +++++++++++++++------------------------
1 file changed, 27 insertions(+), 42 deletions(-)
diff --git a/pages/managers/[slug].vue b/pages/managers/[slug].vue
index d46a94f75..abb2c26a7 100644
--- a/pages/managers/[slug].vue
+++ b/pages/managers/[slug].vue
@@ -13,7 +13,6 @@ const route = useRoute()
const slug = computed(() => route.params.slug as string)
const {
entity,
- productEntries,
managedMarkets,
earnVaults,
isLoading,
@@ -83,47 +82,27 @@ const socialLinks = computed(() => entity.value ? getManagerProfileSocialLinks(e
>
No profile description is available yet.
+
-
-
-
-
- Products
-
-
- {{ productEntries.length }}
-
-
-
-
- Markets
-
-
- {{ managedMarkets.length }}
-
-
-
-
-
entity.value ? getManagerProfileSocialLinks(e
class="flex flex-col gap-12"
>
-
+
Markets
+
+ {{ managedMarkets.length }}
+
@@ -142,8 +124,11 @@ const socialLinks = computed(() => entity.value ? getManagerProfileSocialLinks(e
v-if="earnVaults.length"
class="flex flex-col gap-12"
>
-
+
Earn vaults
+
+ {{ earnVaults.length }}
+
Date: Thu, 25 Jun 2026 13:08:16 +0100
Subject: [PATCH 10/10] fix: clarify manager profile links
---
pages/managers/[slug].vue | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pages/managers/[slug].vue b/pages/managers/[slug].vue
index abb2c26a7..496c39fe4 100644
--- a/pages/managers/[slug].vue
+++ b/pages/managers/[slug].vue
@@ -94,11 +94,11 @@ const socialLinks = computed(() => entity.value ? getManagerProfileSocialLinks(e
rel="noopener noreferrer"
class="inline-flex items-center gap-6 rounded-8 border border-line-default bg-surface-elevated px-12 py-8 text-p3 text-content-primary hover:border-line-emphasis hover:text-accent-600 transition-colors"
>
+ {{ link.label }}
- {{ link.label }}