diff --git a/next.config.js b/next.config.js index 4a208ebb4..4ab2cd00d 100644 --- a/next.config.js +++ b/next.config.js @@ -9,22 +9,6 @@ const nextConfig = { 'xdefi-static.s3.eu-west-1.amazonaws.com', ], }, - async redirects() { - return [ - { - source: '/((?!_next|mobile).*)', - has: [ - { - type: 'header', - key: 'User-Agent', - value: '.*(Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop).*', - }, - ], - permanent: true, - destination: '/mobile', - }, - ] - }, async headers() { return [ { diff --git a/package.json b/package.json index ae1e23adb..0d346def7 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,11 @@ "dependencies": { "@cosmjs/cosmwasm-stargate": "^0.32.2", "@delphi-labs/shuttle-react": "^3.19.1", - "@keplr-wallet/cosmos": "^0.12.67", + "@keplr-wallet/cosmos": "^0.12.70", "@splinetool/react-spline": "^2.2.6", - "@splinetool/runtime": "^1.0.52", + "@splinetool/runtime": "^1.0.55", "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-table": "^8.11.8", + "@tanstack/react-table": "^8.13.2", "@tippyjs/react": "^4.2.6", "bignumber.js": "^9.1.2", "classnames": "^2.5.1", @@ -42,31 +42,31 @@ "react-draggable": "^4.4.6", "react-helmet-async": "^2.0.4", "react-qr-code": "^2.0.12", - "react-router-dom": "^6.22.0", + "react-router-dom": "^6.22.2", "react-spring": "^9.7.3", "react-toastify": "^10.0.4", "react-use-clipboard": "^1.0.9", - "recharts": "^2.12.0", - "swr": "^2.2.4", + "recharts": "^2.12.1", + "swr": "^2.2.5", "tailwind-scrollbar-hide": "^1.1.7", - "zustand": "^4.5.0" + "zustand": "^4.5.1" }, "devDependencies": { "@svgr/webpack": "^8.1.0", "@types/debounce-promise": "^3.1.9", "@types/lodash.debounce": "^4.0.9", "@types/lodash.throttle": "^4.1.9", - "@types/node": "^20.11.17", - "@types/react": "18.2.55", + "@types/node": "^20.11.24", + "@types/react": "18.2.61", "@types/react-dom": "18.2.19", "@types/react-helmet": "^6.1.11", "autoprefixer": "^10.4.17", - "dotenv": "^16.4.3", + "dotenv": "^16.4.5", "dotenv-cli": "^7.3.0", - "eslint": "^8.56.0", + "eslint": "^8.57.0", "eslint-config-next": "^14.1.0", "eslint-plugin-import": "^2.29.1", - "husky": "^9.0.10", + "husky": "^9.0.11", "identity-obj-proxy": "^3.0.0", "lint-staged": "^15.2.2", "prettier": "^3.2.5", diff --git a/public/images/bg-v1.svg b/public/images/bg-v1.svg new file mode 100644 index 000000000..c15fa8f94 --- /dev/null +++ b/public/images/bg-v1.svg @@ -0,0 +1,63 @@ + + + + + + + + + diff --git a/public/images/tokens/stkatom.svg b/public/images/tokens/stkatom.svg new file mode 100644 index 000000000..c06792697 --- /dev/null +++ b/public/images/tokens/stkatom.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + diff --git a/public/images/tokens/wsteth.svg b/public/images/tokens/wsteth.svg new file mode 100644 index 000000000..656b02448 --- /dev/null +++ b/public/images/tokens/wsteth.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/public/tradingview.css b/public/tradingview.css index c0031ccc6..8ba9b612a 100644 --- a/public/tradingview.css +++ b/public/tradingview.css @@ -19,6 +19,9 @@ --tv-color-toolbar-button-background-expanded: var(--tv-background); --tv-color-toolbar-button-background-active: var(--tv-background); --tv-color-toolbar-button-background-active-hover: var(--tv-background); + --tv-horizontal-scroll-fade-color: var(--tv-background); + --tv-vertical-scroll-fade-color: var(--tv-background); + --tv-color-popup-background: var(--tv-background); --tv-color-toolbar-toggle-button-background-active: rgba(255, 255, 255, 0.2); --tv-color-toolbar-toggle-button-background-active-hover: rgba(255, 255, 255, 0.2); --tv-color-toolbar-divider-background: var(--tv-menu-text); @@ -318,3 +321,16 @@ button[class^='button-'][class*='secondary-']:hover { [class^='tabContent-']::-webkit-scrollbar-track { border-radius: 2px; } + +[class^='fadeTop-'] { + background-image: linear-gradient(180deg, var(--tv-background), transparent) !important; +} + +[class^='fadeBot-'] { + background-image: linear-gradient(0deg, var(--tv-background), transparent) !important; +} + +html.theme-dark ::selection { + background: var(--tv-background) !important; + color: var(--tv-menu-text-hover) !important; +} diff --git a/sentry.client.config.js b/sentry.client.config.js deleted file mode 100644 index dc6b933a7..000000000 --- a/sentry.client.config.js +++ /dev/null @@ -1,19 +0,0 @@ -// This file configures the initialization of Sentry on the browser. -// The config you add here will be used whenever a page is visited. -// https://docs.sentry.io/platforms/javascript/guides/nextjs/ - -import * as Sentry from '@sentry/nextjs' - -const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN - -Sentry.init({ - dsn: SENTRY_DSN, - environment: process.env.NEXT_PUBLIC_SENTRY_ENV, - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: 0.5, - enabled: process.env.NODE_ENV !== 'development', - // ... - // Note: if you want to override the automatic release value, do not set a - // `release` value here - use the environment variable `SENTRY_RELEASE`, so - // that it will also get attached to your source maps -}) diff --git a/sentry.properties b/sentry.properties deleted file mode 100644 index 46f155aac..000000000 --- a/sentry.properties +++ /dev/null @@ -1,4 +0,0 @@ -defaults.url=https://sentry.io/ -defaults.org=delphi-mars -defaults.project=mars-v2 -cli.executable=../../../.npm/_npx/a8388072043b4cbc/node_modules/@sentry/cli/bin/sentry-cli diff --git a/sentry.server.config.js b/sentry.server.config.js deleted file mode 100644 index 9f9ce8ab2..000000000 --- a/sentry.server.config.js +++ /dev/null @@ -1,19 +0,0 @@ -// This file configures the initialization of Sentry on the server. -// The config you add here will be used whenever the server handles a request. -// https://docs.sentry.io/platforms/javascript/guides/nextjs/ - -import * as Sentry from '@sentry/nextjs' - -const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN - -Sentry.init({ - dsn: SENTRY_DSN, - environment: process.env.NEXT_PUBLIC_SENTRY_ENV, - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: 0.5, - enabled: process.env.NODE_ENV !== 'development', - // ... - // Note: if you want to override the automatic release value, do not set a - // `release` value here - use the environment variable `SENTRY_RELEASE`, so - // that it will also get attached to your source maps -}) diff --git a/src/api/cache.ts b/src/api/cache.ts index 3ce93c94d..9dc08d1c8 100644 --- a/src/api/cache.ts +++ b/src/api/cache.ts @@ -10,7 +10,10 @@ import { TotalDepositResponse, VaultConfigBaseForAddr, } from 'types/generated/mars-params/MarsParams.types' -import { ArrayOfMarket } from 'types/generated/mars-red-bank/MarsRedBank.types' +import { + ArrayOfMarket, + ArrayOfUserDebtResponse, +} from 'types/generated/mars-red-bank/MarsRedBank.types' interface Cache extends Map {} @@ -62,3 +65,4 @@ export const underlyingDebtCache: Cache = new Map() export const previewDepositCache: Cache<{ vaultAddress: string; amount: string }> = new Map() export const stakingAprCache: Cache = new Map() export const assetParamsCache: Cache = new Map() +export const userDebtCache: Cache = new Map() diff --git a/src/api/cosmwasm-client.ts b/src/api/cosmwasm-client.ts index 357b61671..c391367ce 100644 --- a/src/api/cosmwasm-client.ts +++ b/src/api/cosmwasm-client.ts @@ -6,7 +6,9 @@ import { MarsMockVaultQueryClient } from 'types/generated/mars-mock-vault/MarsMo import { MarsOracleOsmosisQueryClient } from 'types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client' import { MarsParamsQueryClient } from 'types/generated/mars-params/MarsParams.client' import { MarsPerpsQueryClient } from 'types/generated/mars-perps/MarsPerps.client' +import { MarsRedBankQueryClient } from 'types/generated/mars-red-bank/MarsRedBank.client' import { MarsSwapperOsmosisQueryClient } from 'types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.client' +import { getUrl } from 'utils/url' let _cosmWasmClient: Map = new Map() let _creditManagerQueryClient: Map = new Map() @@ -15,6 +17,7 @@ let _paramsQueryClient: Map = new Map() let _incentivesQueryClient: Map = new Map() let _swapperOsmosisClient: Map = new Map() let _perpsClient: Map = new Map() +let _redBankQueryClient: Map = new Map() const getClient = async (rpc: string) => { try { @@ -32,7 +35,7 @@ const getClient = async (rpc: string) => { const getCreditManagerQueryClient = async (chainConfig: ChainConfig) => { try { const contract = chainConfig.contracts.creditManager - const rpc = chainConfig.endpoints.rpc + const rpc = getUrl(chainConfig.endpoints.rpc) const key = rpc + contract if (!_creditManagerQueryClient.get(key)) { @@ -49,7 +52,7 @@ const getCreditManagerQueryClient = async (chainConfig: ChainConfig) => { const getParamsQueryClient = async (chainConfig: ChainConfig) => { try { const contract = chainConfig.contracts.params - const rpc = chainConfig.endpoints.rpc + const rpc = getUrl(chainConfig.endpoints.rpc) const key = rpc + contract if (!_paramsQueryClient.get(key)) { @@ -66,7 +69,7 @@ const getParamsQueryClient = async (chainConfig: ChainConfig) => { const getOracleQueryClient = async (chainConfig: ChainConfig) => { try { const contract = chainConfig.contracts.oracle - const rpc = chainConfig.endpoints.rpc + const rpc = getUrl(chainConfig.endpoints.rpc) const key = rpc + contract if (!_oracleQueryClient.get(key)) { @@ -82,7 +85,7 @@ const getOracleQueryClient = async (chainConfig: ChainConfig) => { const getVaultQueryClient = async (chainConfig: ChainConfig, address: string) => { try { - const client = await getClient(chainConfig.endpoints.rpc) + const client = await getClient(getUrl(chainConfig.endpoints.rpc)) return new MarsMockVaultQueryClient(client, address) } catch (error) { throw error @@ -92,7 +95,7 @@ const getVaultQueryClient = async (chainConfig: ChainConfig, address: string) => const getIncentivesQueryClient = async (chainConfig: ChainConfig) => { try { const contract = chainConfig.contracts.incentives - const rpc = chainConfig.endpoints.rpc + const rpc = getUrl(chainConfig.endpoints.rpc) const key = rpc + contract if (!_incentivesQueryClient.get(key)) { const client = await getClient(rpc) @@ -108,7 +111,7 @@ const getIncentivesQueryClient = async (chainConfig: ChainConfig) => { const getSwapperQueryClient = async (chainConfig: ChainConfig) => { try { const contract = chainConfig.contracts.swapper - const rpc = chainConfig.endpoints.rpc + const rpc = getUrl(chainConfig.endpoints.rpc) const key = rpc + contract if (!_swapperOsmosisClient.get(key)) { const client = await getClient(rpc) @@ -124,7 +127,7 @@ const getSwapperQueryClient = async (chainConfig: ChainConfig) => { const getPerpsQueryClient = async (chainConfig: ChainConfig) => { try { const contract = chainConfig.contracts.perps - const rpc = chainConfig.endpoints.rpc + const rpc = getUrl(chainConfig.endpoints.rpc) const key = rpc + contract if (!_perpsClient.get(key)) { const client = await getClient(rpc) @@ -137,13 +140,31 @@ const getPerpsQueryClient = async (chainConfig: ChainConfig) => { } } +const getRedBankQueryClient = async (chainConfig: ChainConfig) => { + try { + const contract = chainConfig.contracts.redBank + const rpc = getUrl(chainConfig.endpoints.rpc) + const key = rpc + contract + + if (!_redBankQueryClient.get(key)) { + const client = await getClient(rpc) + _redBankQueryClient.set(key, new MarsRedBankQueryClient(client, contract)) + } + + return _redBankQueryClient.get(key)! + } catch (error) { + throw error + } +} + export { getClient, getCreditManagerQueryClient, getIncentivesQueryClient, getOracleQueryClient, getParamsQueryClient, + getPerpsQueryClient, + getRedBankQueryClient, getSwapperQueryClient, getVaultQueryClient, - getPerpsQueryClient, } diff --git a/src/api/v1/getV1Debts.ts b/src/api/v1/getV1Debts.ts new file mode 100644 index 000000000..6fd0f38e0 --- /dev/null +++ b/src/api/v1/getV1Debts.ts @@ -0,0 +1,20 @@ +import { getRedBankQueryClient } from 'api/cosmwasm-client' +import { ArrayOfUserDebtResponse } from 'types/generated/mars-red-bank/MarsRedBank.types' + +export default async function getV1Debts( + chainConfig: ChainConfig, + user: string, +): Promise { + const redBankQueryClient = await getRedBankQueryClient(chainConfig) + + const userDebt: ArrayOfUserDebtResponse = await redBankQueryClient.userDebts({ + user: user, + limit: 50, + }) + + if (userDebt) { + return userDebt + } + + return new Promise((_, reject) => reject('No account found')) +} diff --git a/src/api/v1/getV1Deposits.ts b/src/api/v1/getV1Deposits.ts new file mode 100644 index 000000000..1963ab7b8 --- /dev/null +++ b/src/api/v1/getV1Deposits.ts @@ -0,0 +1,20 @@ +import { getRedBankQueryClient } from 'api/cosmwasm-client' +import { ArrayOfUserCollateralResponse } from 'types/generated/mars-red-bank/MarsRedBank.types' + +export default async function getV1Deposits( + chainConfig: ChainConfig, + user: string, +): Promise { + const redBankQueryClient = await getRedBankQueryClient(chainConfig) + + const userCollateral: ArrayOfUserCollateralResponse = await redBankQueryClient.userCollaterals({ + user: user, + limit: 50, + }) + + if (userCollateral) { + return userCollateral + } + + return new Promise((_, reject) => reject('No account found')) +} diff --git a/src/api/vaults/getDepositedVaults.ts b/src/api/vaults/getDepositedVaults.ts index 14ccb9393..cb6c67182 100644 --- a/src/api/vaults/getDepositedVaults.ts +++ b/src/api/vaults/getDepositedVaults.ts @@ -20,6 +20,7 @@ import { } from 'types/generated/mars-credit-manager/MarsCreditManager.types' import { getCoinValue } from 'utils/formatters' import { BN } from 'utils/helpers' +import { getUrl } from 'utils/url' async function getUnlocksAtTimestamp( chainConfig: ChainConfig, @@ -27,7 +28,7 @@ async function getUnlocksAtTimestamp( vaultAddress: string, ) { try { - const client = await getClient(chainConfig.endpoints.rpc) + const client = await getClient(getUrl(chainConfig.endpoints.rpc)) const vaultExtension = (await cacheFn( () => diff --git a/src/components/Modals/AlertDialog/index.tsx b/src/components/Modals/AlertDialog/index.tsx index c61438f86..79bd1b28f 100644 --- a/src/components/Modals/AlertDialog/index.tsx +++ b/src/components/Modals/AlertDialog/index.tsx @@ -1,9 +1,9 @@ import classNames from 'classnames' +import { NoIcon, YesIcon } from 'components/Modals/AlertDialog/ButtonIcons' +import Modal from 'components/Modals/Modal' import Button from 'components/common/Button' import Checkbox from 'components/common/Checkbox' -import Modal from 'components/Modals/Modal' -import { NoIcon, YesIcon } from 'components/Modals/AlertDialog/ButtonIcons' import Text from 'components/common/Text' import useAlertDialog from 'hooks/useAlertDialog' import useToggle from 'hooks/useToggle' @@ -50,9 +50,10 @@ function AlertDialog(props: Props) { {title} } - modalClassName='max-w-modal-sm' - headerClassName='p-8' - contentClassName='px-8 pb-8' + className='md:h-auto h-screen-full' + modalClassName='max-w-screen-full md:max-w-modal-sm h-screen-full flex items-center justify-center ' + headerClassName='p-4 md:p-8' + contentClassName='md:px-8 md:pb-8 p-4' hideCloseBtn > {typeof content === 'string' ? ( @@ -61,7 +62,10 @@ function AlertDialog(props: Props) { content )}
{positiveButton && ( diff --git a/src/components/Modals/AssetAmountSelectActionModal.tsx b/src/components/Modals/AssetAmountSelectActionModal.tsx index e0b2f05ae..d34c50c28 100644 --- a/src/components/Modals/AssetAmountSelectActionModal.tsx +++ b/src/components/Modals/AssetAmountSelectActionModal.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames' import { useCallback, useState } from 'react' import Modal from 'components/Modals/Modal' @@ -10,12 +11,12 @@ import Text from 'components/common/Text' import TokenInputWithSlider from 'components/common/TokenInput/TokenInputWithSlider' import AssetImage from 'components/common/assets/AssetImage' import { BN_ZERO } from 'constants/math' -import useCurrentAccount from 'hooks/accounts/useCurrentAccount' import { BNCoin } from 'types/classes/BNCoin' import { byDenom } from 'utils/array' import { BN } from 'utils/helpers' interface Props { + account: Account asset: Asset title: string coinBalances: BNCoin[] @@ -29,6 +30,7 @@ interface Props { export default function AssetAmountSelectActionModal(props: Props) { const { + account, asset, title, coinBalances, @@ -41,7 +43,6 @@ export default function AssetAmountSelectActionModal(props: Props) { } = props const [amount, setAmount] = useState(BN_ZERO) const maxAmount = BN(coinBalances.find(byDenom(asset.denom))?.amount ?? 0) - const account = useCurrentAccount() const handleAmountChange = useCallback( (value: BigNumber) => { setAmount(value) @@ -54,12 +55,11 @@ export default function AssetAmountSelectActionModal(props: Props) { onAction(amount, amount.isEqualTo(maxAmount)) }, [amount, maxAmount, onAction]) - if (!account) return return ( + {title} @@ -68,10 +68,16 @@ export default function AssetAmountSelectActionModal(props: Props) { contentClassName='flex flex-col min-h-[400px]' > {contentHeader} -
+
-} - function RepayNotAvailable(props: { asset: Asset; repayFromWallet: boolean }) { return ( - +
@@ -90,7 +82,6 @@ function BorrowModal(props: Props) { const apy = modal.marketData.apy.borrow const isAutoLendEnabled = autoLendEnabledAccountIds.includes(account.id) const { computeMaxBorrowAmount } = useHealthComputer(account) - const totalDebt = BN(getDebtAmount(modal)) const accountDebt = account.debts.find(byDenom(asset.denom))?.amount ?? BN_ZERO const markets = useMarkets() @@ -236,8 +227,8 @@ function BorrowModal(props: Props) { - {getAssetLogo(modal)} + + {isRepay ? 'Repay' : 'Borrow'} {asset.symbol} @@ -246,19 +237,19 @@ function BorrowModal(props: Props) { headerClassName='gradient-header pl-2 pr-2.5 py-2.5 border-b-white/5 border-b' contentClassName='flex flex-col' > -
+
- {totalDebt.isGreaterThan(0) && ( + {accountDebt.isGreaterThan(0) && ( <> -
-
+
@@ -277,8 +268,7 @@ function BorrowModal(props: Props) {
)} -
-
+
-
+
-
- - {isRepay && maxRepayAmount.isZero() && ( - - )} - {isRepay ? ( - <> - + + {isRepay && maxRepayAmount.isZero() && ( + + )} + {isRepay ? ( + <> + +
Repay from Wallet Repay your debt directly from your wallet
-
- -
- - ) : ( - <> - + +
+ + ) : ( + <> + +
Receive funds to Wallet Your borrowed funds will directly go to your wallet
-
- -
- - )} -
+ +
+ + )} -
+
+ + ) +} diff --git a/src/components/Modals/v1/Deposit.tsx b/src/components/Modals/v1/Deposit.tsx new file mode 100644 index 000000000..cb9000f29 --- /dev/null +++ b/src/components/Modals/v1/Deposit.tsx @@ -0,0 +1,82 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' + +import WalletBridges from 'components/Wallet/WalletBridges' +import { BN_ZERO } from 'constants/math' +import useBaseAsset from 'hooks/assets/useBasetAsset' +import useCurrentWalletBalance from 'hooks/useCurrentWalletBalance' +import { useUpdatedAccount } from 'hooks/useUpdatedAccount' +import useWalletBalances from 'hooks/useWalletBalances' +import useStore from 'store' +import { BNCoin } from 'types/classes/BNCoin' +import { byDenom } from 'utils/array' +import { defaultFee } from 'utils/constants' +import { BN } from 'utils/helpers' +import AssetAmountSelectActionModal from 'components/Modals/AssetAmountSelectActionModal' +import DetailsHeader from 'components/Modals/LendAndReclaim/DetailsHeader' + +interface Props { + account: Account +} + +export default function Deposit(props: Props) { + const { account } = props + const baseAsset = useBaseAsset() + const modal = useStore((s) => s.v1DepositAndWithdrawModal) + const address = useStore((s) => s.address) + const asset = modal?.data.asset ?? baseAsset + const [fundingAsset, setFundingAsset] = useState( + BNCoin.fromDenomAndBigNumber(modal?.data.asset.denom ?? baseAsset.denom, BN_ZERO), + ) + const { data: walletBalances } = useWalletBalances(address) + const { simulateDeposits } = useUpdatedAccount(account) + const balance = useCurrentWalletBalance(asset.denom) + const v1Action = useStore((s) => s.v1Action) + + const baseBalance = useMemo( + () => walletBalances.find(byDenom(baseAsset.denom))?.amount ?? '0', + [walletBalances, baseAsset], + ) + + const close = useCallback(() => { + useStore.setState({ v1DepositAndWithdrawModal: null }) + }, []) + + const handleClick = useCallback(async () => { + v1Action('deposit', fundingAsset) + close() + }, [v1Action, fundingAsset, close]) + + useEffect(() => { + if (BN(baseBalance).isLessThan(defaultFee.amount[0].amount)) { + useStore.setState({ focusComponent: { component: } }) + } + }, [baseBalance]) + + const onDebounce = useCallback(() => { + simulateDeposits('lend', [fundingAsset]) + }, [fundingAsset, simulateDeposits]) + + const handleAmountChange = useCallback( + (value: BigNumber) => { + setFundingAsset(BNCoin.fromDenomAndBigNumber(asset.denom, value)) + }, + [asset.denom], + ) + + if (!modal) return + + return ( + } + coinBalances={balance ? [BNCoin.fromCoin(balance)] : []} + actionButtonText={`Deposit ${asset.symbol}`} + title={`Deposit ${asset.symbol} into the Red Bank`} + onClose={close} + onAction={handleClick} + onChange={handleAmountChange} + onDebounce={onDebounce} + /> + ) +} diff --git a/src/components/Modals/v1/Repay.tsx b/src/components/Modals/v1/Repay.tsx new file mode 100644 index 000000000..782f7cfec --- /dev/null +++ b/src/components/Modals/v1/Repay.tsx @@ -0,0 +1,213 @@ +import BigNumber from 'bignumber.js' +import { useCallback, useEffect, useMemo, useState } from 'react' +import classNames from 'classnames' + +import Modal from 'components/Modals/Modal' +import AccountSummaryInModal from 'components/account/AccountSummary/AccountSummaryInModal' +import Button from 'components/common/Button' +import Card from 'components/common/Card' +import DisplayCurrency from 'components/common/DisplayCurrency' +import Divider from 'components/common/Divider' +import { FormattedNumber } from 'components/common/FormattedNumber' +import { ArrowRight, InfoCircle } from 'components/common/Icons' +import Text from 'components/common/Text' +import TitleAndSubCell from 'components/common/TitleAndSubCell' +import TokenInputWithSlider from 'components/common/TokenInput/TokenInputWithSlider' +import AssetImage from 'components/common/assets/AssetImage' +import { BN_ZERO } from 'constants/math' +import useBaseAsset from 'hooks/assets/useBasetAsset' +import useMarkets from 'hooks/markets/useMarkets' +import useCurrentWalletBalance from 'hooks/useCurrentWalletBalance' +import { useUpdatedAccount } from 'hooks/useUpdatedAccount' +import useStore from 'store' +import { BNCoin } from 'types/classes/BNCoin' +import { formatPercent } from 'utils/formatters' +import { BN } from 'utils/helpers' +import { getDebtAmountWithInterest } from 'utils/tokens' + +interface Props { + account: Account +} + +function RepayNotAvailable(props: { asset: Asset }) { + return ( + +
+ +
+ No funds for repay + {`Unfortunately you don't have any ${props.asset.symbol} in your Wallet to repay the debt.`} +
+
+
+ ) +} + +export default function Repay(props: Props) { + const { account } = props + const modal = useStore((s) => s.v1BorrowAndRepayModal) + const baseAsset = useBaseAsset() + const asset = modal?.data.asset ?? baseAsset + const [amount, setAmount] = useState(BN_ZERO) + const balance = useCurrentWalletBalance(asset.denom) + const v1Action = useStore((s) => s.v1Action) + const [max, setMax] = useState(BN_ZERO) + const { simulateRepay } = useUpdatedAccount(account) + const apy = modal?.data.apy.borrow ?? 0 + const accountDebt = modal?.data.accountDebtAmount ?? BN_ZERO + const markets = useMarkets() + + const accountDebtWithInterest = useMemo( + () => getDebtAmountWithInterest(accountDebt, apy), + [accountDebt, apy], + ) + + const overpayExeedsCap = useMemo(() => { + const marketAsset = markets.find((market) => market.asset.denom === asset.denom) + if (!marketAsset) return + const overpayAmount = accountDebtWithInterest.minus(accountDebt) + const marketCapAfterOverpay = marketAsset.cap.used.plus(overpayAmount) + + return marketAsset.cap.max.isLessThanOrEqualTo(marketCapAfterOverpay) + }, [markets, asset.denom, accountDebt, accountDebtWithInterest]) + + const maxRepayAmount = useMemo(() => { + const maxBalance = BN(balance?.amount ?? 0) + return BigNumber.min(maxBalance, overpayExeedsCap ? accountDebt : accountDebtWithInterest) + }, [accountDebtWithInterest, overpayExeedsCap, accountDebt, balance?.amount]) + + const close = useCallback(() => { + setAmount(BN_ZERO) + useStore.setState({ v1BorrowAndRepayModal: null }) + }, [setAmount]) + + const onConfirmClick = useCallback(() => { + v1Action('repay', BNCoin.fromDenomAndBigNumber(asset.denom, amount)) + close() + }, [v1Action, asset, amount, close]) + + const handleChange = useCallback( + (newAmount: BigNumber) => { + if (!amount.isEqualTo(newAmount)) setAmount(newAmount) + }, + [amount, setAmount], + ) + + const onDebounce = useCallback(() => { + const repayCoin = BNCoin.fromDenomAndBigNumber( + asset.denom, + amount.isGreaterThan(accountDebt) ? accountDebt : amount, + ) + simulateRepay(repayCoin, true) + }, [amount, accountDebt, asset, simulateRepay]) + + useEffect(() => { + if (maxRepayAmount.isEqualTo(max)) return + setMax(maxRepayAmount) + }, [max, maxRepayAmount]) + + useEffect(() => { + if (amount.isLessThanOrEqualTo(max)) return + handleChange(max) + setAmount(max) + }, [amount, max, handleChange]) + + if (!modal) return null + + return ( + + + + {'Repay'} {asset.symbol} + + + } + headerClassName='gradient-header pl-2 pr-2.5 py-2.5 border-b-white/5 border-b' + contentClassName='flex flex-col' + > +
+ + +
+
+ + +
+ + Total Borrowed + +
+
+
+ + +
+ + Liquidity available + +
+
+
+ + + + {maxRepayAmount.isZero() && } +
+
+ ) +} diff --git a/src/components/Modals/v1/V1BorrowAndRepay.tsx b/src/components/Modals/v1/V1BorrowAndRepay.tsx new file mode 100644 index 000000000..141a052f8 --- /dev/null +++ b/src/components/Modals/v1/V1BorrowAndRepay.tsx @@ -0,0 +1,15 @@ +import useAccount from 'hooks/accounts/useAccount' +import useStore from 'store' +import Borrow from 'components/Modals/v1/Borrow' +import Repay from 'components/Modals/v1/Repay' + +export default function V1BorrowAndRepayModal() { + const address = useStore((s) => s.address) + const { data: account } = useAccount(address) + const modal = useStore((s) => s.v1BorrowAndRepayModal) + const isBorrow = modal?.type === 'borrow' + + if (!modal || !account) return null + if (isBorrow) return + return +} diff --git a/src/components/Modals/v1/V1DepositAndWithdraw.tsx b/src/components/Modals/v1/V1DepositAndWithdraw.tsx new file mode 100644 index 000000000..475f0c061 --- /dev/null +++ b/src/components/Modals/v1/V1DepositAndWithdraw.tsx @@ -0,0 +1,15 @@ +import useAccount from 'hooks/accounts/useAccount' +import useStore from 'store' +import Deposit from 'components/Modals/v1/Deposit' +import Withdraw from 'components/Modals/v1/Withdraw' + +export default function V1DepositAndWithdraw() { + const address = useStore((s) => s.address) + const { data: account } = useAccount(address) + const modal = useStore((s) => s.v1DepositAndWithdrawModal) + const isDeposit = modal?.type === 'deposit' + + if (!modal || !account) return null + if (isDeposit) return + return +} diff --git a/src/components/Modals/v1/Withdraw.tsx b/src/components/Modals/v1/Withdraw.tsx new file mode 100644 index 000000000..f4d111cef --- /dev/null +++ b/src/components/Modals/v1/Withdraw.tsx @@ -0,0 +1,66 @@ +import { useCallback, useState } from 'react' + +import AssetAmountSelectActionModal from 'components/Modals/AssetAmountSelectActionModal' +import DetailsHeader from 'components/Modals/LendAndReclaim/DetailsHeader' +import { BN_ZERO } from 'constants/math' +import useBaseAsset from 'hooks/assets/useBasetAsset' +import useHealthComputer from 'hooks/useHealthComputer' +import { useUpdatedAccount } from 'hooks/useUpdatedAccount' +import useStore from 'store' +import { BNCoin } from 'types/classes/BNCoin' + +interface Props { + account: Account +} + +export default function Withdraw(props: Props) { + const { account } = props + const baseAsset = useBaseAsset() + const modal = useStore((s) => s.v1DepositAndWithdrawModal) + const asset = modal?.data.asset ?? baseAsset + const [withdrawAsset, setWithdrawAsset] = useState( + BNCoin.fromDenomAndBigNumber(modal?.data.asset.denom ?? baseAsset.denom, BN_ZERO), + ) + const { computeMaxWithdrawAmount } = useHealthComputer(account) + const maxWithdrawAmount = computeMaxWithdrawAmount(asset.denom) + const { simulateWithdraw } = useUpdatedAccount(account) + const balance = BNCoin.fromDenomAndBigNumber(asset.denom, maxWithdrawAmount) + const v1Action = useStore((s) => s.v1Action) + + const close = useCallback(() => { + useStore.setState({ v1DepositAndWithdrawModal: null }) + }, []) + + const handleClick = useCallback(async () => { + v1Action('withdraw', withdrawAsset) + close() + }, [v1Action, withdrawAsset, close]) + + const onDebounce = useCallback(() => { + simulateWithdraw(false, withdrawAsset) + }, [withdrawAsset, simulateWithdraw]) + + const handleAmountChange = useCallback( + (value: BigNumber) => { + setWithdrawAsset(BNCoin.fromDenomAndBigNumber(asset.denom, value)) + }, + [asset.denom], + ) + + if (!modal) return + + return ( + } + coinBalances={[balance]} + actionButtonText={`Withdraw ${asset.symbol}`} + title={`Withdraw ${asset.symbol} from the Red Bank`} + onClose={close} + onAction={handleClick} + onChange={handleAmountChange} + onDebounce={onDebounce} + /> + ) +} diff --git a/src/components/Wallet/RecentTransactions.tsx b/src/components/Wallet/RecentTransactions.tsx index 567da7d9b..29dccc962 100644 --- a/src/components/Wallet/RecentTransactions.tsx +++ b/src/components/Wallet/RecentTransactions.tsx @@ -7,8 +7,8 @@ import Text from 'components/common/Text' import { TextLink } from 'components/common/TextLink' import { generateToastContent } from 'components/common/Toaster' import useTransactions from 'hooks/localStorage/useTransactions' -import useStore from 'store' import useChainConfig from 'hooks/useChainConfig' +import useStore from 'store' export default function RecentTransactions() { const address = useStore((s) => s.address) @@ -46,8 +46,10 @@ export default function RecentTransactions() { }} key={hash} > -
- Credit Account {accountId} +
+ + {accountId === address ? 'Red Bank' : `Credit Account ${accountId}`} + {moment.unix(timestamp).format('lll')} diff --git a/src/components/Wallet/WalletConnectButton.tsx b/src/components/Wallet/WalletConnectButton.tsx index 08bb00efe..68b0f38c6 100644 --- a/src/components/Wallet/WalletConnectButton.tsx +++ b/src/components/Wallet/WalletConnectButton.tsx @@ -15,6 +15,7 @@ interface Props { color?: ButtonProps['color'] variant?: ButtonProps['variant'] size?: ButtonProps['size'] + short?: boolean } export default function WalletConnectButton(props: Props) { diff --git a/src/components/Wallet/WalletConnectedButton.tsx b/src/components/Wallet/WalletConnectedButton.tsx index 8c05a572a..12670ea42 100644 --- a/src/components/Wallet/WalletConnectedButton.tsx +++ b/src/components/Wallet/WalletConnectedButton.tsx @@ -5,15 +5,16 @@ import { resolvePrimaryDomainByAddress } from 'ibc-domains-sdk' import { useCallback, useEffect, useState } from 'react' import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' import useClipboard from 'react-use-clipboard' +import { isMobile } from 'react-device-detect' +import RecentTransactions from 'components/Wallet/RecentTransactions' +import WalletSelect from 'components/Wallet/WalletSelect' import Button from 'components/common/Button' import { CircularProgress } from 'components/common/CircularProgress' import { FormattedNumber } from 'components/common/FormattedNumber' import { Check, Copy, ExternalLink, Wallet } from 'components/common/Icons' import Overlay from 'components/common/Overlay' import Text from 'components/common/Text' -import RecentTransactions from 'components/Wallet/RecentTransactions' -import WalletSelect from 'components/Wallet/WalletSelect' import chains from 'configs/chains' import { BN_ZERO } from 'constants/math' import useBaseAsset from 'hooks/assets/useBasetAsset' @@ -150,7 +151,9 @@ export default function WalletConnectedButton() { }} hasFocus={showDetails} > - {userDomain?.domain ? userDomain.domain : truncate(address, [2, 4])} + + {userDomain?.domain ? userDomain.domain : truncate(address, [2, 4])} +
-
+
-
- {baseAsset.symbol} -
+
{baseAsset.symbol}
@@ -205,17 +209,17 @@ export default function WalletConnectedButton() { > {truncate(address, [14, 14])} -
+