Mp 3104 delete account (#336)

* MP-3104: added deleteAccount and alerts for vaults and debts

* MP-3104: added Borrow and Farm links to the AlertDialog

* MP-3104: finished delete flow

* fix: adjusted according to feedback

* refactor: tidy
This commit is contained in:
Linkie Link 2023-08-03 11:45:32 +02:00 committed by GitHub
parent 69dece2600
commit 1a55d8bd39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 466 additions and 128 deletions

View File

@ -56,7 +56,7 @@ export default function AccountComposition(props: Props) {
className='pb-3' className='pb-3'
/> />
<Item <Item
title='Total Liabilities' title='Total Debt'
current={debtBalance} current={debtBalance}
change={debtBalance.plus(debtBalanceChange)} change={debtBalance.plus(debtBalanceChange)}
className='pb-3' className='pb-3'

View File

@ -1,5 +1,5 @@
import classNames from 'classnames' import classNames from 'classnames'
import { useEffect } from 'react' import { useCallback, useEffect } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom' import { useLocation, useNavigate, useParams } from 'react-router-dom'
import AccountFundFirst from 'components/Account/AccountFund' import AccountFundFirst from 'components/Account/AccountFund'
@ -11,10 +11,10 @@ import Radio from 'components/Radio'
import SwitchWithLabel from 'components/SwitchWithLabel' import SwitchWithLabel from 'components/SwitchWithLabel'
import Text from 'components/Text' import Text from 'components/Text'
import useAutoLendEnabledAccountIds from 'hooks/useAutoLendEnabledAccountIds' import useAutoLendEnabledAccountIds from 'hooks/useAutoLendEnabledAccountIds'
import useCurrentAccount from 'hooks/useCurrentAccount'
import usePrices from 'hooks/usePrices' import usePrices from 'hooks/usePrices'
import useStore from 'store' import useStore from 'store'
import { calculateAccountDepositsValue } from 'utils/accounts' import { calculateAccountDepositsValue } from 'utils/accounts'
import { hardcodedFee } from 'utils/constants'
import { getPage, getRoute } from 'utils/route' import { getPage, getRoute } from 'utils/route'
interface Props { interface Props {
@ -30,21 +30,19 @@ const accountCardHeaderClasses = classNames(
export default function AccountList(props: Props) { export default function AccountList(props: Props) {
const navigate = useNavigate() const navigate = useNavigate()
const { pathname } = useLocation() const { pathname } = useLocation()
const { accountId, address } = useParams() const { address } = useParams()
const { data: prices } = usePrices() const { data: prices } = usePrices()
const { autoLendEnabledAccountIds, toggleAutoLend } = useAutoLendEnabledAccountIds() const { autoLendEnabledAccountIds, toggleAutoLend } = useAutoLendEnabledAccountIds()
const deleteAccount = useStore((s) => s.deleteAccount) const account = useCurrentAccount()
const accountSelected = !!accountId && !isNaN(Number(accountId)) const accountId = account?.id
async function deleteAccountHandler() { const deleteAccountHandler = useCallback(() => {
if (!accountSelected) return if (!account) return
const isSuccess = await deleteAccount({ fee: hardcodedFee, accountId: accountId }) useStore.setState({ accountDeleteModal: account })
if (isSuccess) { }, [account])
navigate(getRoute(getPage(pathname), address))
}
}
useEffect(() => { useEffect(() => {
if (!accountId) return
const element = document.getElementById(`account-${accountId}`) const element = document.getElementById(`account-${accountId}`)
if (element) { if (element) {
element.scrollIntoView({ behavior: 'smooth' }) element.scrollIntoView({ behavior: 'smooth' })
@ -69,6 +67,7 @@ export default function AccountList(props: Props) {
contentClassName='bg-white/10 group-hover/account:bg-white/20' contentClassName='bg-white/10 group-hover/account:bg-white/20'
onClick={() => { onClick={() => {
if (isActive) return if (isActive) return
useStore.setState({ accountDeleteModal: null })
navigate(getRoute(getPage(pathname), address, account.id)) navigate(getRoute(getPage(pathname), address, account.id))
}} }}
title={ title={

View File

@ -0,0 +1,34 @@
import AssetImage from 'components/AssetImage'
import DisplayCurrency from 'components/DisplayCurrency'
import { FormattedNumber } from 'components/FormattedNumber'
import Text from 'components/Text'
import { BNCoin } from 'types/classes/BNCoin'
import { demagnify } from 'utils/formatters'
interface Props {
asset: Asset
coin: BNCoin
}
export default function AssetBalanceRow(props: Props) {
return (
<div className='flex w-full items-center gap-4'>
<AssetImage asset={props.asset} size={32} />
<div className='flex flex-1 flex-wrap'>
<Text className='w-full'>{props.asset.symbol}</Text>
<Text size='sm' className='w-full text-white/50'>
{props.asset.name}
</Text>
</div>
<div className='flex flex-wrap'>
<DisplayCurrency coin={props.coin} className='w-full text-right' />
<FormattedNumber
amount={demagnify(props.coin.amount, props.asset)}
className='w-full text-right text-sm text-white/50'
options={{ suffix: ` ${props.asset.symbol}` }}
animate
/>
</div>
</div>
)
}

View File

@ -1,5 +1,5 @@
import classNames from 'classnames' import classNames from 'classnames'
import React, { LegacyRef, ReactElement, ReactNode, useMemo } from 'react' import React, { LegacyRef, useMemo } from 'react'
import { import {
buttonBorderClasses, buttonBorderClasses,
@ -22,6 +22,7 @@ import useLocalStorage from 'hooks/useLocalStorage'
const Button = React.forwardRef(function Button( const Button = React.forwardRef(function Button(
{ {
autoFocus,
children, children,
className = '', className = '',
color = 'primary', color = 'primary',
@ -93,6 +94,7 @@ const Button = React.forwardRef(function Button(
ref={ref as LegacyRef<HTMLButtonElement>} ref={ref as LegacyRef<HTMLButtonElement>}
onClick={isDisabled ? () => {} : onClick} onClick={isDisabled ? () => {} : onClick}
tabIndex={tabIndex} tabIndex={tabIndex}
autoFocus={autoFocus}
> >
{showProgressIndicator ? ( {showProgressIndicator ? (
<CircularProgress size={circularProgressSize[size]} /> <CircularProgress size={circularProgressSize[size]} />

View File

@ -14,29 +14,25 @@ export default function Tab(props: Props) {
const { address, accountId } = useParams() const { address, accountId } = useParams()
return ( return (
<div className='mb-4 w-full'> <div className='relative mb-4 w-full'>
<div className='flex gap-2'> <NavLink
<div className='relative'> to={getRoute('lend', address, accountId)}
<NavLink className={classNames(
to={getRoute('farm', address, accountId)} props.isFarm ? 'text-white/20' : underlineClasses,
className={classNames( 'relative mr-8 text-xl ',
!props.isFarm ? 'text-white/20' : underlineClasses, )}
'relative mr-8 text-xl', >
)} Lend
> </NavLink>
Farm <NavLink
</NavLink> to={getRoute('farm', address, accountId)}
</div> className={classNames(
<NavLink !props.isFarm ? 'text-white/20' : underlineClasses,
to={getRoute('lend', address, accountId)} 'relative text-xl',
className={classNames( )}
props.isFarm ? 'text-white/20' : underlineClasses, >
'relative text-xl', Farm
)} </NavLink>
>
Lend
</NavLink>
</div>
</div> </div>
) )
} }

View File

@ -9,7 +9,7 @@ import useStore from 'store'
export const menuTree: { page: Page; label: string }[] = [ export const menuTree: { page: Page; label: string }[] = [
{ page: 'trade', label: 'Trade' }, { page: 'trade', label: 'Trade' },
{ page: 'farm', label: 'Earn' }, { page: 'lend', label: 'Earn' },
{ page: 'borrow', label: 'Borrow' }, { page: 'borrow', label: 'Borrow' },
{ page: 'portfolio', label: 'Portfolio' }, { page: 'portfolio', label: 'Portfolio' },
{ page: 'council', label: 'Council' }, { page: 'council', label: 'Council' },

View File

@ -33,11 +33,16 @@ export default function MarketDetails({ data, type }: Props) {
const totalBorrowed = marketDepositAmount.minus(marketLiquidityAmount) const totalBorrowed = marketDepositAmount.minus(marketLiquidityAmount)
const details: Detail[] = useMemo(() => { const details: Detail[] = useMemo(() => {
const isDollar = displayCurrencySymbol === '$'
function getLendingMarketDetails() { function getLendingMarketDetails() {
return [ return [
{ {
amount: convertAmount(asset, marketDepositAmount).toNumber(), amount: convertAmount(asset, marketDepositAmount).toNumber(),
options: { abbreviated: true, suffix: ` ${displayCurrencySymbol}` }, options: {
abbreviated: true,
suffix: isDollar ? undefined : ` ${displayCurrencySymbol}`,
prefix: isDollar ? '$' : undefined,
},
title: 'Total Supplied', title: 'Total Supplied',
}, },
{ {
@ -52,7 +57,12 @@ export default function MarketDetails({ data, type }: Props) {
}, },
{ {
amount: getConversionRate(asset.denom).toNumber(), amount: getConversionRate(asset.denom).toNumber(),
options: { minDecimals: 2, maxDecimals: 2, suffix: ` ${displayCurrencySymbol}` }, options: {
minDecimals: 2,
maxDecimals: 2,
suffix: isDollar ? undefined : ` ${displayCurrencySymbol}`,
prefix: isDollar ? '$' : undefined,
},
title: 'Oracle Price', title: 'Oracle Price',
}, },
{ {
@ -67,12 +77,21 @@ export default function MarketDetails({ data, type }: Props) {
return [ return [
{ {
amount: convertAmount(asset, totalBorrowed).toNumber(), amount: convertAmount(asset, totalBorrowed).toNumber(),
options: { abbreviated: true, suffix: ` ${displayCurrencySymbol}` }, options: {
abbreviated: true,
suffix: isDollar ? undefined : ` ${displayCurrencySymbol}`,
prefix: isDollar ? '$' : undefined,
},
title: 'Total Borrowed', title: 'Total Borrowed',
}, },
{ {
amount: getConversionRate(asset.denom).toNumber(), amount: getConversionRate(asset.denom).toNumber(),
options: { minDecimals: 2, maxDecimals: 2, suffix: ` ${displayCurrencySymbol}` }, options: {
minDecimals: 2,
maxDecimals: 2,
suffix: isDollar ? undefined : ` ${displayCurrencySymbol}`,
prefix: isDollar ? '$' : undefined,
},
title: 'Oracle Price', title: 'Oracle Price',
}, },
{ {

View File

@ -0,0 +1,36 @@
import { useEffect } from 'react'
import { Enter, InfoCircle } from 'components/Icons'
import useAlertDialog from 'hooks/useAlertDialog'
interface Props {
title: string
description: string
closeHandler: () => void
positiveButton: AlertDialogButton
}
export default function AccoundDeleteAlertDialog(props: Props) {
const { open: showAlertDialog } = useAlertDialog()
const { title, description, closeHandler, positiveButton } = props
useEffect(() => {
showAlertDialog({
icon: (
<div className='flex h-full w-full p-3'>
<InfoCircle />
</div>
),
title,
description,
negativeButton: {
text: 'Close',
icon: <Enter />,
onClick: closeHandler,
},
positiveButton,
})
}, [showAlertDialog, title, description, closeHandler, positiveButton])
return null
}

View File

@ -0,0 +1,144 @@
import { useCallback, useMemo, useState } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import AssetBalanceRow from 'components/AssetBalanceRow'
import Button from 'components/Button'
import { ArrowRight } from 'components/Icons'
import Modal from 'components/Modal'
import AccoundDeleteAlertDialog from 'components/Modals/Account/AccountDeleteAlertDialog'
import Text from 'components/Text'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { getAssetByDenom } from 'utils/assets'
import { hardcodedFee } from 'utils/constants'
import { combineBNCoins } from 'utils/parsers'
import { getPage, getRoute } from 'utils/route'
interface Props {
modal: Account
}
export default function AccountDeleteController() {
const modal = useStore((s) => s.accountDeleteModal)
if (!modal) return null
return <AccountDeleteModal modal={modal} />
}
function AccountDeleteModal(props: Props) {
const modal = props.modal
const deleteAccount = useStore((s) => s.deleteAccount)
const navigate = useNavigate()
const { pathname } = useLocation()
const { address } = useParams()
const { debts, vaults, id: accountId } = modal || {}
const [isConfirming, setIsConfirming] = useState(false)
const closeDeleteAccountModal = useCallback(() => {
useStore.setState({ accountDeleteModal: null })
}, [])
const deleteAccountHandler = useCallback(async () => {
setIsConfirming(true)
const options = { fee: hardcodedFee, accountId: modal.id, lends: modal.lends }
const isSuccess = await deleteAccount(options)
setIsConfirming(false)
if (isSuccess) {
navigate(getRoute(getPage(pathname), address))
closeDeleteAccountModal()
}
}, [modal, deleteAccount, navigate, pathname, address, closeDeleteAccountModal])
const depositsAndLends = useMemo(
() => combineBNCoins([...modal.deposits, ...modal.lends]),
[modal],
)
if (debts.length > 0)
return (
<AccoundDeleteAlertDialog
title='Repay your Debts to delete your account'
description='You must repay all borrowings before deleting your account.'
closeHandler={closeDeleteAccountModal}
positiveButton={{
text: 'Repay Debts',
icon: <ArrowRight />,
onClick: () => {
navigate(getRoute('borrow', address, accountId))
closeDeleteAccountModal()
},
}}
/>
)
if (vaults.length > 0)
return (
<AccoundDeleteAlertDialog
title='Close your positions to delete your account'
description='You must first close your farming positions before deleting your account.'
closeHandler={closeDeleteAccountModal}
positiveButton={{
text: 'Close Positions',
icon: <ArrowRight />,
onClick: () => {
navigate(getRoute('farm', address, accountId))
closeDeleteAccountModal()
},
}}
/>
)
if (depositsAndLends.length === 0)
return (
<AccoundDeleteAlertDialog
title={`Delete Credit Account ${accountId}`}
description='Deleting your credit account is irreversible.'
closeHandler={closeDeleteAccountModal}
positiveButton={{
text: 'Close Positions',
icon: <ArrowRight />,
isAsync: true,
onClick: deleteAccountHandler,
}}
/>
)
return (
<Modal
onClose={closeDeleteAccountModal}
header={
<span className='flex items-center'>
<Text>{`Delete Account ${modal.id}`}</Text>
</span>
}
modalClassName='max-w-modal-sm'
headerClassName='gradient-header p-4 border-b-white/5 border-b'
contentClassName='w-full'
>
<div className='w-full border-b border-white/5 p-4 gradient-header'>
<Text className='text-white/50' size='sm'>
The following assets within your credit account will be sent to your wallet.
</Text>
</div>
<div className='max-h-100 flex w-full flex-col gap-4 overflow-y-scroll p-4 scrollbar-hide'>
{depositsAndLends.map((position, index) => {
const coin = BNCoin.fromDenomAndBigNumber(position.denom, position.amount)
const asset = getAssetByDenom(position.denom)
if (!asset) return null
return <AssetBalanceRow key={index} asset={asset} coin={coin} />
})}
</div>
<div className='w-full px-4 pb-4'>
<Button
className='w-full'
onClick={deleteAccountHandler}
text='Delete Account'
rightIcon={<ArrowRight />}
showProgressIndicator={isConfirming}
/>
</div>
</Modal>
)
}

View File

@ -1,3 +1,6 @@
import { useState } from 'react'
import classNames from 'classnames'
import Button from 'components/Button' import Button from 'components/Button'
import { ExclamationMarkCircled } from 'components/Icons' import { ExclamationMarkCircled } from 'components/Icons'
import Modal from 'components/Modal' import Modal from 'components/Modal'
@ -21,11 +24,21 @@ interface Props {
function AlertDialog(props: Props) { function AlertDialog(props: Props) {
const { title, icon, description, negativeButton, positiveButton } = props.config const { title, icon, description, negativeButton, positiveButton } = props.config
const [isConfirming, setIsConfirming] = useState(false)
const handleButtonClick = (button?: AlertDialogButton) => { const handleButtonClick = (button?: AlertDialogButton) => {
button?.onClick && button.onClick() button?.onClick && button.onClick()
props.close() props.close()
} }
async function handleAsyncButtonClick(button?: AlertDialogButton) {
if (!button?.onClick) return
setIsConfirming(true)
await button.onClick()
setIsConfirming(false)
props.close()
}
return ( return (
<Modal <Modal
onClose={props.close} onClose={props.close}
@ -41,19 +54,29 @@ function AlertDialog(props: Props) {
> >
<Text size='xl'>{title}</Text> <Text size='xl'>{title}</Text>
<Text className='mt-2 text-white/60'>{description}</Text> <Text className='mt-2 text-white/60'>{description}</Text>
<div className='mt-10 flex flex-row-reverse justify-between'> <div
<Button className={classNames('mt-10 flex justify-between', positiveButton && 'flex-row-reverse')}
text={positiveButton.text ?? 'Yes'} >
color='tertiary' {positiveButton && (
className='px-6' <Button
rightIcon={positiveButton.icon ?? <YesIcon />} text={positiveButton.text ?? 'Yes'}
onClick={() => handleButtonClick(positiveButton)} color='tertiary'
/> className='px-6'
rightIcon={positiveButton.icon ?? <YesIcon />}
showProgressIndicator={isConfirming}
onClick={() =>
positiveButton.isAsync
? handleAsyncButtonClick(positiveButton)
: handleButtonClick(positiveButton)
}
/>
)}
<Button <Button
text={negativeButton?.text ?? 'No'} text={negativeButton?.text ?? 'No'}
color='secondary' color='secondary'
className='px-6' className='px-6'
rightIcon={negativeButton?.icon ?? <NoIcon />} rightIcon={negativeButton?.icon ?? <NoIcon />}
disabled={isConfirming}
tabIndex={1} tabIndex={1}
onClick={() => handleButtonClick(negativeButton)} onClick={() => handleButtonClick(negativeButton)}
/> />

View File

@ -1,4 +1,5 @@
import { import {
AccountDeleteController,
AddVaultBorrowAssetsModal, AddVaultBorrowAssetsModal,
AlertDialogController, AlertDialogController,
BorrowModal, BorrowModal,
@ -14,6 +15,7 @@ import {
export default function ModalsContainer() { export default function ModalsContainer() {
return ( return (
<> <>
<AccountDeleteController />
<AddVaultBorrowAssetsModal /> <AddVaultBorrowAssetsModal />
<BorrowModal /> <BorrowModal />
<FundAndWithdrawModal /> <FundAndWithdrawModal />

View File

@ -1,3 +1,4 @@
export { default as AccountDeleteController } from 'components/Modals/Account/AccountDeleteModal'
export { default as AddVaultBorrowAssetsModal } from 'components/Modals/AddVaultAssets' export { default as AddVaultBorrowAssetsModal } from 'components/Modals/AddVaultAssets'
export { default as AlertDialogController } from 'components/Modals/AlertDialog' export { default as AlertDialogController } from 'components/Modals/AlertDialog'
export { default as BorrowModal } from 'components/Modals/BorrowModal' export { default as BorrowModal } from 'components/Modals/BorrowModal'

View File

@ -1,13 +1,12 @@
import classNames from 'classnames' import classNames from 'classnames'
import { ChangeEvent, LegacyRef } from 'react' import React, { ChangeEvent, LegacyRef } from 'react'
import React from 'react'
import { Search } from 'components/Icons' import { Search } from 'components/Icons'
interface Props { interface Props {
value: string value: string
placeholder: string placeholder: string
autofocus?: boolean autoFocus?: boolean
onChange: (value: string) => void onChange: (value: string) => void
} }
@ -31,7 +30,7 @@ const SearchBar = (props: Props, ref: LegacyRef<HTMLDivElement>) => {
className='h-full w-full bg-transparent text-xs placeholder-white/50 outline-none' className='h-full w-full bg-transparent text-xs placeholder-white/50 outline-none'
placeholder={props.placeholder} placeholder={props.placeholder}
onChange={(event) => onChange(event)} onChange={(event) => onChange(event)}
autoFocus={props.autofocus} autoFocus={props.autoFocus}
/> />
</div> </div>
) )

View File

@ -58,7 +58,7 @@ export default function TermsOfService() {
> >
<Benefits <Benefits
benefits={[ benefits={[
'Swap tokens with margin acress any whitelisted pair', 'Swap tokens with margin across any whitelisted pair',
'Amplify your LP rewards with leveraged yield farming', 'Amplify your LP rewards with leveraged yield farming',
'Earn interest on deposited tokens', 'Earn interest on deposited tokens',
]} ]}

View File

@ -63,7 +63,7 @@ export default function AssetOverlay(props: Props) {
value={searchString} value={searchString}
onChange={onChangeSearch} onChange={onChangeSearch}
placeholder='Search for e.g. "ETH" or "Ethereum"' placeholder='Search for e.g. "ETH" or "Ethereum"'
autofocus autoFocus
/> />
</div> </div>
<Divider /> <Divider />

View File

@ -15,6 +15,22 @@ import { getSingleValueFromBroadcastResult } from 'utils/broadcast'
import { formatAmountWithSymbol } from 'utils/formatters' import { formatAmountWithSymbol } from 'utils/formatters'
import getTokenOutFromSwapResponse from 'utils/getTokenOutFromSwapResponse' import getTokenOutFromSwapResponse from 'utils/getTokenOutFromSwapResponse'
function generateExecutionMessage(
sender: string | undefined,
contract: string,
msg: CreditManagerExecuteMsg | AccountNftExecuteMsg,
funds: Coin[],
) {
if (!sender) return
return new MsgExecuteContract({
sender,
contract,
msg,
funds,
})
}
export default function createBroadcastSlice( export default function createBroadcastSlice(
set: SetState<Store>, set: SetState<Store>,
get: GetState<Store>, get: GetState<Store>,
@ -60,7 +76,10 @@ export default function createBroadcastSlice(
}, },
} }
const response = await get().executeMsg({ msg, fee: options.fee }) const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
fee: options.fee,
})
handleResponseMessages( handleResponseMessages(
response, response,
@ -74,8 +93,11 @@ export default function createBroadcastSlice(
const msg: CreditManagerExecuteMsg = { const msg: CreditManagerExecuteMsg = {
create_credit_account: 'default', create_credit_account: 'default',
} }
set({ createAccountModal: true })
const response = await get().executeMsg({ msg, fee: options.fee }) const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
fee: options.fee,
})
if (response.result) { if (response.result) {
set({ createAccountModal: false }) set({ createAccountModal: false })
@ -93,16 +115,33 @@ export default function createBroadcastSlice(
return null return null
} }
}, },
deleteAccount: async (options: { fee: StdFee; accountId: string }) => { deleteAccount: async (options: { fee: StdFee; accountId: string; lends: BNCoin[] }) => {
const msg: AccountNftExecuteMsg = { const reclaimMsg = options.lends.map((coin) => {
return {
reclaim: coin.toActionCoin(true),
}
})
const refundMessage: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: [...reclaimMsg, { refund_all_coin_balances: {} }],
},
}
const burnMessage: AccountNftExecuteMsg = {
burn: { burn: {
token_id: options.accountId, token_id: options.accountId,
}, },
} }
set({ deleteAccountModal: true })
const response = await get().executeMsg({ msg, fee: options.fee })
set({ deleteAccountModal: false }) const response = await get().executeMsg({
messages: [
generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, refundMessage, []),
generateExecutionMessage(get().address, ENV.ADDRESS_ACCOUNT_NFT, burnMessage, []),
],
fee: options.fee,
})
handleResponseMessages(response, `Account ${options.accountId} deleted`) handleResponseMessages(response, `Account ${options.accountId} deleted`)
@ -118,7 +157,12 @@ export default function createBroadcastSlice(
}, },
} }
const response = await get().executeMsg({ msg, fee: options.fee, funds: options.coins }) const response = await get().executeMsg({
messages: [
generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, options.coins),
],
fee: options.fee,
})
const depositString = options.coins.map((coin) => formatAmountWithSymbol(coin)).join('and ') const depositString = options.coins.map((coin) => formatAmountWithSymbol(coin)).join('and ')
handleResponseMessages(response, `Deposited ${depositString} to Account ${options.accountId}`) handleResponseMessages(response, `Deposited ${depositString} to Account ${options.accountId}`)
@ -145,9 +189,8 @@ export default function createBroadcastSlice(
} }
const response = await get().executeMsg({ const response = await get().executeMsg({
msg, messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
fee: options.fee, fee: options.fee,
funds: [],
}) })
handleResponseMessages(response, `Requested unlock for ${options.vault.name}`) handleResponseMessages(response, `Requested unlock for ${options.vault.name}`)
@ -176,7 +219,11 @@ export default function createBroadcastSlice(
}, },
} }
const response = await get().executeMsg({ msg, fee: options.fee }) const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
fee: options.fee,
})
const vaultsString = options.vaults.length === 1 ? 'vault' : 'vaults' const vaultsString = options.vaults.length === 1 ? 'vault' : 'vaults'
handleResponseMessages( handleResponseMessages(
response, response,
@ -191,7 +238,11 @@ export default function createBroadcastSlice(
actions: options.actions, actions: options.actions,
}, },
} }
const response = await get().executeMsg({ msg, fee: options.fee })
const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
fee: options.fee,
})
handleResponseMessages(response, `Deposited into vault`) handleResponseMessages(response, `Deposited into vault`)
return !!response.result return !!response.result
@ -206,7 +257,11 @@ export default function createBroadcastSlice(
}, },
} }
const response = await get().executeMsg({ msg, fee: options.fee }) const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
fee: options.fee,
})
const withdrawString = options.coins.map((coin) => formatAmountWithSymbol(coin)).join('and ') const withdrawString = options.coins.map((coin) => formatAmountWithSymbol(coin)).join('and ')
handleResponseMessages( handleResponseMessages(
response, response,
@ -214,44 +269,6 @@ export default function createBroadcastSlice(
) )
return !!response.result return !!response.result
}, },
executeMsg: async (options: {
fee: StdFee
msg: Record<string, unknown>
funds?: Coin[]
}): Promise<BroadcastResult> => {
const funds = options.funds ?? []
try {
const client = get().client
if (!client) return { error: 'no client detected' }
const broadcastOptions = {
messages: [
new MsgExecuteContract({
sender: client.connectedWallet.account.address,
contract: ENV.ADDRESS_CREDIT_MANAGER,
msg: options.msg,
funds,
}),
],
feeAmount: options.fee.amount[0].amount,
gasLimit: options.fee.gas,
memo: undefined,
wallet: client.connectedWallet,
mobile: isMobile,
}
const result = await client.broadcast(broadcastOptions)
if (result.hash) {
return { result }
}
return { result: undefined, error: 'Transaction failed' }
} catch (e: unknown) {
const error = typeof e === 'string' ? e : 'Transaction failed'
return { result: undefined, error }
}
},
repay: async (options: { repay: async (options: {
fee: StdFee fee: StdFee
accountId: string accountId: string
@ -271,7 +288,10 @@ export default function createBroadcastSlice(
}, },
} }
const response = await get().executeMsg({ msg, fee: options.fee, funds: [] }) const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
fee: options.fee,
})
handleResponseMessages( handleResponseMessages(
response, response,
@ -291,7 +311,10 @@ export default function createBroadcastSlice(
}, },
} }
const response = await get().executeMsg({ msg, fee: options.fee }) const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
fee: options.fee,
})
handleResponseMessages( handleResponseMessages(
response, response,
@ -311,7 +334,10 @@ export default function createBroadcastSlice(
}, },
} }
const response = await get().executeMsg({ msg, fee: options.fee }) const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
fee: options.fee,
})
handleResponseMessages( handleResponseMessages(
response, response,
@ -341,7 +367,11 @@ export default function createBroadcastSlice(
}, },
} }
const response = await get().executeMsg({ msg, fee: options.fee }) const response = await get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
fee: options.fee,
})
const coinOut = getTokenOutFromSwapResponse(response, options.denomOut) const coinOut = getTokenOutFromSwapResponse(response, options.denomOut)
const successMessage = `Swapped ${formatAmountWithSymbol( const successMessage = `Swapped ${formatAmountWithSymbol(
options.coinIn.toCoin(), options.coinIn.toCoin(),
@ -350,5 +380,37 @@ export default function createBroadcastSlice(
handleResponseMessages(response, successMessage) handleResponseMessages(response, successMessage)
return !!response.result return !!response.result
}, },
executeMsg: async (options: {
messages: MsgExecuteContract[]
fee: StdFee
}): Promise<BroadcastResult> => {
try {
const client = get().client
if (!client) return { error: 'no client detected' }
const broadcastOptions = {
messages: options.messages,
feeAmount: options.fee.amount[0].amount,
gasLimit: options.fee.gas,
memo: undefined,
wallet: client.connectedWallet,
mobile: isMobile,
}
const result = await client.broadcast(broadcastOptions)
if (result.hash) {
return { result }
}
return {
result: undefined,
error: 'Transaction failed',
}
} catch (e: unknown) {
const error = typeof e === 'string' ? e : 'Transaction failed'
return { result: undefined, error }
}
},
} }
} }

View File

@ -3,10 +3,10 @@ import { GetState, SetState } from 'zustand'
export default function createModalSlice(set: SetState<ModalSlice>, get: GetState<ModalSlice>) { export default function createModalSlice(set: SetState<ModalSlice>, get: GetState<ModalSlice>) {
return { return {
addVaultBorrowingsModal: null, addVaultBorrowingsModal: null,
accountDeleteModal: null,
alertDialog: null, alertDialog: null,
borrowModal: null, borrowModal: null,
createAccountModal: false, createAccountModal: false,
deleteAccountModal: false,
fundAccountModal: false, fundAccountModal: false,
fundAndWithdrawModal: null, fundAndWithdrawModal: null,
lendAndReclaimModal: null, lendAndReclaimModal: null,

View File

@ -1,4 +1,5 @@
interface ButtonProps { interface ButtonProps {
autoFocus?: boolean
children?: string | ReactNode children?: string | ReactNode
className?: string className?: string
color?: 'primary' | 'secondary' | 'tertiary' | 'quaternary' color?: 'primary' | 'secondary' | 'tertiary' | 'quaternary'

View File

@ -7,11 +7,7 @@ interface BroadcastResult {
interface BroadcastSlice { interface BroadcastSlice {
toast: { message: string; isError?: boolean; title?: string } | null toast: { message: string; isError?: boolean; title?: string } | null
executeMsg: (options: { executeMsg: (options: { messages: MsgExecuteContract[]; fee: StdFee }) => Promise<BroadcastResult>
msg: Record<string, unknown>
fee: StdFee
funds?: Coin[]
}) => Promise<BroadcastResult>
borrow: (options: { borrow: (options: {
fee: StdFee fee: StdFee
accountId: string accountId: string
@ -19,7 +15,7 @@ interface BroadcastSlice {
borrowToWallet: boolean borrowToWallet: boolean
}) => Promise<boolean> }) => Promise<boolean>
createAccount: (options: { fee: StdFee }) => Promise<string | null> createAccount: (options: { fee: StdFee }) => Promise<string | null>
deleteAccount: (options: { fee: StdFee; accountId: string }) => Promise<boolean> deleteAccount: (options: { fee: StdFee; accountId: string; lends: BNCoin[] }) => Promise<boolean>
deposit: (options: { fee: StdFee; accountId: string; coins: Coin[] }) => Promise<boolean> deposit: (options: { fee: StdFee; accountId: string; coins: Coin[] }) => Promise<boolean>
unlock: (options: { unlock: (options: {
fee: StdFee fee: StdFee

View File

@ -1,9 +1,9 @@
interface ModalSlice { interface ModalSlice {
accountDeleteModal: Account | null
addVaultBorrowingsModal: AddVaultBorrowingsModal | null addVaultBorrowingsModal: AddVaultBorrowingsModal | null
alertDialog: AlertDialogConfig | null alertDialog: AlertDialogConfig | null
borrowModal: BorrowModal | null borrowModal: BorrowModal | null
createAccountModal: boolean createAccountModal: boolean
deleteAccountModal: boolean
fundAccountModal: boolean fundAccountModal: boolean
fundAndWithdrawModal: 'fund' | 'withdraw' | null fundAndWithdrawModal: 'fund' | 'withdraw' | null
lendAndReclaimModal: LendAndReclaimModalConfig | null lendAndReclaimModal: LendAndReclaimModalConfig | null
@ -17,6 +17,7 @@ interface ModalSlice {
interface AlertDialogButton { interface AlertDialogButton {
text?: string text?: string
icon?: JSX.Element icon?: JSX.Element
isAsync?: boolean
onClick?: () => void onClick?: () => void
} }
@ -25,7 +26,12 @@ interface AlertDialogConfig {
title: JSX.Element | string title: JSX.Element | string
description: JSX.Element | string description: JSX.Element | string
negativeButton?: AlertDialogButton negativeButton?: AlertDialogButton
positiveButton: AlertDialogButton positiveButton?: AlertDialogButton
}
interface BorrowModal {
asset: Asset
marketData: BorrowMarketTableData
isRepay?: boolean
} }
type LendAndReclaimModalAction = 'lend' | 'reclaim' type LendAndReclaimModalAction = 'lend' | 'reclaim'
@ -34,12 +40,6 @@ interface LendAndReclaimModalConfig {
action: LendAndReclaimModalAction action: LendAndReclaimModalAction
} }
interface BorrowModal {
asset: Asset
marketData: BorrowMarketTableData
isRepay?: boolean
}
interface VaultModal { interface VaultModal {
vault: Vault | DepositedVault vault: Vault | DepositedVault
isDeposited?: boolean isDeposited?: boolean

View File

@ -1,3 +1,5 @@
import { BNCoin } from 'types/classes/BNCoin'
import { BN } from 'utils/helpers'
export function isNumber(value: unknown) { export function isNumber(value: unknown) {
if (typeof value === 'string' && value !== '') { if (typeof value === 'string' && value !== '') {
return !isNaN(Number(value)) return !isNaN(Number(value))
@ -13,3 +15,25 @@ export function isNumber(value: unknown) {
export const convertAprToApy = (apr: number, numberOfCompoundingPeriods: number): number => { export const convertAprToApy = (apr: number, numberOfCompoundingPeriods: number): number => {
return ((1 + apr / 100 / numberOfCompoundingPeriods) ** numberOfCompoundingPeriods - 1) * 100 return ((1 + apr / 100 / numberOfCompoundingPeriods) ** numberOfCompoundingPeriods - 1) * 100
} }
export const combineBNCoins = (coins: BNCoin[]): BNCoin[] => {
const combinedMap: { [key: string]: number } = {}
coins.forEach((coin) => {
if (combinedMap.hasOwnProperty(coin.denom)) {
combinedMap[coin.denom] += coin.amount.toNumber()
} else {
combinedMap[coin.denom] = coin.amount.toNumber()
}
})
const combinedArray: BNCoin[] = Object.keys(combinedMap).map(
(denom) =>
new BNCoin({
denom,
amount: BN(combinedMap[denom]).toString(),
}),
)
return combinedArray
}

View File

@ -166,7 +166,7 @@ module.exports = {
maxWidth: { maxWidth: {
content: '1024px', content: '1024px',
modal: '895px', modal: '895px',
'modal-sm': '517px', 'modal-sm': '526px',
'modal-xs': '442px', 'modal-xs': '442px',
}, },
minWidth: { minWidth: {