diff --git a/__tests__/components/Modals/Unlock/UnlockModal.test.tsx b/__tests__/components/Modals/Unlock/UnlockModal.test.tsx new file mode 100644 index 00000000..6bb51cc8 --- /dev/null +++ b/__tests__/components/Modals/Unlock/UnlockModal.test.tsx @@ -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(() =>
Modal
) + +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('', () => { + beforeAll(() => { + useStore.setState({ unlockModal: null }) + }) + it('should render', () => { + const { container } = render() + 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() + 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() + expect(mockedModal).toHaveBeenLastCalledWith( + expect.objectContaining({ open: true }), + expect.anything(), + ) + }) + }) +}) diff --git a/__tests__/components/Modals/vault/VaultBorrowings.test.tsx b/__tests__/components/Modals/vault/VaultBorrowings.test.tsx index 3c1f0c0a..0fea75f1 100644 --- a/__tests__/components/Modals/vault/VaultBorrowings.test.tsx +++ b/__tests__/components/Modals/vault/VaultBorrowings.test.tsx @@ -45,7 +45,6 @@ describe('', () => { beforeAll(() => { useStore.setState({ baseCurrency: ASSETS[0], - selectedBorrowDenoms: [ASSETS[1].denom], }) }) diff --git a/jest.config.js b/jest.config.js index 9389685e..088fcf81 100644 --- a/jest.config.js +++ b/jest.config.js @@ -35,7 +35,6 @@ module.exports = { '^styles/(.*)$': '/src/styles/$1', '^types/(.*)$': '/src/types/$1', '^utils/(.*)$': '/src/utils/$1', - '^store': '/src/store', }, // Add more setup options before each test is run diff --git a/src/api/vaults/getDepositedVaults.ts b/src/api/vaults/getDepositedVaults.ts index 0e011ee8..577e76a6 100644 --- a/src/api/vaults/getDepositedVaults.ts +++ b/src/api/vaults/getDepositedVaults.ts @@ -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), }, diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index e7fd4aef..ee4efda1 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -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} onClick={isDisabled ? () => {} : onClick} + tabIndex={tabIndex} > {showProgressIndicator ? ( diff --git a/src/components/Icons/Enter.svg b/src/components/Icons/Enter.svg new file mode 100644 index 00000000..383ae5cd --- /dev/null +++ b/src/components/Icons/Enter.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Icons/LockUnlocked.svg b/src/components/Icons/LockUnlocked.svg new file mode 100644 index 00000000..8973c211 --- /dev/null +++ b/src/components/Icons/LockUnlocked.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Icons/index.ts b/src/components/Icons/index.ts index 09d8e84a..591adf49 100644 --- a/src/components/Icons/index.ts +++ b/src/components/Icons/index.ts @@ -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' diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 2ec67649..ed5547db 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -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) { >
{props.header} - + {!props.hideCloseBtn && ( + + )}
{props.children ? props.children : props.content} diff --git a/src/components/Modals/ModalsContainer.tsx b/src/components/Modals/ModalsContainer.tsx index 0870ac79..c06efcb8 100644 --- a/src/components/Modals/ModalsContainer.tsx +++ b/src/components/Modals/ModalsContainer.tsx @@ -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() { + ) } diff --git a/src/components/Modals/Unlock/UnlockModal.tsx b/src/components/Modals/Unlock/UnlockModal.tsx new file mode 100644 index 00000000..fe8333a6 --- /dev/null +++ b/src/components/Modals/Unlock/UnlockModal.tsx @@ -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 ( + + +
+ } + modalClassName='w-[577px]' + headerClassName='p-8' + contentClassName='px-8 pb-8' + hideCloseBtn + > + {modal ? ( + + ) : ( + + )} + + ) +} diff --git a/src/components/Modals/Unlock/UnlockModalContent.tsx b/src/components/Modals/Unlock/UnlockModalContent.tsx new file mode 100644 index 00000000..609b9242 --- /dev/null +++ b/src/components/Modals/Unlock/UnlockModalContent.tsx @@ -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 ( +
+ ESC +
+ ) +} + +function YesIcon() { + return ( +
+ +
+ ) +} + +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 ( + <> + Are you sure you would like to unlock this position? + + {`Are you sure you want to unlock this position? The unlocking period will take ${props.depositedVault.lockup.duration} ${props.depositedVault.lockup.timeframe}.`} + +
+
+ + ) +} diff --git a/src/store/slices/broadcast.ts b/src/store/slices/broadcast.ts index e34771b6..aae36174 100644 --- a/src/store/slices/broadcast.ts +++ b/src/store/slices/broadcast.ts @@ -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: { diff --git a/src/store/slices/modal.ts b/src/store/slices/modal.ts index 9779a4ed..63c06fc6 100644 --- a/src/store/slices/modal.ts +++ b/src/store/slices/modal.ts @@ -8,6 +8,7 @@ export default function createModalSlice(set: SetState, get: GetStat deleteAccountModal: false, fundAccountModal: false, fundAndWithdrawModal: null, + unlockModal: null, vaultModal: null, } } diff --git a/src/types/interfaces/store/broadcast.d.ts b/src/types/interfaces/store/broadcast.d.ts index 7b9f5201..8d67a83b 100644 --- a/src/types/interfaces/store/broadcast.d.ts +++ b/src/types/interfaces/store/broadcast.d.ts @@ -14,6 +14,7 @@ interface BroadcastSlice { createAccount: (options: { fee: StdFee }) => Promise deleteAccount: (options: { fee: StdFee; accountId: string }) => Promise deposit: (options: { fee: StdFee; accountId: string; coin: Coin }) => Promise + unlock: (options: { fee: StdFee; vault: Vault; amount: string }) => Promise withdraw: (options: { fee: StdFee; accountId: string; coin: Coin }) => Promise repay: (options: { fee: StdFee diff --git a/src/types/interfaces/store/modals.d.ts b/src/types/interfaces/store/modals.d.ts index bbc41004..4841502a 100644 --- a/src/types/interfaces/store/modals.d.ts +++ b/src/types/interfaces/store/modals.d.ts @@ -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 +} diff --git a/src/types/interfaces/vaults.d.ts b/src/types/interfaces/vaults.d.ts index 97866803..098c1e0b 100644 --- a/src/types/interfaces/vaults.d.ts +++ b/src/types/interfaces/vaults.d.ts @@ -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 }