Mp 2547 unlocking modal (#256)
* add modal * add tests and update naming * fix pr comments
This commit is contained in:
parent
5aabf6f725
commit
0037c3dedf
66
__tests__/components/Modals/Unlock/UnlockModal.test.tsx
Normal file
66
__tests__/components/Modals/Unlock/UnlockModal.test.tsx
Normal 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(),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
@ -45,7 +45,6 @@ describe('<VaultBorrowings />', () => {
|
||||
beforeAll(() => {
|
||||
useStore.setState({
|
||||
baseCurrency: ASSETS[0],
|
||||
selectedBorrowDenoms: [ASSETS[1].denom],
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
},
|
||||
|
@ -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} />
|
||||
|
3
src/components/Icons/Enter.svg
Normal file
3
src/components/Icons/Enter.svg
Normal 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 |
3
src/components/Icons/LockUnlocked.svg
Normal file
3
src/components/Icons/LockUnlocked.svg
Normal 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 |
@ -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'
|
||||
|
@ -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}
|
||||
|
@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
35
src/components/Modals/Unlock/UnlockModal.tsx
Normal file
35
src/components/Modals/Unlock/UnlockModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
65
src/components/Modals/Unlock/UnlockModalContent.tsx
Normal file
65
src/components/Modals/Unlock/UnlockModalContent.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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: {
|
||||
|
@ -8,6 +8,7 @@ export default function createModalSlice(set: SetState<ModalSlice>, get: GetStat
|
||||
deleteAccountModal: false,
|
||||
fundAccountModal: false,
|
||||
fundAndWithdrawModal: null,
|
||||
unlockModal: null,
|
||||
vaultModal: null,
|
||||
}
|
||||
}
|
||||
|
1
src/types/interfaces/store/broadcast.d.ts
vendored
1
src/types/interfaces/store/broadcast.d.ts
vendored
@ -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
|
||||
|
5
src/types/interfaces/store/modals.d.ts
vendored
5
src/types/interfaces/store/modals.d.ts
vendored
@ -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
|
||||
}
|
||||
|
9
src/types/interfaces/vaults.d.ts
vendored
9
src/types/interfaces/vaults.d.ts
vendored
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user