feat: auto-lend foundation (#283)
This commit is contained in:
parent
21c8d04824
commit
0123685f79
@ -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>
|
||||
|
@ -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 }
|
||||
|
@ -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)}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
),
|
||||
},
|
||||
|
17
src/components/Modals/AlertDialog/ButtonIcons.tsx
Normal file
17
src/components/Modals/AlertDialog/ButtonIcons.tsx
Normal 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>
|
||||
)
|
||||
}
|
66
src/components/Modals/AlertDialog/index.tsx
Normal file
66
src/components/Modals/AlertDialog/index.tsx
Normal 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
|
@ -1,5 +1,6 @@
|
||||
import {
|
||||
AddVaultBorrowAssetsModal,
|
||||
AlertDialogController,
|
||||
BorrowModal,
|
||||
FundAndWithdrawModal,
|
||||
LendAndReclaimModalController,
|
||||
@ -18,6 +19,7 @@ export default function ModalsContainer() {
|
||||
<UnlockModal />
|
||||
<LendAndReclaimModalController />
|
||||
<WithdrawFromVaults />
|
||||
<AlertDialogController />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -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',
|
||||
|
@ -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'
|
||||
|
19
src/hooks/useAlertDialog.ts
Normal file
19
src/hooks/useAlertDialog.ts
Normal 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
|
33
src/hooks/useAutoLendEnabledAccountIds.ts
Normal file
33
src/hooks/useAutoLendEnabledAccountIds.ts
Normal 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
|
45
src/hooks/useLocalStorage.ts
Normal file
45
src/hooks/useLocalStorage.ts
Normal 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]
|
||||
}
|
@ -12,5 +12,6 @@ export default function createModalSlice(set: SetState<ModalSlice>, get: GetStat
|
||||
lendAndReclaimModal: null,
|
||||
vaultModal: null,
|
||||
withdrawFromVaultsModal: null,
|
||||
alertDialog: null,
|
||||
}
|
||||
}
|
||||
|
1
src/types/interfaces/asset.d.ts
vendored
1
src/types/interfaces/asset.d.ts
vendored
@ -15,6 +15,7 @@ interface Asset {
|
||||
isDisplayCurrency?: boolean
|
||||
isStable?: boolean
|
||||
isFavorite?: boolean
|
||||
isAutoLendEnabled?: boolean
|
||||
}
|
||||
|
||||
interface OtherAsset extends Omit<Asset, 'symbol'> {
|
||||
|
15
src/types/interfaces/store/modals.d.ts
vendored
15
src/types/interfaces/store/modals.d.ts
vendored
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user