Skip to content
Draft
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
19 changes: 14 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,16 @@ NUXT_PUBLIC_CONFIG_LOGO_URL=
# Recommended: 1200x630 or larger (2400x1260), PNG or JPG. Must be publicly
# reachable — X, Slack, Discord fetch it directly. Empty = no preview image.
NUXT_PUBLIC_CONFIG_SOCIAL_IMAGE_URL=
NUXT_PUBLIC_CONFIG_LABELS_REPO="euler-xyz/euler-labels"
NUXT_PUBLIC_CONFIG_LABELS_REPO_BRANCH="master"
# Base URL for labels data (S3/CDN). When set, LABELS_REPO and LABELS_REPO_BRANCH are ignored.
# Must serve the same directory structure (e.g. {baseUrl}/{chainId}/products.json, {baseUrl}/logo/{file}).
NUXT_PUBLIC_CONFIG_LABELS_BASE_URL=
# Hosted mode reads V3 only. Forks can select their own static label files.
LABELS_SOURCE=v3
# V3 metadata dataset and default publication (latest or an immutable published version key).
# Geo policies, visibility and governance addresses remain live and shared.
LABELS_V3_SET=public
LABELS_V3_VERSION=latest
# Enabled DEPRECATED_CHAINS use V3 metadata with Lite on-chain verification.
# They must also be in ONCHAIN_SDK_CHAINS. Other enabled chains use V3 assessments.
# Required only for LABELS_SOURCE=static: operator-owned five-file labels directory.
STATIC_LABELS_BASE_URL=
# Full URL for EulerChains.json (contract addresses). Takes precedence over
# EULER_SDK_EULER_INTERFACES_BRANCH and its public aliases — it is the
# emergency repoint lever when the default GitHub source is unavailable.
Expand Down Expand Up @@ -224,3 +229,7 @@ SUBGRAPH_URL_80094="https://api.goldsky.com/api/public/project_cm4iagnemt1wp01xn
SUBGRAPH_URL_8453="https://api.goldsky.com/api/public/project_cm4iagnemt1wp01xn4gh1agft/subgraphs/euler-simple-base/latest/gn"
SUBGRAPH_URL_9745="https://api.goldsky.com/api/public/project_cm4iagnemt1wp01xn4gh1agft/subgraphs/euler-simple-plasma/latest/gn"
SUBGRAPH_URL_999=https://api.goldsky.com/api/public/project_cm4iagnemt1wp01xn4gh1agft/subgraphs/euler-simple-hyperevm/1.0.2/gn

# Mount this directory on persistent storage to retain geo policies across redeploys.
# Default: .data/geo-policies (survives process restarts only on the same filesystem).
GEO_POLICY_CACHE_DIR=
27 changes: 5 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ Euler Lite uses the [Euler V2 SDK](https://github.com/euler-xyz/euler-sdks) for
| `TENDERLY_ACCESS_KEY`, `TENDERLY_ACCOUNT_SLUG`, `TENDERLY_PROJECT_SLUG` | Optional Tenderly simulation configuration. |
| `FUUL_API_URL` or `NUXT_PUBLIC_FUUL_API_URL` | Optional Fuul API upstream override. |
| `INCENTRA_API_URL` or `NUXT_PUBLIC_INCENTRA_API_URL` | Optional Incentra/Brevis API upstream override. |
| `LABELS_SOURCE` | `v3` (default) or `static`; selects the shared label snapshot producer. |
| `STATIC_LABELS_BASE_URL` | Required directory URL for static authoring files; unused by hosted V3 mode. |
| `TURTLE_EARN_API_URL` or `NUXT_PUBLIC_TURTLE_EARN_API_URL` | Optional Turtle reward-proof upstream override; defaults to `https://earn.turtle.xyz/v1`. |

#### Branding & Feature Flags
Expand All @@ -95,9 +97,6 @@ These settings use `NUXT_PUBLIC_CONFIG_*` env vars. Branding and announcement va
| `NUXT_PUBLIC_CONFIG_APP_DESCRIPTION` | `Lightweight interface for Euler Finance.` | App description |
| `NUXT_PUBLIC_CONFIG_LOGO_URL` | — | Custom logo URL (falls back to built-in Euler logo) |
| `NUXT_PUBLIC_CONFIG_SOCIAL_IMAGE_URL` | — | Absolute URL to social share image (og:image / twitter:image), 1200×630+ |
| `NUXT_PUBLIC_CONFIG_LABELS_REPO` | `euler-xyz/euler-labels` | GitHub labels repo |
| `NUXT_PUBLIC_CONFIG_LABELS_REPO_BRANCH` | `master` | Branch to fetch labels from |
| `NUXT_PUBLIC_CONFIG_LABELS_BASE_URL` | — | S3/CDN base URL for labels (overrides repo/branch) |
| `NUXT_PUBLIC_CONFIG_EULER_CHAINS_URL` | — | Explicit URL for EulerChains.json; takes precedence over the Euler interfaces branch |
| `NUXT_PUBLIC_CONFIG_EULER_ABIS_BASE_URL` | — | Base URL serving `{contract}.json` for runtime ABIs; takes precedence over the Euler interfaces branch. |
| `NUXT_PUBLIC_CONFIG_DOCS_URL` | — | Documentation link |
Expand Down Expand Up @@ -219,27 +218,11 @@ The resolution order in `getAssetLogoUrl(address, symbol)`:
2. `logoURI` from the unified token list (Euler SDK token list > DefiLlama > Uniswap > Merkl)
3. Empty string (component shows initials fallback)

#### EulerEarn Vaults
#### Public Labels

If using a custom labels repository, create chain-specific `earn-vaults.json` files to curate which EulerEarn vaults appear:
Vault inventory, products, entities, hosted logos, tags, campaigns, and deprecation metadata come from the versioned Public Labels API at `V3_API_URL`. Lite resolves `version=latest` to one immutable publication before fetching a chain bundle and follows every paginated list through `meta.total`.

```
your-labels-repo/
├── 1/earn-vaults.json # Ethereum
├── 42161/earn-vaults.json # Arbitrum
└── 8453/earn-vaults.json # Base
```

Each file is a JSON array of vault addresses:

```json
[
"0x1234567890123456789012345678901234567890",
"0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
]
```

When using the default `euler-xyz/euler-labels` repository, all verified EulerEarn vaults are shown automatically.
Hosted labels, visibility and geo policies come exclusively from V3. Fork deployments can set `LABELS_SOURCE=static` and provide their own authoring files through `STATIC_LABELS_BASE_URL`. See [Static labels](docs/static-labels.md) for the format, failure semantics and on-chain data options.

### 4. Development

Expand Down
19 changes: 18 additions & 1 deletion app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ let isAnnouncementOpen = false
const { loadEulerConfig, chainId } = useEulerAddresses()
const { loadVaults, isReady: isVaultsReady, resetVaultsState, refreshVaults, setShowAllLabelEntries } = useVaults()
const { loadTokenList, isLoaded: isTokenListLoaded } = useTokenList()
const { loadLabels, retryLabels, isReady: isLabelsReady, isLoading: isLabelsLoading, loadError: labelsLoadError } = useEulerLabels()
const { loadLabels, refreshLabelsIfStale, retryLabels, isReady: isLabelsReady, isLoading: isLabelsLoading, loadError: labelsLoadError } = useEulerLabels()
const { loadCountry } = useGeoBlock()
const { updateBalances, resetBalances } = useWallets()
const { isConnected, address } = useWagmi()
Expand Down Expand Up @@ -162,6 +162,23 @@ watch([chainId, showAllLabelEntries], () => {
})
}, { immediate: true })

// Refresh live label verdicts for an open tab and after returning from a
// suspended/background tab. The labels store owns freshness and deduplication.
let labelsRefreshInterval: ReturnType<typeof setInterval> | undefined
const refreshVisibleLabels = () => {
if (document.visibilityState === 'visible') void refreshLabelsIfStale()
}
onMounted(() => {
labelsRefreshInterval = setInterval(refreshVisibleLabels, POLL_INTERVAL_60S_MS)
window.addEventListener('focus', refreshVisibleLabels)
document.addEventListener('visibilitychange', refreshVisibleLabels)
})
onUnmounted(() => {
clearInterval(labelsRefreshInterval)
window.removeEventListener('focus', refreshVisibleLabels)
document.removeEventListener('visibilitychange', refreshVisibleLabels)
})

// Refresh balances when token list finishes loading (includes new DefiLlama tokens)
watch(isTokenListLoaded, (loaded) => {
if (loaded && (isConnected.value || isVaultsReady.value)) {
Expand Down
2 changes: 1 addition & 1 deletion components/entities/portfolio/PortfolioNotice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defineProps<{ notice: string }>()
name="info-circle"
class="!w-20 !h-20 text-accent-600 shrink-0"
/>
<!-- eslint-disable vue/no-v-html -- trusted label content -->
<!-- eslint-disable vue/no-v-html -- autoLink escapes label text before adding links -->
<span
class="text-accent-700 text-p4 auto-link"
@click="stopLinkPropagation"
Expand Down
5 changes: 5 additions & 0 deletions components/entities/vault/VaultBorrowItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,11 @@ const linkPath = computed(() => ({
data-field="borrow-apy"
:data-value="borrowApyWithRewards"
>
<VaultPoints
class="mr-4"
:vault="pair.borrow"
campaign-type="borrow"
/>
<UiModalPreviewTrigger
v-if="hasBorrowApyRewards"
:component="VaultApyModal"
Expand Down
34 changes: 15 additions & 19 deletions components/entities/vault/VaultPoints.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
<script setup lang="ts">
import type { EulerEarn, EVault, SecuritizeCollateralVault } from '@eulerxyz/euler-v2-sdk'
import { getEulerLabelEntityLogo } from '~/entities/euler/labels'
import { getEulerLabelPointLogo } from '~/entities/euler/labels'
import { useModal } from '~/components/ui/composables/useModal'
import { VaultPointsModal } from '#components'

const { vault } = defineProps<{ vault: EVault | EulerEarn | SecuritizeCollateralVault }>()
const { vault, campaignType = 'deposit' } = defineProps<{
vault: EVault | EulerEarn | SecuritizeCollateralVault
campaignType?: 'deposit' | 'borrow'
}>()

const points = useEulerPointsOfVault(vault.address)
const allPoints = useEulerPointsOfVault(vault.address)
const points = computed(() => allPoints.filter(point =>
point.type ? point.type === campaignType : campaignType === 'deposit',
))
const modal = useModal()

const onLogoError = (event: Event, logoFileName: string) => {
const img = event.target as HTMLImageElement
if (!img.dataset.triedFallback) {
img.dataset.triedFallback = 'true'
img.src = getEulerLabelEntityLogo(logoFileName)
}
}

const onPointClick = (
point: { name: string, logo: string },
event: MouseEvent | TouchEvent,
Expand All @@ -27,6 +25,7 @@ const onPointClick = (
props: {
pointName: point.name,
pointLogo: point.logo,
campaignType,
},
})
}
Expand All @@ -37,18 +36,15 @@ const onPointClick = (
v-if="points.length"
class="text-p1 flex items-center gap-0 hover:gap-8 transition-[gap] duration-300 ease-in-out"
>
<img
<BaseAvatar
v-for="(point, index) in points"
:key="point.name"
class="w-16 h-16 rounded-full cursor-pointer select-none"
class="icon--16 rounded-full cursor-pointer select-none"
:class="{ '-ml-6': index > 0 }"
:src="`/entities/${point.logo}`"
alt="Points entity logo"
referrerpolicy="no-referrer"
draggable="false"
:src="getEulerLabelPointLogo(point.logo)"
:label="point.name"
@click="onPointClick(point, $event)"
@error="onLogoError($event, point.logo)"
@contextmenu.prevent
>
/>
</div>
</template>
29 changes: 10 additions & 19 deletions components/entities/vault/VaultPointsModal.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
<script setup lang="ts">
import { getEulerLabelEntityLogo } from '~/entities/euler/labels'
import { getEulerLabelPointLogo } from '~/entities/euler/labels'

defineEmits(['close'])
const { pointName, pointLogo } = defineProps<{
const { pointName, pointLogo, campaignType = 'deposit' } = defineProps<{
pointName: string
pointLogo: string
campaignType?: 'deposit' | 'borrow'
}>()

const onLogoError = (event: Event) => {
const img = event.target as HTMLImageElement
if (!img.dataset.triedFallback) {
img.dataset.triedFallback = 'true'
img.src = getEulerLabelEntityLogo(pointLogo)
}
}

const escapeHtml = (unsafe: string): string => {
return unsafe
.replace(/&/g, '&amp;')
Expand Down Expand Up @@ -67,15 +60,13 @@ const formattedPointName = computed(() => convertMarkdownLinks(pointName))
@close="$emit('close')"
>
<div class="flex items-center gap-12">
<span class="text-p2">Supply earns</span>
<img
:src="`/entities/${pointLogo}`"
alt="Point logo"
referrerpolicy="no-referrer"
class="w-20 h-20 rounded-full"
@error="onLogoError"
>
<!-- eslint-disable vue/no-v-html -- trusted label content -->
<span class="text-p2">{{ campaignType === 'borrow' ? 'Borrowing earns' : 'Supply earns' }}</span>
<BaseAvatar
:src="getEulerLabelPointLogo(pointLogo)"
:label="pointName"
class="icon--20 rounded-full"
/>
<!-- eslint-disable vue/no-v-html -- formattedPointName escapes campaign text before adding links -->
<span
class="text-p2"
v-html="formattedPointName"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const getMaxRoeModalData = (result: BestMaxRoeResult) => ({
class="icon--40 shrink-0"
:class="{ 'opacity-20': isGovernanceLimited }"
:src="marketEntities.logos"
:label="marketEntities.name"
:label="marketEntities.labels"
/>
<div
class="flex-grow min-w-0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ const supplyCapPercentageDisplay = computed(() => {
v-if="description"
class="w-full rounded-12 p-16 bg-surface-tertiary"
>
<!-- eslint-disable vue/no-v-html -- trusted label content -->
<!-- eslint-disable vue/no-v-html -- autoLink escapes label text before adding links -->
<p
class="text-p3 text-content-secondary auto-link"
v-html="autoLink(description)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defineProps<{ reason: string }>()
name="warning"
class="!w-20 !h-20 flex-shrink-0"
/>
<!-- eslint-disable vue/no-v-html -- deprecationReason is validated server-side in server/api/internal/labels/[file].get.ts -->
<!-- eslint-disable vue/no-v-html -- autoLink escapes label text before adding links -->
<p
v-if="reason"
class="text-p3 text-warning-500 auto-link"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ watchEffect(() => {
</p>
</div>
</div>
<!-- eslint-disable vue/no-v-html -- trusted label content -->
<!-- eslint-disable vue/no-v-html -- autoLink escapes label text before adding links -->
<p
v-if="description"
class="text-p2 text-content-secondary auto-link"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const feeDisplay = computed(() => {
</p>
</div>
</div>
<!-- eslint-disable vue/no-v-html -- trusted label content -->
<!-- eslint-disable vue/no-v-html -- autoLink escapes label text before adding links -->
<p
v-if="earnDescription"
class="text-p2 text-content-secondary auto-link"
Expand Down
8 changes: 5 additions & 3 deletions composables/guards/useUnverifiedVaultGuard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface UnverifiedVaultGuardContext {
account: Ref<string | undefined>
chainId: Ref<number | undefined>
operation: ComputedRef<string>
allowUnavailableLabels?: boolean
}

let unverifiedVaultGuardSequence = 0
Expand Down Expand Up @@ -59,12 +60,12 @@ export const useUnverifiedVaultGuard = (
return !!entry && !!context.chainId.value && entry.vault.chainId === context.chainId.value
})
})
const isVerificationReady = computed(() => labelsReady.value && hasVaultMetadata.value)
const isVerificationReady = computed(() => (labelsReady.value || context.allowUnavailableLabels === true) && hasVaultMetadata.value)
const isVerificationLoading = computed(() =>
(!labelsReady.value && !labelsError.value) || (!hasVaultMetadata.value && isResolvingVaults.value),
(!context.allowUnavailableLabels && !labelsReady.value && !labelsError.value) || (!hasVaultMetadata.value && isResolvingVaults.value),
)
const verificationError = computed(() => {
if (!labelsReady.value && labelsError.value) return labelsError.value
if (!context.allowUnavailableLabels && !labelsReady.value && labelsError.value) return labelsError.value
if (!hasVaultMetadata.value && !isResolvingVaults.value) return 'Unable to load vault verification. Please retry.'
return undefined
})
Expand All @@ -74,6 +75,7 @@ export const useUnverifiedVaultGuard = (
}

const hasCanonicalVerification = (address: string): boolean => {
if (!labelsReady.value) return false
const entry = get(address)
if (!entry || !context.chainId.value || entry.vault.chainId !== context.chainId.value) return false

Expand Down
7 changes: 0 additions & 7 deletions composables/useDeployConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ export const useDeployConfig = () => {
const s = String(val)
return s !== 'false' && s !== '0'
}
const labelsBaseUrl = (rc.configLabelsBaseUrl || '').trim().replace(/\/+$/, '')

return {
// URLs (empty string = not configured, hide UI element)
docsUrl: rc.configDocsUrl,
Expand All @@ -28,11 +26,6 @@ export const useDeployConfig = () => {
logoUrl: envConfig.logoUrl,
socialImageUrl: envConfig.socialImageUrl,

// Repos (labelsRepo/branch/baseUrl still needed for logo URL construction)
labelsRepo: rc.configLabelsRepo || 'euler-xyz/euler-labels',
labelsRepoBranch: rc.configLabelsRepoBranch || 'master',
labelsBaseUrl,

// Feature flags: all enabled by default, set env var to 'false' to disable
enableTosSignature: !!rc.configTosMdUrl,
enableEntityBranding: isEnabled(rc.configEnableEntityBranding),
Expand Down
9 changes: 0 additions & 9 deletions composables/useEulerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,12 @@ import {

export const useEulerConfig = () => {
const envConfig = useEnvConfig()
const { labelsRepo, labelsRepoBranch, labelsBaseUrl: configLabelsBaseUrl } = useDeployConfig()

const resolvedLabelsBaseUrl = (
configLabelsBaseUrl
|| `https://raw.githubusercontent.com/${labelsRepo}/refs/heads/${labelsRepoBranch}`
).replace(/\/+$/, '')

return {
// APIs (from constants)
DEFILLAMA_YIELDS_URL,
MERKL_API_BASE_URL,

// Labels
EULER_LABELS_ENTITY_LOGO_URL: `${resolvedLabelsBaseUrl}/logo`,

// Runtime app config APIs
SWAP_API_URL: envConfig.swapApiUrl,
PYTH_HERMES_URL: envConfig.pythHermesUrl,
Expand Down
Loading
Loading