Mp 2547 unlocking modal (#256)

* add modal

* add tests and update naming

* fix pr comments
This commit is contained in:
Bob van der Helm 2023-06-19 20:46:32 +02:00 committed by GitHub
parent 5aabf6f725
commit 0037c3dedf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 236 additions and 14 deletions

View File

@ -0,0 +1,66 @@
import { render } from '@testing-library/react'
import Modal from 'components/Modal'
import UnlockModal from 'components/Modals/Unlock/UnlockModal'
import { TESTNET_VAULTS } from 'constants/vaults'
import useStore from 'store'
import { BN } from 'utils/helpers'
jest.mock('components/Modal')
const mockedModal = jest.mocked(Modal).mockImplementation(() => <div>Modal</div>)
const mockedDepositedVault: DepositedVault = {
...TESTNET_VAULTS[0],
status: 'active',
apy: 1,
ltv: {
max: 0.65,
liq: 0.7,
},
amounts: {
primary: BN(1),
secondary: BN(1),
locked: BN(1),
unlocked: BN(1),
unlocking: BN(1),
},
values: {
primary: BN(0),
secondary: BN(0),
},
cap: {
denom: 'mock',
max: 10,
used: 1,
},
}
describe('<UnlockModal />', () => {
beforeAll(() => {
useStore.setState({ unlockModal: null })
})
it('should render', () => {
const { container } = render(<UnlockModal />)
expect(mockedModal).toHaveBeenCalledTimes(1)
expect(container).toBeInTheDocument()
})
describe('should set open attribute correctly', () => {
it('should set open = false when no modal is present in store', () => {
render(<UnlockModal />)
expect(mockedModal).toHaveBeenCalledWith(
expect.objectContaining({ open: false }),
expect.anything(),
)
})
it('should set open = true when no modal is present in store', () => {
useStore.setState({ unlockModal: { vault: mockedDepositedVault } })
render(<UnlockModal />)
expect(mockedModal).toHaveBeenLastCalledWith(
expect.objectContaining({ open: true }),
expect.anything(),
)
})
})
})

View File

@ -45,7 +45,6 @@ describe('<VaultBorrowings />', () => {
beforeAll(() => {
useStore.setState({
baseCurrency: ASSETS[0],
selectedBorrowDenoms: [ASSETS[1].denom],
})
})

View File

@ -35,7 +35,6 @@ module.exports = {
'^styles/(.*)$': '<rootDir>/src/styles/$1',
'^types/(.*)$': '<rootDir>/src/types/$1',
'^utils/(.*)$': '<rootDir>/src/utils/$1',
'^store': '<rootDir>/src/store',
},
// Add more setup options before each test is run

View File

@ -52,17 +52,17 @@ function flatVaultPositionAmount(
vaultPositionAmount: VaultPositionAmount,
): VaultPositionFlatAmounts {
const amounts = {
locked: '0',
unlocking: '0',
unlocked: '0',
locked: BN(0),
unlocking: BN(0),
unlocked: BN(0),
}
if ('locking' in vaultPositionAmount) {
const { locked, unlocking } = vaultPositionAmount.locking
amounts.locked = locked
amounts.unlocking = unlocking[0]?.coin.amount ?? '0'
amounts.locked = BN(locked)
amounts.unlocking = BN(unlocking[0]?.coin.amount ?? '0')
} else if ('unlocked' in vaultPositionAmount) {
amounts.unlocked = vaultPositionAmount.unlocked
amounts.unlocked = BN(vaultPositionAmount.unlocked)
}
return amounts
@ -121,6 +121,7 @@ async function getVaultValuesAndAmounts(
])
const lpTokensQuery = getLpTokensForVaultPosition(vault, vaultPosition)
const amounts = flatVaultPositionAmount(vaultPosition.amount)
const [[primaryLpToken, secondaryLpToken], [primaryAsset, secondaryAsset]] = await Promise.all([
lpTokensQuery,
@ -129,6 +130,7 @@ async function getVaultValuesAndAmounts(
return {
amounts: {
...amounts,
primary: BN(primaryLpToken.amount),
secondary: BN(secondaryLpToken.amount),
},

View File

@ -34,6 +34,7 @@ interface Props {
hasSubmenu?: boolean
hasFocus?: boolean
dataTestId?: string
tabIndex?: number
}
const Button = React.forwardRef(function Button(
@ -54,6 +55,7 @@ const Button = React.forwardRef(function Button(
hasSubmenu,
hasFocus,
dataTestId,
tabIndex = 0,
}: Props,
ref,
) {
@ -106,6 +108,7 @@ const Button = React.forwardRef(function Button(
id={id}
ref={ref as LegacyRef<HTMLButtonElement>}
onClick={isDisabled ? () => {} : onClick}
tabIndex={tabIndex}
>
{showProgressIndicator ? (
<CircularProgress size={size === 'small' ? 10 : size === 'medium' ? 12 : 18} />

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 13 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.5 4.00004H9.5C11.1569 4.00004 12.5 5.34319 12.5 7.00004C12.5 8.6569 11.1569 10 9.5 10H6.5M0.5 4.00004L3.16667 1.33337M0.5 4.00004L3.16667 6.66671" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 310 B

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 9V6C5 3.23858 7.23858 1 10 1C12.419 1 14.4367 2.71776 14.9 5M5.8 19H14.2C15.8802 19 16.7202 19 17.362 18.673C17.9265 18.3854 18.3854 17.9265 18.673 17.362C19 16.7202 19 15.8802 19 14.2V13.8C19 12.1198 19 11.2798 18.673 10.638C18.3854 10.0735 17.9265 9.6146 17.362 9.32698C16.7202 9 15.8802 9 14.2 9H5.8C4.11984 9 3.27976 9 2.63803 9.32698C2.07354 9.6146 1.6146 10.0735 1.32698 10.638C1 11.2798 1 12.1198 1 13.8V14.2C1 15.8802 1 16.7202 1.32698 17.362C1.6146 17.9265 2.07354 18.3854 2.63803 18.673C3.27976 19 4.11984 19 5.8 19Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 691 B

View File

@ -14,11 +14,13 @@ export { default as ChevronUp } from 'components/Icons/ChevronUp.svg'
export { default as Copy } from 'components/Icons/Copy.svg'
export { default as Cross } from 'components/Icons/Cross.svg'
export { default as CrossCircled } from 'components/Icons/CrossCircled.svg'
export { default as Enter } from 'components/Icons/Enter.svg'
export { default as ExclamationMarkCircled } from 'components/Icons/ExclamationMarkCircled.svg'
export { default as ExclamationMarkTriangle } from 'components/Icons/ExclamationMarkTriangle.svg'
export { default as ExternalLink } from 'components/Icons/ExternalLink.svg'
export { default as Gear } from 'components/Icons/Gear.svg'
export { default as Heart } from 'components/Icons/Heart.svg'
export { default as LockUnlocked } from 'components/Icons/LockUnlocked.svg'
export { default as Logo } from 'components/Icons/Logo.svg'
export { default as MarsProtocol } from 'components/Icons/MarsProtocol.svg'
export { default as Osmo } from 'components/Icons/Osmo.svg'

View File

@ -9,6 +9,7 @@ import Text from 'components/Text'
interface Props {
header: string | ReactNode
headerClassName?: string
hideCloseBtn?: boolean
children?: ReactNode | string
content?: ReactNode | string
className?: string
@ -60,9 +61,11 @@ export default function Modal(props: Props) {
>
<div className={classNames('flex justify-between', props.headerClassName)}>
{props.header}
<Button onClick={onClose} leftIcon={<Cross />} iconClassName='h-3 w-3' color='tertiary'>
<Text size='sm'>ESC</Text>
</Button>
{!props.hideCloseBtn && (
<Button onClick={onClose} leftIcon={<Cross />} iconClassName='h-3 w-3' color='tertiary'>
<Text size='sm'>ESC</Text>
</Button>
)}
</div>
<div className={classNames(props.contentClassName, 'flex-grow')}>
{props.children ? props.children : props.content}

View File

@ -2,6 +2,7 @@ import VaultModal from 'components/Modals/Vault/VaultModal'
import BorrowModal from 'components/Modals/Borrow/BorrowModal'
import FundAndWithdrawModal from 'components/Modals/FundWithdraw/FundAndWithdrawModal'
import AddVaultBorrowAssetsModal from 'components/Modals/AddVaultAssets/AddVaultBorrowAssetsModal'
import UnlockModal from 'components/Modals/Unlock/UnlockModal'
export default function ModalsContainer() {
return (
@ -10,6 +11,7 @@ export default function ModalsContainer() {
<FundAndWithdrawModal />
<VaultModal />
<AddVaultBorrowAssetsModal />
<UnlockModal />
</>
)
}

View File

@ -0,0 +1,35 @@
import { LockUnlocked } from 'components/Icons'
import Modal from 'components/Modal'
import useStore from 'store'
import UnlockModalContent from 'components/Modals/Unlock/UnlockModalContent'
import { CircularProgress } from 'components/CircularProgress'
export default function UnlockModal() {
const modal = useStore((s) => s.unlockModal)
function onClose() {
useStore.setState({ unlockModal: null })
}
return (
<Modal
open={!!modal}
onClose={onClose}
header={
<div className='grid h-12 w-12 place-items-center rounded-sm bg-white/5'>
<LockUnlocked width={18} />
</div>
}
modalClassName='w-[577px]'
headerClassName='p-8'
contentClassName='px-8 pb-8'
hideCloseBtn
>
{modal ? (
<UnlockModalContent depositedVault={modal.vault} onClose={onClose} />
) : (
<CircularProgress />
)}
</Modal>
)
}

View File

@ -0,0 +1,65 @@
import Button from 'components/Button'
import { Enter } from 'components/Icons'
import Text from 'components/Text'
import useStore from 'store'
import { hardcodedFee } from 'utils/contants'
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)
async function onConfirm() {
await unlock({
fee: hardcodedFee,
vault: props.depositedVault,
amount: props.depositedVault.amounts.locked.toString(),
})
props.onClose()
}
return (
<>
<Text size='xl'>Are you sure you would like to unlock this position?</Text>
<Text className='mt-2 text-white/60'>
{`Are you sure you want to unlock this position? The unlocking period will take ${props.depositedVault.lockup.duration} ${props.depositedVault.lockup.timeframe}.`}
</Text>
<div className='mt-10 flex flex-row-reverse justify-between'>
<Button
text='Yes'
color='tertiary'
className='px-6'
rightIcon={<YesIcon />}
onClick={onConfirm}
/>
<Button
text='No'
color='secondary'
className='px-6'
rightIcon={<NoIcon />}
tabIndex={1}
onClick={props.onClose}
/>
</div>
</>
)
}

View File

@ -119,6 +119,36 @@ export default function createBroadcastSlice(
}
return !!response.result
},
unlock: async (options: { fee: StdFee; vault: Vault; amount: string }) => {
const msg = {
request_vault_unlock: {
vault: { address: options.vault.address },
amount: options.amount,
},
}
const response = await get().executeMsg({
msg,
fee: options.fee,
funds: [],
})
if (response.result) {
set({
toast: {
message: `Requested unlock for ${options.vault.name}`,
},
})
} else {
set({
toast: {
message: response.error ?? `Request unlocked failed: ${response.error}`,
isError: true,
},
})
}
return !!response.result
},
withdraw: async (options: { fee: StdFee; accountId: string; coin: Coin }) => {
const msg = {
update_credit_account: {

View File

@ -8,6 +8,7 @@ export default function createModalSlice(set: SetState<ModalSlice>, get: GetStat
deleteAccountModal: false,
fundAccountModal: false,
fundAndWithdrawModal: null,
unlockModal: null,
vaultModal: null,
}
}

View File

@ -14,6 +14,7 @@ interface BroadcastSlice {
createAccount: (options: { fee: StdFee }) => Promise<string | null>
deleteAccount: (options: { fee: StdFee; accountId: string }) => Promise<boolean>
deposit: (options: { fee: StdFee; accountId: string; coin: Coin }) => Promise<boolean>
unlock: (options: { fee: StdFee; vault: Vault; amount: string }) => Promise<boolean>
withdraw: (options: { fee: StdFee; accountId: string; coin: Coin }) => Promise<boolean>
repay: (options: {
fee: StdFee

View File

@ -6,6 +6,7 @@ interface ModalSlice {
fundAccountModal: boolean
fundAndWithdrawModal: 'fund' | 'withdraw' | null
vaultModal: VaultModal | null
unlockModal: UnlockModal | null
}
interface BorrowModal {
@ -22,3 +23,7 @@ interface VaultModal {
interface AddVaultBorrowingsModal {
selectedDenoms: string[]
}
interface UnlockModal {
vault: DepositedVault
}

View File

@ -42,6 +42,9 @@ interface VaultValuesAndAmounts {
amounts: {
primary: BigNumber
secondary: BigNumber
locked: BigNumber
unlocked: BigNumber
unlocking: BigNumer
}
values: {
primary: BigNumber
@ -65,7 +68,7 @@ interface VaultExtensionResponse {
}
interface VaultPositionFlatAmounts {
locked: string
unlocking: string
unlocked: string
locked: BigNumber
unlocking: BigNumber
unlocked: BigNumber
}