Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
6 changes: 3 additions & 3 deletions components/entities/asset/AssetInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ watch(() => props.collateralOptions, (options) => {
const firstEnabled = options.findIndex(o => !o.disabled)
if (firstEnabled >= 0) {
selectedIdx.value = firstEnabled
emits('change-collateral', firstEnabled)
emits('change-collateral', firstEnabled, options[firstEnabled])
}
}
})
Expand Down Expand Up @@ -184,9 +184,9 @@ const openChooseCollateralModal = () => {
selected: selectedIdx.value,
title: props.collateralModalTitle,
apyLabel: props.collateralModalApyLabel,
onSave: (selectedIndex: number) => {
onSave: (selectedIndex: number, selectedOption: CollateralOption) => {
selectedIdx.value = selectedIndex
emits('change-collateral', selectedIndex)
emits('change-collateral', selectedIndex, selectedOption)
modal.close()
},
},
Expand Down
4 changes: 3 additions & 1 deletion components/entities/operation/OperationReviewModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface REULUnlockInfo {
}

const { type, asset, assetIconUrl, reulUnlockInfo, amount, onConfirm, plan, prepared, swapToAsset, swapToAmount, swapMode, swapEstimatedSide, supplyingAssetForBorrow, supplyingAmount, transferAmounts, submittingLabel, quoteFetchedAt, hideExecute, subAccount, marketLabel } = defineProps<{
type?: 'supply' | 'withdraw' | 'borrow' | 'repay' | 'swap' | 'transfer' | 'reward' | 'brevis-reward' | 'fuul-reward' | 'reul-unlock' | 'disableCollateral' | 'swap-supply' | 'swap-withdraw' | 'swap-borrow'
type?: 'supply' | 'withdraw' | 'borrow' | 'repay' | 'swap' | 'transfer' | 'refinance' | 'reward' | 'brevis-reward' | 'fuul-reward' | 'reul-unlock' | 'disableCollateral' | 'swap-supply' | 'swap-withdraw' | 'swap-borrow'
asset: VaultAsset
assetIconUrl?: string
amount: number | string
Expand Down Expand Up @@ -293,6 +293,8 @@ const btnLabel = computed(() => {
return 'Swap'
case 'transfer':
return 'Transfer'
case 'refinance':
return 'Refinance'
case 'reul-unlock':
return 'Unlock'
case 'reward':
Expand Down
266 changes: 187 additions & 79 deletions components/entities/vault/ChooseCollateralModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const { productName, symbol, collateralOptions, selected = 0, title = 'Select co
selected?: number
title?: string
apyLabel?: string
onSave: (selectedIndex: number) => void
onSave: (selectedIndex: number, selectedOption: CollateralOption) => void
}>()

const { isEscrowVault } = useVaultRegistry()
Expand Down Expand Up @@ -69,6 +69,80 @@ const filteredOptions = computed(() => {
)
})

type FilteredCollateralOption = { option: CollateralOption, idx: number }
type GroupedRow
= | { kind: 'header', key: string, label: string, note?: string }
| { kind: 'empty', key: string, message: string }
| { kind: 'option', key: string, option: CollateralOption, idx: number }

// Split the already-filtered list into the two display groups.
// NOTE: each entry keeps its ORIGINAL `idx` from collateralOptions. Selection
// and existing callers depend on it, so never re-index here.
const compatibleOptions = computed(() =>
filteredOptions.value.filter(({ option }) => !option.compatibilityWarning),
)
const incompatibleOptions = computed(() =>
filteredOptions.value.filter(({ option }) => option.compatibilityWarning),
)

// Every incompatible option shares the same warning copy, so read the note once.
const incompatibleNote = computed(() =>
incompatibleOptions.value[0]?.option.compatibilityWarning?.message ?? '',
)
const compatibleNote = computed(() => {
return title.toLowerCase().includes('debt')
? 'Debt vaults accepting your current collateral'
: 'Collateral vaults accepted by your current debt'
})

// Keep group headers visible whenever options require migration, even if the
// compatible bucket is empty, so the absence is explicit.
const showGroups = computed(() =>
incompatibleOptions.value.length > 0,
)

const groupedRows = computed<GroupedRow[]>(() => {
const rows: GroupedRow[] = []

const pushOptions = (items: FilteredCollateralOption[]) => {
for (const { option, idx } of items) {
rows.push({ kind: 'option', key: `opt-${idx}`, option, idx })
}
}

if (!showGroups.value) {
pushOptions(filteredOptions.value)
return rows
}

rows.push({
kind: 'header',
key: 'hdr-compatible',
label: 'Compatible',
note: compatibleNote.value,
})
if (compatibleOptions.value.length) {
pushOptions(compatibleOptions.value)
}
else {
rows.push({
kind: 'empty',
key: 'empty-compatible',
message: 'No compatible vaults found',
})
}

rows.push({
kind: 'header',
key: 'hdr-incompatible',
label: 'Requires migration',
note: incompatibleNote.value,
})
pushOptions(incompatibleOptions.value)

return rows
})

const handleClose = () => {
emits('close')
}
Expand All @@ -89,95 +163,129 @@ const handleClose = () => {
/>
</div>
<div class="flex-1 min-h-0 overflow-auto styled-scrollbar">
<div
v-for="{ option, idx } in filteredOptions"
:key="`options-${idx}`"
data-id="collateral-option"
:data-option-index="String(idx)"
:data-option-label="getOptionLabel(option)"
:data-option-symbol="getOptionSymbol(option)"
:data-option-type="getOptionType(option)"
:data-option-asset-address="option.assetAddress?.toLowerCase() ?? ''"
:data-option-vault-address="option.vaultAddress?.toLowerCase() ?? ''"
:data-option-sub-account="option.subAccount?.toLowerCase() ?? ''"
:data-option-disabled="option.disabled ? 'true' : 'false'"
class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-10 py-12 px-16 rounded-16"
:class="[
option.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
selectedIdx === idx && !option.disabled ? 'bg-card-hover' : '',
]"
@click="
if (!option.disabled) { selectedIdx = idx;onSave(idx) }
"
<template
v-for="row in groupedRows"
:key="row.key"
>
<AssetAvatar
:asset="{ address: option.assetAddress || '', symbol: getOptionSymbol(option) }"
size="36"
/>
<div class="grid grid-cols-[minmax(0,1fr)_max-content] items-center gap-12 min-w-0">
<div class="min-w-0">
<div class="text-content-primary mb-2 truncate">
{{ getOptionLabel(option) }}
</div>
<div class="text-h5 flex items-center min-w-0">
<span class="truncate">{{ getOptionSymbol(option) }}</span>
<div
v-if="getOptionType(option) === 'wallet'"
class="ml-6 text-[12px] leading-[16px] py-4 px-8 rounded-8 bg-accent-600/10 text-accent-600"
>
Wallet
</div>
<div
v-else-if="getOptionType(option) === 'saving'"
class="ml-6 text-[12px] leading-[16px] py-4 px-8 rounded-8 bg-[#CBC0951A] text-yellow-600"
>
Savings
</div>
<div
v-if="getSubAccountLabel(option)"
class="ml-6 text-[12px] leading-[16px] py-4 px-8 rounded-8 bg-card text-content-secondary"
>
{{ getSubAccountLabel(option) }}
</div>
<span
v-for="tag in (option.tags || [])"
:key="tag"
class="ml-6 inline-flex items-center gap-4 rounded-8 px-8 py-2 bg-warning-100 text-warning-500 text-p5"
>
<SvgIcon
name="warning"
class="!w-14 !h-14"
/>
{{ tag }}
</span>
</div>
<div
v-if="row.kind === 'header'"
class="sticky top-0 z-10 bg-card px-16 pt-12 pb-6"
>
<div class="flex items-center gap-6">
<SvgIcon
:name="row.label === 'Compatible' ? 'check-circle' : 'warning'"
class="!w-14 !h-14"
:class="row.label === 'Compatible' ? 'text-accent-600' : 'text-warning-500'"
/>
<span class="text-[12px] font-semibold uppercase tracking-[0.04em] text-content-tertiary">
{{ row.label }}
</span>
</div>
<div
class="grid justify-end text-right"
:class="showBalanceMetric(option) ? 'grid-cols-[max-content_max-content] gap-32' : 'grid-cols-[max-content]'"
<p
v-if="row.note"
class="mt-4 text-p3 text-content-tertiary leading-snug"
>
<div
v-if="showBalanceMetric(option)"
class="flex flex-col items-end"
>
<div class="text-content-primary mb-2">
Balance
{{ row.note }}
</p>
</div>

<div
v-else-if="row.kind === 'empty'"
class="px-16 py-12 text-p3 text-content-tertiary"
>
{{ row.message }}
</div>

<div
v-else-if="row.kind === 'option'"
data-id="collateral-option"
:data-option-index="String(row.idx)"
:data-option-label="getOptionLabel(row.option)"
:data-option-symbol="getOptionSymbol(row.option)"
:data-option-type="getOptionType(row.option)"
:data-option-asset-address="row.option.assetAddress?.toLowerCase() ?? ''"
:data-option-vault-address="row.option.vaultAddress?.toLowerCase() ?? ''"
:data-option-sub-account="row.option.subAccount?.toLowerCase() ?? ''"
:data-option-disabled="row.option.disabled ? 'true' : 'false'"
:data-option-compatibility-warning="row.option.compatibilityWarning ? 'true' : 'false'"
class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-10 py-12 px-16 rounded-16"
:class="[
row.option.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
selectedIdx === row.idx && !row.option.disabled ? 'bg-card-hover' : '',
]"
@click="
if (!row.option.disabled) { selectedIdx = row.idx;onSave(row.idx, row.option) }
"
>
<AssetAvatar
:asset="{ address: row.option.assetAddress || '', symbol: getOptionSymbol(row.option) }"
size="36"
/>
<div class="grid grid-cols-[minmax(0,1fr)_max-content] items-center gap-12 min-w-0">
<div class="min-w-0">
<div class="text-content-primary mb-2 truncate">
{{ getOptionLabel(row.option) }}
</div>
<div class="text-h5">
{{ getFormattedAmount(option) }}
<div class="text-h5 flex items-center min-w-0">
<span class="truncate">{{ getOptionSymbol(row.option) }}</span>
<div
v-if="getOptionType(row.option) === 'wallet'"
class="ml-6 text-[12px] leading-[16px] py-4 px-8 rounded-8 bg-accent-600/10 text-accent-600"
>
Wallet
</div>
<div
v-else-if="getOptionType(row.option) === 'saving'"
class="ml-6 text-[12px] leading-[16px] py-4 px-8 rounded-8 bg-[#CBC0951A] text-yellow-600"
>
Savings
</div>
<div
v-if="getSubAccountLabel(row.option)"
class="ml-6 text-[12px] leading-[16px] py-4 px-8 rounded-8 bg-card text-content-secondary"
>
{{ getSubAccountLabel(row.option) }}
</div>
<span
v-for="tag in (row.option.tags || [])"
:key="tag"
class="ml-6 inline-flex items-center gap-4 rounded-8 px-8 py-2 bg-warning-100 text-warning-500 text-p5"
>
<SvgIcon
name="warning"
class="!w-14 !h-14"
/>
{{ tag }}
</span>
</div>
</div>
<div class="flex flex-col items-end min-w-[110px]">
<div class="text-content-primary mb-2">
{{ getApyLabel(option) }}
<div
class="grid justify-end text-right"
:class="showBalanceMetric(row.option) ? 'grid-cols-[max-content_max-content] gap-32' : 'grid-cols-[max-content]'"
>
<div
v-if="showBalanceMetric(row.option)"
class="flex flex-col items-end"
>
<div class="text-content-primary mb-2">
Balance
</div>
<div class="text-h5">
{{ getFormattedAmount(row.option) }}
</div>
</div>
<div class="text-h5">
{{ option.apy !== undefined ? `${formatNumber(option.apy)}%` : '-' }}
<div class="flex flex-col items-end min-w-[110px]">
<div class="text-content-primary mb-2">
{{ getApyLabel(row.option) }}
</div>
<div class="text-h5">
{{ row.option.apy !== undefined ? `${formatNumber(row.option.apy)}%` : '-' }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<div
v-if="!filteredOptions.length && searchQuery"
class="py-24 text-center text-content-tertiary text-p3"
Expand Down
3 changes: 2 additions & 1 deletion components/entities/vault/form/VaultForm.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
defineProps<{ title?: string, description?: string, loading?: boolean, back?: boolean, backFallback?: string }>()
defineProps<{ title?: string, description?: string, loading?: boolean, back?: boolean, backFallback?: string, backAlwaysFallback?: boolean }>()
</script>

<template>
Expand All @@ -16,6 +16,7 @@ defineProps<{ title?: string, description?: string, loading?: boolean, back?: bo
v-if="back"
class="tablet:hidden"
:fallback="backFallback"
:always-fallback="backAlwaysFallback"
/>
<h1
v-if="title"
Expand Down
4 changes: 2 additions & 2 deletions composables/borrow/useMultiplyCowSwap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export const useMultiplyCowSwap = (options: UseMultiplyCowSwapOptions) => {
idx = sellTokenApproval.nextIndex

signSteps.push({ index: idx++, label: 'Sign EVC permit', isSeparateTx: false })
signSteps.push({ index: idx++, label: 'Sign CoW order', isSeparateTx: false })
signSteps.push({ index: idx, label: 'Sign CoW order', isSeparateTx: false })

const collateralVaultName = options.multiplySupplyProduct.value.name || undefined
const borrowVaultName = options.multiplyShortProduct.value.name || undefined
Expand All @@ -259,7 +259,7 @@ export const useMultiplyCowSwap = (options: UseMultiplyCowSwapOptions) => {
{ index: wIdx++, label: 'Supply', isSeparateTx: false, assetInfo: { symbol: collateralAsset.symbol, address: collateralAsset.address, amount: options.multiplyInputAmount.value } },
{ index: wIdx++, label: 'Borrow', isSeparateTx: false, assetInfo: { symbol: borrowAsset.symbol, address: borrowAsset.address, amount: borrowAmountStr } },
{ index: wIdx++, label: 'Swap', isSeparateTx: false, assetInfo: { symbol: borrowAsset.symbol, address: borrowAsset.address, amount: borrowAmountStr }, toAssetInfo: { symbol: collateralAsset.symbol, address: collateralAsset.address, amount: options.multiplyLongAmount.value } },
{ index: wIdx++, label: 'Verify min received', isSeparateTx: false, assetInfo: { symbol: collateralAsset.symbol, address: collateralAsset.address, amount: swapOutMinAmount } },
{ index: wIdx, label: 'Verify min received', isSeparateTx: false, assetInfo: { symbol: collateralAsset.symbol, address: collateralAsset.address, amount: swapOutMinAmount } },
]

const walletWarningsDescription
Expand Down
4 changes: 2 additions & 2 deletions composables/cowswap/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
export { useCowSwapExecutionCore } from './useCowSwapExecutionCore'
export { useCowSwapOpenPositionExecution } from './useCowSwapOpenPositionExecution'
export { useCowSwapCollateralSwapExecution } from './useCowSwapCollateralSwapExecution'
export { useCowSwapClosePositionExecution } from './useCowSwapClosePositionExecution'
export { useCowSwapCollateralSwapExecution } from './useCowSwapCollateralSwapExecution'
export { useCowSwapOrderStatus } from './useCowSwapOrderStatus'
export { openCowSwapReviewModal, buildApprovalSignSteps } from './openCowSwapReviewModal'

export type { CowSwapOpenPositionExecuteParams } from './useCowSwapOpenPositionExecution'
export type { CowSwapCollateralSwapExecuteParams } from './useCowSwapCollateralSwapExecution'
export type { CowSwapClosePositionExecuteParams } from './useCowSwapClosePositionExecution'
export type { CowSwapCollateralSwapExecuteParams } from './useCowSwapCollateralSwapExecution'
Loading