feat: auto-lend foundation (#283)

This commit is contained in:
Yusuf Seyrek 2023-07-05 17:43:11 +03:00 committed by GitHub
parent 21c8d04824
commit 0123685f79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 252 additions and 41 deletions

View File

@ -9,13 +9,13 @@ import { ArrowCircledTopRight, ArrowDownLine, ArrowUpLine, TrashBin } from 'comp
import Radio from 'components/Radio'
import SwitchWithLabel from 'components/SwitchWithLabel'
import Text from 'components/Text'
import useToggle from 'hooks/useToggle'
import useStore from 'store'
import { calculateAccountDeposits } from 'utils/accounts'
import { hardcodedFee } from 'utils/constants'
import { BN } from 'utils/helpers'
import { getPage, getRoute } from 'utils/route'
import usePrices from 'hooks/usePrices'
import useAutoLendEnabledAccountIds from 'hooks/useAutoLendEnabledAccountIds'
interface Props {
setShowFundAccount: (showFundAccount: boolean) => void
@ -32,10 +32,9 @@ export default function AccountList(props: Props) {
const { pathname } = useLocation()
const { accountId, address } = useParams()
const { data: prices } = usePrices()
const { autoLendEnabledAccountIds, toggleAutoLend } = useAutoLendEnabledAccountIds()
const deleteAccount = useStore((s) => s.deleteAccount)
const [isLending, setIsLending] = useToggle()
const accountSelected = !!accountId && !isNaN(Number(accountId))
const selectedAccountDetails = props.accounts.find((account) => account.id === accountId)
const selectedAccountBalance = selectedAccountDetails
@ -50,11 +49,6 @@ export default function AccountList(props: Props) {
}
}
function onChangeLendSwitch() {
setIsLending(!isLending)
/* TODO: handle lending assets */
}
useEffect(() => {
const element = document.getElementById(`account-${accountId}`)
if (element) {
@ -69,6 +63,8 @@ export default function AccountList(props: Props) {
{props.accounts.map((account) => {
const positionBalance = calculateAccountDeposits(account, prices)
const isActive = accountId === account.id
const isAutoLendEnabled = autoLendEnabledAccountIds.includes(account.id)
return (
<div key={account.id} id={`account-${account.id}`} className='w-full pt-4'>
<Card
@ -138,8 +134,8 @@ export default function AccountList(props: Props) {
<SwitchWithLabel
name='isLending'
label='Lend assets to earn yield'
value={isLending}
onChange={onChangeLendSwitch}
value={isAutoLendEnabled}
onChange={() => toggleAutoLend(account.id)}
tooltip={`Fund your account and lend assets effortlessly! By lending, you'll earn attractive interest (APY) without impacting your LTV. It's a win-win situation - don't miss out on this easy opportunity to grow your holdings!`}
/>
</div>

View File

@ -16,6 +16,7 @@ import { hardcodedFee } from 'utils/constants'
import { isNumber } from 'utils/parsers'
const menuClasses = 'absolute isolate flex w-full flex-wrap scrollbar-hide'
const ACCOUNT_MENU_BUTTON_ID = 'account-menu-button'
interface Props {
accounts: Account[]
@ -57,6 +58,7 @@ export default function AccountMenuContent(props: Props) {
return (
<div className='relative'>
<Button
id={ACCOUNT_MENU_BUTTON_ID}
onClick={hasCreditAccounts ? () => setShowMenu(!showMenu) : createAccountHandler}
leftIcon={hasCreditAccounts ? <Account /> : <PlusCircled />}
color={hasCreditAccounts ? 'tertiary' : 'primary'}
@ -133,3 +135,5 @@ export default function AccountMenuContent(props: Props) {
</div>
)
}
export { ACCOUNT_MENU_BUTTON_ID }

View File

@ -13,6 +13,7 @@ import useStore from 'store'
import { getAmount } from 'utils/accounts'
import { hardcodedFee } from 'utils/constants'
import { BN } from 'utils/helpers'
import useAutoLendEnabledAccountIds from 'hooks/useAutoLendEnabledAccountIds'
interface Props {
setShowFundAccount: (show: boolean) => void
@ -26,7 +27,7 @@ export default function FundAccount(props: Props) {
const [amount, setAmount] = useState(BN(0))
const [asset, setAsset] = useState<Asset>(ASSETS[0])
const [isLending, setIsLending] = useToggle()
const { autoLendEnabledAccountIds, toggleAutoLend } = useAutoLendEnabledAccountIds()
const [isFunding, setIsFunding] = useToggle()
const max = getAmount(asset.denom, balances ?? [])
@ -39,14 +40,6 @@ export default function FundAccount(props: Props) {
setAsset(asset)
}, [])
const handleLendAssets = useCallback(
(val: boolean) => {
setIsLending(val)
/* TODO: handle lending assets */
},
[setIsLending],
)
async function onDeposit() {
if (!accountId) return
setIsFunding(true)
@ -65,6 +58,8 @@ export default function FundAccount(props: Props) {
}
}
if (!accountId) return null
return (
<>
<div className='absolute right-4 top-4'>
@ -100,8 +95,8 @@ export default function FundAccount(props: Props) {
<SwitchWithLabel
name='isLending'
label='Lend assets to earn yield'
value={isLending}
onChange={() => handleLendAssets(!isLending)}
value={autoLendEnabledAccountIds.includes(accountId)}
onChange={() => toggleAutoLend(accountId)}
className='mb-4'
tooltip="Fund your account and lend assets effortlessly! By lending, you'll earn attractive interest (APY) without impacting your LTV. It's a win-win situation - don't miss out on this easy opportunity to grow your holdings!"
disabled={isFunding || amount.isEqualTo(0)}

View File

@ -1,11 +1,16 @@
import { useCallback } from 'react'
import Button from 'components/Button'
import { ArrowDownLine, ArrowUpLine } from 'components/Icons'
import { ArrowDownLine, ArrowUpLine, Enter } from 'components/Icons'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import ConditionalWrapper from 'hocs/ConditionalWrapper'
import useAlertDialog from 'hooks/useAlertDialog'
import useCurrentAccountDeposits from 'hooks/useCurrentAccountDeposits'
import useLendAndReclaimModal from 'hooks/useLendAndReclaimModal'
import { byDenom } from 'utils/array'
import useAutoLendEnabledAccountIds from 'hooks/useAutoLendEnabledAccountIds'
import { ACCOUNT_MENU_BUTTON_ID } from 'components/Account/AccountMenuContent'
interface Props {
data: LendingMarketTableData
@ -18,8 +23,32 @@ function LendingActionButtons(props: Props) {
const { asset, accountLentValue: accountLendValue } = props.data
const accountDeposits = useCurrentAccountDeposits()
const { openLend, openReclaim } = useLendAndReclaimModal()
const { open: showAlertDialog } = useAlertDialog()
const { isAutoLendEnabledForCurrentAccount } = useAutoLendEnabledAccountIds()
const assetDepositAmount = accountDeposits.find(byDenom(asset.denom))?.amount
const handleWithdraw = useCallback(() => {
if (isAutoLendEnabledForCurrentAccount) {
showAlertDialog({
title: 'Disable Automatically Lend Assets',
description:
"Your auto-lend feature is currently enabled. To recover your funds, please confirm if you'd like to disable this feature in order to continue.",
positiveButton: {
onClick: () => document.getElementById(ACCOUNT_MENU_BUTTON_ID)?.click(),
text: 'Continue to Account Settings',
icon: <Enter />,
},
negativeButton: {
text: 'Cancel',
},
})
return
}
openReclaim(props.data)
}, [isAutoLendEnabledForCurrentAccount, openReclaim, props.data, showAlertDialog])
return (
<div className='flex flex-row space-x-2'>
{accountLendValue && (
@ -27,7 +56,7 @@ function LendingActionButtons(props: Props) {
leftIcon={<ArrowDownLine />}
iconClassName={iconClassnames}
color='secondary'
onClick={() => openReclaim(props.data)}
onClick={handleWithdraw}
className={buttonClassnames}
>
Withdraw

View File

@ -117,7 +117,7 @@ function LendingMarketsTable(props: Props) {
header: 'Manage',
cell: ({ row }) => (
<div className='flex items-center justify-end'>
<div className='w-4'>{row.getIsExpanded() ? <ChevronUp /> : <ChevronDown />}</div>
<div className='w-4'>{row.getIsExpanded() ? <ChevronDown /> : <ChevronUp />}</div>
</div>
),
},

View File

@ -0,0 +1,17 @@
import { Enter } from 'components/Icons'
export function NoIcon() {
return (
<div className='ml-1 flex items-center rounded-xs border-[1px] border-white/5 bg-white/5 px-1 py-0.5 text-[8px] font-bold leading-[10px] text-white/60 '>
ESC
</div>
)
}
export function YesIcon() {
return (
<div className='ml-1 rounded-xs border-[1px] border-white/5 bg-white/5 px-1 py-0.5'>
<Enter width={12} />
</div>
)
}

View File

@ -0,0 +1,66 @@
import Button from 'components/Button'
import { ExclamationMarkCircled } from 'components/Icons'
import Modal from 'components/Modal'
import Text from 'components/Text'
import useAlertDialog from 'hooks/useAlertDialog'
import { NoIcon, YesIcon } from 'components/Modals/AlertDialog/ButtonIcons'
function AlertDialogController() {
const { config, close } = useAlertDialog()
if (!config) return null
return <AlertDialog config={config} close={close} />
}
interface Props {
config: AlertDialogConfig
close: () => void
}
function AlertDialog(props: Props) {
const { title, icon, description, negativeButton, positiveButton } = props.config
const handleButtonClick = (button?: AlertDialogButton) => {
button?.onClick && button.onClick()
props.close()
}
return (
<Modal
open
onClose={props.close}
header={
<div className='grid h-12 w-12 place-items-center rounded-sm bg-white/5'>
{icon ?? <ExclamationMarkCircled width={18} />}
</div>
}
modalClassName='w-[577px]'
headerClassName='p-8'
contentClassName='px-8 pb-8'
hideCloseBtn
>
<Text size='xl'>{title}</Text>
<Text className='mt-2 text-white/60'>{description}</Text>
<div className='mt-10 flex flex-row-reverse justify-between'>
<Button
text={positiveButton.text ?? 'Yes'}
color='tertiary'
className='px-6'
rightIcon={positiveButton.icon ?? <YesIcon />}
onClick={() => handleButtonClick(positiveButton)}
/>
<Button
text={negativeButton?.text ?? 'No'}
color='secondary'
className='px-6'
rightIcon={negativeButton?.icon ?? <NoIcon />}
tabIndex={1}
onClick={() => handleButtonClick(negativeButton)}
/>
</div>
</Modal>
)
}
export default AlertDialogController

View File

@ -1,5 +1,6 @@
import {
AddVaultBorrowAssetsModal,
AlertDialogController,
BorrowModal,
FundAndWithdrawModal,
LendAndReclaimModalController,
@ -18,6 +19,7 @@ export default function ModalsContainer() {
<UnlockModal />
<LendAndReclaimModalController />
<WithdrawFromVaults />
<AlertDialogController />
</>
)
}

View File

@ -2,32 +2,16 @@ import { useState } from 'react'
import { useParams } from 'react-router-dom'
import Button from 'components/Button'
import { Enter } from 'components/Icons'
import Text from 'components/Text'
import useStore from 'store'
import { hardcodedFee } from 'utils/constants'
import { NoIcon, YesIcon } from 'components/Modals/AlertDialog/ButtonIcons'
interface Props {
depositedVault: DepositedVault
onClose: () => void
}
function NoIcon() {
return (
<div className='ml-1 flex items-center rounded-xs border-[1px] border-white/5 bg-white/5 px-1 py-0.5 text-[8px] font-bold leading-[10px] text-white/60 '>
ESC
</div>
)
}
function YesIcon() {
return (
<div className='ml-1 rounded-xs border-[1px] border-white/5 bg-white/5 px-1 py-0.5'>
<Enter width={12} />
</div>
)
}
export default function UnlockModalContent(props: Props) {
const unlock = useStore((s) => s.unlock)
const [isWating, setIsConfirming] = useState(false)

View File

@ -5,3 +5,4 @@ export { default as LendAndReclaimModalController } from 'components/Modals/Lend
export { default as UnlockModal } from 'components/Modals/Unlock/UnlockModal'
export { default as VaultModal } from 'components/Modals/Vault'
export { default as WithdrawFromVaults } from 'components/Modals/WithdrawFromVaults/WithdrawFromVaults'
export { default as AlertDialogController } from 'components/Modals/AlertDialog'

View File

@ -13,6 +13,7 @@ export const ASSETS: Asset[] = [
isEnabled: true,
isMarket: true,
isDisplayCurrency: true,
isAutoLendEnabled: true,
},
{
symbol: 'ATOM',
@ -28,6 +29,7 @@ export const ASSETS: Asset[] = [
isEnabled: true,
isMarket: true,
isDisplayCurrency: true,
isAutoLendEnabled: true,
},
{
symbol: 'stATOM',

View File

@ -1,3 +1,4 @@
export const DISPLAY_CURRENCY_KEY = 'displayCurrency'
export const ENABLE_ANIMATIONS_KEY = 'enableAnimations'
export const FAVORITE_ASSETS = 'favoriteAssets'
export const AUTO_LEND_ENABLED_ACCOUNT_IDS_KEY = 'autoLendEnabledAccountIds'

View File

@ -0,0 +1,19 @@
import { useCallback } from 'react'
import useStore from 'store'
function useAlertDialog() {
const config = useStore((s) => s.alertDialog)
const open = useCallback((config: AlertDialogConfig) => {
useStore.setState({ alertDialog: config })
}, [])
const close = useCallback(() => {
useStore.setState({ alertDialog: null })
}, [])
return { config, open, close }
}
export default useAlertDialog

View File

@ -0,0 +1,33 @@
import useLocalStorage from 'hooks/useLocalStorage'
import { AUTO_LEND_ENABLED_ACCOUNT_IDS_KEY } from 'constants/localStore'
import useCurrentAccount from 'hooks/useCurrentAccount'
function useAutoLendEnabledAccountIds(): {
autoLendEnabledAccountIds: string[]
toggleAutoLend: (accountId: string) => void
isAutoLendEnabledForCurrentAccount: boolean
} {
const currentAccount = useCurrentAccount()
const [autoLendEnabledAccountIds, setAutoLendEnabledAccountIds] = useLocalStorage<string[]>(
AUTO_LEND_ENABLED_ACCOUNT_IDS_KEY,
[],
)
const toggleAutoLend = (accountId: string) => {
const setOfAccountIds = new Set(autoLendEnabledAccountIds)
setOfAccountIds.has(accountId)
? setOfAccountIds.delete(accountId)
: setOfAccountIds.add(accountId)
setAutoLendEnabledAccountIds(Array.from(setOfAccountIds))
}
const isAutoLendEnabledForCurrentAccount = currentAccount
? autoLendEnabledAccountIds.includes(currentAccount.id)
: false
return { autoLendEnabledAccountIds, toggleAutoLend, isAutoLendEnabledForCurrentAccount }
}
export default useAutoLendEnabledAccountIds

View File

@ -0,0 +1,45 @@
import { useCallback, useEffect, useRef, useState } from 'react'
export default function useLocalStorage<T>(key: string, defaultValue: T): [T, (value: T) => void] {
const keyRef = useRef(key)
const defaultValueRef = useRef(defaultValue)
const [value, _setValue] = useState(defaultValueRef.current)
useEffect(() => {
const savedItem = localStorage.getItem(keyRef.current)
if (!savedItem) {
localStorage.setItem(keyRef.current, JSON.stringify(defaultValueRef.current))
}
_setValue(savedItem ? JSON.parse(savedItem) : defaultValueRef.current)
function handler(e: StorageEvent) {
if (e.key !== keyRef.current) return
const item = localStorage.getItem(keyRef.current)
_setValue(JSON.parse(item ?? JSON.stringify(defaultValueRef.current)))
}
window.addEventListener('storage', handler)
return () => {
window.removeEventListener('storage', handler)
}
}, [])
const setValue = useCallback((value: T) => {
try {
_setValue(value)
localStorage.setItem(keyRef.current, JSON.stringify(value))
if (typeof window !== 'undefined') {
window.dispatchEvent(new StorageEvent('storage', { key: keyRef.current }))
}
} catch (e) {
console.error(e)
}
}, [])
return [value, setValue]
}

View File

@ -12,5 +12,6 @@ export default function createModalSlice(set: SetState<ModalSlice>, get: GetStat
lendAndReclaimModal: null,
vaultModal: null,
withdrawFromVaultsModal: null,
alertDialog: null,
}
}

View File

@ -15,6 +15,7 @@ interface Asset {
isDisplayCurrency?: boolean
isStable?: boolean
isFavorite?: boolean
isAutoLendEnabled?: boolean
}
interface OtherAsset extends Omit<Asset, 'symbol'> {

View File

@ -9,6 +9,21 @@ interface ModalSlice {
withdrawFromVaultsModal: DepositedVault[] | null
unlockModal: UnlockModal | null
lendAndReclaimModal: LendAndReclaimModalConfig | null
alertDialog: AlertDialogConfig | null
}
interface AlertDialogButton {
text?: string
icon?: JSX.Element
onClick?: () => void
}
interface AlertDialogConfig {
icon?: JSX.Element
title: JSX.Element | string
description: JSX.Element | string
negativeButton?: AlertDialogButton
positiveButton: AlertDialogButton
}
type LendAndReclaimModalAction = 'lend' | 'reclaim'