diff --git a/frontend/jest.config.js b/frontend/jest.config.js index d704a6fd..bb18538e 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -5,6 +5,7 @@ module.exports = { }, moduleNameMapper: { '\\.svg$': '/test/__mocks__/svgMock.js', + '\\.css$': '/test/__mocks__/styleMock.js', '^src/(.*)$': ['/src/$1'], }, transformIgnorePatterns: [ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d330ceeb..6aca0e2f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -13,14 +13,14 @@ import { getTelegramUserWalletId } from 'services/telegram'; import Documentation from 'pages/spotnet/documentation/Documentation'; import Withdraw from 'pages/vault/withdraw/Withdraw'; import { useWalletStore } from 'stores/useWalletStore'; -import { Notifier } from 'components/Notifier/Notifier'; +import { Notifier, notify } from 'components/Notifier/Notifier'; import { useConnectWallet } from 'hooks/useConnectWallet'; import OverviewPage from 'pages/spotnet/overview/Overview'; import { ActionModal } from 'components/ui/ActionModal'; import Stake from 'pages/vault/stake/Stake'; import { TELEGRAM_BOT_LINK } from 'utils/constants'; import { useCheckMobile } from 'hooks/useCheckMobile'; -import { notifyError } from 'utils/notification'; +import PositionHistory from 'pages/spotnet/position_history/PositionHistory'; function App() { @@ -34,7 +34,6 @@ function App() { const connectWalletMutation = useConnectWallet(setWalletId); - const handleConnectWallet = () => { connectWalletMutation.mutate(); }; @@ -65,14 +64,16 @@ function App() { useEffect(() => { if (window.Telegram?.WebApp?.initData) { - getTelegramUserWalletId(window.Telegram.WebApp.initDataUnsafe.user.id).then((linked_wallet_id) => { - setWalletId(linked_wallet_id); - window.Telegram.WebApp.ready(); - }).catch((error) => { - console.error('Error getting Telegram user wallet ID:', error); - notifyError('Error loading wallet'); - window.Telegram.WebApp.ready(); - }); + getTelegramUserWalletId(window.Telegram.WebApp.initDataUnsafe.user.id) + .then((linked_wallet_id) => { + setWalletId(linked_wallet_id); + window.Telegram.WebApp.ready(); + }) + .catch((error) => { + console.error('Error getting Telegram user wallet ID:', error); + notify('Error loading wallet', "error"); + window.Telegram.WebApp.ready(); + }); } }, [window.Telegram?.WebApp?.initDataUnsafe]); @@ -93,14 +94,11 @@ function App() { />, document.body )} -
+
} /> - : } /> @@ -109,6 +107,7 @@ function App() { } /> } /> } /> + } /> } />
diff --git a/frontend/src/assets/icons/filter-horizontal.svg b/frontend/src/assets/icons/filter-horizontal.svg new file mode 100644 index 00000000..34706cd4 --- /dev/null +++ b/frontend/src/assets/icons/filter-horizontal.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/src/components/Card/card.css b/frontend/src/components/Card/card.css index e69f5406..ee530c89 100644 --- a/frontend/src/components/Card/card.css +++ b/frontend/src/components/Card/card.css @@ -1,9 +1,9 @@ .card { width: 309px; height: 101px; - background: var(--dark-background); + background: var(--bg); border: 1px solid var(--light-purple); - border-radius: 900px; + border-radius: 12px; padding-top: 4px; padding-right: 24px; padding-left: 24px; diff --git a/frontend/src/components/GasFee/GasFee.jsx b/frontend/src/components/GasFee/GasFee.jsx index b30a7c3e..48b89cf5 100644 --- a/frontend/src/components/GasFee/GasFee.jsx +++ b/frontend/src/components/GasFee/GasFee.jsx @@ -6,7 +6,7 @@ import './gasfee.css'; export default function GasFee() { return (
-
+
diff --git a/frontend/src/components/GasFee/gasfee.css b/frontend/src/components/GasFee/gasfee.css index 00bc8e87..6ca653c9 100644 --- a/frontend/src/components/GasFee/gasfee.css +++ b/frontend/src/components/GasFee/gasfee.css @@ -3,6 +3,8 @@ display: flex; align-items: center; justify-content: space-between; + padding: 0rem 3rem; + padding-bottom: 1rem; } .gas-fee-circle { diff --git a/frontend/src/components/LendingForm.js b/frontend/src/components/LendingForm.js index 3289f18c..d2749392 100644 --- a/frontend/src/components/LendingForm.js +++ b/frontend/src/components/LendingForm.js @@ -1,10 +1,10 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { getTokenBalances, sendTransaction } from 'services/wallet'; -import { notifyError } from 'utils/notification'; import { axiosInstance } from 'utils/axios'; import Button from 'components/ui/Button/Button'; import { useWalletStore } from 'stores /useWalletStore'; +import { notify } from './Notifier/Notifier'; const LendingForm = () => { @@ -29,7 +29,7 @@ const navigate = useNavigate(); setBalances(tokenBalances); } catch (error) { console.error('Failed to fetch balances:', error); - notifyError('Failed to fetch token balances. Please try again.'); + notify('Failed to fetch token balances. Please try again.', "error"); } }, [walletId]); diff --git a/frontend/src/components/MultiplierSelector.jsx b/frontend/src/components/MultiplierSelector.jsx index 5ba11bf4..82dac369 100644 --- a/frontend/src/components/MultiplierSelector.jsx +++ b/frontend/src/components/MultiplierSelector.jsx @@ -6,7 +6,7 @@ import './multiplier.css'; const MultiplierSelector = ({ setSelectedMultiplier, selectedToken }) => { const minMultiplier = 1.1; - const { data, isLoading, error } = useMaxMultiplier(); + const { data, isLoading } = useMaxMultiplier(); const [actualValue, setActualValue] = useState(minMultiplier); const sliderRef = useRef(null); const isDragging = useRef(false); @@ -104,7 +104,6 @@ const MultiplierSelector = ({ setSelectedMultiplier, selectedToken }) => { }, [maxMultiplier, actualValue, setSelectedMultiplier]); if (isLoading) return
Loading multiplier data...
; - if (error) return
Error loading multiplier data: {error.message}
; return (
diff --git a/frontend/src/components/Notifier/Notifier.jsx b/frontend/src/components/Notifier/Notifier.jsx index ea6a3573..6072e57c 100644 --- a/frontend/src/components/Notifier/Notifier.jsx +++ b/frontend/src/components/Notifier/Notifier.jsx @@ -2,14 +2,27 @@ import React from 'react'; import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; -const notify = (message) => toast(message); +const defaultStyles = { + success: { backgroundColor: 'green', color: 'white' }, + error: { backgroundColor: 'red', color: 'white' }, + warning: { backgroundColor: 'orange', color: 'white' }, + info: { backgroundColor: 'blue', color: 'white' }, +}; + +const ToastWithLink = (message, link, linkMessage) => ( +
+ {message} {linkMessage} +
+); + +const notify = (message, type='info', autoClose=3000) => toast(message, { type, autoClose, style: defaultStyles[type] || defaultStyles.info }); const Notifier = () => { return (
- +
); }; -export { Notifier, notify }; +export { Notifier, notify, ToastWithLink}; diff --git a/frontend/src/components/SlideBarFour.jsx b/frontend/src/components/SlideBarFour.jsx index afb5aa54..f40e0f8c 100644 --- a/frontend/src/components/SlideBarFour.jsx +++ b/frontend/src/components/SlideBarFour.jsx @@ -1,6 +1,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import { useMaxMultiplier } from 'hooks/useMaxMultiplier'; import './slider-three.css'; +import { notify } from 'components/Notifier/Notifier'; const StepSlider = ({ min = 0, max = 10, step = 1, defaultValue = 1, setSelectedMultiplier, selectedToken }) => { const { data, isLoading, error } = useMaxMultiplier(); @@ -34,7 +35,7 @@ const StepSlider = ({ min = 0, max = 10, step = 1, defaultValue = 1, setSelected }, [value, maxMultiplier, TOTAL_MARKS]); if (isLoading) return
Loading multiplier data...
; - if (error) return
Error loading multiplier data: {error.message}
; + if (error) return notify(error.message, 'error'); const currentMark = getCurrentMark(); diff --git a/frontend/src/components/StakeCard/metricCard.css b/frontend/src/components/StakeCard/metricCard.css index 58cee379..9b23f9e0 100644 --- a/frontend/src/components/StakeCard/metricCard.css +++ b/frontend/src/components/StakeCard/metricCard.css @@ -1,12 +1,14 @@ .stake-card { width: 309px; + background: var(--header-button-bg); border: var(--midnight-purple-border); - border-radius: 900px; + border-radius: 999px; display: flex; flex-direction: column; justify-content: center; align-items: center; + } .stake-card .img { diff --git a/frontend/src/components/WalletSection.jsx b/frontend/src/components/WalletSection.jsx index 0dc94869..394562dd 100644 --- a/frontend/src/components/WalletSection.jsx +++ b/frontend/src/components/WalletSection.jsx @@ -74,7 +74,7 @@ const WalletSection = ({ onConnectWallet, onLogout }) => { onLogout(); }} > - Log out + Disconnect
)} diff --git a/frontend/src/components/collateral/Collateral.jsx b/frontend/src/components/collateral/Collateral.jsx index ef50530e..7714705e 100644 --- a/frontend/src/components/collateral/Collateral.jsx +++ b/frontend/src/components/collateral/Collateral.jsx @@ -14,7 +14,7 @@ function Collateral({ data, startSum, currentSum, getCurrentSumColor }) { {data[0]?.currencyName || 'N/A'}
- Balance: + Position Balance: {data[0]?.balance ? Number(data[0].balance).toFixed(8) : '0.00'} diff --git a/frontend/src/components/ui/Button/button.css b/frontend/src/components/ui/Button/button.css index d8b85163..91d2b8e3 100644 --- a/frontend/src/components/ui/Button/button.css +++ b/frontend/src/components/ui/Button/button.css @@ -3,7 +3,7 @@ border: none; cursor: pointer; font-weight: 600; - border-radius: 900px; + border-radius: 8px; transition: all 0.2s ease-in-out; display: flex; justify-content: center; @@ -71,7 +71,7 @@ left: 0; right: 0; bottom: 0; - border-radius: 50px; + border-radius: 8px; padding: 2px; background: linear-gradient(90deg, #74d6fd 0%, #e01dee 100%); -webkit-mask: @@ -101,7 +101,7 @@ @media (max-width: 768px) { .button { - border-radius: 16px; + border-radius: 8px; } .button--primary { @@ -109,7 +109,7 @@ } .button--secondary::before { - border-radius: 16px; + border-radius: 8px; } .button--lg { diff --git a/frontend/src/globals.css b/frontend/src/globals.css index 14e0ceb7..844d104d 100644 --- a/frontend/src/globals.css +++ b/frontend/src/globals.css @@ -49,6 +49,9 @@ --gray: #83919f; --dark-purple: #120721; --light-blue: #74d5fd; + --status-opened: #1edc9e; + --status-closed: #433b5a; + --status-pending: #83919f; --slider-gray: #393942; --light-blue: #88b4fa; --thumb-image: url('./assets/icons/slider-thumb.svg'); @@ -63,6 +66,7 @@ --dark-background: #130713; --light-dark-background: #130713; --text-gray: #798795; + --modal-border: #170f2e; } body { diff --git a/frontend/src/hooks/useClosePosition.js b/frontend/src/hooks/useClosePosition.js index 859cbfe2..8df35d34 100644 --- a/frontend/src/hooks/useClosePosition.js +++ b/frontend/src/hooks/useClosePosition.js @@ -2,6 +2,7 @@ import { useMutation, useQuery } from '@tanstack/react-query'; import { axiosInstance } from 'utils/axios'; import { closePosition } from 'services/transaction'; import { useWalletStore } from 'stores/useWalletStore'; +import { notify } from 'components/Notifier/Notifier'; export const useClosePosition = () => { const { walletId } = useWalletStore(); @@ -18,6 +19,7 @@ export const useClosePosition = () => { }, onError: (error) => { console.error('Error during closePositionEvent', error); + notify(`Error during closePositionEvent: ${error.message}`, 'error') }, }); }; diff --git a/frontend/src/hooks/useConnectWallet.js b/frontend/src/hooks/useConnectWallet.js index 61e7519a..576ffff9 100644 --- a/frontend/src/hooks/useConnectWallet.js +++ b/frontend/src/hooks/useConnectWallet.js @@ -1,6 +1,6 @@ import { useMutation } from '@tanstack/react-query'; +import { notify } from 'components/Notifier/Notifier'; import { connectWallet, checkForCRMToken } from 'services/wallet'; -import { notifyError } from 'utils/notification'; export const useConnectWallet = (setWalletId) => { return useMutation({ @@ -22,7 +22,7 @@ export const useConnectWallet = (setWalletId) => { }, onError: (error) => { console.error('Wallet connection failed:', error); - notifyError('Failed to connect wallet. Please try again.'); + notify('Failed to connect wallet. Please try again.', "error"); }, }); }; \ No newline at end of file diff --git a/frontend/src/hooks/useMaxMultiplier.js b/frontend/src/hooks/useMaxMultiplier.js index cc03e540..3d8f7eeb 100644 --- a/frontend/src/hooks/useMaxMultiplier.js +++ b/frontend/src/hooks/useMaxMultiplier.js @@ -1,9 +1,10 @@ import { useQuery } from '@tanstack/react-query'; import { ONE_HOUR_IN_MILLISECONDS } from '../utils/constants'; import { axiosInstance } from 'utils/axios'; +import { notify } from 'components/Notifier/Notifier'; export const useMaxMultiplier = () => { - const { data, isPending, error } = useQuery({ + const { data, isPending } = useQuery({ queryKey: ['max-multiplier'], queryFn: async () => { const response = await axiosInstance.get(`/api/get-multipliers`); @@ -11,7 +12,8 @@ export const useMaxMultiplier = () => { }, staleTime: ONE_HOUR_IN_MILLISECONDS, refetchInterval: ONE_HOUR_IN_MILLISECONDS, + onError: (error) => notify(`Error using multiplier: ${error.message}`, 'error') }); - return { data, isLoading: isPending, error }; + return { data, isLoading: isPending }; }; diff --git a/frontend/src/hooks/usePositionHistory.js b/frontend/src/hooks/usePositionHistory.js new file mode 100644 index 00000000..c1dfe420 --- /dev/null +++ b/frontend/src/hooks/usePositionHistory.js @@ -0,0 +1,49 @@ +import { useQuery } from '@tanstack/react-query'; +import { axiosInstance } from 'utils/axios'; +import { formatDate } from 'utils/formatDate'; +import { useWalletStore } from 'stores/useWalletStore'; + +const fetchPositionHistoryTable = async (walletId) => { + if (!walletId) { + throw new Error('Wallet ID is undefined'); + } + const response = await axiosInstance.get(`/api/user-positions/${walletId}`); + return response.data; +}; + +const formatPositionHistoryTable = (data) => { + return data.map((position) => ({ + ...position, + amount: Number(position.amount).toFixed(2), + start_price: `$${position.start_price.toFixed(2)}`, + multiplier: position.multiplier.toFixed(1), + created_at: formatDate(position.created_at), + datetime_liquidation: formatDate(position.datetime_liquidation), + status: position.status.charAt(0).toUpperCase() + position.status.slice(1), + is_liquidated: position.is_liquidated ? 'Yes' : 'No', + })); +}; + +const usePositionHistoryTable = () => { + const walletId = useWalletStore((state) => state.walletId); + + const { data, isPending, error } = useQuery({ + queryKey: ['positionHistory', walletId], + queryFn: () => fetchPositionHistoryTable(walletId), + enabled: !!walletId, + onError: (err) => { + console.error('Error during fetching position history:', err); + }, + select: formatPositionHistoryTable, + }); + + const showSpinner = !!walletId && isPending; + + return { + data, + isPending: showSpinner, + error: walletId ? error : 'Wallet ID is required', + }; +}; + +export { usePositionHistoryTable }; diff --git a/frontend/src/hooks/useTelegramNotification.js b/frontend/src/hooks/useTelegramNotification.js index ec0e51b5..883261b7 100644 --- a/frontend/src/hooks/useTelegramNotification.js +++ b/frontend/src/hooks/useTelegramNotification.js @@ -1,6 +1,6 @@ import { useMutation } from "@tanstack/react-query"; import { subscribeToNotification, generateTelegramLink } from "services/telegram"; -import { notifyError, notifySuccess } from "utils/notification"; +import { notify } from "components/Notifier/Notifier"; const useTelegramNotification = () => { const mutation = useMutation({ @@ -14,10 +14,10 @@ const useTelegramNotification = () => { return await subscribeToNotification(telegramId, walletId); }, onSuccess: () => { - notifySuccess("Subscribed to notifications successfully!"); + notify("Subscribed to notifications successfully!", "success"); }, onError: (error) => { - notifyError(error?.message || "Failed to subscribe. Please try again."); + notify(error?.message || "Failed to subscribe. Please try again.", "error"); }, }); diff --git a/frontend/src/pages/forms/Form.jsx b/frontend/src/pages/forms/Form.jsx index 8a0d817f..659f4d73 100644 --- a/frontend/src/pages/forms/Form.jsx +++ b/frontend/src/pages/forms/Form.jsx @@ -7,7 +7,6 @@ import BalanceCards from 'components/BalanceCards'; import MultiplierSelector from 'components/MultiplierSelector'; import { handleTransaction } from 'services/transaction'; import Spinner from 'components/spinner/Spinner'; -import { ReactComponent as AlertHexagon } from 'assets/icons/alert_hexagon.svg'; import './form.css'; import { createPortal } from 'react-dom'; import useLockBodyScroll from 'hooks/useLockBodyScroll'; @@ -19,6 +18,7 @@ import { useCheckPosition } from 'hooks/useClosePosition'; import { useNavigate } from 'react-router-dom'; import { ActionModal } from 'components/ui/ActionModal'; import { useHealthFactor } from 'hooks/useHealthRatio'; +import { notify } from 'components/Notifier/Notifier'; const Form = () => { const navigate = useNavigate(); @@ -26,9 +26,7 @@ const Form = () => { const [tokenAmount, setTokenAmount] = useState(''); const [selectedToken, setSelectedToken] = useState('ETH'); const [selectedMultiplier, setSelectedMultiplier] = useState(''); - const [error, setError] = useState(''); const [loading, setLoading] = useState(false); - const [alertMessage, setAlertMessage] = useState(''); const [successful, setSuccessful] = useState(false); useLockBodyScroll(successful); @@ -70,19 +68,17 @@ const Form = () => { } if (tokenAmount === '' || selectedToken === '' || selectedMultiplier === '') { - setAlertMessage('Please fill the form'); + notify("Please fill the form", 'error') return; } - setAlertMessage(''); - const formData = { wallet_id: connectedWalletId, token_symbol: selectedToken, amount: tokenAmount, multiplier: selectedMultiplier, }; - await handleTransaction(connectedWalletId, formData, setError, setTokenAmount, setLoading, setSuccessful); + await handleTransaction(connectedWalletId, formData, setTokenAmount, setLoading, setSuccessful); }; const handleCloseModal = () => { @@ -117,11 +113,6 @@ const Form = () => {

Please submit your leverage details

- {alertMessage && ( -

- {alertMessage} -

- )} @@ -132,13 +123,11 @@ const Form = () => { />
- {error &&

{error}

} setTokenAmount(e.target.value)} - className={error ? 'error' : ''} />
diff --git a/frontend/src/pages/spotnet/dashboard/Dashboard.jsx b/frontend/src/pages/spotnet/dashboard/Dashboard.jsx index 677e8b23..25d4e2c1 100644 --- a/frontend/src/pages/spotnet/dashboard/Dashboard.jsx +++ b/frontend/src/pages/spotnet/dashboard/Dashboard.jsx @@ -12,7 +12,6 @@ import Button from 'components/ui/Button/Button'; import { useWalletStore } from 'stores/useWalletStore'; import { ActionModal } from 'components/ui/ActionModal'; import useTelegramNotification from 'hooks/useTelegramNotification'; -import { ReactComponent as AlertHexagon } from 'assets/icons/alert_hexagon.svg'; import Borrow from 'components/borrow/Borrow'; import { ReactComponent as CollateralIcon } from 'assets/icons/collateral_dynamic.svg'; import Collateral from 'components/collateral/Collateral'; @@ -32,7 +31,7 @@ export default function Component({ telegramId }) { data: { health_ratio: '1.5', current_sum: '0.05', start_sum: '0.04', borrowed: '10.0' }, isLoading: false, }; - const { mutate: closePositionEvent, isLoading: isClosing, error: closePositionError } = useClosePosition(walletId); + const { mutate: closePositionEvent, isLoading: isClosing } = useClosePosition(walletId); const { data: positionData } = useCheckPosition(); const { subscribe } = useTelegramNotification(); @@ -214,11 +213,6 @@ export default function Component({ telegramId }) { > {isClosing ? 'Closing...' : 'Redeem'} - {closePositionError && ( -
- Error: {closePositionError.message} -
- )} +
- -
-
); } - - -export default Stake; \ No newline at end of file +export default Stake; diff --git a/frontend/src/pages/vault/stake/stake.css b/frontend/src/pages/vault/stake/stake.css index 012e4b38..3b68ea54 100644 --- a/frontend/src/pages/vault/stake/stake.css +++ b/frontend/src/pages/vault/stake/stake.css @@ -10,6 +10,7 @@ width: calc(100vw - 372px); } + .token-icon { display: flex; align-items: center; @@ -19,59 +20,13 @@ background-color: hsla(261, 49%, 15%, 1); } -.stake-card { - width: 309px; - background: var(--header-button-bg); - border: var(--midnight-purple-border); - border-radius: 900px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.stake-title { - font-size: 20px; - font-weight: 600; - color: var(--primary); - text-align: center; -} - -.stake-container { - display: flex; - justify-content: center; - flex-direction: column; - gap: 20px; -} - -.main-container { - width: 642px; - gap: 24px; - padding-top: 37px; - border-radius: 20px; - color: var(--primary); - text-align: center; - display: flex; - justify-content: center; - flex-direction: column; - align-items: center; -} - -.top-cards { - width: 100%; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 24px; - align-items: stretch; -} - .network-selector-container { position: relative; width: 100%; } -.main-card { - padding: 3rem 1.5rem; +.main-stake-card { + padding:4px 4px; height: auto; display: flex; flex-direction: column; @@ -185,10 +140,6 @@ text-align: center; } -.stake-button { - width: 100%; -} - .balance-display-container .large-screen-balance { display: block; } @@ -197,216 +148,220 @@ display: none; } -/* Tablet and Mobile Responsiveness */ -@media screen and (max-width: 1024px) { - .stake-wrapper { - display: block; - width: 100%; - margin-right: 374px; - margin-left: 0px; - padding: 0 16px; - background: url('../../../../public/background.png') no-repeat; - background-size: cover; - background-position: -250px center; - } +.divider1 { + height: 1px; + width: 80%; + background: var(--footer-divider-bg); + margin:auto; - .stake-container { - padding: 0; - margin-left: 0; - margin-right: 0; - width: 100%; - } +} +.stake-wrapper { + height: 100vh; + overflow-y: auto; + display: flex; + justify-content: center; + align-items: flex-start; + width: 100vw; + padding: 5rem 24px; +} - .balance-container { - flex-direction: row; - justify-content: flex-start; - align-items: center; - margin-bottom: 0; - overflow-x: auto; - scroll-snap-type: x mandatory; - gap: 5px; - max-width: 100%; - scrollbar-width: none; - -ms-overflow-style: none; - } +.main-container { + width: 642px; + gap: 24px; + padding-top: 37px; + border-radius: 20px; + color: var(--primary); + text-align: center; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; +} +.top-cards{ + display: flex; + gap: 24px; +} +.stake-card { + width: 309px; + height:101px; + background: var(--header-button-bg); + border: var(--midnight-purple-border); + border-radius: 8px; + font-weight: 600; + font-size: 24px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} - /* .balance-item { - height: fit-content; - padding: 16px 24px; - display: flex; - width: 167px; - height: 88px; - flex-direction: column; - align-items: center; - margin-right: 0; - border-radius: 16px; - border: 1px solid var(--light-purple); - background-color: var(--dark-purple); - } */ - - .balance-item:first-child { - margin-left: 20px; - } +.stake-title { + font-size: 14px; + font-weight: 400; + color: var(--primary); + text-align: center; + padding: 1rem 0rem; +} +.stake-button1 { + width: 642px; + height: 60px; + padding: 16px 24px; + border-radius: 8px; + font-weight: 600; + font-size: 14px; + margin-top: 2rem; +} + +.cancel{ + display: none; +} - .balance-item:last-child { - margin-right: 20px; - } - .balance-item .balance-title { - font-size: 1rem; - display: flex; - align-items: center; - } +/********Responsiveness **********/ - .balance-item .title-container + label:nth-of-type(1) { - font-size: 20px; - font-weight: 600; +@media (min-width: 769px) and (max-width: 1024px){ + .stake-wrapper{ + margin-left:0px; } +} - .balance-item label:nth-child(2) { - color: var(--secondary); - /* font-size: 14px; */ - } - .stake-title { - font-size: 16px; - font-weight: 400; +/* Mobile view */ +@media (max-width:768px) { + .sidenav{ + display: none; } +.top-cards { + width:100%; + display: flex; + gap: 16px; + justify-content: space-between; + height: 88px; +} - .main-card { - gap: 26px; - flex-direction: column; - border-radius: 12px; + .main-container { + width: 100%; + padding: 0; } - .clicked-network-selector-container .network-dropdown { - display: block; + .stake-card { + width: 167px; + height:88px; + padding: 16px 24px; + border: 1px; + background: var(--header-button-bg); + border: var(--midnight-purple-border); + border-radius: 8px; + font-weight: 600; + font-size: 16px; + display: flex; + justify-content: center; } - .network-selector { - padding: 15px; + .stake-wrapper { + top: 80px; + display: flex; + flex-direction: column-reverse; + justify-content: center; + align-items: flex-start; + padding: 3rem 24px; + margin-left: 0px; } - .amount-field { - font-size: 32px; - padding: 12px; + .stake-container { + width: 100%; + max-width: 390px; + height: 610px; + margin:0 auto; + margin-bottom: 2rem; } - .currency { - right: 10%; + .stake-title { font-size: 14px; + font-weight: 400; + color: var(--primary); + display: block; + margin: 0 auto; + text-align: center; + align-items: center; + padding: 1.5rem 0rem; } - .apy-rate { - font-size: 14px; + .can-stk{ + display: flex; + gap: 8px; + align-items: center; + justify-content: space-between; + width:100%; + margin-top: 2rem; } - - .stake-button { + .cancel { + width: 167px; + height: 60px; + padding: 16px 24px; + border-radius: 8px; + font-weight: 600; font-size: 14px; - padding: 18px 24px; - - align-self: center; + border-color:#36294E ; + color: white; + display: block; } + .stake-button1 { + width: 167px; + height: 60px; + padding: 16px 24px; + border-radius: 8px; + font-weight: 600; + font-size: 14px; + margin-top: 0px; + } + .form{ + margin-bottom: 2rem; + width: 100%; + display: block; + margin: 0 auto; - .balance-card { - max-width: 100%; - margin-top: 20px; } - .balance-display-container .large-screen-balance { - display: none; - } - .balance-display-container .mobile-screen-balance { - display: block; - } +.divider1 { + height: 1px; + width: 80%; + background: var(--footer-divider-bg); + margin:auto; } -@media (min-width: 768px) and (max-width: 1024px) { - .stake-wrapper { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - margin: 0 auto; - padding: 0 20px; - background-position: center; - } - - .stake-container { - width: 100%; - padding: 1rem; - margin: 0 auto; - } + +} - .top-cards { - display: flex; - width: 100%; - /* flex-wrap: wrap; */ - /* gap: 6px; */ - } - .main-card { - margin: 0 auto; - gap: 20px; - } .amount-field { font-size: 24px; padding: 10px; } - .stake-button { - font-size: 16px; - padding: 16px 24px; - margin-left: 12px; - margin-right: 11px; - align-self: center; - width: 85%; - } -} - @media (max-width: 678px) { .main-container { padding-top: 0px; } .top-cards { - width: 470px; gap: 8px; } - - .main-card { - padding: 0 20px; - border: none; - } - } @media (max-width: 480px) { .top-cards { - width: 380px; gap: 10px; } - .main-card { - width: 450px; - } } @media (max-width: 400px) { - .top-cards { - width: 330px; - } + } @media (max-width: 320px) { - .top-cards { - width: 280px; - } - .main-card { - width: 330px; - margin:auto; - } } diff --git a/frontend/src/pages/vault/vaultLayout.css b/frontend/src/pages/vault/vaultLayout.css index bb17408d..9337ddf4 100644 --- a/frontend/src/pages/vault/vaultLayout.css +++ b/frontend/src/pages/vault/vaultLayout.css @@ -79,10 +79,16 @@ position: relative; width: 100%; } - +@media (min-width: 1024px) { + .sidebar { + width: 220px; + display: block; + } +} @media (max-width: 1024px) { .sidebar { width: 220px; + display: none; } .sidebar-title { diff --git a/frontend/src/services/contract.js b/frontend/src/services/contract.js index 68a5016a..afe866e1 100644 --- a/frontend/src/services/contract.js +++ b/frontend/src/services/contract.js @@ -1,6 +1,7 @@ import { connect } from 'get-starknet'; import { getDeployContractData } from '../utils/constants'; import { axiosInstance } from '../utils/axios'; +import { notify, ToastWithLink } from '../components/Notifier/Notifier'; export async function deployContract(walletId) { try { @@ -39,6 +40,7 @@ export async function checkAndDeployContract(walletId) { const result = await deployContract(walletId); const contractAddress = result.contractAddress; + notify(ToastWithLink("Contract Deployed Successfully", `https://starkscan.co/tx/${result.transactionHash}`, "Transaction ID"), "success") console.log('Contract address:', contractAddress); // Update the backend with transaction hash and wallet ID diff --git a/frontend/src/services/transaction.js b/frontend/src/services/transaction.js index 94983e21..ef24e166 100644 --- a/frontend/src/services/transaction.js +++ b/frontend/src/services/transaction.js @@ -4,6 +4,7 @@ import { erc20abi } from '../abis/erc20'; import { abi } from '../abis/abi'; import { axiosInstance } from '../utils/axios'; import {checkAndDeployContract} from './contract'; +import { notify, ToastWithLink } from '../components/Notifier/Notifier'; export async function sendTransaction(loopLiquidityData, contractAddress) { try { @@ -32,8 +33,11 @@ export async function sendTransaction(loopLiquidityData, contractAddress) { }; console.log(depositTransaction); let result = await starknet.account.execute([approveTransaction, depositTransaction]); + console.log('Resp: '); console.log(result); + notify(ToastWithLink("Transaction successfully sent", `https://starkscan.co/tx/${result.transaction_hash}`, "Transaction ID"), "success") + return { loopTransaction: result.transaction_hash, }; @@ -63,20 +67,21 @@ export async function closePosition(transactionData) { console.log(compiled); const starknet = await connect(); console.log(transactionData.contract_address); - await starknet.account.execute([ + let result = await starknet.account.execute([ { contractAddress: transactionData.contract_address, entrypoint: 'close_position', calldata: compiled }, ]); + notify(ToastWithLink("Close position successfully sent", `https://starkscan.co/tx/${result.transaction_hash}`, "Transaction ID"), "success") + } -export const handleTransaction = async (connectedWalletId, formData, setError, setTokenAmount, setLoading, setSuccessful) => { +export const handleTransaction = async (connectedWalletId, formData, setTokenAmount, setLoading, setSuccessful) => { setLoading(true); - setError(''); try{ await checkAndDeployContract(connectedWalletId); } catch (error) { console.error('Error deploying contract:', error); - setError('Error deploying contract. Please try again.'); + notify('Error deploying contract. Please try again.', 'error') setSuccessful(false) setLoading(false); return; @@ -98,7 +103,7 @@ export const handleTransaction = async (connectedWalletId, formData, setError, s setTokenAmount(''); } catch (err) { console.error('Failed to create position:', err); - setError('Failed to create position. Please try again.'); + notify(`Error sending transaction: ${err}`, 'error') setSuccessful(false) } finally { setLoading(false); diff --git a/frontend/src/utils/formatDate.js b/frontend/src/utils/formatDate.js new file mode 100644 index 00000000..f626f868 --- /dev/null +++ b/frontend/src/utils/formatDate.js @@ -0,0 +1,11 @@ +export function formatDate(timestamp) { + const date = new Date(timestamp); + return new Intl.DateTimeFormat('en-GB', { + day: '2-digit', + month: '2-digit', + year: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: true + }).format(date).replace(',', ' -'); +} \ No newline at end of file diff --git a/frontend/src/utils/notification.js b/frontend/src/utils/notification.js deleted file mode 100644 index 620a3856..00000000 --- a/frontend/src/utils/notification.js +++ /dev/null @@ -1,21 +0,0 @@ -import { notify } from 'components/Notifier/Notifier'; - -const defaultStyles = { - success: { backgroundColor: 'green', color: 'white' }, - error: { backgroundColor: 'red', color: 'white' }, - warning: { backgroundColor: 'orange', color: 'white' }, - info: { backgroundColor: 'blue', color: 'white' }, -}; - -export const showNotification = (message, type = 'info') => { - const style = defaultStyles[type] || defaultStyles.info; - - notify(message, { - style, - }); -}; - -export const notifySuccess = (message) => showNotification(message, 'success'); -export const notifyError = (message) => showNotification(message, 'error'); -export const notifyWarning = (message) => showNotification(message, 'warning'); -export const notifyInfo = (message) => showNotification(message, 'info'); diff --git a/frontend/test/__mocks__/styleMock.js b/frontend/test/__mocks__/styleMock.js new file mode 100644 index 00000000..a0995453 --- /dev/null +++ b/frontend/test/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; \ No newline at end of file diff --git a/frontend/test/services/transaction.test.js b/frontend/test/services/transaction.test.js index dcc4729c..25781c85 100644 --- a/frontend/test/services/transaction.test.js +++ b/frontend/test/services/transaction.test.js @@ -132,15 +132,13 @@ describe('Transaction Functions', () => { }); }); - describe('handleTransaction', () => { - const mockSetError = jest.fn(); + describe('handleTransaction', () => { const mockSetTokenAmount = jest.fn(); const mockSetLoading = jest.fn(); const mockSetSuccessful = jest.fn(); const mockFormData = { position_id: 1 }; - beforeEach(() => { - mockSetError.mockClear(); + beforeEach(() => { mockSetTokenAmount.mockClear(); mockSetLoading.mockClear(); mockSetSuccessful.mockClear(); @@ -162,7 +160,7 @@ describe('Transaction Functions', () => { data: { status: 'open' }, }); - await handleTransaction(mockWalletId, mockFormData, mockSetError, mockSetTokenAmount, mockSetLoading, mockSetSuccessful); + await handleTransaction(mockWalletId, mockFormData, mockSetTokenAmount, mockSetLoading, mockSetSuccessful); expect(mockSetLoading).toHaveBeenCalledWith(true); expect(axiosInstance.post).toHaveBeenCalledWith('/api/create-position', mockFormData); @@ -170,8 +168,7 @@ describe('Transaction Functions', () => { params: { position_id: mockTransactionData.position_id }, }); expect(mockSetTokenAmount).toHaveBeenCalledWith(''); - expect(mockSetLoading).toHaveBeenCalledWith(false); - expect(mockSetError).toHaveBeenCalledWith(''); + expect(mockSetLoading).toHaveBeenCalledWith(false); }); it('should handle create position error', async () => { @@ -180,9 +177,8 @@ describe('Transaction Functions', () => { console.error = jest.fn(); - await handleTransaction(mockWalletId, mockFormData, mockSetError, mockSetTokenAmount, mockSetLoading, mockSetSuccessful); - - expect(mockSetError).toHaveBeenCalledWith('Failed to create position. Please try again.'); + await handleTransaction(mockWalletId, mockFormData, mockSetTokenAmount, mockSetLoading, mockSetSuccessful); + expect(console.error).toHaveBeenCalledWith('Failed to create position:', mockError); expect(mockSetLoading).toHaveBeenCalledWith(false); expect(mockSetSuccessful).toHaveBeenCalledWith(false); diff --git a/scripts/migrate.sh b/scripts/migrate.sh new file mode 100755 index 00000000..c44248c1 --- /dev/null +++ b/scripts/migrate.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +docker-compose -f docker-compose.back.yaml up --build + +echo "Installing Poetry globally..." +curl -sSL https://install.python-poetry.org | python3 - + +echo "Installing all dependencies for \"data_handler\" with Poetry..." +poetry install + +echo "Activating ..." +poetry shell + +echo "Applying latest existing migrations..." +poetry run alembic -c web_app/alembic.ini upgrade head + +echo "Generating new migration..." +poetry run alembic -c web_app/alembic.ini revision --autogenerate -m "Migration" \ No newline at end of file diff --git a/src/deposit.cairo b/src/deposit.cairo index b53dbd80..eed7aae2 100644 --- a/src/deposit.cairo +++ b/src/deposit.cairo @@ -244,7 +244,7 @@ mod Deposit { let mut total_borrowed = 0; let mut accumulated = 0; let mut deposited = amount; - + while (amount + accumulated) * 10 / amount < multiplier.into() { let borrow_capacity = ((deposited * ZK_SCALE_DECIMALS diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 69e7b235..e488ac70 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -65,5 +65,12 @@ pub trait IAirdrop { pub trait IVault { fn store_liquidity(ref self: TContractState, amount: TokenAmount); fn withdraw_liquidity(ref self: TContractState, amount: TokenAmount); + fn add_deposit_contract(ref self: TContractState, deposit_contract: ContractAddress); + fn protect_position( + ref self: TContractState, + deposit_contract: ContractAddress, + user: ContractAddress, + amount: TokenAmount + ); } diff --git a/src/vault.cairo b/src/vault.cairo index 85641653..2c8fbfd3 100644 --- a/src/vault.cairo +++ b/src/vault.cairo @@ -5,7 +5,7 @@ mod Vault { use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use openzeppelin::upgrades::UpgradeableComponent; use openzeppelin::upgrades::interface::IUpgradeable; - use spotnet::interfaces::IVault; + use spotnet::interfaces::{IVault, IDepositDispatcher, IDepositDispatcherTrait}; use spotnet::types::{TokenAmount}; use starknet::ContractAddress; @@ -30,6 +30,7 @@ mod Vault { struct Storage { token: ContractAddress, amounts: Map, + activeContracts: Map, #[substorage(v0)] ownable: OwnableComponent::Storage, #[substorage(v0)] @@ -45,6 +46,8 @@ mod Vault { UpgradeableEvent: UpgradeableComponent::Event, LiquidityAdded: LiquidityAdded, LiquidityWithdrawn: LiquidityWithdrawn, + ContractAdded: ContractAdded, + PositionProtected: PositionProtected } #[derive(Drop, starknet::Event)] @@ -65,6 +68,27 @@ mod Vault { amount: TokenAmount, } + #[derive(Drop, starknet::Event)] + struct ContractAdded { + #[key] + token: ContractAddress, + #[key] + user: ContractAddress, + #[key] + deposit_contract: ContractAddress + } + + #[derive(Drop, starknet::Event)] + struct PositionProtected { + #[key] + token: ContractAddress, + #[key] + deposit_contract: ContractAddress, + #[key] + contract_owner: ContractAddress, + amount: TokenAmount, + } + #[constructor] fn constructor(ref self: ContractState, owner: ContractAddress, token: ContractAddress) { @@ -108,7 +132,7 @@ mod Vault { let user = get_caller_address(); let vault_contract = get_contract_address(); let current_amount = self.amounts.entry(user).read(); - + let token = self.token.read(); let token_dispatcher = IERC20Dispatcher { contract_address: token }; assert( @@ -116,16 +140,16 @@ mod Vault { 'Approved amount insufficient' ); assert(token_dispatcher.balance_of(user) >= amount, 'Insufficient balance'); - + // update new amount self.amounts.entry(user).write(current_amount + amount); - + // transfer token to vault token_dispatcher.transfer_from(user, vault_contract, amount); - + self.emit(LiquidityAdded { user, token, amount }); } - + /// Withdraws liquidity from the vault by transferring tokens back to the user. /// /// # Arguments @@ -147,14 +171,85 @@ mod Vault { let token = self.token.read(); let current_amount = self.amounts.entry(user).read(); assert(current_amount >= amount, 'Not enough tokens to withdraw'); - + // update new amount self.amounts.entry(user).write(current_amount - amount); - + // transfer token to user IERC20Dispatcher { contract_address: token }.transfer(user, amount); - + self.emit(LiquidityWithdrawn { user, token, amount }); } - } + + /// Add deposit contract to vault + /// + /// # Arguments + /// + /// * `deposit_contract` - The address of the deposit contract + /// + /// # Panics + /// + /// * When deposit contract is equal to zero + /// + /// # Events + /// + /// Emits a `ContractAdded` event with: + /// * `token` - The address of the token from storage + /// * `user` - The address of the user from storage + /// * `deposit_contract` - The address of the deposit contract + fn add_deposit_contract(ref self: ContractState, deposit_contract: ContractAddress) { + let user = get_caller_address(); + assert(deposit_contract.is_non_zero(), 'Deposit contract is zero'); + self.activeContracts.entry(user).write(deposit_contract); + self.emit(ContractAdded { token: self.token.read(), user, deposit_contract }); + } + + /// Makes a protect deposit into open zkLend position to control stability + /// + /// # Arguments + /// + /// * `deposit_contract` - The address of the deposit contract + /// * `user` - The address of the withdrawer + /// * `amount` - amount to withdraw + /// + /// # Panics + /// + /// * When caller don't equal to user or owner + /// * If the current amount is less than the amount to withdraw + /// * When deposit contract address is zero + /// * When user address is zero + /// + /// # Events + /// + /// Emits a `PositionProtected` event with: + /// * `token` - The address of the token from storage + /// * `deposit_contract` - The address of the deposit contract + /// * `contract_owner` - The address of contract owner + /// * `amount` - amount to withdraw + fn protect_position( + ref self: ContractState, + deposit_contract: ContractAddress, + user: ContractAddress, + amount: TokenAmount + ) { + let token = self.token.read(); + let caller = get_caller_address(); + let current_amount = self.amounts.entry(caller).read(); + + assert(deposit_contract.is_non_zero(), 'Deposit contract is zero'); + assert(user.is_non_zero(), 'User address is zero'); + assert(current_amount >= amount, 'Insufficient balance!'); + assert(self.ownable.owner() == caller || user == caller, 'Caller must be owner or user'); + + // update new amount + self.amounts.entry(user).write(current_amount - amount); + + // transfer token to user + IERC20Dispatcher { contract_address: token }.approve(deposit_contract, amount); + + IDepositDispatcher { contract_address: deposit_contract }.extra_deposit(token, amount); + + self.emit(PositionProtected { token, deposit_contract, contract_owner: user, amount }); + } + } } diff --git a/tests/constants.cairo b/tests/constants.cairo new file mode 100644 index 00000000..12f43e5a --- /dev/null +++ b/tests/constants.cairo @@ -0,0 +1,26 @@ +pub const HYPOTHETICAL_OWNER_ADDR: felt252 = + 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff; + +pub mod contracts { + pub const EKUBO_CORE_MAINNET: felt252 = + 0x00000005dd3d2f4429af886cd1a3b08289dbcea99a294197e9eb43b0e0325b4b; + + pub const ZKLEND_MARKET: felt252 = + 0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05; + + pub const PRAGMA_ADDRESS: felt252 = + 0x02a85bd616f912537c50a49a4076db02c00b29b2cdc8a197ce92ed1837fa875b; + + pub const TREASURY_ADDRESS: felt252 = 0x123; // Mock Address +} + +pub mod tokens { + pub const ETH: felt252 = 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; + pub const USDC: felt252 = 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8; +} + +pub mod pool_key { + pub const FEE: u128 = 170141183460469235273462165868118016; + pub const TICK_SPACING: u128 = 1000; + pub const EXTENSION: felt252 = 0; +} diff --git a/tests/lib.cairo b/tests/lib.cairo index 058544af..45cfb4e2 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -1,3 +1,4 @@ +mod constants; pub mod interfaces; #[cfg(test)] @@ -6,5 +7,6 @@ mod test_defispring; mod test_loop; #[cfg(test)] mod test_vault; +mod types; mod utils; diff --git a/tests/test_defispring.cairo b/tests/test_defispring.cairo index 11f940eb..a197e5ca 100644 --- a/tests/test_defispring.cairo +++ b/tests/test_defispring.cairo @@ -6,10 +6,10 @@ use spotnet::constants::STRK_ADDRESS; use spotnet::interfaces::{IDepositDispatcher, IDepositDispatcherTrait}; use spotnet::types::Claim; use starknet::ContractAddress; +use super::constants::HYPOTHETICAL_OWNER_ADDR; const ADDRESS_ELIGIBLE_FOR_ZKLEND_REWARDS: felt252 = 0x020281104e6cb5884dabcdf3be376cf4ff7b680741a7bb20e5e07c26cd4870af; -const HYPOTHETICAL_OWNER_ADDR: felt252 = 0x56789; #[test] #[fork("MAINNET_FIXED_BLOCK")] diff --git a/tests/test_loop.cairo b/tests/test_loop.cairo index 6f3aa1fd..3f121c06 100644 --- a/tests/test_loop.cairo +++ b/tests/test_loop.cairo @@ -1,14 +1,11 @@ use alexandria_math::fast_power::fast_power; use core::panic_with_felt252; -use ekubo::interfaces::core::{ICoreDispatcher, ICoreDispatcherTrait}; use ekubo::types::keys::{PoolKey}; use openzeppelin::access::ownable::interface::{ OwnableTwoStepABIDispatcherTrait, OwnableTwoStepABIDispatcher }; use openzeppelin::token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; -use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait}; -use pragma_lib::types::{AggregationMode, DataType, PragmaPricesResponse}; use snforge_std::cheatcodes::execution_info::account_contract_address::{ start_cheat_account_contract_address, stop_cheat_account_contract_address }; @@ -18,49 +15,17 @@ use snforge_std::cheatcodes::execution_info::block_timestamp::{ use snforge_std::cheatcodes::execution_info::caller_address::{ start_cheat_caller_address, stop_cheat_caller_address }; -use snforge_std::{declare, DeclareResultTrait, ContractClassTrait}; use spotnet::constants::ZK_SCALE_DECIMALS; use spotnet::interfaces::{ IDepositDispatcher, IDepositSafeDispatcher, IDepositSafeDispatcherTrait, IDepositDispatcherTrait }; -use spotnet::types::{DepositData, EkuboSlippageLimits}; +use spotnet::types::DepositData; use starknet::{ContractAddress, get_block_timestamp}; +use super::constants::{contracts, tokens, HYPOTHETICAL_OWNER_ADDR, pool_key}; use super::interfaces::{IMarketTestingDispatcher, IMarketTestingDispatcherTrait}; - -mod contracts { - pub const EKUBO_CORE_MAINNET: felt252 = - 0x00000005dd3d2f4429af886cd1a3b08289dbcea99a294197e9eb43b0e0325b4b; - - pub const ZKLEND_MARKET: felt252 = - 0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05; - - pub const PRAGMA_ADDRESS: felt252 = - 0x02a85bd616f912537c50a49a4076db02c00b29b2cdc8a197ce92ed1837fa875b; - - pub const TREASURY_ADDRESS: felt252 = 0x123; // Mock Address -} - -mod tokens { - pub const ETH: felt252 = 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7; - pub const USDC: felt252 = 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8; -} - -fn deploy_deposit_contract(user: ContractAddress) -> ContractAddress { - let deposit_contract = declare("Deposit").unwrap().contract_class(); - let (deposit_address, _) = deposit_contract - .deploy( - @array![ - user.try_into().unwrap(), - contracts::EKUBO_CORE_MAINNET, - contracts::ZKLEND_MARKET, - contracts::TREASURY_ADDRESS - ] - ) - .expect('Deploy failed'); - deposit_address -} +use super::utils::{deploy_deposit_contract, get_asset_price_pragma, get_slippage_limits}; fn get_deposit_dispatcher(user: ContractAddress) -> IDepositDispatcher { IDepositDispatcher { contract_address: deploy_deposit_contract(user) } @@ -70,45 +35,19 @@ fn get_safe_deposit_dispatcher(user: ContractAddress) -> IDepositSafeDispatcher IDepositSafeDispatcher { contract_address: deploy_deposit_contract(user) } } -fn get_asset_price_pragma(pair: felt252) -> u128 { - let oracle_dispatcher = IPragmaABIDispatcher { - contract_address: contracts::PRAGMA_ADDRESS.try_into().unwrap() - }; - let output: PragmaPricesResponse = oracle_dispatcher - .get_data(DataType::SpotEntry(pair), AggregationMode::Median(())); - output.price / 100 // Make 6 decimals wide instead of 8. -} - -fn get_slippage_limits(pool_key: PoolKey) -> EkuboSlippageLimits { - let ekubo_core = ICoreDispatcher { - contract_address: contracts::EKUBO_CORE_MAINNET.try_into().unwrap() - }; - let sqrt_ratio = ekubo_core.get_pool_price(pool_key).sqrt_ratio; - let tolerance = sqrt_ratio * 15 / 100; - EkuboSlippageLimits { lower: sqrt_ratio - tolerance, upper: sqrt_ratio + tolerance } -} - #[test] #[fork("MAINNET")] fn test_loop_eth_valid() { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let pool_price = get_asset_price_pragma('ETH/USD').into(); let token_disp = ERC20ABIDispatcher { contract_address: eth_addr }; @@ -134,23 +73,16 @@ fn test_loop_eth_valid() { #[feature("safe_dispatcher")] #[fork("MAINNET")] fn test_loop_eth_fuzz(amount: u64) { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); + let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let pool_price = get_asset_price_pragma('ETH/USD').into(); @@ -186,24 +118,16 @@ fn test_loop_eth_fuzz(amount: u64) { #[test] #[fork("MAINNET")] fn test_loop_usdc_valid() { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let token_disp = ERC20ABIDispatcher { contract_address: usdc_addr }; @@ -240,24 +164,16 @@ fn test_loop_usdc_valid() { #[should_panic(expected: 'Caller is not the owner')] #[fork("MAINNET")] fn test_loop_unauthorized() { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let decimals_sum_power: u128 = fast_power( @@ -291,24 +207,16 @@ fn test_loop_unauthorized() { #[should_panic(expected: 'Open position already exists')] #[fork("MAINNET")] fn test_loop_position_exists() { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let token_disp = ERC20ABIDispatcher { contract_address: usdc_addr }; @@ -355,23 +263,16 @@ fn test_loop_position_exists() { #[feature("safe_dispatcher")] #[fork("MAINNET")] fn test_loop_position_exists_fuzz(amount: u64) { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); + let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let pool_price = get_asset_price_pragma('ETH/USD').into(); @@ -415,24 +316,16 @@ fn test_loop_position_exists_fuzz(amount: u64) { #[test] #[fork("MAINNET")] fn test_close_position_usdc_valid_time_passed() { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let quote_token_price = get_asset_price_pragma('ETH/USD').into(); @@ -494,24 +387,16 @@ fn test_close_position_usdc_valid_time_passed() { #[test] #[fork("MAINNET")] fn test_close_position_amounts_cleared() { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let quote_token_price = get_asset_price_pragma('ETH/USD').into(); @@ -570,24 +455,16 @@ fn test_close_position_amounts_cleared() { #[test] #[fork("MAINNET")] fn test_close_position_partial_debt_utilization() { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let pool_price = get_asset_price_pragma('ETH/USD').into(); @@ -612,7 +489,10 @@ fn test_close_position_partial_debt_utilization() { deposit_disp .loop_liquidity( DepositData { - token: eth_addr, amount: 1000000000000000, multiplier: 40, borrow_portion_percent: 98 + token: eth_addr, + amount: 1000000000000000, + multiplier: 40, + borrow_portion_percent: 98 }, pool_key, get_slippage_limits(pool_key), @@ -649,24 +529,16 @@ fn test_close_position_partial_debt_utilization() { #[test] #[fork("MAINNET")] fn test_extra_deposit_valid() { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let quote_token_price = get_asset_price_pragma('ETH/USD').into(); @@ -722,24 +594,16 @@ fn test_extra_deposit_valid() { #[test] #[fork("MAINNET")] fn test_extra_deposit_supply_token_close_position_fuzz(extra_amount: u32) { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let quote_token_price = get_asset_price_pragma('ETH/USD').into(); @@ -814,24 +678,16 @@ fn test_extra_deposit_supply_token_close_position_fuzz(extra_amount: u32) { #[test] #[fork("MAINNET")] fn test_withdraw_valid_fuzz(amount: u32) { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user: ContractAddress = 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5 - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let quote_token_price = get_asset_price_pragma('ETH/USD').into(); @@ -927,13 +783,8 @@ fn test_withdraw_valid_fuzz(amount: u32) { #[should_panic(expected: 'Open position not exists')] #[fork("MAINNET")] fn test_extra_deposit_position_not_exists() { - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff - .try_into() - .unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); let token_disp = ERC20ABIDispatcher { contract_address: eth_addr }; let deposit_disp = get_deposit_dispatcher(user); @@ -948,23 +799,16 @@ fn test_extra_deposit_position_not_exists() { #[should_panic(expected: 'Deposit amount is zero')] #[fork("MAINNET")] fn test_extra_deposit_position_zero_amount() { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); + let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let pool_price = get_asset_price_pragma('ETH/USD').into(); @@ -995,23 +839,16 @@ fn test_extra_deposit_position_zero_amount() { #[test] #[fork("MAINNET")] fn test_withdraw_position_open() { - let usdc_addr: ContractAddress = - 0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 - .try_into() - .unwrap(); - let eth_addr: ContractAddress = - 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - .try_into() - .unwrap(); - let user: ContractAddress = 0x059a943ca214c10234b9a3b61c558ac20c005127d183b86a99a8f3c60a08b4ff - .try_into() - .unwrap(); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); + let pool_key = PoolKey { token0: eth_addr, token1: usdc_addr, - fee: 170141183460469235273462165868118016, - tick_spacing: 1000, - extension: 0.try_into().unwrap() + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() }; let pool_price = get_asset_price_pragma('ETH/USD').into(); diff --git a/tests/test_vault.cairo b/tests/test_vault.cairo index 14876d19..e57c7121 100644 --- a/tests/test_vault.cairo +++ b/tests/test_vault.cairo @@ -1,19 +1,26 @@ -use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use openzeppelin::token::erc20::interface::IERC20DispatcherTrait; +use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; use snforge_std::cheatcodes::execution_info::caller_address::{ start_cheat_caller_address, stop_cheat_caller_address }; -use snforge_std::{load}; -use spotnet::interfaces::{IVaultDispatcher, IVaultDispatcherTrait}; +use snforge_std::{load, map_entry_address}; +use spotnet::interfaces::{IVaultDispatcherTrait, IMarketDispatcher, IMarketDispatcherTrait}; -use starknet::{ContractAddress}; -use super::utils::{setup_test_suite, setup_user, assert_vault_amount}; +use starknet::ContractAddress; +use super::constants::{HYPOTHETICAL_OWNER_ADDR, tokens, contracts}; +use super::utils::{ + setup_test_suite, setup_user, assert_vault_amount, deploy_deposit_contract, setup_test_deposit, + deploy_erc20_mock +}; const MOCK_USER: felt252 = 0x1234; const MOCK_USER_2: felt252 = 0x5678; +const DEPOSIT_MOCK_USER: felt252 = + 0x0038925b0bcf4dce081042ca26a96300d9e181b910328db54a6c89e5451503f5; #[test] fn test_deploy() { - let suite = setup_test_suite(); + let suite = setup_test_suite(HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(), deploy_erc20_mock()); let token = load(suite.vault.contract_address, selector!("token"), 1,); assert!(*token[0] == suite.token.contract_address.try_into().unwrap(), "token not match"); @@ -21,7 +28,7 @@ fn test_deploy() { #[test] fn test_store_and_withdraw_liquidity_happy_path() { - let suite = setup_test_suite(); + let suite = setup_test_suite(HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(), deploy_erc20_mock()); let user1: ContractAddress = MOCK_USER.try_into().unwrap(); let amount: u256 = 100; @@ -46,7 +53,7 @@ fn test_store_and_withdraw_liquidity_happy_path() { #[test] fn test_store_and_withdraw_multiple_users() { - let suite = setup_test_suite(); + let suite = setup_test_suite(HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(), deploy_erc20_mock()); let user1: ContractAddress = MOCK_USER.try_into().unwrap(); let user2: ContractAddress = MOCK_USER_2.try_into().unwrap(); @@ -77,7 +84,7 @@ fn test_store_and_withdraw_multiple_users() { #[test] #[should_panic(expected: ('Approved amount insufficient',))] fn test_insufficient_allowance() { - let suite = setup_test_suite(); + let suite = setup_test_suite(HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(), deploy_erc20_mock()); let user1: ContractAddress = MOCK_USER.try_into().unwrap(); let user_amount: u256 = 100; let approved_amount: u256 = 50; // less than needed @@ -97,10 +104,168 @@ fn test_insufficient_allowance() { #[test] #[should_panic(expected: ('Not enough tokens to withdraw',))] fn test_insufficient_balance_withdraw() { - let suite = setup_test_suite(); + let suite = setup_test_suite(HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(), deploy_erc20_mock()); let user1: ContractAddress = MOCK_USER.try_into().unwrap(); start_cheat_caller_address(suite.vault.contract_address, user1); suite.vault.withdraw_liquidity(100); stop_cheat_caller_address(suite.vault.contract_address); } + +#[test] +fn test_add_deposit_contract() { + let suite = setup_test_suite(HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(), deploy_erc20_mock()); + let user: ContractAddress = MOCK_USER.try_into().unwrap(); + let deposit_address: ContractAddress = deploy_deposit_contract(user); + + start_cheat_caller_address(suite.vault.contract_address, user); + suite.vault.add_deposit_contract(deposit_address); + // Check activeContracts + let activeContracts_after_adding = load( + suite.vault.contract_address, + map_entry_address(selector!("activeContracts"), array![user.try_into().unwrap()].span()), + 1, + ); + + stop_cheat_caller_address(suite.vault.contract_address); + assert( + (*activeContracts_after_adding[0]).try_into().unwrap() == deposit_address, + 'Deposit contract mismatch' + ); +} + +#[test] +#[should_panic(expected: ('Deposit contract is zero',))] +fn test_add_deposit_contract_address_is_zero() { + let suite = setup_test_suite(HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(), deploy_erc20_mock()); + let user: ContractAddress = MOCK_USER.try_into().unwrap(); + let deposit_address: ContractAddress = 0.try_into().unwrap(); + + start_cheat_caller_address(suite.vault.contract_address, user); + suite.vault.add_deposit_contract(deposit_address); + stop_cheat_caller_address(suite.vault.contract_address); +} + +#[test] +#[fork("MAINNET")] +fn test_protect_position_with_owner() { + let user_amount: u256 = 685000000000000; + let withdrawn_amount: u256 = 1000000; + let user = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); + let token = tokens::ETH.try_into().unwrap(); + let mut suite = setup_test_suite(user, token); + let deposit_address = setup_test_deposit(ref suite, user, user_amount); + + start_cheat_caller_address(suite.vault.contract_address, user); + suite.vault.protect_position(deposit_address, user, withdrawn_amount); + stop_cheat_caller_address(suite.vault.contract_address); + + let expected_amount_vault: felt252 = (user_amount - withdrawn_amount).try_into().unwrap(); + assert_vault_amount(suite.vault.contract_address, user, expected_amount_vault); + + let z_token_address = IMarketDispatcher { + contract_address: contracts::ZKLEND_MARKET.try_into().unwrap() + } + .get_reserve_data(token) + .z_token_address; + let deposit_balance: u256 = ERC20ABIDispatcher { contract_address: z_token_address } + .balanceOf(deposit_address); + + println!("Deposit {}", deposit_balance); + + assert(deposit_balance >= user_amount, 'Deposit amount mismatch'); +} + +#[test] +#[fork("MAINNET")] +fn test_protect_position_with_user() { + let owner_amount: u256 = 685000000000000; + let user_amount: u256 = 675000000000000; + let withdrawn_amount: u256 = 1000000; + let owner = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); + let user = DEPOSIT_MOCK_USER.try_into().unwrap(); + let token = tokens::ETH.try_into().unwrap(); + + let mut suite_owner = setup_test_suite(owner, token); + let _deposit_address_owner = setup_test_deposit(ref suite_owner, owner, owner_amount); + let mut suite_user = setup_test_suite(user, token); + let deposit_address_user = setup_test_deposit(ref suite_user, user, user_amount); + + start_cheat_caller_address(suite_owner.vault.contract_address, owner); + suite_owner.vault.protect_position(deposit_address_user, owner, withdrawn_amount); + stop_cheat_caller_address(suite_owner.vault.contract_address); + + let expected_amount_owner: felt252 = (owner_amount - withdrawn_amount).try_into().unwrap(); + assert_vault_amount(suite_owner.vault.contract_address, owner, expected_amount_owner); + + let z_token_address = IMarketDispatcher { + contract_address: contracts::ZKLEND_MARKET.try_into().unwrap() + } + .get_reserve_data(token) + .z_token_address; + let deposit_balance: u256 = ERC20ABIDispatcher { contract_address: z_token_address } + .balanceOf(deposit_address_user); + + println!("Deposit {}", deposit_balance); + + assert(deposit_balance >= user_amount, 'Deposit amount mismatch'); +} + +#[test] +#[fork("MAINNET")] +#[should_panic(expected: ('Caller must be owner or user',))] +fn test_protect_position_panic_on_caller() { + let caller_amount: u256 = 685000000000000; + let withdrawn_amount: u256 = 1000000; + let caller = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); + let owner = MOCK_USER.try_into().unwrap(); + let user2 = MOCK_USER_2.try_into().unwrap(); + let mut suite = setup_test_suite(owner, deploy_erc20_mock()); + setup_user(@suite, caller, caller_amount); + let deposit_address = setup_test_deposit(ref suite, caller, caller_amount); + + start_cheat_caller_address(suite.vault.contract_address, caller); + suite.vault.protect_position(deposit_address, user2, withdrawn_amount); + stop_cheat_caller_address(suite.vault.contract_address); +} + +#[test] +#[fork("MAINNET")] +#[should_panic(expected: ('Insufficient balance!',))] +fn test_protect_position_insufficient_balance() { + let user_amount: u256 = 685000000000000; + let user = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); + let mut suite = setup_test_suite(user, tokens::ETH.try_into().unwrap()); + let deposit_address = setup_test_deposit(ref suite, user, user_amount); + + start_cheat_caller_address(suite.vault.contract_address, user); + suite.vault.protect_position(deposit_address, user, user_amount + 10000); + stop_cheat_caller_address(suite.vault.contract_address); +} + +#[test] +#[should_panic(expected: ('Deposit contract is zero',))] +fn test_protect_position_deposit_contract_is_zero() { + let user_amount: u256 = 685000000000000; + let user: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); + let suite = setup_test_suite(user, deploy_erc20_mock()); + let deposit_address: ContractAddress = 0.try_into().unwrap(); + + start_cheat_caller_address(suite.vault.contract_address, user); + suite.vault.protect_position(deposit_address, user, user_amount); + stop_cheat_caller_address(suite.vault.contract_address); +} + + +#[test] +#[should_panic(expected: ('User address is zero',))] +fn test_protect_position_user_address_is_zero() { + let suite = setup_test_suite(HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(), deploy_erc20_mock()); + let user_amount: u256 = 685000000000000; + let user: ContractAddress = 0.try_into().unwrap(); + let deposit_address: ContractAddress = tokens::ETH.try_into().unwrap(); + + start_cheat_caller_address(suite.vault.contract_address, user); + suite.vault.protect_position(deposit_address, user, user_amount); + stop_cheat_caller_address(suite.vault.contract_address); +} diff --git a/tests/types.cairo b/tests/types.cairo new file mode 100644 index 00000000..3bf3953d --- /dev/null +++ b/tests/types.cairo @@ -0,0 +1,10 @@ +use openzeppelin::token::erc20::interface::IERC20Dispatcher; +use spotnet::interfaces::IVaultDispatcher; +use starknet::ContractAddress; + +#[derive(Drop)] +pub struct VaultTestSuite { + pub vault: IVaultDispatcher, + pub token: IERC20Dispatcher, + pub owner: ContractAddress, +} diff --git a/tests/utils.cairo b/tests/utils.cairo index bcfeb76d..73c8044a 100644 --- a/tests/utils.cairo +++ b/tests/utils.cairo @@ -1,18 +1,45 @@ use alexandria_math::fast_power::fast_power; +use ekubo::interfaces::core::{ICoreDispatcher, ICoreDispatcherTrait}; +use ekubo::types::keys::PoolKey; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait}; +use pragma_lib::types::{AggregationMode, DataType, PragmaPricesResponse}; +use snforge_std::cheatcodes::execution_info::account_contract_address::{ + start_cheat_account_contract_address, stop_cheat_account_contract_address +}; use snforge_std::cheatcodes::execution_info::caller_address::{ start_cheat_caller_address, stop_cheat_caller_address }; use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, load, map_entry_address}; -use spotnet::interfaces::{IVaultDispatcher, IVaultDispatcherTrait}; +use spotnet::interfaces::IVaultDispatcher; +use spotnet::interfaces::{IVaultDispatcherTrait, IDepositDispatcherTrait, IDepositDispatcher}; +use spotnet::types::{DepositData, EkuboSlippageLimits}; use starknet::{ContractAddress, contract_address_const, get_contract_address}; -const HYPOTHETICAL_OWNER_ADDR: felt252 = 0x56789; - +use super::constants::{contracts, tokens, pool_key}; +use super::types::VaultTestSuite; pub fn ERC20_MOCK_CONTRACT() -> ContractAddress { contract_address_const::<'erc20mock'>() } +pub fn get_asset_price_pragma(pair: felt252) -> u128 { + let oracle_dispatcher = IPragmaABIDispatcher { + contract_address: contracts::PRAGMA_ADDRESS.try_into().unwrap() + }; + let output: PragmaPricesResponse = oracle_dispatcher + .get_data(DataType::SpotEntry(pair), AggregationMode::Median(())); + output.price / 100 // Make 6 decimals wide instead of 8. +} + +pub fn get_slippage_limits(pool_key: PoolKey) -> EkuboSlippageLimits { + let ekubo_core = ICoreDispatcher { + contract_address: contracts::EKUBO_CORE_MAINNET.try_into().unwrap() + }; + let sqrt_ratio = ekubo_core.get_pool_price(pool_key).sqrt_ratio; + let tolerance = sqrt_ratio * 15 / 100; + EkuboSlippageLimits { lower: sqrt_ratio - tolerance, upper: sqrt_ratio + tolerance } +} + pub fn deploy_erc20_mock() -> ContractAddress { let contract = declare("SnakeERC20Mock").unwrap().contract_class(); let name: ByteArray = "erc20 mock"; @@ -31,18 +58,8 @@ pub fn deploy_erc20_mock() -> ContractAddress { contract_addr } - -#[derive(Drop)] -pub struct VaultTestSuite { - pub vault: IVaultDispatcher, - pub token: IERC20Dispatcher, - pub owner: ContractAddress, -} - -pub fn setup_test_suite() -> VaultTestSuite { - let owner: ContractAddress = HYPOTHETICAL_OWNER_ADDR.try_into().unwrap(); +pub fn setup_test_suite(owner: ContractAddress, token_address: ContractAddress) -> VaultTestSuite { let contract = declare("Vault").unwrap().contract_class(); - let token_address: ContractAddress = deploy_erc20_mock(); let (vault_address, _) = contract .deploy(@array![owner.try_into().unwrap(), token_address.try_into().unwrap()]) @@ -73,3 +90,60 @@ pub fn assert_vault_amount( ); assert(*balance_after_withdraw[0] == expected_amount, 'balance mismatch'); } + +pub fn deploy_deposit_contract(user: ContractAddress) -> ContractAddress { + let deposit_contract = declare("Deposit").unwrap().contract_class(); + let (deposit_address, _) = deposit_contract + .deploy( + @array![ + user.try_into().unwrap(), + contracts::EKUBO_CORE_MAINNET, + contracts::ZKLEND_MARKET, + contracts::TREASURY_ADDRESS + ] + ) + .expect('Deploy failed'); + deposit_address +} + +pub fn setup_test_deposit( + ref suite: VaultTestSuite, user: ContractAddress, amount: u256 +) -> ContractAddress { + let deposit_address: ContractAddress = deploy_deposit_contract(user); + let usdc_addr: ContractAddress = tokens::USDC.try_into().unwrap(); + let eth_addr: ContractAddress = tokens::ETH.try_into().unwrap(); + + let pool_key = PoolKey { + token0: eth_addr, + token1: usdc_addr, + fee: pool_key::FEE, + tick_spacing: pool_key::TICK_SPACING, + extension: pool_key::EXTENSION.try_into().unwrap() + }; + + let pool_price = get_asset_price_pragma('ETH/USD').into(); + let deposit_data = DepositData { + token: eth_addr, amount: amount, multiplier: 40, borrow_portion_percent: 98 + }; + let ekubo_limits = get_slippage_limits(pool_key); + + start_cheat_caller_address(deposit_data.token, user); + IERC20Dispatcher { contract_address: deposit_data.token } + .approve(suite.vault.contract_address, amount); + stop_cheat_caller_address(deposit_data.token); + + start_cheat_caller_address(suite.vault.contract_address, user); + suite.vault.store_liquidity(amount); + stop_cheat_caller_address(suite.vault.contract_address); + + start_cheat_caller_address(deposit_data.token, user); + IERC20Dispatcher { contract_address: deposit_data.token }.approve(deposit_address, amount); + stop_cheat_caller_address(deposit_data.token); + + start_cheat_account_contract_address(deposit_address, user); + IDepositDispatcher { contract_address: deposit_address } + .loop_liquidity(deposit_data, pool_key, ekubo_limits, pool_price); + stop_cheat_account_contract_address(deposit_address); + + deposit_address +} diff --git a/web_app/api/dashboard.py b/web_app/api/dashboard.py index 3d5c31d0..85ec8419 100644 --- a/web_app/api/dashboard.py +++ b/web_app/api/dashboard.py @@ -3,12 +3,13 @@ """ import collections +from decimal import Decimal, DivisionByZero from fastapi import APIRouter + from web_app.api.serializers.dashboard import DashboardResponse from web_app.contract_tools.mixins import DashboardMixin, HealthRatioMixin from web_app.db.crud import PositionDBConnector -from decimal import Decimal, DivisionByZero router = APIRouter() position_db_connector = PositionDBConnector() @@ -40,18 +41,21 @@ async def get_dashboard(wallet_id: str) -> DashboardResponse: wallet_id ) default_dashboard_response = DashboardResponse( - health_ratio="0", - multipliers={}, - start_dates={}, - current_sum=0, - start_sum=0, - borrowed="0", - balance="0", - ) + health_ratio="0", + multipliers={}, + start_dates={}, + current_sum=0, + start_sum=0, + borrowed="0", + balance="0", + ) if not contract_address: return default_dashboard_response - opened_positions = position_db_connector.get_positions_by_wallet_id(wallet_id) + # Fetching first 10 positions at the moment + opened_positions = position_db_connector.get_positions_by_wallet_id( + wallet_id, 0, 10 + ) # At the moment, we only support one position per wallet first_opened_position = ( diff --git a/web_app/api/position.py b/web_app/api/position.py index 88e52dd5..fd291446 100644 --- a/web_app/api/position.py +++ b/web_app/api/position.py @@ -2,27 +2,29 @@ This module handles position-related API endpoints. """ +from typing import Optional + from fastapi import APIRouter, HTTPException, Request -from web_app.api.serializers.transaction import ( - LoopLiquidityData, - RepayTransactionDataResponse, -) -from web_app.contract_tools.constants import ( - TokenParams, - TokenMultipliers, -) from web_app.api.serializers.position import ( + PositionFormData, TokenMultiplierResponse, UserPositionResponse, - PositionFormData, ) -from web_app.contract_tools.mixins import DepositMixin, DashboardMixin, PositionMixin +from web_app.api.serializers.transaction import ( + LoopLiquidityData, + RepayTransactionDataResponse, +) +from web_app.contract_tools.constants import TokenMultipliers, TokenParams +from web_app.contract_tools.mixins import DashboardMixin, DepositMixin, PositionMixin from web_app.db.crud import PositionDBConnector router = APIRouter() # Initialize the router position_db_connector = PositionDBConnector() # Initialize the PositionDBConnector +# Constants +PAGINATION_STEP = 10 + @router.get( "/api/get-multipliers", @@ -92,7 +94,7 @@ async def create_position_with_transaction_data( position_db_connector.get_contract_address_by_wallet_id(form_data.wallet_id) ) deposit_data["position_id"] = str(position.id) - + return LoopLiquidityData(**deposit_data) @@ -117,14 +119,18 @@ async def get_repay_data( if not wallet_id: raise HTTPException(status_code=404, detail="Wallet not found") - contract_address, position_id, token_symbol = position_db_connector.get_repay_data(wallet_id) - is_opened_position = await PositionMixin.is_opened_position(contract_address) + contract_address, position_id, token_symbol = position_db_connector.get_repay_data( + wallet_id + ) + is_opened_position = await PositionMixin.is_opened_position(contract_address) if not is_opened_position: raise HTTPException(status_code=400, detail="Position was closed") if not position_id: raise HTTPException(status_code=404, detail="Position not found or closed") - repay_data = await DepositMixin.get_repay_data(token_symbol, request.app.state.ekubo_contract) + repay_data = await DepositMixin.get_repay_data( + token_symbol, request.app.state.ekubo_contract + ) repay_data["contract_address"] = contract_address repay_data["position_id"] = str(position_id) return repay_data @@ -179,10 +185,7 @@ async def open_position(position_id: str) -> str: summary="Add extra deposit to a user position", response_description="Returns the result of extra deposit", ) -async def add_extra_deposit( - position_id: int, - amount: str -): +async def add_extra_deposit(position_id: int, amount: str): """ This endpoint adds extra deposit to a user position. @@ -193,10 +196,10 @@ async def add_extra_deposit( if not position_id: raise HTTPException(status_code=404, detail="Position ID is required") - + if not amount: raise HTTPException(status_code=404, detail="Amount is required") - + position = position_db_connector.get_position_by_id(position_id) if not position: @@ -207,23 +210,27 @@ async def add_extra_deposit( return {"detail": "Successfully added extra deposit"} - @router.get( "/api/user-positions/{wallet_id}", tags=["Position Operations"], response_model=list[UserPositionResponse], summary="Get all positions for a user", - response_description="Returns list of all positions for the given wallet ID", + response_description="Returns paginated list of positions for the given wallet ID", ) -async def get_user_positions(wallet_id: str) -> list: +async def get_user_positions(wallet_id: str, start: Optional[int] = None) -> list: """ Get all positions for a specific user by their wallet ID. :param wallet_id: The wallet ID of the user - :return: UserPositionsListResponse containing list of positions + :param start: Optional starting index for pagination (0-based). If not provided, defaults to 0 + :return: UserPositionsListResponse containing paginated list of positions :raises: HTTPException: If wallet ID is empty or invalid """ if not wallet_id: raise HTTPException(status_code=400, detail="Wallet ID is required") - - positions = position_db_connector.get_positions_by_wallet_id(wallet_id) + + start_index = max(0, start) if start is not None else 0 + + positions = position_db_connector.get_positions_by_wallet_id( + wallet_id, start_index, PAGINATION_STEP + ) return positions diff --git a/web_app/db/crud/position.py b/web_app/db/crud/position.py index 4722252e..1e042f01 100644 --- a/web_app/db/crud/position.py +++ b/web_app/db/crud/position.py @@ -11,8 +11,9 @@ from sqlalchemy import Numeric, cast, func from sqlalchemy.exc import SQLAlchemyError +from web_app.db.models import Base, Position, Status, Transaction, User + from .user import UserDBConnector -from web_app.db.models import Base, Position, Status, User, Transaction logger = logging.getLogger(__name__) ModelType = TypeVar("ModelType", bound=Base) @@ -59,11 +60,15 @@ def _get_user_by_wallet_id(self, wallet_id: str) -> User | None: """ return self.get_user_by_wallet_id(wallet_id) - def get_positions_by_wallet_id(self, wallet_id: str) -> list: + def get_positions_by_wallet_id( + self, wallet_id: str, start: int, limit: int + ) -> list: """ - Retrieves all positions for a user by their wallet ID + Retrieves paginated positions for a user by their wallet ID and returns them as a list of dictionaries. :param wallet_id: str + :param start: starting index for pagination + :param limit: number of records to return :return: list of dict """ with self.Session() as db: @@ -78,6 +83,8 @@ def get_positions_by_wallet_id(self, wallet_id: str) -> list: Position.user_id == user.id, Position.status == Status.OPENED.value, ) + .offset(start) + .limit(limit) .all() ) # Convert positions to a list of dictionaries @@ -173,7 +180,7 @@ def get_position_id_by_wallet_id(self, wallet_id: str) -> str | None: :param wallet_id: wallet ID :return: Position ID """ - position = self.get_positions_by_wallet_id(wallet_id) + position = self.get_positions_by_wallet_id(wallet_id, 0, 1) if position: return position[0]["id"] return None @@ -236,9 +243,7 @@ def get_repay_data(self, wallet_id: str) -> tuple: """ with self.Session() as db: result = ( - db.query( - User.contract_address, Position.id, Position.token_symbol - ) + db.query(User.contract_address, Position.id, Position.token_symbol) .join(Position, Position.user_id == User.id) .filter(User.wallet_id == wallet_id) .first() @@ -288,19 +293,16 @@ def save_current_price(self, position: Position, price_dict: dict) -> None: logger.error(f"Error while saving current_price for position: {e}") def save_transaction( - self, - position_id: uuid.UUID, - status: str, - transaction_hash: str + self, position_id: uuid.UUID, status: str, transaction_hash: str ) -> bool: """ Creates a new transaction record associated with a position. - + Args: position_id: UUID of the position status: Transaction status (opened/closed) transaction_hash: Blockchain transaction hash - + Returns: Transaction object if successful, None if failed """ @@ -308,7 +310,7 @@ def save_transaction( transaction = Transaction( position_id=position_id, status=status, - transaction_hash=transaction_hash + transaction_hash=transaction_hash, ) return self.write_to_db(transaction) except SQLAlchemyError as e: diff --git a/web_app/test_integration/test_close_position.py b/web_app/test_integration/test_close_position.py index 30decfbd..177e8884 100644 --- a/web_app/test_integration/test_close_position.py +++ b/web_app/test_integration/test_close_position.py @@ -7,8 +7,9 @@ from typing import Any, Dict import pytest + from web_app.contract_tools.mixins.dashboard import DashboardMixin -from web_app.db.crud import PositionDBConnector, UserDBConnector, AirDropDBConnector +from web_app.db.crud import AirDropDBConnector, PositionDBConnector, UserDBConnector from web_app.db.models import Status user_db = UserDBConnector() @@ -106,5 +107,5 @@ def test_close_position(self, form_data: Dict[str, Any]) -> None: user = position_db.get_user_by_wallet_id(wallet_id) airdrop.delete_all_users_airdrop(user.id) position_db.delete_position(position) - if not position_db.get_positions_by_wallet_id(wallet_id): + if not position_db.get_positions_by_wallet_id(wallet_id, 0, 1): position_db.delete_user_by_wallet_id(wallet_id) diff --git a/web_app/tests/db/test_PositionDBConnector.py b/web_app/tests/db/test_PositionDBConnector.py index 73f6345c..f43e8611 100644 --- a/web_app/tests/db/test_PositionDBConnector.py +++ b/web_app/tests/db/test_PositionDBConnector.py @@ -72,7 +72,9 @@ def test_get_positions_by_wallet_id_success( mock_position_db_connector.get_positions_by_wallet_id.return_value = [position_dict] - positions = mock_position_db_connector.get_positions_by_wallet_id("test_wallet_id") + positions = mock_position_db_connector.get_positions_by_wallet_id( + "test_wallet_id", 0, 1 + ) assert len(positions) == 1 assert positions[0]["id"] == str(sample_position.id) @@ -272,7 +274,7 @@ def test_get_positions_by_wallet_id_no_user(mock_position_db_connector): mock_position_db_connector.get_positions_by_wallet_id.return_value = [] positions = mock_position_db_connector.get_positions_by_wallet_id( - "nonexistent_wallet" + "nonexistent_wallet", 0, 1 ) assert positions == [] @@ -282,7 +284,9 @@ def test_get_positions_by_wallet_id_db_error(mock_position_db_connector, sample_ """Test handling database error when retrieving positions.""" mock_position_db_connector._get_user_by_wallet_id.return_value = sample_user - positions = mock_position_db_connector.get_positions_by_wallet_id("test_wallet_id") + positions = mock_position_db_connector.get_positions_by_wallet_id( + "test_wallet_id", 0, 1 + ) assert positions == [] @@ -328,7 +332,9 @@ def test_get_position_id_by_wallet_id_no_positions( mock_position_db_connector.get_positions_by_wallet_id.return_value = [] mock_position_db_connector.get_position_id_by_wallet_id.return_value = None - result = mock_position_db_connector.get_position_id_by_wallet_id("test_wallet_id") + result = mock_position_db_connector.get_position_id_by_wallet_id( + "test_wallet_id", 0, 1 + ) assert result is None