Compare commits

..
Author SHA1 Message Date
Linkie Link 51916683ac feat: added ChartLoading component 2023-11-24 10:32:54 +01:00
Linkie Link 160165aa22 Merge branch 'develop' of https://github.com/mars-protocol/mars-v2-frontend into stats-page
# Conflicts:
#	src/components/Routes.tsx
#	src/utils/route.ts
2023-11-24 09:40:57 +01:00
Linkie Link e3d3d30700 feat: added a first Chart component 2023-11-22 17:01:25 +01:00
Linkie Link ca9e608952 fix: fixed the build 2023-11-22 15:54:44 +01:00
Linkie Link d414e38b88 fix: color adjustments 2023-11-22 15:11:17 +01:00
Linkie Link 31eceeb9fb feat: added additional metric cards 2023-11-22 15:10:25 +01:00
Linkie Link 61c64aa724 feat: first preparations for stats 2023-11-22 12:13:32 +01:00
472 changed files with 7103 additions and 11181 deletions
+33 -11
View File
@@ -1,15 +1,37 @@
# DEVNET #
NEXT_PUBLIC_NETWORK=devnet
NEXT_PUBLIC_CHAIN_ID=devnet
NEXT_PUBLIC_RPC=https://rpc.devnet.osmosis.zone/
NEXT_PUBLIC_GQL=https://devnet-osmosis-gql.marsprotocol.io/graphql
NEXT_PUBLIC_REST=https://lcd.devnet.osmosis.zone/
# MAINNET #
NEXT_PUBLIC_NETWORK=mainnet
NEXT_PUBLIC_CHAIN_ID=osmosis-1
NEXT_PUBLIC_RPC=https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-rpc-front/
NEXT_PUBLIC_GQL=https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-hive-front/graphql
NEXT_PUBLIC_REST=https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-lcd-front/
NEXT_PUBLIC_OSMOSIS_RPC=https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-rpc-front/
NEXT_PUBLIC_OSMOSIS_REST=https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-lcd-front/
NEXT_PUBLIC_OSMOSIS_TEST_RPC=https://rpc.devnet.osmosis.zone/
NEXT_PUBLIC_OSMOSIS_TEST_REST=https://lcd.devnet.osmosis.zone/
NEXT_PUBLIC_NEUTRON_TEST_RPC=https://rpc-palvus.pion-1.ntrn.tech/
NEXT_PUBLIC_NEUTRON_TEST_REST=https://rest-palvus.pion-1.ntrn.tech/
# COMMON #
NEXT_PUBLIC_SWAP=https://app.osmosis.zone
NEXT_PUBLIC_VAULT_APR=https://api.marsprotocol.io/v1/vaults/osmosis
NEXT_PUBLIC_ACCOUNT_NFT=osmo1450hrg6dv2l58c0rvdwx8ec2a0r6dd50hn4frk370tpvqjhy8khqw7sw09
NEXT_PUBLIC_ORACLE=osmo1mhznfr60vjdp2gejhyv2gax9nvyyzhd3z0qcwseyetkfustjauzqycsy2g
NEXT_PUBLIC_RED_BANK=osmo1c3ljch9dfw5kf52nfwpxd2zmj2ese7agnx0p9tenkrryasrle5sqf3ftpg
NEXT_PUBLIC_CREDIT_MANAGER=osmo1f2m24wktq0sw3c0lexlg7fv4kngwyttvzws3a3r3al9ld2s2pvds87jqvf
NEXT_PUBLIC_INCENTIVES=osmo1nkahswfr8shg8rlxqwup0vgahp0dk4x8w6tkv3rra8rratnut36sk22vrm
NEXT_PUBLIC_SWAPPER=osmo1wee0z8c7tcawyl647eapqs4a88q8jpa7ddy6nn2nrs7t47p2zhxswetwla
NEXT_PUBLIC_PYTH=osmo13ge29x4e2s63a8ytz2px8gurtyznmue4a69n5275692v3qn3ks8q7cwck7
NEXT_PUBLIC_ZAPPER=osmo17qwvc70pzc9mudr8t02t3pl74hhqsgwnskl734p4hug3s8mkerdqzduf7c
NEXT_PUBLIC_PARAMS=osmo1nlmdxt9ctql2jr47qd4fpgzg84cjswxyw6q99u4y4u4q6c2f5ksq7ysent
NEXT_PUBLIC_PYTH_ENDPOINT=https://hermes.pyth.network/api
NEXT_PUBLIC_MAINNET_REST=https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-rpc-front/
NEXT_PUBLIC_CANDLES_ENDPOINT_THE_GRAPH=https://osmosis-candles.marsprotocol.io/
NEXT_PUBLIC_CANDLES_ENDPOINT_PYTH=https://benchmarks.pyth.network
NEXT_PUBLIC_WALLET_CONNECT_ID=d93fdffb159bae5ec87d8fee4cdbb045
CHARTING_LIBRARY_USERNAME=git_username
CHARTING_LIBRARY_ACCESS_TOKEN=access_token
CHARTING_LIBRARY_REPOSITORY=github.com/tradingview/charting_library/
CHARTING_LIBRARY_REPOSITORY=github.com/tradingview/charting_library
CHARTING_LIBRARY_ACCESS_TOKEN=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
CHARTING_LIBRARY_USERNAME=mars-git-demo
NEXT_PUBLIC_STRIDE_APRS=https://edge.stride.zone/api/stake-stats
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
src: '/img.jpg',
height: 24,
width: 24,
blurDataURL: 'data:image/png;base64,imagedata',
}
+17
View File
@@ -0,0 +1,17 @@
jest.mock('react-helmet-async', () => {
const React = require('react')
const plugin = jest.requireActual('react-helmet-async')
const mockHelmet = ({ children, ...props }) =>
React.createElement(
'div',
{
...props,
className: 'mock-helmet',
},
children,
)
return {
...plugin,
Helmet: jest.fn().mockImplementation(mockHelmet),
}
})
+23
View File
@@ -0,0 +1,23 @@
jest.mock('store', () => {
let state = {}
const mockUseStore = (selectorFn) => {
return selectorFn(state)
}
mockUseStore.setState = (newState) => {
state = {
...state,
...newState,
}
}
mockUseStore.clearState = () => {
state = {}
}
return {
__esModule: true,
default: mockUseStore,
}
})
+1
View File
@@ -0,0 +1 @@
module.exports = {}
+7
View File
@@ -0,0 +1,7 @@
/* eslint-disable react/display-name */
import React from 'react'
const SvgrMock = React.forwardRef((props, ref) => <svg ref={ref} {...props} />)
export const ReactComponent = SvgrMock
export default SvgrMock
+14
View File
@@ -0,0 +1,14 @@
import { render } from '@testing-library/react'
import Footer from 'components/Footer'
import packageJSON from '../package.json'
describe('<Footer />', () => {
it('should render package version correctly', () => {
const { getByText, container } = render(<Footer />)
const versionText = getByText(`v${packageJSON.version}`)
expect(versionText).toBeInTheDocument()
})
})
@@ -0,0 +1,67 @@
import { render, screen } from '@testing-library/react'
import AccountDetails from 'components/Account/AccountDetails'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useStore from 'store'
jest.mock('react-router', () => ({
...(jest.requireActual('react-router') as {}),
useLocation: jest.fn().mockImplementation(() => {
return { pathname: '/testroute' }
}),
}))
jest.mock('hooks/useCurrentAccount', () => jest.fn(() => null))
jest.mock('hooks/useHealthComputer', () =>
jest.fn(() => ({
health: 0,
})),
)
// AccountBalancesTable component has wallet provider dependency, so we mock it
jest.mock('components/Account/AccountBalancesTable', () => jest.fn(() => null))
const mockedUseCurrentAccount = useCurrentAccount as jest.Mock
const mockedAccounts: Account[] = [
{ id: '1', deposits: [], lends: [], debts: [], vaults: [], kind: 'default' },
{ id: '2', deposits: [], lends: [], debts: [], vaults: [], kind: 'default' },
]
jest.mock('hooks/useAccountId', () => jest.fn(() => '1'))
jest.mock('hooks/useAccounts', () =>
jest.fn(() => ({
data: mockedAccounts,
})),
)
jest.mock('hooks/useAccountIds', () =>
jest.fn(() => ({
data: ['1', '2'],
})),
)
jest.mock('hooks/useCurrentAccount', () => jest.fn(() => mockedAccounts[0]))
describe('<AccountDetails />', () => {
beforeAll(() => {
useStore.setState({
address: 'walletAddress',
accounts: mockedAccounts,
})
})
afterAll(() => {
useStore.clearState()
})
it('renders account details WHEN account is selected', () => {
mockedUseCurrentAccount.mockReturnValue(mockedAccounts)
render(<AccountDetails />)
const container = screen.queryByTestId('account-details')
expect(container).toBeInTheDocument()
})
it('does not render WHEN account is NOT selected', () => {
mockedUseCurrentAccount.mockReturnValue(null)
render(<AccountDetails />)
const container = screen.queryByTestId('account-details')
expect(container).not.toBeInTheDocument()
})
})
+157
View File
@@ -0,0 +1,157 @@
import { cleanup, render } from '@testing-library/react'
import Button from 'components/Button'
import {
buttonColorClasses,
buttonSizeClasses,
buttonVariantClasses,
focusClasses,
} from 'components/Button/constants'
import { parseMockComponentProps } from 'utils/testing'
jest.mock('components/CircularProgress', () => {
return {
CircularProgress: (props: any) =>
require('utils/testing').createMockComponent('circular-progress-component', props),
}
})
describe('<Button />', () => {
afterAll(() => {
jest.unmock('components/CircularProgress')
})
it('should render', () => {
const { container } = render(<Button />)
expect(container).toBeInTheDocument()
})
it('should render `children` when its passed', () => {
const children = <span data-testid='test-id'>Hello World!</span>
const { getByTestId } = render(<Button>{children}</Button>)
expect(getByTestId('test-id')).toBeInTheDocument()
})
it('should handle `className` prop correctly', () => {
const testClass = 'test-class'
const { container } = render(<Button className={testClass} />)
expect(container.querySelector('button')).toHaveClass(testClass)
})
it('should handle `color` prop correctly', () => {
const colors = Object.keys(buttonColorClasses) as [keyof typeof buttonColorClasses]
colors.forEach((color) => {
const { container } = render(<Button color={color} />)
expect(container.querySelector('button')).toHaveClass(buttonColorClasses[color])
})
})
it('should handle `disabled=true` prop correctly', () => {
const testFunction = jest.fn()
const { container } = render(<Button disabled={true} onClick={testFunction} />)
const button = container.querySelector('button')
button?.click()
expect(button).toHaveClass('pointer-events-none')
expect(testFunction).not.toBeCalled()
})
it('should handle `disabled=false` prop correctly', () => {
const testFunction = jest.fn()
const { container } = render(<Button disabled={false} onClick={testFunction} />)
const button = container.querySelector('button')
button?.click()
expect(button).not.toHaveClass('pointer-events-none')
expect(testFunction).toBeCalled()
})
it('should show progress indicator when `showProgressIndicator=true`', () => {
const { getByTestId } = render(<Button showProgressIndicator={true} />)
const circularProgressComponent = getByTestId('circular-progress-component')
expect(circularProgressComponent).toBeInTheDocument()
})
it('should set correct values for progress indicator size', () => {
const sizeValues = { xs: 8, sm: 10, md: 12, lg: 18 }
Object.entries(sizeValues).forEach(([size, value]) => {
const { getByTestId } = render(
<Button showProgressIndicator={true} size={size as keyof typeof buttonSizeClasses} />,
)
const circularProgressComponent = getByTestId('circular-progress-component')
const sizeProp = parseMockComponentProps(circularProgressComponent).size
expect(sizeProp).toBe(value)
cleanup()
})
})
it('should handle `size` prop correctly', () => {
const sizes = Object.keys(buttonSizeClasses) as [keyof typeof buttonSizeClasses]
sizes.forEach((size) => {
const { container } = render(<Button size={size} />)
expect(container.querySelector('button')).toHaveClass(buttonSizeClasses[size])
})
})
it('should show `text` when its passed', () => {
const text = 'Hello!'
const { getByText } = render(<Button text={text} />)
expect(getByText(text)).toBeInTheDocument()
})
it('should handle `variant` prop correctly', () => {
const variants = Object.keys(buttonVariantClasses) as [keyof typeof buttonVariantClasses]
variants.forEach((variant) => {
const { container } = render(<Button variant={variant} />)
expect(container.querySelector('button')).toHaveClass(buttonVariantClasses[variant])
})
})
it('should show left icon when `leftIcon` prop is passed', () => {
const icon = <span data-testid='left-icon'>this is the left icon</span>
const { getByTestId } = render(<Button leftIcon={icon} />)
expect(getByTestId('left-icon')).toBeInTheDocument()
})
it('should show right icon when `rightIcon` prop is passed', () => {
const icon = <span data-testid='right-icon'>this is the right icon</span>
const { getByTestId } = render(<Button rightIcon={icon} />)
expect(getByTestId('right-icon')).toBeInTheDocument()
})
it('should handle `iconClassName` prop correctly', () => {
const icon = <span data-testid='icon'>just an icon</span>
const { getByTestId } = render(<Button rightIcon={icon} iconClassName='test-icon-class' />)
expect(getByTestId('icon').parentElement).toHaveClass('test-icon-class')
})
it('should show submenu indicator when `hasSubmenu=true`', () => {
const { getByTestId } = render(<Button hasSubmenu={true} />)
expect(getByTestId('button-submenu-indicator')).toBeInTheDocument()
})
it('should set focus classes when `hasFocus=true`', () => {
const { container } = render(<Button hasFocus={true} color='primary' />)
const button = container.querySelector('button')
expect(button).toHaveClass(focusClasses['primary'])
})
})
+60
View File
@@ -0,0 +1,60 @@
import { render, screen } from '@testing-library/react'
import Card from 'components/Card'
jest.mock('components/Text', () => {
return {
__esModule: true,
default: (props: any) =>
require('utils/testing').createMockComponent('mock-text-component', props),
}
})
describe('<Card />', () => {
const defaultProps = {
children: <></>,
}
afterAll(() => {
jest.unmock('components/Text')
})
it('should render', () => {
const { container } = render(<Card {...defaultProps} />)
expect(container).toBeInTheDocument()
})
it('should handle `className` prop correctly', () => {
const testClass = 'test-class'
const { container } = render(<Card {...defaultProps} className={testClass} />)
expect(container.querySelector('section')).toHaveClass(testClass)
})
it('should handle `contentClassName` prop correctly', () => {
const testClass = 'test-class'
const { container } = render(<Card {...defaultProps} contentClassName={testClass} />)
expect(container.querySelector('div')).toHaveClass(testClass)
})
it('should handle `title` prop as string correctly', () => {
const testTitle = 'this-is-the-test-title'
const { queryByText } = render(<Card {...defaultProps} title={testTitle} />)
expect(queryByText(testTitle)).toBeInTheDocument()
})
it('should handle `title` prop as element correctly', () => {
const testTitle = <p data-testid='test-title'>Test title</p>
const { queryByTestId } = render(<Card {...defaultProps} title={testTitle} />)
expect(queryByTestId('test-title')).toBeInTheDocument()
expect(queryByTestId('mock-text-component')).not.toBeInTheDocument()
})
it('should handle `id` prop as element correctly', () => {
const testId = 'test-id'
const { container } = render(<Card {...defaultProps} id={testId} />)
expect(container.querySelector(`section#${testId}`)).toBeInTheDocument()
})
})
@@ -0,0 +1,34 @@
import { render } from '@testing-library/react'
import { CircularProgress } from 'components/CircularProgress'
import { LocalStorageKeys } from 'constants/localStorageKeys'
describe('<CircularProgress />', () => {
afterAll(() => {
localStorage.removeItem(LocalStorageKeys.REDUCE_MOTION)
})
it('should render', () => {
const { container } = render(<CircularProgress />)
expect(container).toBeInTheDocument()
})
it('should render `...` when animations disabled', () => {
localStorage.setItem(LocalStorageKeys.REDUCE_MOTION, 'true')
const { getByText } = render(<CircularProgress />)
const threeDots = getByText('...')
expect(threeDots).toBeInTheDocument()
})
it('should render the component with animation classes when animations enabled', () => {
localStorage.setItem(LocalStorageKeys.REDUCE_MOTION, 'false')
const { container } = render(<CircularProgress />)
const progressWithAnimations = container.querySelector('.animate-progress')
expect(progressWithAnimations).toBeInTheDocument()
})
})
@@ -0,0 +1,65 @@
import { render } from '@testing-library/react'
import Modal from 'components/Modal'
import UnlockModal from 'components/Modals/Unlock'
import { BN_ONE, BN_ZERO } from 'constants/math'
import { TESTNET_VAULTS_META_DATA } from 'constants/vaults'
import useStore from 'store'
import { BN } from 'utils/helpers'
jest.mock('components/Modal')
const mockedModal = jest.mocked(Modal).mockImplementation(() => <div>Mock</div>)
const mockedDepositedVault: DepositedVault = {
...TESTNET_VAULTS_META_DATA[0],
status: 'active',
apy: 1,
apr: null,
ltv: {
max: 0.65,
liq: 0.7,
},
amounts: {
primary: BN_ONE,
secondary: BN_ONE,
locked: BN_ONE,
unlocked: BN_ONE,
unlocking: BN_ONE,
},
values: {
primary: BN_ZERO,
secondary: BN_ZERO,
unlocked: BN_ZERO,
unlocking: BN_ZERO,
},
cap: {
denom: 'mock',
max: BN(10),
used: BN_ONE,
},
}
describe('<UnlockModal />', () => {
afterAll(() => {
useStore.clearState()
})
it('should render', () => {
const { container } = render(<UnlockModal />)
expect(container).toBeInTheDocument()
})
describe('should set content correctly', () => {
it('should have no content when no modal is present in store', () => {
useStore.setState({ unlockModal: null })
render(<UnlockModal />)
expect(mockedModal).toHaveBeenCalledTimes(0)
})
it('should have content when modal is present in store', () => {
useStore.setState({ unlockModal: { vault: mockedDepositedVault } })
render(<UnlockModal />)
expect(mockedModal).toHaveBeenCalledTimes(1)
})
})
})
@@ -0,0 +1,32 @@
import { render } from '@testing-library/react'
import * as rrd from 'react-router-dom'
import PageMetadata from 'components/PageMetadata'
import PAGE_METADATA from 'constants/pageMetadata'
jest.mock('react-router-dom')
const mockedUseLocation = rrd.useLocation as jest.Mock
describe('<PageMetadata />', () => {
afterAll(() => {
jest.clearAllMocks()
})
Object.keys(PAGE_METADATA).forEach((page) => {
it(`should render correct ${page} metadata`, () => {
const pageKey = page as keyof typeof PAGE_METADATA
const pageMetadata = PAGE_METADATA[pageKey]
mockedUseLocation.mockReturnValue({ pathname: pageKey })
const { container } = render(<PageMetadata />)
const titleElement = container.querySelector('title')
const descriptionElement = container.querySelector('meta[name="description"]')
const keywordsElement = container.querySelector('meta[name="keywords"]')
expect(titleElement).toHaveTextContent(pageMetadata.title)
expect(descriptionElement).toHaveAttribute('content', pageMetadata.description)
expect(keywordsElement).toHaveAttribute('content', pageMetadata.keywords)
})
})
})
+98
View File
@@ -0,0 +1,98 @@
import { fireEvent, render } from '@testing-library/react'
import BigNumber from 'bignumber.js'
import TokenInput from 'components/TokenInput'
import { ASSETS } from 'constants/assets'
jest.mock('components/DisplayCurrency', () => {
return {
__esModule: true,
default: (props: any) =>
require('utils/testing').createMockComponent('mock-display-currency-component', props),
}
})
describe('<TokenInput />', () => {
const asset = ASSETS[0]
const defaultProps = {
amount: new BigNumber(1),
asset,
max: new BigNumber(100),
onChangeAsset: jest.fn(),
onChange: jest.fn(),
}
afterAll(() => {
jest.unmock('components/DisplayCurrency')
})
it('should render', () => {
const { container } = render(<TokenInput warningMessages={[]} {...defaultProps} />)
expect(container).toBeInTheDocument()
})
it('should handle `className` prop correctly', () => {
const testClass = 'test-class'
const { getByTestId } = render(
<TokenInput warningMessages={[]} {...defaultProps} className={testClass} />,
)
expect(getByTestId('token-input-component')).toHaveClass(testClass)
})
it('should handle `disabled` prop correctly', () => {
const { getByTestId } = render(
<TokenInput warningMessages={[]} {...defaultProps} disabled={true} />,
)
expect(getByTestId('token-input-component')).toHaveClass('pointer-events-none', 'opacity-50')
})
it('should handle `maxText` prop correctly', () => {
const { getByTestId } = render(
<TokenInput warningMessages={[]} {...defaultProps} maxText='Max' />,
)
expect(getByTestId('token-input-max-button')).toBeInTheDocument()
})
describe('should render the max button', () => {
it('when `maxText` prop is defined', () => {
const { getByTestId } = render(
<TokenInput warningMessages={[]} {...defaultProps} maxText='Max' />,
)
expect(getByTestId('token-input-max-button')).toBeInTheDocument()
})
it('not when `maxText` prop is undefined', () => {
const { queryByTestId } = render(<TokenInput warningMessages={[]} {...defaultProps} />)
expect(queryByTestId('token-input-max-button')).not.toBeInTheDocument()
})
})
describe('should render <Select />', () => {
it('when `hasSelect` prop is true and balances is defined', () => {
const { getByTestId } = render(
<TokenInput warningMessages={[]} {...defaultProps} balances={[]} hasSelect />,
)
expect(getByTestId('select-component')).toBeInTheDocument()
})
it('not when `hasSelect` prop is true and balances is not defined', () => {
const { queryByTestId } = render(
<TokenInput warningMessages={[]} {...defaultProps} hasSelect />,
)
expect(queryByTestId('select-component')).not.toBeInTheDocument()
})
it('not when `hasSelect` prop is false and balances is defined', () => {
const { queryByTestId } = render(
<TokenInput warningMessages={[]} {...defaultProps} balances={[]} />,
)
expect(queryByTestId('select-component')).not.toBeInTheDocument()
})
})
it('should call onMaxBtnClick when the user clicks on max button', () => {
const { getByTestId } = render(
<TokenInput warningMessages={[]} {...defaultProps} maxText='max' />,
)
const maxBtn = getByTestId('token-input-max-button')
fireEvent.click(maxBtn)
expect(defaultProps.onChange).toBeCalledWith(defaultProps.max)
})
})
@@ -0,0 +1,45 @@
import { render } from '@testing-library/react'
import { Tooltip } from 'components/Tooltip'
describe('<Tooltip />', () => {
const defaultProps = {
content: <></>,
}
it('should render', () => {
const { container } = render(<Tooltip {...defaultProps} type='info' />)
expect(container).toBeInTheDocument()
})
it('should handle `children` prop correctly', () => {
const { getByTestId } = render(
<Tooltip {...defaultProps} type='info'>
<p data-testid='test-child'>Test text</p>
</Tooltip>,
)
expect(getByTestId('test-child')).toBeInTheDocument()
})
it('should handle `className` prop correctly', () => {
const testClass = 'test-class'
const { container } = render(<Tooltip {...defaultProps} type='info' className={testClass} />)
expect(container.getElementsByClassName(testClass)).toHaveLength(1)
})
describe('should handle `underline` prop correctly', () => {
it('should have border class when children are passed', () => {
const { container } = render(
<Tooltip {...defaultProps} type='info' underline>
<></>
</Tooltip>,
)
expect(container.getElementsByClassName('border-b-1')).toHaveLength(1)
})
it('should not have border class when children are passed', () => {
const { container } = render(<Tooltip {...defaultProps} type='info' underline />)
expect(container.getElementsByClassName('border-b-1')).toHaveLength(0)
})
})
})
@@ -0,0 +1,43 @@
import { render } from '@testing-library/react'
import TooltipContent from 'components/Tooltip/TooltipContent'
describe('<Tooltip />', () => {
const defaultProps = {
content: <></>,
}
it('should render', () => {
const { container } = render(<TooltipContent {...defaultProps} type='info' />)
expect(container).toBeInTheDocument()
})
it('should handle `type` prop correctly', () => {
const types = { info: 'bg-white/20', warning: 'bg-warning', error: 'bg-error' }
Object.entries(types).forEach(([key, value]) => {
const { container } = render(<TooltipContent {...defaultProps} type={key as TooltipType} />)
expect(container.getElementsByClassName(value)).toHaveLength(1)
})
})
describe('should handle `content` props correctly', () => {
it('should render Text component when type is string', () => {
const testText = 'testText'
const { getByTestId } = render(
<TooltipContent {...defaultProps} type='info' content={testText} />,
)
const textComponent = getByTestId('text-component')
expect(textComponent).toHaveTextContent(testText)
})
it('should render content when type is ReactNode', () => {
const testNode = <p data-testid='test-node'>Test node</p>
const { queryByTestId } = render(
<TooltipContent {...defaultProps} type='info' content={testNode} />,
)
expect(queryByTestId('text-component')).not.toBeInTheDocument()
expect(queryByTestId('test-node')).toBeInTheDocument()
})
})
})
+60
View File
@@ -0,0 +1,60 @@
module.exports = {
collectCoverage: true,
coverageProvider: 'v8',
collectCoverageFrom: [
'**/*.{js,jsx,ts,tsx}',
'!**/*.d.ts',
'!**/node_modules/**',
'!<rootDir>/out/**',
'!<rootDir>/.next/**',
'!<rootDir>/*.config.js',
'!<rootDir>/coverage/**',
'!<rootDir>/src/types/**',
'!<rootDir>/src/utils/charting_library/**',
'!<rootDir>/src/utils/datafeeds/**',
'!<rootDir>/public/charting_library/**',
'!<rootDir>/public/datafeeds/**',
'!<rootDir>/src/utils/health_computer/**',
],
moduleNameMapper: {
// Handle CSS imports (with CSS modules)
// https://jestjs.io/docs/webpack#mocking-css-modules
'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',
// Handle CSS imports (without CSS modules)
'^.+\\.(css|sass|scss)$': '<rootDir>/__mocks__/styleMock.js',
// Handle image imports
// https://jestjs.io/docs/webpack#handling-static-assets
'^.+\\.(png|jpg|jpeg|gif|webp|avif|ico|bmp)$/i': `<rootDir>/__mocks__/fileMock.js`,
'^.+\\.svg$': `<rootDir>/__mocks__/svgMock.js`,
// Handle module aliases
'^app/(.*)$': '<rootDir>/src/app/$1',
'^api/(.*)$': '<rootDir>/src/api/$1',
'^components/(.*)$': '<rootDir>/src/components/$1',
'^constants/(.*)$': '<rootDir>/src/constants/$1',
'^fonts/(.*)$': '<rootDir>/src/fonts/$1',
'^hooks/(.*)$': '<rootDir>/src/hooks/$1',
'^pages/(.*)$': '<rootDir>/src/pages/$1',
'^store/(.*)$': '<rootDir>/src/store/$1',
'^styles/(.*)$': '<rootDir>/src/styles/$1',
'^types/(.*)$': '<rootDir>/src/types/$1',
'^utils/(.*)$': '<rootDir>/src/utils/$1',
'^store': '<rootDir>/src/store',
},
// Add more setup options before each test is run
setupFilesAfterEnv: [
'<rootDir>/jest.setup.js',
'<rootDir>/__mocks__/store.js',
'<rootDir>/__mocks__/helmet.js',
],
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/.next/'],
testEnvironment: 'jsdom',
transform: {
// Use babel-jest to transpile tests with the next/babel preset
// https://jestjs.io/docs/configuration#transform-objectstring-pathtotransformer--pathtotransformer-object
'^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
},
transformIgnorePatterns: ['/node_modules/', '^.+\\.module\\.(css|sass|scss)$'],
}
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom/extend-expect'
+23 -17
View File
@@ -1,12 +1,14 @@
{
"name": "mars-v2-frontend",
"version": "2.2.0",
"version": "2.0.6",
"private": true,
"scripts": {
"build": "yarn validate-env && next build",
"dev": "next dev",
"test": "jest",
"test:cov": "jest --coverage",
"lint": "eslint ./src/ && yarn prettier-check",
"format": "eslint ./src/ --fix && prettier --write ./src/ ",
"format": "eslint ./src/ ./__tests__/ --fix && prettier --write ./src/ ./__tests__/",
"prettier-check": "prettier --ignore-path .gitignore --check ./src/",
"start": "next start",
"validate-env": "node ./validate-env",
@@ -15,17 +17,17 @@
},
"lint-staged": {
"*.ts*": [
"eslint ./src/ --fix",
"prettier --write ./src/"
"eslint ./src/ ./__tests__/ --fix",
"prettier --write ./src/ ./__tests__/"
]
},
"dependencies": {
"@cosmjs/cosmwasm-stargate": "^0.32.2",
"@cosmjs/cosmwasm-stargate": "^0.31.1",
"@delphi-labs/shuttle-react": "^3.10.0",
"@keplr-wallet/cosmos": "^0.12.42",
"@sentry/nextjs": "^7.91.0",
"@sentry/nextjs": "^7.77.0",
"@splinetool/react-spline": "^2.2.6",
"@splinetool/runtime": "^0.9.521",
"@splinetool/runtime": "^0.9.482",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-table": "^8.10.6",
"@tippyjs/react": "^4.2.6",
@@ -41,36 +43,40 @@
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.6",
"react-helmet-async": "^2.0.3",
"react-helmet-async": "^1.3.0",
"react-qr-code": "^2.0.12",
"react-router-dom": "^6.17.0",
"react-spring": "^9.7.3",
"react-toastify": "^9.1.3",
"react-use-clipboard": "^1.0.9",
"recharts": "^2.10.1",
"sharp": "^0.33.1",
"swr": "^2.2.4",
"tailwind-scrollbar-hide": "^1.1.7",
"zustand": "^4.4.7"
"zustand": "^4.4.6"
},
"devDependencies": {
"@svgr/webpack": "^8.1.0",
"@types/debounce-promise": "^3.1.9",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.0.0",
"@types/debounce-promise": "^3.1.7",
"@types/lodash.debounce": "^4.0.9",
"@types/lodash.throttle": "^4.1.8",
"@types/node": "^20.10.6",
"@types/react": "18.2.41",
"@types/node": "^20.8.6",
"@types/react": "18.2.33",
"@types/react-dom": "18.2.15",
"@types/react-helmet": "^6.1.11",
"@types/react-helmet": "^6.1.9",
"autoprefixer": "^10.4.16",
"babel-jest": "^29.7.0",
"dotenv": "^16.3.1",
"dotenv-cli": "^7.3.0",
"eslint": "^8.56.0",
"eslint-config-next": "^14.0.4",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.0",
"eslint-plugin-import": "^2.29.0",
"husky": "^8.0.3",
"identity-obj-proxy": "^3.0.0",
"lint-staged": "^15.2.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^15.0.1",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.6",
"shelljs": "^0.8.5",
-16
View File
@@ -1,16 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 24 24">
<path
fill="#FFF"
d="M23.6,14.9C22,21.3,15.5,25.2,9,23.6C2.6,22-1.3,15.5,0.3,9.1S8.4-1.2,14.8,0.4C21.3,2,25.2,8.5,23.6,14.9
L23.6,14.9L23.6,14.9z"
/>
<path
fill="#000"
d="M12,15.1c-1.7,0-3.1-1.4-3.1-3.1c0-0.6,0.2-1.2,0.5-1.6L5.4,7.1v11.5h11.5l-3.3-3.9C13.1,15,12.5,15.1,12,15.1z
"
/>
<path
fill="#000"
d="M6.4,5.4l3.5,4.2c0.5-0.5,1.3-0.8,2-0.8c1.7,0,3.1,1.4,3.1,3.1c0,0.7-0.3,1.4-0.7,2l4.1,3.6V5.4H6.4z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

-10
View File
@@ -40,16 +40,6 @@
cursor: pointer;
}
.layout__area--center {
background: var(--tv-background) !important;
}
.chart-widget.chart-widget--themed-dark.chart-widget__top--themed-dark.chart-widget__bottom--themed-dark
> table
canvas {
background: transparent !important;
}
/* Floating menu */
.floating-toolbar-react-widgets__button:hover,
[class^='button-']:hover:before {
+4 -9
View File
@@ -3,25 +3,21 @@ import { getCreditManagerQueryClient } from 'api/cosmwasm-client'
import getDepositedVaults from 'api/vaults/getDepositedVaults'
import { BNCoin } from 'types/classes/BNCoin'
import { Positions } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { resolvePerpsPositions } from 'utils/resolvers'
export default async function getAccount(
chainConfig: ChainConfig,
accountId?: string,
): Promise<Account> {
export default async function getAccount(accountId?: string): Promise<Account> {
if (!accountId) return new Promise((_, reject) => reject('No account ID found'))
const creditManagerQueryClient = await getCreditManagerQueryClient(chainConfig)
const creditManagerQueryClient = await getCreditManagerQueryClient()
const accountPosition: Positions = await cacheFn(
() => creditManagerQueryClient.positions({ accountId: accountId }),
positionsCache,
`${chainConfig.id}/account/${accountId}`,
`account/${accountId}`,
)
const accountKind = await creditManagerQueryClient.accountKind({ accountId: accountId })
const depositedVaults = await getDepositedVaults(accountId, chainConfig, accountPosition)
const depositedVaults = await getDepositedVaults(accountId, accountPosition)
if (accountPosition) {
return {
@@ -30,7 +26,6 @@ export default async function getAccount(
lends: accountPosition.lends.map((lend) => new BNCoin(lend)),
deposits: accountPosition.deposits.map((deposit) => new BNCoin(deposit)),
vaults: depositedVaults,
perps: resolvePerpsPositions(accountPosition.perps),
kind: accountKind,
}
}
-1
View File
@@ -61,4 +61,3 @@ export const allParamsCache: Cache<AssetParamsBaseForAddr[]> = new Map()
export const underlyingDebtCache: Cache<string> = new Map()
export const previewDepositCache: Cache<{ vaultAddress: string; amount: string }> = new Map()
export const stakingAprCache: Cache<StakingApr[]> = new Map()
export const assetParamsCache: Cache<AssetParamsBaseForAddr[]> = new Map()
+59 -104
View File
@@ -1,5 +1,6 @@
import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { ENV } from 'constants/env'
import { ICNSQueryClient } from 'types/classes/ICNSClient.client'
import { MarsAccountNftQueryClient } from 'types/generated/mars-account-nft/MarsAccountNft.client'
import { MarsCreditManagerQueryClient } from 'types/generated/mars-credit-manager/MarsCreditManager.client'
@@ -7,187 +8,142 @@ import { MarsIncentivesQueryClient } from 'types/generated/mars-incentives/MarsI
import { MarsMockVaultQueryClient } from 'types/generated/mars-mock-vault/MarsMockVault.client'
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'
let _cosmWasmClient: Map<string, CosmWasmClient> = new Map()
let _accountNftQueryClient: Map<string, MarsAccountNftQueryClient> = new Map()
let _creditManagerQueryClient: Map<string, MarsCreditManagerQueryClient> = new Map()
let _oracleQueryClient: Map<string, MarsOracleOsmosisQueryClient> = new Map()
let _redBankQueryClient: Map<string, MarsRedBankQueryClient> = new Map()
let _paramsQueryClient: Map<string, MarsParamsQueryClient> = new Map()
let _incentivesQueryClient: Map<string, MarsIncentivesQueryClient> = new Map()
let _swapperOsmosisClient: Map<string, MarsSwapperOsmosisQueryClient> = new Map()
let _perpsClient: Map<string, MarsPerpsQueryClient> = new Map()
let _ICNSQueryClient: Map<string, ICNSQueryClient> = new Map()
let _cosmWasmClient: CosmWasmClient
let _accountNftQueryClient: MarsAccountNftQueryClient
let _creditManagerQueryClient: MarsCreditManagerQueryClient
let _oracleQueryClient: MarsOracleOsmosisQueryClient
let _redBankQueryClient: MarsRedBankQueryClient
let _paramsQueryClient: MarsParamsQueryClient
let _incentivesQueryClient: MarsIncentivesQueryClient
let _swapperOsmosisClient: MarsSwapperOsmosisQueryClient
let _ICNSQueryClient: ICNSQueryClient
const getClient = async (rpc: string) => {
const getClient = async () => {
try {
if (!_cosmWasmClient.get(rpc)) {
const client = await CosmWasmClient.connect(rpc)
_cosmWasmClient.set(rpc, client)
if (!_cosmWasmClient) {
_cosmWasmClient = await CosmWasmClient.connect(ENV.URL_RPC)
}
return _cosmWasmClient.get(rpc)!
return _cosmWasmClient
} catch (error) {
throw error
}
}
const getAccountNftQueryClient = async (chainConfig: ChainConfig) => {
const getAccountNftQueryClient = async () => {
try {
const contract = chainConfig.contracts.accountNft
const rpc = chainConfig.endpoints.rpc
const key = rpc + contract
if (!_accountNftQueryClient.get(key)) {
const client = await getClient(rpc)
_accountNftQueryClient.set(key, new MarsAccountNftQueryClient(client, contract))
if (!_accountNftQueryClient) {
const client = await getClient()
_accountNftQueryClient = new MarsAccountNftQueryClient(client, ENV.ADDRESS_ACCOUNT_NFT)
}
return _accountNftQueryClient.get(key)!
return _accountNftQueryClient
} catch (error) {
throw error
}
}
const getCreditManagerQueryClient = async (chainConfig: ChainConfig) => {
const getCreditManagerQueryClient = async () => {
try {
const contract = chainConfig.contracts.creditManager
const rpc = chainConfig.endpoints.rpc
const key = rpc + contract
if (!_creditManagerQueryClient.get(key)) {
const client = await getClient(rpc)
_creditManagerQueryClient.set(key, new MarsCreditManagerQueryClient(client, contract))
if (!_creditManagerQueryClient) {
const client = await getClient()
_creditManagerQueryClient = new MarsCreditManagerQueryClient(
client,
ENV.ADDRESS_CREDIT_MANAGER,
)
}
return _creditManagerQueryClient.get(key)!
return _creditManagerQueryClient
} catch (error) {
throw error
}
}
const getParamsQueryClient = async (chainConfig: ChainConfig) => {
const getParamsQueryClient = async () => {
try {
const contract = chainConfig.contracts.params
const rpc = chainConfig.endpoints.rpc
const key = rpc + contract
if (!_paramsQueryClient.get(key)) {
const client = await getClient(rpc)
_paramsQueryClient.set(key, new MarsParamsQueryClient(client, contract))
if (!_paramsQueryClient) {
const client = await getClient()
_paramsQueryClient = new MarsParamsQueryClient(client, ENV.ADDRESS_PARAMS)
}
return _paramsQueryClient.get(key)!
return _paramsQueryClient
} catch (error) {
throw error
}
}
const getOracleQueryClient = async (chainConfig: ChainConfig) => {
const getOracleQueryClient = async () => {
try {
const contract = chainConfig.contracts.oracle
const rpc = chainConfig.endpoints.rpc
const key = rpc + contract
if (!_oracleQueryClient.get(key)) {
const client = await getClient(rpc)
_oracleQueryClient.set(key, new MarsOracleOsmosisQueryClient(client, contract))
if (!_oracleQueryClient) {
const client = await getClient()
_oracleQueryClient = new MarsOracleOsmosisQueryClient(client, ENV.ADDRESS_ORACLE)
}
return _oracleQueryClient.get(key)!
return _oracleQueryClient
} catch (error) {
throw error
}
}
const getRedBankQueryClient = async (chainConfig: ChainConfig) => {
const getRedBankQueryClient = async () => {
try {
const contract = chainConfig.contracts.redBank
const rpc = chainConfig.endpoints.rpc
const key = rpc + contract
if (!_redBankQueryClient.get(key)) {
const client = await getClient(rpc)
_redBankQueryClient.set(key, new MarsRedBankQueryClient(client, contract))
if (!_redBankQueryClient) {
const client = await getClient()
_redBankQueryClient = new MarsRedBankQueryClient(client, ENV.ADDRESS_RED_BANK)
}
return _redBankQueryClient.get(key)!
return _redBankQueryClient
} catch (error) {
throw error
}
}
const getVaultQueryClient = async (chainConfig: ChainConfig, address: string) => {
const getVaultQueryClient = async (address: string) => {
try {
const client = await getClient(chainConfig.endpoints.rpc)
const client = await getClient()
return new MarsMockVaultQueryClient(client, address)
} catch (error) {
throw error
}
}
const getIncentivesQueryClient = async (chainConfig: ChainConfig) => {
const getIncentivesQueryClient = async () => {
try {
const contract = chainConfig.contracts.incentives
const rpc = chainConfig.endpoints.rpc
const key = rpc + contract
if (!_incentivesQueryClient.get(key)) {
const client = await getClient(rpc)
_incentivesQueryClient.set(key, new MarsIncentivesQueryClient(client, contract))
if (!_incentivesQueryClient) {
const client = await getClient()
_incentivesQueryClient = new MarsIncentivesQueryClient(client, ENV.ADDRESS_INCENTIVES)
}
return _incentivesQueryClient.get(key)!
return _incentivesQueryClient
} catch (error) {
throw error
}
}
const getSwapperQueryClient = async (chainConfig: ChainConfig) => {
const getSwapperQueryClient = async () => {
try {
const contract = chainConfig.contracts.swapper
const rpc = chainConfig.endpoints.rpc
const key = rpc + contract
if (!_swapperOsmosisClient.get(key)) {
const client = await getClient(rpc)
_swapperOsmosisClient.set(key, new MarsSwapperOsmosisQueryClient(client, contract))
if (!_swapperOsmosisClient) {
const client = await getClient()
_swapperOsmosisClient = new MarsSwapperOsmosisQueryClient(client, ENV.ADDRESS_SWAPPER)
}
return _swapperOsmosisClient.get(key)!
return _swapperOsmosisClient
} catch (error) {
throw error
}
}
const getPerpsQueryClient = async (chainConfig: ChainConfig) => {
const getICNSQueryClient = async () => {
try {
const contract = chainConfig.contracts.perps
const rpc = chainConfig.endpoints.rpc
const key = rpc + contract
if (!_perpsClient.get(key)) {
const client = await getClient(rpc)
_perpsClient.set(key, new MarsPerpsQueryClient(client, contract))
if (!_ICNSQueryClient) {
const client = await getClient()
_ICNSQueryClient = new ICNSQueryClient(client)
}
return _perpsClient.get(key)!
} catch (error) {
throw error
}
}
const getICNSQueryClient = async (chainConfig: ChainConfig) => {
try {
const contract = chainConfig.contracts.params
const rpc = chainConfig.endpoints.rpc
const key = rpc + contract
if (!_ICNSQueryClient.get(key)) {
const client = await getClient(rpc)
_ICNSQueryClient.set(key, new ICNSQueryClient(client))
}
return _ICNSQueryClient.get(key)!
return _ICNSQueryClient
} catch (error) {
throw error
}
@@ -204,5 +160,4 @@ export {
getRedBankQueryClient,
getSwapperQueryClient,
getVaultQueryClient,
getPerpsQueryClient,
}
+6 -3
View File
@@ -1,8 +1,9 @@
import { cacheFn, stakingAprCache } from 'api/cache'
import { ENV } from 'constants/env'
export default async function getStakingAprs(url: string) {
export default async function getStakingAprs() {
try {
return cacheFn(() => fetchAprs(url), stakingAprCache, `stakingAprs`)
return cacheFn(() => fetchAprs(), stakingAprCache, `stakingAprs`)
} catch (error) {
throw error
}
@@ -12,7 +13,9 @@ interface StakingAprResponse {
stats: StakingApr[]
}
async function fetchAprs(url: string): Promise<StakingApr[]> {
async function fetchAprs(): Promise<StakingApr[]> {
const url = ENV.STRIDE_APRS
const response = await fetch(url)
const body = (await response.json()) as StakingAprResponse
return body.stats
+5 -10
View File
@@ -4,13 +4,12 @@ import getAccounts from 'api/wallets/getAccounts'
import { calculateAccountLeverage, getAccountPositionValues, isAccountEmpty } from 'utils/accounts'
export default async function getHLSStakingAccounts(
chainConfig: ChainConfig,
address?: string,
): Promise<HLSAccountWithStrategy[]> {
const accounts = await getAccounts('high_levered_strategy', chainConfig, address)
const accounts = await getAccounts('high_levered_strategy', address)
const activeAccounts = accounts.filter((account) => !isAccountEmpty(account))
const hlsStrategies = await getHLSStakingAssets(chainConfig)
const prices = await getPrices(chainConfig)
const hlsStrategies = await getHLSStakingAssets()
const prices = await getPrices()
const hlsAccountsWithStrategy: HLSAccountWithStrategy[] = []
activeAccounts.forEach((account) => {
@@ -22,11 +21,7 @@ export default async function getHLSStakingAccounts(
if (!strategy) return
const [deposits, lends, debts, vaults] = getAccountPositionValues(
account,
prices,
chainConfig.assets,
)
const [deposits, lends, debts, vaults] = getAccountPositionValues(account, prices)
hlsAccountsWithStrategy.push({
...account,
@@ -36,7 +31,7 @@ export default async function getHLSStakingAccounts(
debt: debts,
total: deposits,
},
leverage: calculateAccountLeverage(account, prices, chainConfig.assets).toNumber(),
leverage: calculateAccountLeverage(account, prices).toNumber(),
})
})
+8 -9
View File
@@ -1,29 +1,28 @@
import { getParamsQueryClient } from 'api/cosmwasm-client'
import getStakingAprs from 'api/hls/getAprs'
import getAssetParams from 'api/params/getAssetParams'
import { byDenom } from 'utils/array'
import { getAssetByDenom, getStakingAssets } from 'utils/assets'
import { BN } from 'utils/helpers'
import { resolveHLSStrategies } from 'utils/resolvers'
export default async function getHLSStakingAssets(chainConfig: ChainConfig) {
const stakingAssetDenoms = chainConfig.assets
.filter((asset) => asset.isStaking)
.map((asset) => asset.denom)
const assetParams = await getAssetParams(chainConfig)
export default async function getHLSStakingAssets() {
const stakingAssetDenoms = getStakingAssets().map((asset) => asset.denom)
const assetParams = await getAssetParams()
const HLSAssets = assetParams
.filter((asset) => stakingAssetDenoms.includes(asset.denom))
.filter((asset) => asset.credit_manager.hls)
const strategies = resolveHLSStrategies('coin', HLSAssets)
const client = await getParamsQueryClient(chainConfig)
const client = await getParamsQueryClient()
const depositCaps$ = strategies.map((strategy) =>
client.totalDeposit({ denom: strategy.denoms.deposit }),
)
const aprs = await getStakingAprs(chainConfig.endpoints.aprs.stride)
const aprs = await getStakingAprs()
return Promise.all(depositCaps$).then((depositCaps) => {
return depositCaps.map((depositCap, index) => {
const borrowSymbol = chainConfig.assets.find(byDenom(strategies[index].denoms.borrow))?.symbol
const borrowSymbol = getAssetByDenom(strategies[index].denoms.borrow)?.symbol
return {
...strategies[index],
depositCap: {
+4 -4
View File
@@ -4,10 +4,10 @@ import { getVaultConfigs } from 'api/vaults/getVaultConfigs'
import { BN } from 'utils/helpers'
import { resolveHLSStrategies } from 'utils/resolvers'
export default async function getHLSVaults(chainConfig: ChainConfig) {
const assetParams = await getAssetParams(chainConfig)
const client = await getCreditManagerQueryClient(chainConfig)
const vaultConfigs = await getVaultConfigs(chainConfig)
export default async function getHLSVaults() {
const assetParams = await getAssetParams()
const client = await getCreditManagerQueryClient()
const vaultConfigs = await getVaultConfigs()
const HLSAssets = assetParams.filter((asset) => asset.credit_manager.hls)
const strategies = resolveHLSStrategies('vault', HLSAssets)
@@ -1,35 +1,35 @@
import getTotalActiveEmissionValue from 'api/incentives/getTotalActiveEmissionValue'
import getMarket from 'api/markets/getMarket'
import getTotalActiveEmissionValue from 'api/incentives/getTotalActiveEmissionValue'
import getUnderlyingLiquidityAmount from 'api/markets/getMarketUnderlyingLiquidityAmount'
import getPrice from 'api/prices/getPrice'
import { byDenom } from 'utils/array'
import { SECONDS_IN_A_YEAR } from 'utils/constants'
import { BN } from 'utils/helpers'
import { SECONDS_IN_A_YEAR } from 'utils/constants'
import getPrice from 'api/prices/getPrice'
import { ASSETS } from 'constants/assets'
import { byDenom } from 'utils/array'
export default async function calculateAssetIncentivesApy(
chainConfig: ChainConfig,
denom: string,
): Promise<BigNumber | null> {
try {
const [totalActiveEmissionValue, market] = await Promise.all([
getTotalActiveEmissionValue(chainConfig, denom),
getMarket(chainConfig, denom),
getTotalActiveEmissionValue(denom),
getMarket(denom),
])
if (!totalActiveEmissionValue) return null
const [marketLiquidityAmount, assetPrice] = await Promise.all([
getUnderlyingLiquidityAmount(chainConfig, market),
getPrice(chainConfig, denom),
getUnderlyingLiquidityAmount(market),
getPrice(denom),
])
const assetDecimals = (chainConfig.assets.find(byDenom(denom)) as Asset).decimals
const assetDecimals = (ASSETS.find(byDenom(denom)) as Asset).decimals
const marketLiquidityValue = BN(marketLiquidityAmount)
.shiftedBy(-assetDecimals)
.multipliedBy(assetPrice)
const marketReturns = BN(market.apy.deposit).multipliedBy(marketLiquidityValue)
const marketReturns = BN(market.liquidityRate).multipliedBy(marketLiquidityValue)
const annualEmission = totalActiveEmissionValue.multipliedBy(SECONDS_IN_A_YEAR)
const totalAnnualReturnsValue = annualEmission.plus(marketReturns)
@@ -1,16 +1,16 @@
import { cacheFn, emissionsCache } from 'api/cache'
import { getIncentivesQueryClient } from 'api/cosmwasm-client'
import getPrice from 'api/prices/getPrice'
import { ASSETS } from 'constants/assets'
import { BN_ZERO } from 'constants/math'
import { byDenom } from 'utils/array'
import { BN } from 'utils/helpers'
export default async function getTotalActiveEmissionValue(
chainConfig: ChainConfig,
denom: string,
): Promise<BigNumber | null> {
try {
const client = await getIncentivesQueryClient(chainConfig)
const client = await getIncentivesQueryClient()
const activeEmissions = await cacheFn(
() =>
client.activeEmissions({
@@ -26,12 +26,12 @@ export default async function getTotalActiveEmissionValue(
}
const prices = await Promise.all(
activeEmissions.map((activeEmission) => getPrice(chainConfig, activeEmission.denom)),
activeEmissions.map((activeEmission) => getPrice(activeEmission.denom)),
)
return activeEmissions.reduce((accumulation, current, index) => {
const price = prices[index]
const decimals = chainConfig.assets.find(byDenom(current.denom))?.decimals as number
const decimals = ASSETS.find(byDenom(current.denom))?.decimals as number
const emissionValue = BN(current.emission_rate).shiftedBy(-decimals).multipliedBy(price)
return accumulation.plus(emissionValue)
+4 -6
View File
@@ -1,19 +1,17 @@
import { cacheFn, unclaimedRewardsCache } from 'api/cache'
import { getIncentivesQueryClient } from 'api/cosmwasm-client'
import { ENV } from 'constants/env'
import { BNCoin } from 'types/classes/BNCoin'
import iterateContractQuery from 'utils/iterateContractQuery'
export default async function getUnclaimedRewards(
chainConfig: ChainConfig,
accountId: string,
): Promise<BNCoin[]> {
export default async function getUnclaimedRewards(accountId: string): Promise<BNCoin[]> {
try {
const client = await getIncentivesQueryClient(chainConfig)
const client = await getIncentivesQueryClient()
const unclaimedRewards = await cacheFn(
() =>
iterateContractQuery(() =>
client.userUnclaimedRewards({
user: chainConfig.contracts.creditManager,
user: ENV.ADDRESS_CREDIT_MANAGER,
accountId,
}),
),
+5 -5
View File
@@ -2,14 +2,14 @@ import { cacheFn, marketCache } from 'api/cache'
import { getParamsQueryClient, getRedBankQueryClient } from 'api/cosmwasm-client'
import { resolveMarketResponse } from 'utils/resolvers'
export default async function getMarket(chainConfig: ChainConfig, denom: string): Promise<Market> {
return cacheFn(() => fetchMarket(chainConfig, denom), marketCache, denom, 60)
export default async function getMarket(denom: string): Promise<Market> {
return cacheFn(() => fetchMarket(denom), marketCache, denom, 60)
}
async function fetchMarket(chainConfig: ChainConfig, denom: string) {
async function fetchMarket(denom: string) {
try {
const redBankClient = await getRedBankQueryClient(chainConfig)
const paramsClient = await getParamsQueryClient(chainConfig)
const redBankClient = await getRedBankQueryClient()
const paramsClient = await getParamsQueryClient()
const [market, assetParams, assetCap] = await Promise.all([
redBankClient.market({ denom }),
+8 -10
View File
@@ -1,25 +1,23 @@
import getMarketLiquidities from 'api/markets/getMarketLiquidities'
import getMarkets from 'api/markets/getMarkets'
import getPrices from 'api/prices/getPrices'
import { getEnabledMarketAssets } from 'utils/assets'
import { BN } from 'utils/helpers'
export default async function getMarketBorrowings(
chainConfig: ChainConfig,
): Promise<BorrowAsset[]> {
const liquidities = await getMarketLiquidities(chainConfig)
const borrowEnabledMarkets = (await getMarkets(chainConfig)).filter(
(market: Market) => market.borrowEnabled,
)
const prices = await getPrices(chainConfig)
export default async function getMarketBorrowings(): Promise<BorrowAsset[]> {
const liquidities = await getMarketLiquidities()
const enabledAssets = getEnabledMarketAssets()
const borrowEnabledMarkets = (await getMarkets()).filter((market: Market) => market.borrowEnabled)
const prices = await getPrices()
const borrow: BorrowAsset[] = borrowEnabledMarkets.map((market) => {
const price = prices.find((coin) => coin.denom === market.denom)?.amount ?? '1'
const amount = liquidities.find((coin) => coin.denom === market.denom)?.amount ?? '0'
const asset = chainConfig.assets.find((asset) => asset.denom === market.denom)!
const asset = enabledAssets.find((asset) => asset.denom === market.denom)!
return {
...asset,
borrowRate: market.apy.borrow ?? 0,
borrowRate: market.borrowRate ?? 0,
liquidity: {
amount: BN(amount),
value: BN(amount).multipliedBy(price),
+3 -3
View File
@@ -3,10 +3,10 @@ import { getRedBankQueryClient } from 'api/cosmwasm-client'
import getMarkets from 'api/markets/getMarkets'
import { BNCoin } from 'types/classes/BNCoin'
export default async function getMarketDebts(chainConfig: ChainConfig): Promise<BNCoin[]> {
export default async function getMarketDebts(): Promise<BNCoin[]> {
try {
const markets: Market[] = await getMarkets(chainConfig)
const redBankQueryClient = await getRedBankQueryClient(chainConfig)
const markets: Market[] = await getMarkets()
const redBankQueryClient = await getRedBankQueryClient()
const debtQueries = markets.map((asset) =>
cacheFn(
+3 -5
View File
@@ -2,12 +2,10 @@ import getMarkets from 'api/markets/getMarkets'
import getUnderlyingLiquidityAmount from 'api/markets/getMarketUnderlyingLiquidityAmount'
import { BNCoin } from 'types/classes/BNCoin'
export default async function getMarketDeposits(chainConfig: ChainConfig): Promise<BNCoin[]> {
export default async function getMarketDeposits(): Promise<BNCoin[]> {
try {
const markets: Market[] = await getMarkets(chainConfig)
const depositQueries = markets.map((market) =>
getUnderlyingLiquidityAmount(chainConfig, market),
)
const markets: Market[] = await getMarkets()
const depositQueries = markets.map(getUnderlyingLiquidityAmount)
const depositsResults = await Promise.all(depositQueries)
return depositsResults.map<BNCoin>(
+5 -4
View File
@@ -1,10 +1,11 @@
import getMarketDebts from 'api/markets/getMarketDebts'
import { BN } from 'utils/helpers'
import getMarketDeposits from 'api/markets/getMarketDeposits'
import getMarketDebts from 'api/markets/getMarketDebts'
import { BNCoin } from 'types/classes/BNCoin'
export default async function getMarketLiquidities(chainConfig: ChainConfig): Promise<BNCoin[]> {
const deposits = await getMarketDeposits(chainConfig)
const debts = await getMarketDebts(chainConfig)
export default async function getMarketLiquidities(): Promise<BNCoin[]> {
const deposits = await getMarketDeposits()
const debts = await getMarketDebts()
const liquidity: BNCoin[] = deposits.map((deposit) => {
const debt = debts.find((debt) => debt.denom === deposit.denom)
@@ -1,21 +1,18 @@
import { cacheFn, underlyingLiquidityAmountCache } from 'api/cache'
import { getRedBankQueryClient } from 'api/cosmwasm-client'
export default async function getUnderlyingLiquidityAmount(
chainConfig: ChainConfig,
market: Market,
): Promise<string> {
export default async function getUnderlyingLiquidityAmount(market: Market): Promise<string> {
return cacheFn(
() => fetchUnderlyingLiquidityAmount(chainConfig, market),
() => fetchUnderlyingLiquidityAmount(market),
underlyingLiquidityAmountCache,
`underlyingLiquidity/${market.denom}/amount/${market.collateralTotalScaled}`,
60,
)
}
async function fetchUnderlyingLiquidityAmount(chainConfig: ChainConfig, market: Market) {
async function fetchUnderlyingLiquidityAmount(market: Market) {
try {
const client = await getRedBankQueryClient(chainConfig)
const client = await getRedBankQueryClient()
return await client.underlyingLiquidityAmount({
denom: market.denom,
amountScaled: market.collateralTotalScaled,
+14 -25
View File
@@ -6,46 +6,36 @@ import {
} from 'types/generated/mars-params/MarsParams.types'
import { Market as RedBankMarket } from 'types/generated/mars-red-bank/MarsRedBank.types'
import { byDenom } from 'utils/array'
import { getEnabledMarketAssets } from 'utils/assets'
import iterateContractQuery from 'utils/iterateContractQuery'
import { resolveMarketResponse } from 'utils/resolvers'
export default async function getMarkets(chainConfig: ChainConfig): Promise<Market[]> {
export default async function getMarkets(): Promise<Market[]> {
try {
const redBankClient = await getRedBankQueryClient(chainConfig)
const paramsClient = await getParamsQueryClient(chainConfig)
const redBankClient = await getRedBankQueryClient()
const paramsClient = await getParamsQueryClient()
const marketAssets = chainConfig.assets.filter((asset) => asset.isMarket)
const capQueries = marketAssets
.filter((asset) => asset.isMarket)
.map((asset) =>
cacheFn(
() => paramsClient.totalDeposit({ denom: asset.denom }),
totalDepositCache,
`chains/${chainConfig.id}/enabledMarkets/${asset.denom}`,
60,
),
)
const caps = await Promise.all(capQueries)
const [markets, assetParams, assetCaps] = await Promise.all([
const enabledAssets = getEnabledMarketAssets()
const capQueries = enabledAssets.map((asset) =>
cacheFn(
() => iterateContractQuery(redBankClient.markets),
marketsCache,
`chains/${chainConfig.id}/markets`,
() => paramsClient.totalDeposit({ denom: asset.denom }),
totalDepositCache,
`enabledMarkets/${asset.denom}`,
60,
),
)
const [markets, assetParams, assetCaps] = await Promise.all([
cacheFn(() => iterateContractQuery(redBankClient.markets), marketsCache, 'markets', 60),
cacheFn(
async () => await iterateContractQuery(paramsClient.allAssetParams),
allParamsCache,
`chains/${chainConfig.id}/params`,
'params',
60,
),
Promise.all(capQueries),
])
return marketAssets.map((asset) =>
return enabledAssets.map((asset) =>
resolveMarketResponse(
markets.find(byDenom(asset.denom)) as RedBankMarket,
assetParams.find(byDenom(asset.denom)) as AssetParams,
@@ -53,7 +43,6 @@ export default async function getMarkets(chainConfig: ChainConfig): Promise<Mark
),
)
} catch (ex) {
console.log(ex)
throw ex
}
}
+3 -13
View File
@@ -1,21 +1,11 @@
import { assetParamsCache, cacheFn } from 'api/cache'
import { getParamsQueryClient } from 'api/cosmwasm-client'
import { AssetParamsBaseForAddr } from 'types/generated/mars-params/MarsParams.types'
import iterateContractQuery from 'utils/iterateContractQuery'
export default async function getAssetParams(
chainConfig: ChainConfig,
): Promise<AssetParamsBaseForAddr[]> {
export default async function getAssetParams(): Promise<AssetParamsBaseForAddr[]> {
try {
return await cacheFn(
async () => {
const paramsQueryClient = await getParamsQueryClient(chainConfig)
return iterateContractQuery(paramsQueryClient.allAssetParams)
},
assetParamsCache,
`${chainConfig.id}/assetParams`,
600,
)
const paramsQueryClient = await getParamsQueryClient()
return iterateContractQuery(paramsQueryClient.allAssetParams)
} catch (ex) {
throw ex
}
-10
View File
@@ -1,10 +0,0 @@
import { getPerpsQueryClient } from 'api/cosmwasm-client'
import { BNCoin } from 'types/classes/BNCoin'
export default async function getOpeningFee(chainConfig: ChainConfig, denom: string, size: string) {
const perpsClient = await getPerpsQueryClient(chainConfig)
return perpsClient
.openingFee({ denom, size: size as any })
.then((resp) => BNCoin.fromCoin(resp.fee))
}
+4 -3
View File
@@ -1,10 +1,11 @@
import getPoolPrice from 'api/prices/getPoolPrice'
import { ASSETS } from 'constants/assets'
import { bySymbol } from 'utils/array'
async function getMarsPrice(chainConfig: ChainConfig) {
const marsAsset = chainConfig.assets.find(bySymbol('MARS'))
async function getMarsPrice() {
const marsAsset = ASSETS.find(bySymbol('MARS'))
if (!marsAsset) return 0
return await getPoolPrice(chainConfig, marsAsset)
return await getPoolPrice(marsAsset)
}
export default getMarsPrice
+4 -8
View File
@@ -1,6 +1,5 @@
import { cacheFn, oraclePriceCache } from 'api/cache'
import { getOracleQueryClient } from 'api/cosmwasm-client'
import { BN_ZERO } from 'constants/math'
import { PRICE_ORACLE_DECIMALS } from 'constants/query'
import { BNCoin } from 'types/classes/BNCoin'
import { PriceResponse } from 'types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types'
@@ -8,18 +7,15 @@ import { byDenom } from 'utils/array'
import { BN } from 'utils/helpers'
import iterateContractQuery from 'utils/iterateContractQuery'
export default async function getOraclePrices(
chainConfig: ChainConfig,
assets: Asset[],
): Promise<BNCoin[]> {
export default async function getOraclePrices(...assets: Asset[]): Promise<BNCoin[]> {
try {
if (!assets.length) return []
const oracleQueryClient = await getOracleQueryClient(chainConfig)
const oracleQueryClient = await getOracleQueryClient()
const priceResults = await cacheFn(
() => iterateContractQuery(oracleQueryClient.prices),
oraclePriceCache,
`${chainConfig.id}/oraclePrices`,
'oraclePrices',
60,
)
@@ -28,7 +24,7 @@ export default async function getOraclePrices(
const decimalDiff = asset.decimals - PRICE_ORACLE_DECIMALS
return BNCoin.fromDenomAndBigNumber(
asset.denom,
BN(priceResponse?.price ?? BN_ZERO).shiftedBy(decimalDiff),
BN(priceResponse.price).shiftedBy(decimalDiff),
)
})
} catch (ex) {
+6 -6
View File
@@ -1,5 +1,6 @@
import { cacheFn, poolPriceCache } from 'api/cache'
import getPrice from 'api/prices/getPrice'
import { ENV } from 'constants/env'
import { BN_ONE } from 'constants/math'
import { BNCoin } from 'types/classes/BNCoin'
import { byDenom, byTokenDenom, partition } from 'utils/array'
@@ -16,29 +17,28 @@ interface PoolAsset {
}
export default async function getPoolPrice(
chainConfig: ChainConfig,
asset: Asset,
lookupPricesForBaseAsset?: BNCoin[],
): Promise<BigNumber> {
if (!asset.poolId) throw 'given asset should have a poolId to fetch the price'
const [assetRate, baseAsset] = await getAssetRate(chainConfig, asset)
const [assetRate, baseAsset] = await getAssetRate(asset)
const baseAssetPrice =
(lookupPricesForBaseAsset &&
lookupPricesForBaseAsset.find(byDenom(baseAsset.token.denom))?.amount) ||
(await getPrice(chainConfig, baseAsset.token.denom))
(await getPrice(baseAsset.token.denom))
if (!baseAssetPrice) throw 'base asset price must be available on Pyth or in Oracle contract'
return assetRate.multipliedBy(baseAssetPrice)
}
const getAssetRate = async (chainConfig: ChainConfig, asset: Asset) => {
const url = chainConfig.endpoints.pools.replace('POOL_ID', asset.poolId!.toString())
const getAssetRate = async (asset: Asset) => {
const url = `${ENV.URL_REST}osmosis/gamm/v1beta1/pools/${asset.poolId}`
const response = await cacheFn(
() => fetch(url).then((res) => res.json()),
poolPriceCache,
`${chainConfig.id}/poolPrices/${(asset.poolId || 0).toString()}`,
`poolPrices/${(asset.poolId || 0).toString()}`,
60,
)
const pool = response.pool
+27 -10
View File
@@ -1,20 +1,37 @@
import { cacheFn, priceCache } from 'api/cache'
import getPrices from 'api/prices/getPrices'
import { BN_ZERO } from 'constants/math'
import { getOracleQueryClient } from 'api/cosmwasm-client'
import getPoolPrice from 'api/prices/getPoolPrice'
import getPythPrice from 'api/prices/getPythPrices'
import { ASSETS } from 'constants/assets'
import { PRICE_ORACLE_DECIMALS } from 'constants/query'
import { byDenom } from 'utils/array'
import { BN } from 'utils/helpers'
export default async function getPrice(
chainConfig: ChainConfig,
denom: string,
): Promise<BigNumber> {
return cacheFn(() => fetchPrice(chainConfig, denom), priceCache, `price/${denom}`, 60)
export default async function getPrice(denom: string): Promise<BigNumber> {
return cacheFn(() => fetchPrice(denom), priceCache, `price/${denom}`, 60)
}
async function fetchPrice(chainConfig: ChainConfig, denom: string) {
async function fetchPrice(denom: string) {
try {
const prices = await getPrices(chainConfig)
const asset = ASSETS.find(byDenom(denom)) as Asset
return prices.find(byDenom(denom))?.amount ?? BN_ZERO
if (asset.pythPriceFeedId) {
return (await getPythPrice(asset.pythPriceFeedId))[0]
}
if (asset.hasOraclePrice) {
const oracleQueryClient = await getOracleQueryClient()
const priceResponse = await oracleQueryClient.price({ denom: asset.denom })
const decimalDiff = asset.decimals - PRICE_ORACLE_DECIMALS
return BN(priceResponse.price).shiftedBy(decimalDiff)
}
if (asset.poolId) {
return await getPoolPrice(asset)
}
throw `could not fetch the price info for the given denom: ${denom}`
} catch (ex) {
throw ex
}
+6 -4
View File
@@ -1,9 +1,11 @@
import fetchPythPriceData from 'api/prices/getPythPriceData'
import { getPythAssets } from 'utils/assets'
export default async function getPricesData(assets: Asset[]): Promise<string[]> {
export default async function getPricesData(): Promise<string[]> {
try {
const assetsWithPythPriceFeedId = assets.filter((asset) => !!asset.pythPriceFeedId)
return await requestPythPriceData(assetsWithPythPriceFeedId)
const assetsWithPythPriceFeedId = getPythAssets()
const pythAndOraclePriceData = await requestPythPriceData(assetsWithPythPriceFeedId)
return pythAndOraclePriceData
} catch (ex) {
console.error(ex)
throw ex
@@ -14,5 +16,5 @@ async function requestPythPriceData(assets: Asset[]): Promise<string[]> {
if (!assets.length) return []
const priceFeedIds = assets.map((a) => a.pythPriceFeedId) as string[]
return await fetchPythPriceData(priceFeedIds)
return await fetchPythPriceData(...priceFeedIds)
}
+28 -36
View File
@@ -1,35 +1,29 @@
import getOraclePrices from 'api/prices/getOraclePrices'
import getPoolPrice from 'api/prices/getPoolPrice'
import fetchPythPrices from 'api/prices/getPythPrices'
import chains from 'configs/chains'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { partition } from 'utils/array'
import { getAllAssetsWithPythId } from 'utils/assets'
import { getAssetsMustHavePriceInfo } from 'utils/assets'
export default async function getPrices(chainConfig: ChainConfig): Promise<BNCoin[]> {
export default async function getPrices(): Promise<BNCoin[]> {
const usdPrice = new BNCoin({ denom: 'usd', amount: '1' })
const pythAndOraclePrices = []
const assetsToFetchPrices = useStore
.getState()
.chainConfig.assets.filter(
(asset) => (asset.isEnabled && asset.isMarket) || asset.forceFetchPrice,
)
const assetsWithPythPriceFeedId = getAllAssetsWithPythId(chains)
const pythPrices = await requestPythPrices(assetsWithPythPriceFeedId)
pythAndOraclePrices.push(...pythPrices)
try {
const [assetsWithOraclePrices, assetsWithPoolIds] =
const assetsToFetchPrices = getAssetsMustHavePriceInfo()
const [assetsWithPythPriceFeedId, assetsWithOraclePrices, assetsWithPoolIds] =
separateAssetsByPriceSources(assetsToFetchPrices)
const oraclePrices = await getOraclePrices(chainConfig, assetsWithOraclePrices)
const poolPrices = await requestPoolPrices(chainConfig, assetsWithPoolIds, pythAndOraclePrices)
if (oraclePrices) useStore.setState({ isOracleStale: false })
const pythAndOraclePrices = (
await Promise.all([
requestPythPrices(assetsWithPythPriceFeedId),
getOraclePrices(...assetsWithOraclePrices),
])
).flat()
const poolPrices = await requestPoolPrices(assetsWithPoolIds, pythAndOraclePrices)
return [...pythAndOraclePrices, ...oraclePrices, ...poolPrices, usdPrice]
useStore.setState({ isOracleStale: false })
return [...pythAndOraclePrices, ...poolPrices, usdPrice]
} catch (ex) {
console.error(ex)
let message = 'Unknown Error'
@@ -37,43 +31,41 @@ export default async function getPrices(chainConfig: ChainConfig): Promise<BNCoi
if (message.includes('price publish time is too old'))
useStore.setState({ isOracleStale: true })
return [...pythAndOraclePrices, usdPrice]
throw ex
}
}
async function requestPythPrices(assets: Asset[]): Promise<BNCoin[]> {
if (!assets.length) return []
const priceFeedIds = assets
.map((a) => a.pythPriceFeedId)
.filter((priceFeedId, index, array) => array.indexOf(priceFeedId) === index) as string[]
return await fetchPythPrices(priceFeedIds, assets)
const priceFeedIds = assets.map((a) => a.pythPriceFeedId) as string[]
return await fetchPythPrices(...priceFeedIds).then(mapResponseToBnCoin(assets))
}
async function requestPoolPrices(
chainConfig: ChainConfig,
assets: Asset[],
lookupPrices: BNCoin[],
): Promise<BNCoin[]> {
const requests = assets.map((asset) => getPoolPrice(chainConfig, asset, lookupPrices))
async function requestPoolPrices(assets: Asset[], lookupPrices: BNCoin[]): Promise<BNCoin[]> {
const requests = assets.map((asset) => getPoolPrice(asset, lookupPrices))
return await Promise.all(requests).then(mapResponseToBnCoin(assets))
}
const mapResponseToBnCoin = (assets: Asset[]) => (prices: BigNumber[]) => {
return prices.map((price: BigNumber, index: number) =>
const mapResponseToBnCoin = (assets: Asset[]) => (prices: BigNumber[]) =>
prices.map((price: BigNumber, index: number) =>
BNCoin.fromDenomAndBigNumber(assets[index].denom, price),
)
}
function separateAssetsByPriceSources(assets: Asset[]) {
const assetsWithoutPythPriceFeedId = assets.filter((asset) => !asset.pythPriceFeedId)
// Only fetch Pyth prices for mainnet
const [assetsWithPythPriceFeedId, assetsWithoutPythPriceFeedId] = partition(
assets,
(asset) => !!asset.pythPriceFeedId,
)
// Don't get oracle price if it's not mainnet and there is a poolId
const [assetsWithOraclePrice, assetsWithoutOraclePrice] = partition(
assetsWithoutPythPriceFeedId,
(asset) => asset.hasOraclePrice || !asset.poolId,
)
const assetsWithPoolId = assetsWithoutOraclePrice.filter((asset) => !!asset.poolId)
return [assetsWithOraclePrice, assetsWithPoolId]
return [assetsWithPythPriceFeedId, assetsWithOraclePrice, assetsWithPoolId]
}
+3 -3
View File
@@ -1,9 +1,9 @@
import { cacheFn, pythPriceCache } from 'api/cache'
import { pythEndpoints } from 'constants/pyth'
import { ENV } from 'constants/env'
export default async function fetchPythPriceData(priceFeedIds: string[]) {
export default async function fetchPythPriceData(...priceFeedIds: string[]) {
try {
const pricesUrl = new URL(`${pythEndpoints.api}/latest_vaas`)
const pricesUrl = new URL(`${ENV.PYTH_ENDPOINT}/latest_vaas`)
priceFeedIds.forEach((id) => pricesUrl.searchParams.append('ids[]', id))
const pythDataResponse: string[] = await cacheFn(
+4 -16
View File
@@ -1,11 +1,10 @@
import { cacheFn, pythPriceCache } from 'api/cache'
import { pythEndpoints } from 'constants/pyth'
import { BNCoin } from 'types/classes/BNCoin'
import { ENV } from 'constants/env'
import { BN } from 'utils/helpers'
export default async function fetchPythPrices(priceFeedIds: string[], assets: Asset[]) {
export default async function fetchPythPrices(...priceFeedIds: string[]) {
try {
const pricesUrl = new URL(`${pythEndpoints.api}/latest_price_feeds`)
const pricesUrl = new URL(`${ENV.PYTH_ENDPOINT}/latest_price_feeds`)
priceFeedIds.forEach((id) => pricesUrl.searchParams.append('ids[]', id))
const pythResponse: PythPriceData[] = await cacheFn(
@@ -15,18 +14,7 @@ export default async function fetchPythPrices(priceFeedIds: string[], assets: As
30,
)
const mappedPriceData = [] as BNCoin[]
assets.forEach((asset) => {
const price = pythResponse.find((pythPrice) => asset.pythPriceFeedId === pythPrice.id)?.price
if (price)
mappedPriceData.push(
BNCoin.fromDenomAndBigNumber(asset.denom, BN(price.price).shiftedBy(price.expo)),
)
return
})
return mappedPriceData
return pythResponse.map(({ price }) => BN(price.price).shiftedBy(price.expo))
} catch (ex) {
throw ex
}
+2 -6
View File
@@ -2,13 +2,9 @@ import { getSwapperQueryClient } from 'api/cosmwasm-client'
import { BN_ZERO } from 'constants/math'
import { BN } from 'utils/helpers'
export default async function estimateExactIn(
chainConfig: ChainConfig,
coinIn: Coin,
denomOut: string,
) {
export default async function estimateExactIn(coinIn: Coin, denomOut: string) {
try {
const swapperClient = await getSwapperQueryClient(chainConfig)
const swapperClient = await getSwapperQueryClient()
const estimatedAmount = (await swapperClient.estimateExactInSwap({ coinIn, denomOut })).amount
return BN(estimatedAmount)
-53
View File
@@ -1,53 +0,0 @@
import { BN_ZERO } from 'constants/math'
import { STANDARD_SWAP_FEE } from 'utils/constants'
export default async function getOsmosisSwapFee(
chainConfig: ChainConfig,
poolIds: string[],
): Promise<number> {
const promises = poolIds.map((poolId) =>
fetch(chainConfig.endpoints.pools.replace('POOL_ID', poolId)),
)
const responses = await Promise.all(promises)
const pools = await Promise.all(responses.map(async (pool) => (await pool.json()).pool as Pool))
if (!pools?.length) return STANDARD_SWAP_FEE
return pools
.reduce((acc, pool) => acc.plus(pool?.pool_params?.swap_fee || STANDARD_SWAP_FEE), BN_ZERO)
.toNumber()
}
interface Pool {
'@type': string
address: string
future_pool_governor: string
id: string
pool_assets?: PoolAsset[]
pool_liquidity?: PoolLiquidity[]
pool_params: PoolParams
total_shares: TotalShares
total_weight: string
}
interface PoolAsset {
token: TotalShares
weight: string
}
interface PoolLiquidity {
amount: string
denom: string
}
interface TotalShares {
amount: string
denom: string
}
interface PoolParams {
exit_fee: string
smooth_weight_change_params: null
swap_fee: string
}
+2 -6
View File
@@ -1,12 +1,8 @@
import { getSwapperQueryClient } from 'api/cosmwasm-client'
export default async function getSwapRoute(
chainConfig: ChainConfig,
denomIn: string,
denomOut: string,
): Promise<Route[]> {
export default async function getSwapRoute(denomIn: string, denomOut: string): Promise<Route[]> {
try {
const swapperClient = await getSwapperQueryClient(chainConfig)
const swapperClient = await getSwapperQueryClient()
const routes = await swapperClient.route({
denomIn,
denomOut,
+27 -47
View File
@@ -21,13 +21,9 @@ import {
import { getCoinValue } from 'utils/formatters'
import { BN } from 'utils/helpers'
async function getUnlocksAtTimestamp(
chainConfig: ChainConfig,
unlockingId: number,
vaultAddress: string,
) {
async function getUnlocksAtTimestamp(unlockingId: number, vaultAddress: string) {
try {
const client = await getClient(chainConfig.endpoints.rpc)
const client = await getClient()
const vaultExtension = (await cacheFn(
() =>
@@ -46,7 +42,6 @@ async function getUnlocksAtTimestamp(
}
async function getVaultPositionStatusAndUnlockIdAndUnlockTime(
chainConfig: ChainConfig,
vaultPosition: VaultPosition,
): Promise<[VaultStatus, number | undefined, number | undefined]> {
const amount = vaultPosition.amount
@@ -55,11 +50,7 @@ async function getVaultPositionStatusAndUnlockIdAndUnlockTime(
if (amount.locking.unlocking.length) {
const unlockId = amount.locking.unlocking[0].id
const unlocksAtTimestamp = await getUnlocksAtTimestamp(
chainConfig,
unlockId,
vaultPosition.vault.address,
)
const unlocksAtTimestamp = await getUnlocksAtTimestamp(unlockId, vaultPosition.vault.address)
if (moment(unlocksAtTimestamp).isBefore(new Date())) {
return [VaultStatus.UNLOCKED, unlockId, unlocksAtTimestamp]
@@ -71,7 +62,7 @@ async function getVaultPositionStatusAndUnlockIdAndUnlockTime(
}
}
export function flatVaultPositionAmount(
function flatVaultPositionAmount(
vaultPositionAmount: VaultPositionAmount,
): VaultPositionFlatAmounts {
const amounts = {
@@ -91,14 +82,13 @@ export function flatVaultPositionAmount(
return amounts
}
export async function getLpTokensForVaultPosition(
chainConfig: ChainConfig,
async function getLpTokensForVaultPosition(
vault: Vault,
vaultPosition: VaultPosition,
): Promise<Coin[]> {
try {
const vaultQueryClient = await getVaultQueryClient(chainConfig, vault.address)
const creditManagerQueryClient = await getCreditManagerQueryClient(chainConfig)
const vaultQueryClient = await getVaultQueryClient(vault.address)
const creditManagerQueryClient = await getCreditManagerQueryClient()
const amounts = flatVaultPositionAmount(vaultPosition.amount)
const totalAmount = amounts.locked.plus(amounts.unlocked).plus(amounts.unlocking).toString()
@@ -143,16 +133,15 @@ export async function getLpTokensForVaultPosition(
async function getVaultValuesAndAmounts(
vault: Vault,
vaultPosition: VaultPosition,
chainConfig: ChainConfig,
): Promise<VaultValuesAndAmounts> {
try {
const pricesQueries = Promise.all([
getPrice(chainConfig, vault.denoms.primary),
getPrice(chainConfig, vault.denoms.secondary),
getPrice(chainConfig, vault.denoms.lp),
getPrice(vault.denoms.primary),
getPrice(vault.denoms.secondary),
getPrice(vault.denoms.lp),
])
const lpTokensQuery = getLpTokensForVaultPosition(chainConfig, vault, vaultPosition)
const lpTokensQuery = getLpTokensForVaultPosition(vault, vaultPosition)
const amounts = flatVaultPositionAmount(vaultPosition.amount)
const [[primaryLpToken, secondaryLpToken], [primaryPrice, secondaryPrice, lpPrice]] =
@@ -165,26 +154,18 @@ async function getVaultValuesAndAmounts(
secondary: BN(secondaryLpToken.amount),
},
values: {
primary: getCoinValue(
new BNCoin(primaryLpToken),
[BNCoin.fromDenomAndBigNumber(primaryLpToken.denom, primaryPrice)],
chainConfig.assets,
),
secondary: getCoinValue(
new BNCoin(secondaryLpToken),
[BNCoin.fromDenomAndBigNumber(secondaryLpToken.denom, secondaryPrice)],
chainConfig.assets,
),
unlocking: getCoinValue(
BNCoin.fromDenomAndBigNumber(vault.denoms.lp, amounts.unlocking),
[BNCoin.fromDenomAndBigNumber(vault.denoms.lp, lpPrice)],
chainConfig.assets,
),
unlocked: getCoinValue(
BNCoin.fromDenomAndBigNumber(vault.denoms.lp, amounts.unlocked),
[BNCoin.fromDenomAndBigNumber(vault.denoms.lp, lpPrice)],
chainConfig.assets,
),
primary: getCoinValue(new BNCoin(primaryLpToken), [
BNCoin.fromDenomAndBigNumber(primaryLpToken.denom, primaryPrice),
]),
secondary: getCoinValue(new BNCoin(secondaryLpToken), [
BNCoin.fromDenomAndBigNumber(secondaryLpToken.denom, secondaryPrice),
]),
unlocking: getCoinValue(BNCoin.fromDenomAndBigNumber(vault.denoms.lp, amounts.unlocking), [
BNCoin.fromDenomAndBigNumber(vault.denoms.lp, lpPrice),
]),
unlocked: getCoinValue(BNCoin.fromDenomAndBigNumber(vault.denoms.lp, amounts.unlocked), [
BNCoin.fromDenomAndBigNumber(vault.denoms.lp, lpPrice),
]),
},
}
} catch (ex) {
@@ -194,11 +175,10 @@ async function getVaultValuesAndAmounts(
async function getDepositedVaults(
accountId: string,
chainConfig: ChainConfig,
positions?: Positions,
): Promise<DepositedVault[]> {
try {
const creditManagerQueryClient = await getCreditManagerQueryClient(chainConfig)
const creditManagerQueryClient = await getCreditManagerQueryClient()
if (!positions)
positions = await cacheFn(
@@ -209,7 +189,7 @@ async function getDepositedVaults(
if (!positions.vaults.length) return []
const [allVaults] = await Promise.all([getVaults(chainConfig)])
const [allVaults] = await Promise.all([getVaults()])
const depositedVaults = positions.vaults.map(async (vaultPosition) => {
const vault = allVaults.find((v) => v.address === vaultPosition.vault.address)
@@ -219,8 +199,8 @@ async function getDepositedVaults(
}
const [[status, unlockId, unlocksAt], valuesAndAmounts] = await Promise.all([
getVaultPositionStatusAndUnlockIdAndUnlockTime(chainConfig, vaultPosition),
getVaultValuesAndAmounts(vault, vaultPosition, chainConfig),
getVaultPositionStatusAndUnlockIdAndUnlockTime(vaultPosition),
getVaultValuesAndAmounts(vault, vaultPosition),
])
return {
+5 -9
View File
@@ -1,21 +1,17 @@
import { aprsCache, aprsCacheResponse, cacheFn } from 'api/cache'
import { ENV } from 'constants/env'
export default async function getAprs(chainConfig: ChainConfig) {
export default async function getAprs() {
try {
const response = await cacheFn(
() => fetch(chainConfig.endpoints.aprs.vaults),
() => fetch(ENV.URL_VAULT_APR),
aprsCacheResponse,
`${chainConfig.id}/aprsResponse`,
'aprsResponse',
60,
)
if (response.ok) {
const data: AprResponse = await cacheFn(
() => response.json(),
aprsCache,
`${chainConfig.id}/aprs`,
60,
)
const data: AprResponse = await cacheFn(() => response.json(), aprsCache, 'aprs', 60)
return data.vaults.map((aprData) => {
const finalApr = aprData.apr.projected_apr * 100
+3 -5
View File
@@ -3,15 +3,13 @@ import { getParamsQueryClient } from 'api/cosmwasm-client'
import { VaultConfigBaseForAddr } from 'types/generated/mars-params/MarsParams.types'
import iterateContractQuery from 'utils/iterateContractQuery'
export const getVaultConfigs = async (
chainConfig: ChainConfig,
): Promise<VaultConfigBaseForAddr[]> => {
export const getVaultConfigs = async (): Promise<VaultConfigBaseForAddr[]> => {
try {
const paramsQueryClient = await getParamsQueryClient(chainConfig)
const paramsQueryClient = await getParamsQueryClient()
return await cacheFn(
() => iterateContractQuery(paramsQueryClient.allVaultConfigs, 'addr'),
vaultConfigsCache,
`${chainConfig.id}/vaultConfigs`,
'vaultConfigs',
600,
)
} catch (ex) {
+21
View File
@@ -0,0 +1,21 @@
import { cacheFn, previewDepositCache } from 'api/cache'
import { getVaultQueryClient } from 'api/cosmwasm-client'
export async function getVaultTokenFromLp(
vaultAddress: string,
lpAmount: string,
): Promise<{ vaultAddress: string; amount: string }> {
try {
const client = await getVaultQueryClient(vaultAddress)
return cacheFn(
() =>
client.previewDeposit({ amount: lpAmount }).then((amount) => ({ vaultAddress, amount })),
previewDepositCache,
`vaults/${vaultAddress}/amounts/${lpAmount}`,
30,
)
} catch (ex) {
throw ex
}
}
+3 -2
View File
@@ -1,13 +1,14 @@
import { cacheFn, vaultUtilizationCache } from 'api/cache'
import { getCreditManagerQueryClient } from 'api/cosmwasm-client'
import { ENV } from 'constants/env'
import { VaultUtilizationResponse } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { VaultConfigBaseForString } from 'types/generated/mars-params/MarsParams.types'
export const getVaultUtilizations = async (
chainConfig: ChainConfig,
vaultConfigs: VaultConfigBaseForString[],
): Promise<VaultUtilizationResponse[]> => {
const creditManagerQueryClient = await getCreditManagerQueryClient(chainConfig)
if (!ENV.ADDRESS_PARAMS) return []
const creditManagerQueryClient = await getCreditManagerQueryClient()
try {
const vaultUtilizations$ = vaultConfigs.map((vaultConfig) => {
return cacheFn(
+10 -6
View File
@@ -2,16 +2,20 @@ import getAssetParams from 'api/params/getAssetParams'
import getAprs from 'api/vaults/getVaultAprs'
import { getVaultConfigs } from 'api/vaults/getVaultConfigs'
import { getVaultUtilizations } from 'api/vaults/getVaultUtilizations'
import { ENV } from 'constants/env'
import { TESTNET_VAULTS_META_DATA, VAULTS_META_DATA } from 'constants/vaults'
import { NETWORK } from 'types/enums/network'
import { BN } from 'utils/helpers'
import { convertAprToApy } from 'utils/parsers'
import { resolveHLSStrategies } from 'utils/resolvers'
export default async function getVaults(chainConfig: ChainConfig): Promise<Vault[]> {
const assetParams = await getAssetParams(chainConfig)
const vaultConfigs = await getVaultConfigs(chainConfig)
const $vaultUtilizations = getVaultUtilizations(chainConfig, vaultConfigs)
const $aprs = getAprs(chainConfig)
const vaultMetaDatas = chainConfig.vaults
export default async function getVaults(): Promise<Vault[]> {
const assetParams = await getAssetParams()
const vaultConfigs = await getVaultConfigs()
const $vaultUtilizations = getVaultUtilizations(vaultConfigs)
const $aprs = getAprs()
const vaultMetaDatas =
ENV.NETWORK === NETWORK.TESTNET ? TESTNET_VAULTS_META_DATA : VAULTS_META_DATA
const HLSAssets = assetParams.filter((asset) => asset.credit_manager.hls)
const hlsStrategies = resolveHLSStrategies('vault', HLSAssets)
+2 -3
View File
@@ -2,13 +2,12 @@ import { getCreditManagerQueryClient } from 'api/cosmwasm-client'
import { ITEM_LIMIT_PER_QUERY } from 'constants/query'
export default async function getAccountIds(
chainConfig: ChainConfig,
address?: string,
previousResults?: AccountIdAndKind[],
): Promise<AccountIdAndKind[]> {
if (!address) return []
try {
const client = await getCreditManagerQueryClient(chainConfig)
const client = await getCreditManagerQueryClient()
const lastItem = previousResults && previousResults.at(-1)
const accounts = (
@@ -25,7 +24,7 @@ export default async function getAccountIds(
return accumulated.sort((a, b) => parseInt(a.id) - parseInt(b.id))
}
return await getAccountIds(chainConfig, address, accumulated)
return await getAccountIds(address, accumulated)
} catch {
return new Promise((_, reject) => reject('No data'))
}
+4 -7
View File
@@ -2,19 +2,16 @@ import getAccount from 'api/accounts/getAccount'
import getWalletAccountIds from 'api/wallets/getAccountIds'
import { AccountKind } from 'types/generated/mars-rover-health-computer/MarsRoverHealthComputer.types'
export default async function getAccounts(
kind: AccountKind,
chainConfig: ChainConfig,
address?: string,
): Promise<Account[]> {
export default async function getAccounts(kind: AccountKind, address?: string): Promise<Account[]> {
if (!address) return new Promise((_, reject) => reject('No address'))
const accountIdsAndKinds = await getWalletAccountIds(chainConfig, address)
const accountIdsAndKinds = await getWalletAccountIds(address)
const $accounts = accountIdsAndKinds
.filter((a) => a.kind === kind)
.map((account) => getAccount(chainConfig, account.id))
.map((account) => getAccount(account.id))
const accounts = await Promise.all($accounts).then((accounts) => accounts)
if (accounts) {
return accounts.sort((a, b) => Number(a.id) - Number(b.id))
}
+4 -6
View File
@@ -1,13 +1,11 @@
import { getICNSQueryClient } from 'api/cosmwasm-client'
import { ENV } from 'constants/env'
import { ChainInfoID } from 'types/enums/wallet'
export default async function getICNS(
chainConfig: ChainConfig,
address?: string,
): Promise<ICNSResult | undefined> {
if (!address || chainConfig.id !== ChainInfoID.Osmosis1) return
export default async function getICNS(address?: string): Promise<ICNSResult | undefined> {
if (!address || ENV.CHAIN_ID !== ChainInfoID.Osmosis1) return
try {
const icnsQueryClient = await getICNSQueryClient(chainConfig)
const icnsQueryClient = await getICNSQueryClient()
return icnsQueryClient.primaryName({ address })
} catch (ex) {
throw ex
+4 -5
View File
@@ -1,10 +1,9 @@
export default async function getWalletBalances(
chainConfig: ChainConfig,
address: string,
): Promise<Coin[]> {
import { ENV } from 'constants/env'
export default async function getWalletBalances(address: string): Promise<Coin[]> {
const uri = '/cosmos/bank/v1beta1/balances/'
const response = await fetch(`${chainConfig.endpoints.rest}${uri}${address}`)
const response = await fetch(`${ENV.URL_REST}${uri}${address}`)
if (response.ok) {
const data = await response.json()
@@ -1,7 +1,7 @@
import AssetRate from 'components/Asset/AssetRate'
import { byDenom } from 'utils/array'
export const APY_META = { accessorKey: 'apy', header: 'APY', meta: { className: 'w-30' } }
export const APY_META = { accessorKey: 'apy', header: 'APY' }
interface Props {
apy: number
@@ -1,67 +0,0 @@
import { useEffect, useMemo, useState } from 'react'
import DisplayCurrency from 'components/DisplayCurrency'
import { InfoCircle } from 'components/Icons'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import useLiquidationPrice from 'hooks/useLiquidationPrice'
import { BNCoin } from 'types/classes/BNCoin'
import { LiquidationPriceKind } from 'utils/health_computer'
import { BN } from 'utils/helpers'
export const LIQ_META = {
accessorKey: 'symbol',
header: 'Liquidation Price',
id: 'liqPrice',
meta: { className: 'w-40' },
}
interface Props {
amount: number
computeLiquidationPrice: (denom: string, kind: LiquidationPriceKind) => number | null
denom: string
type: 'deposits' | 'borrowing' | 'lending' | 'vault'
account: Account
}
export default function LiqPrice(props: Props) {
const { denom, type, amount, account, computeLiquidationPrice } = props
const [lastLiquidationPrice, setLastLiquidationPrice] = useState<number | null>(null)
const hasDebt = account.debts.length > 0
const liqPrice = useMemo(() => {
if (type === 'vault' || amount === 0) return 0
return computeLiquidationPrice(denom, type === 'borrowing' ? 'debt' : 'asset')
}, [amount, computeLiquidationPrice, denom, type])
const { liquidationPrice } = useLiquidationPrice(liqPrice)
useEffect(() => {
if (lastLiquidationPrice !== liqPrice && liqPrice !== null) setLastLiquidationPrice(liqPrice)
}, [liqPrice, lastLiquidationPrice])
const tooltipText = useMemo(() => {
if (type === 'vault')
return 'Liquidation prices cannot be calculated for farm positions. But it a drop in price of the underlying assets can still cause a liquidation.'
if (!hasDebt) return 'Your position cannot be liquidated as you currently have no debt.'
return 'The position size is too small to liquidate the account, even if the price goes to $0.00.'
}, [type, hasDebt])
if (!lastLiquidationPrice || (liquidationPrice === 0 && lastLiquidationPrice === 0))
return (
<Text size='xs' className='flex items-center justify-end number'>
N/A
<Tooltip content={tooltipText} type='info' className='ml-1'>
<InfoCircle className='w-3.5 h-3.5 text-white/40 hover:text-inherit' />
</Tooltip>
</Text>
)
return (
<DisplayCurrency
className='text-xs text-right number'
coin={BNCoin.fromDenomAndBigNumber('usd', BN(lastLiquidationPrice))}
options={{ abbreviated: false }}
/>
)
}
@@ -1,26 +0,0 @@
import DisplayCurrency from 'components/DisplayCurrency'
import usePrice from 'hooks/usePrice'
import { BNCoin } from 'types/classes/BNCoin'
import { BN } from 'utils/helpers'
export const PRICE_META = { id: 'price', header: 'Price', meta: { className: 'w-30' } }
interface Props {
amount: number
denom: string
type: 'deposits' | 'borrowing' | 'lending' | 'vault'
}
export default function Price(props: Props) {
const price = usePrice(props.denom)
if (props.amount === 0 || props.type === 'vault') return null
return (
<DisplayCurrency
className='text-xs text-right number'
coin={BNCoin.fromDenomAndBigNumber('usd', BN(price))}
options={{ abbreviated: false }}
/>
)
}
@@ -6,7 +6,7 @@ import { FormattedNumber } from 'components/FormattedNumber'
import { MAX_AMOUNT_DECIMALS, MIN_AMOUNT } from 'constants/math'
import { formatAmountToPrecision } from 'utils/formatters'
export const SIZE_META = { accessorKey: 'size', header: 'Size', meta: { className: 'w-40' } }
export const SIZE_META = { accessorKey: 'size', header: 'Size' }
interface Props {
size: number
@@ -1,10 +1,8 @@
import { ColumnDef, Row } from '@tanstack/react-table'
import { ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'
import Apy, { APY_META } from 'components/Account/AccountBalancesTable/Columns/Apy'
import Asset, { ASSET_META } from 'components/Account/AccountBalancesTable/Columns/Asset'
import LiqPrice, { LIQ_META } from 'components/Account/AccountBalancesTable/Columns/LiqPrice'
import Price, { PRICE_META } from 'components/Account/AccountBalancesTable/Columns/Price'
import Size, {
SIZE_META,
sizeSortingFn,
@@ -13,18 +11,10 @@ import Value, {
VALUE_META,
valueSortingFn,
} from 'components/Account/AccountBalancesTable/Columns/Value'
import useMarketAssets from 'hooks/markets/useMarketAssets'
import useHealthComputer from 'hooks/useHealthComputer'
import useStore from 'store'
import useMarketAssets from 'hooks/useMarketAssets'
export default function useAccountBalancesColumns(
account: Account,
showLiquidationPrice?: boolean,
) {
export default function useAccountBalancesColumns() {
const { data: markets } = useMarketAssets()
const updatedAccount = useStore((s) => s.updatedAccount)
const { computeLiquidationPrice } = useHealthComputer(updatedAccount ?? account)
return useMemo<ColumnDef<AccountBalanceRow>[]>(() => {
return [
@@ -56,37 +46,6 @@ export default function useAccountBalancesColumns(
),
sortingFn: sizeSortingFn,
},
...(showLiquidationPrice
? [
{
...PRICE_META,
cell: ({ row }: { row: Row<AccountBalanceRow> }) => (
<Price
type={row.original.type}
amount={row.original.amount.toNumber()}
denom={row.original.denom}
/>
),
},
]
: []),
...(showLiquidationPrice
? [
{
...LIQ_META,
enableSorting: false,
cell: ({ row }: { row: Row<AccountBalanceRow> }) => (
<LiqPrice
denom={row.original.denom}
computeLiquidationPrice={computeLiquidationPrice}
type={row.original.type}
amount={row.original.amount.toNumber()}
account={updatedAccount ?? account}
/>
),
},
]
: []),
{
...APY_META,
cell: ({ row }) => (
@@ -99,5 +58,5 @@ export default function useAccountBalancesColumns(
),
},
]
}, [computeLiquidationPrice, markets, showLiquidationPrice, account, updatedAccount])
}, [markets])
}
@@ -6,7 +6,6 @@ export function getAssetAccountBalanceRow(
type: 'deposits' | 'borrowing' | 'lending',
asset: Asset,
prices: BNCoin[],
assets: Asset[],
position: BNCoin,
apy: number,
prev?: BNCoin,
@@ -18,7 +17,7 @@ export function getAssetAccountBalanceRow(
type,
symbol: asset.symbol,
size: demagnify(amount, asset),
value: getCoinValue(BNCoin.fromDenomAndBigNumber(denom, amount), prices, assets).toString(),
value: getCoinValue(BNCoin.fromDenomAndBigNumber(denom, amount), prices).toString(),
denom,
amount,
apy,
@@ -1,5 +1,5 @@
import classNames from 'classnames'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { useLocation, useNavigate } from 'react-router-dom'
import useAccountBalancesColumns from 'components/Account/AccountBalancesTable/Columns/useAccountBalancesColumns'
import useAccountBalanceData from 'components/Account/AccountBalancesTable/useAccountBalanceData'
@@ -19,19 +19,10 @@ interface Props {
borrowingData: BorrowMarketTableData[]
hideCard?: boolean
tableBodyClassName?: string
showLiquidationPrice?: boolean
}
export default function AccountBalancesTable(props: Props) {
const [searchParams] = useSearchParams()
const {
account,
lendingData,
borrowingData,
tableBodyClassName,
hideCard,
showLiquidationPrice,
} = props
const { account, lendingData, borrowingData, tableBodyClassName, hideCard } = props
const currentAccount = useCurrentAccount()
const navigate = useNavigate()
const { pathname } = useLocation()
@@ -45,7 +36,7 @@ export default function AccountBalancesTable(props: Props) {
isHls: props.isHls,
})
const columns = useAccountBalancesColumns(account, showLiquidationPrice)
const columns = useAccountBalancesColumns()
if (accountBalanceData.length === 0)
return (
@@ -64,7 +55,7 @@ export default function AccountBalancesTable(props: Props) {
color='tertiary'
onClick={() => {
if (currentAccount?.id !== account.id) {
navigate(getRoute(getPage(pathname), searchParams, address, account.id))
navigate(getRoute(getPage(pathname), address, account.id))
}
useStore.setState({
focusComponent: {
@@ -4,10 +4,12 @@ import {
getAssetAccountBalanceRow,
getVaultAccountBalanceRow,
} from 'components/Account/AccountBalancesTable/functions'
import useAllAssets from 'hooks/assets/useAllAssets'
import { ASSETS } from 'constants/assets'
import useHLSStakingAssets from 'hooks/useHLSStakingAssets'
import usePrices from 'hooks/usePrices'
import { byDenom } from 'utils/array'
import { convertLiquidityRateToAPR } from 'utils/formatters'
import { convertAprToApy } from 'utils/parsers'
interface Props {
account: Account
@@ -19,9 +21,10 @@ interface Props {
export default function useAccountBalanceData(props: Props) {
const { account, updatedAccount, lendingData, borrowingData } = props
const { data: hlsStrategies } = useHLSStakingAssets()
const { data: prices } = usePrices()
const assets = useAllAssets()
return useMemo<AccountBalanceRow[]>(() => {
const usedAccount = updatedAccount ?? account
const accountDeposits = usedAccount?.deposits ?? []
@@ -31,26 +34,26 @@ export default function useAccountBalanceData(props: Props) {
const deposits: AccountBalanceRow[] = []
accountDeposits.forEach((deposit) => {
const asset = assets.find(byDenom(deposit.denom))
const asset = ASSETS.find(byDenom(deposit.denom))
if (!asset) return
const apy = props.isHls
? hlsStrategies.find((strategy) => strategy.denoms.deposit === asset.denom)?.apy ?? 0
: 0
const prevDeposit = updatedAccount ? account?.deposits.find(byDenom(deposit.denom)) : deposit
deposits.push(
getAssetAccountBalanceRow('deposits', asset, prices, assets, deposit, apy, prevDeposit),
)
deposits.push(getAssetAccountBalanceRow('deposits', asset, prices, deposit, apy, prevDeposit))
})
const lends = accountLends.map((lending) => {
const asset = assets.find(byDenom(lending.denom)) ?? assets[0]
const apy =
lendingData.find((market) => market.asset.denom === lending.denom)?.apy.deposit ?? 0
const asset = ASSETS.find(byDenom(lending.denom)) ?? ASSETS[0]
const apr = convertLiquidityRateToAPR(
lendingData.find((market) => market.asset.denom === lending.denom)?.marketLiquidityRate ??
0,
)
const apy = convertAprToApy(apr, 365)
const prevLending = updatedAccount
? account?.lends.find((position) => position.denom === lending.denom)
: lending
return getAssetAccountBalanceRow('lending', asset, prices, assets, lending, apy, prevLending)
return getAssetAccountBalanceRow('lending', asset, prices, lending, apy, prevLending)
})
const vaults = accountVaults.map((vault) => {
@@ -62,22 +65,13 @@ export default function useAccountBalanceData(props: Props) {
})
const debts = accountDebts.map((debt) => {
const asset = assets.find(byDenom(debt.denom)) ?? assets[0]
const apy = borrowingData.find((market) => market.asset.denom === debt.denom)?.apy.borrow ?? 0
const asset = ASSETS.find(byDenom(debt.denom)) ?? ASSETS[0]
const apy = borrowingData.find((market) => market.asset.denom === debt.denom)?.borrowRate ?? 0
const prevDebt = updatedAccount
? account?.debts.find((position) => position.denom === debt.denom)
: debt
return getAssetAccountBalanceRow('borrowing', asset, prices, assets, debt, apy, prevDebt)
return getAssetAccountBalanceRow('borrowing', asset, prices, debt, apy, prevDebt)
})
return [...deposits, ...lends, ...vaults, ...debts]
}, [
updatedAccount,
account,
props.isHls,
hlsStrategies,
prices,
assets,
lendingData,
borrowingData,
])
}, [updatedAccount, account, props.isHls, hlsStrategies, prices, lendingData, borrowingData])
}
+9 -24
View File
@@ -8,7 +8,6 @@ import { ArrowRight } from 'components/Icons'
import Text from 'components/Text'
import { BN_ZERO, MAX_AMOUNT_DECIMALS } from 'constants/math'
import { ORACLE_DENOM } from 'constants/oracle'
import useAllAssets from 'hooks/assets/useAllAssets'
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
import useHLSStakingAssets from 'hooks/useHLSStakingAssets'
import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData'
@@ -41,7 +40,6 @@ export default function AccountComposition(props: Props) {
const hasChanged = !!updatedAccount
const { data: prices } = usePrices()
const { data: hlsStrategies } = useHLSStakingAssets()
const assets = useAllAssets()
const { data } = useBorrowMarketAssetsTableData(false)
const borrowAssetsData = useMemo(() => data?.allAssets || [], [data])
const { availableAssets: lendingAvailableAssets, accountLentAssets } =
@@ -52,15 +50,15 @@ export default function AccountComposition(props: Props) {
)
const [depositsBalance, lendsBalance, debtsBalance, vaultsBalance] = useMemo(
() => getAccountPositionValues(account, prices, assets),
[account, assets, prices],
() => getAccountPositionValues(account, prices),
[account, prices],
)
const totalBalance = depositsBalance.plus(lendsBalance).plus(vaultsBalance)
const [updatedPositionValue, updatedDebtsBalance] = useMemo(() => {
const [updatedDepositsBalance, updatedLendsBalance, updatedDebtsBalance, updatedVaultsBalance] =
updatedAccount
? getAccountPositionValues(updatedAccount, prices, assets)
? getAccountPositionValues(updatedAccount, prices)
: [BN_ZERO, BN_ZERO, BN_ZERO]
const updatedPositionValue = updatedDepositsBalance
@@ -68,15 +66,12 @@ export default function AccountComposition(props: Props) {
.plus(updatedVaultsBalance)
return [updatedPositionValue, updatedDebtsBalance]
}, [updatedAccount, prices, assets])
}, [updatedAccount, prices])
const netWorth = useMemo(
() => calculateAccountBalanceValue(account, prices, assets),
[account, assets, prices],
)
const netWorth = useMemo(() => calculateAccountBalanceValue(account, prices), [account, prices])
const updatedTotalBalance = useMemo(
() => (updatedAccount ? calculateAccountBalanceValue(updatedAccount, prices, assets) : BN_ZERO),
[updatedAccount, prices, assets],
() => (updatedAccount ? calculateAccountBalanceValue(updatedAccount, prices) : BN_ZERO),
[updatedAccount, prices],
)
const apr = useMemo(
@@ -87,10 +82,9 @@ export default function AccountComposition(props: Props) {
lendingAssetsData,
prices,
hlsStrategies,
assets,
props.isHls,
),
[account, assets, borrowAssetsData, hlsStrategies, lendingAssetsData, prices, props.isHls],
[account, borrowAssetsData, hlsStrategies, lendingAssetsData, prices, props.isHls],
)
const updatedApr = useMemo(
() =>
@@ -101,19 +95,10 @@ export default function AccountComposition(props: Props) {
lendingAssetsData,
prices,
hlsStrategies,
assets,
props.isHls,
)
: BN_ZERO,
[
updatedAccount,
borrowAssetsData,
lendingAssetsData,
prices,
hlsStrategies,
assets,
props.isHls,
],
[updatedAccount, borrowAssetsData, lendingAssetsData, prices, hlsStrategies, props.isHls],
)
return (
@@ -1,5 +1,5 @@
import { useCallback, useEffect } from 'react'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { useLocation, useNavigate } from 'react-router-dom'
import AccountFundFullPage from 'components/Account/AccountFund/AccountFundFullPage'
import FullOverlayContent from 'components/FullOverlayContent'
@@ -14,7 +14,6 @@ export default function AccountCreateFirst() {
const address = useStore((s) => s.address)
const createAccount = useStore((s) => s.createAccount)
const [isCreating, setIsCreating] = useToggle(false)
const [searchParams] = useSearchParams()
useEffect(() => {
if (!address) useStore.setState({ focusComponent: { component: <WalletSelect /> } })
@@ -25,7 +24,7 @@ export default function AccountCreateFirst() {
const accountId = await createAccount('default')
setIsCreating(false)
if (accountId) {
navigate(getRoute(getPage(pathname), searchParams, address, accountId))
navigate(getRoute(getPage(pathname), address, accountId))
useStore.setState({
focusComponent: {
component: <AccountFundFullPage />,
@@ -35,7 +34,7 @@ export default function AccountCreateFirst() {
},
})
}
}, [setIsCreating, createAccount, navigate, pathname, searchParams, address])
}, [createAccount, navigate, pathname, address, setIsCreating])
return (
<FullOverlayContent
+12 -18
View File
@@ -17,16 +17,15 @@ import Text from 'components/Text'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import { ORACLE_DENOM } from 'constants/oracle'
import useAccountIds from 'hooks/accounts/useAccountIds'
import useAccounts from 'hooks/accounts/useAccounts'
import useAllAssets from 'hooks/assets/useAllAssets'
import useLocalStorage from 'hooks/localStorage/useLocalStorage'
import useAccountId from 'hooks/useAccountId'
import useAccountIds from 'hooks/useAccountIds'
import useAccounts from 'hooks/useAccounts'
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useHealthComputer from 'hooks/useHealthComputer'
import useHLSStakingAssets from 'hooks/useHLSStakingAssets'
import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData'
import useLocalStorage from 'hooks/useLocalStorage'
import usePrices from 'hooks/usePrices'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
@@ -38,16 +37,16 @@ import {
export default function AccountDetailsController() {
const address = useStore((s) => s.address)
const isHLS = useStore((s) => s.isHLS)
const currentAppSection = useStore((s) => s.currentAppSection)
const { data: accounts, isLoading } = useAccounts('default', address)
const { data: accountIds } = useAccountIds(address, false, true)
const { data: accountIds } = useAccountIds(address, false)
const accountId = useAccountId()
const account = useCurrentAccount()
const focusComponent = useStore((s) => s.focusComponent)
const isOwnAccount = accountId && accountIds?.includes(accountId)
if (!address || focusComponent || !isOwnAccount || isHLS) return null
if (!address || focusComponent || !isOwnAccount || currentAppSection !== 'app') return null
if ((isLoading && accountId && !focusComponent) || !account) return <Skeleton />
@@ -73,23 +72,19 @@ function AccountDetails(props: Props) {
updatedAccount || account,
)
const { data: prices } = usePrices()
const assets = useAllAssets()
const accountBalanceValue = useMemo(
() => calculateAccountBalanceValue(updatedAccount ?? account, prices, assets),
[updatedAccount, account, prices, assets],
() => calculateAccountBalanceValue(updatedAccount ?? account, prices),
[updatedAccount, account, prices],
)
const coin = BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, accountBalanceValue)
const leverage = useMemo(
() => calculateAccountLeverage(account, prices, assets),
[account, assets, prices],
)
const leverage = useMemo(() => calculateAccountLeverage(account, prices), [account, prices])
const updatedLeverage = useMemo(() => {
if (!updatedAccount) return null
const updatedLeverage = calculateAccountLeverage(updatedAccount, prices, assets)
const updatedLeverage = calculateAccountLeverage(updatedAccount, prices)
if (updatedLeverage.eq(leverage)) return null
return updatedLeverage
}, [updatedAccount, prices, leverage, assets])
}, [updatedAccount, prices, leverage])
const { data } = useBorrowMarketAssetsTableData(false)
const borrowAssetsData = useMemo(() => data?.allAssets || [], [data])
@@ -109,10 +104,9 @@ function AccountDetails(props: Props) {
lendingAssetsData,
prices,
hlsStrategies,
assets,
account.kind === 'high_levered_strategy',
),
[account, assets, borrowAssetsData, hlsStrategies, lendingAssetsData, prices, updatedAccount],
[account, borrowAssetsData, hlsStrategies, lendingAssetsData, prices, updatedAccount],
)
const isFullWidth = location.pathname.includes('trade') || location.pathname === '/'
@@ -1,22 +1,26 @@
import classNames from 'classnames'
import { useCallback, useEffect, useMemo, useState } from 'react'
import AccountFundRow from 'components/Account/AccountFund/AccountFundRow'
import Button from 'components/Button'
import DepositCapMessage from 'components/DepositCapMessage'
import { ArrowRight, Plus } from 'components/Icons'
import SwitchAutoLend from 'components/Switch/SwitchAutoLend'
import Text from 'components/Text'
import TokenInputWithSlider from 'components/TokenInput/TokenInputWithSlider'
import WalletBridges from 'components/Wallet/WalletBridges'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import { BN_ZERO } from 'constants/math'
import useBaseAsset from 'hooks/assets/useBasetAsset'
import useMarketAssets from 'hooks/markets/useMarketAssets'
import useAutoLend from 'hooks/useAutoLend'
import useLocalStorage from 'hooks/useLocalStorage'
import useMarketAssets from 'hooks/useMarketAssets'
import useToggle from 'hooks/useToggle'
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 { getAssetByDenom, getBaseAsset } from 'utils/assets'
import { defaultFee } from 'utils/constants'
import { getCapLeftWithBuffer } from 'utils/generic'
import { BN } from 'utils/helpers'
@@ -30,15 +34,21 @@ interface Props {
export default function AccountFundContent(props: Props) {
const deposit = useStore((s) => s.deposit)
const accounts = useStore((s) => s.accounts)
const walletAssetModal = useStore((s) => s.walletAssetsModal)
const [isConfirming, setIsConfirming] = useState(false)
const { autoLendEnabledAccountIds } = useAutoLend()
const isLending = autoLendEnabledAccountIds.includes(props.accountId)
const [lendAssets, setLendAssets] = useLocalStorage<boolean>(
LocalStorageKeys.LEND_ASSETS,
DEFAULT_SETTINGS.lendAssets,
)
const [fundingAssets, setFundingAssets] = useState<BNCoin[]>([])
const { data: marketAssets } = useMarketAssets()
const { data: walletBalances } = useWalletBalances(props.address)
const { autoLendEnabledAccountIds } = useAutoLend()
const [isLending, toggleIsLending] = useToggle(lendAssets)
const { simulateDeposits } = useUpdatedAccount(props.account)
const baseAsset = useBaseAsset()
const autoLendEnabled = autoLendEnabledAccountIds.includes(props.accountId)
const baseAsset = getBaseAsset()
const hasAssetSelected = fundingAssets.length > 0
const hasFundingAssets =
@@ -124,6 +134,14 @@ export default function AccountFundContent(props: Props) {
})
}, [])
useEffect(() => {
toggleIsLending(autoLendEnabled)
}, [autoLendEnabled, toggleIsLending])
useEffect(() => {
if (accounts?.length === 1 && isLending) setLendAssets(true)
}, [isLending, accounts, lendAssets, setLendAssets])
const depositCapReachedCoins = useMemo(() => {
const depositCapReachedCoins: BNCoin[] = []
fundingAssets.forEach((asset) => {
@@ -144,20 +162,26 @@ export default function AccountFundContent(props: Props) {
<div>
{!hasAssetSelected && <Text>Please select an asset.</Text>}
{fundingAssets.map((coin) => {
const asset = getAssetByDenom(coin.denom) as Asset
const balance = balances.find(byDenom(coin.denom))?.amount ?? BN_ZERO
return (
<div
key={coin.denom}
key={asset.symbol}
className={classNames(
'w-full mb-4',
props.isFullPage && 'w-full p-4 border rounded-base border-white/20 bg-white/5',
)}
>
<AccountFundRow
denom={coin.denom}
balances={balances}
<TokenInputWithSlider
asset={asset}
onChange={(amount) => updateFundingAssets(amount, asset.denom)}
amount={coin.amount ?? BN_ZERO}
isConfirming={isConfirming}
updateFundingAssets={updateFundingAssets}
max={balance}
balances={balances}
maxText='Max'
disabled={isConfirming}
warningMessages={[]}
/>
</div>
)
@@ -181,6 +205,8 @@ export default function AccountFundContent(props: Props) {
<SwitchAutoLend
className='pt-4 mt-4 border border-transparent border-t-white/10'
accountId={props.accountId}
value={!props.isFullPage ? isLending : undefined}
onChange={!props.isFullPage ? toggleIsLending : undefined}
/>
</div>
<Button
@@ -4,8 +4,8 @@ import AccountFundContent from 'components/Account/AccountFund/AccountFundConten
import Card from 'components/Card'
import { CircularProgress } from 'components/CircularProgress'
import FullOverlayContent from 'components/FullOverlayContent'
import useAccounts from 'hooks/accounts/useAccounts'
import useAccountId from 'hooks/useAccountId'
import useAccounts from 'hooks/useAccounts'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useStore from 'store'
@@ -1,34 +0,0 @@
import TokenInputWithSlider from 'components/TokenInput/TokenInputWithSlider'
import { BN_ZERO } from 'constants/math'
import useAsset from 'hooks/assets/useAsset'
import { BNCoin } from 'types/classes/BNCoin'
import { byDenom } from 'utils/array'
interface Props {
amount: BigNumber
balances: BNCoin[]
denom: string
isConfirming: boolean
updateFundingAssets: (amount: BigNumber, denom: string) => void
}
export default function AccountFundRow(props: Props) {
const asset = useAsset(props.denom)
if (!asset) return null
const balance = props.balances.find(byDenom(props.denom))?.amount ?? BN_ZERO
return (
<TokenInputWithSlider
asset={asset}
onChange={(amount) => props.updateFundingAssets(amount, asset.denom)}
amount={props.amount}
max={balance}
balances={props.balances}
maxText='Max'
disabled={props.isConfirming}
warningMessages={[]}
/>
)
}
@@ -5,8 +5,7 @@ import Skeleton from 'components/Account/AccountList/Skeleton'
import Button from 'components/Button'
import { ArrowDownLine, ArrowUpLine, TrashBin } from 'components/Icons'
import SwitchAutoLend from 'components/Switch/SwitchAutoLend'
import useAccount from 'hooks/accounts/useAccount'
import useAllAssets from 'hooks/assets/useAllAssets'
import useAccount from 'hooks/useAccount'
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
import useHealthComputer from 'hooks/useHealthComputer'
import useHLSStakingAssets from 'hooks/useHLSStakingAssets'
@@ -23,14 +22,13 @@ interface Props {
export default function AccountStats(props: Props) {
const { accountId, isActive, setShowMenu } = props
const assets = useAllAssets()
const { data: account } = useAccount(accountId)
const { data: prices } = usePrices()
const { data: hlsStrategies } = useHLSStakingAssets()
const positionBalance = useMemo(
() => (!account ? null : calculateAccountBalanceValue(account, prices, assets)),
[account, assets, prices],
() => (!account ? null : calculateAccountBalanceValue(account, prices)),
[account, prices],
)
const { health, healthFactor } = useHealthComputer(account)
const { data } = useBorrowMarketAssetsTableData(false)
@@ -51,10 +49,9 @@ export default function AccountStats(props: Props) {
lendingAssetsData,
prices,
hlsStrategies,
assets,
account.kind === 'high_levered_strategy',
),
[account, assets, borrowAssetsData, hlsStrategies, lendingAssetsData, prices],
[account, borrowAssetsData, hlsStrategies, lendingAssetsData, prices],
)
const deleteAccountHandler = useCallback(() => {
+3 -4
View File
@@ -1,13 +1,13 @@
import classNames from 'classnames'
import { useEffect } from 'react'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { useLocation, useNavigate } from 'react-router-dom'
import AccountStats from 'components/Account/AccountList/AccountStats'
import Card from 'components/Card'
import Radio from 'components/Radio'
import Text from 'components/Text'
import useAccountIds from 'hooks/accounts/useAccountIds'
import useAccountId from 'hooks/useAccountId'
import useAccountIds from 'hooks/useAccountIds'
import useStore from 'store'
import { getPage, getRoute } from 'utils/route'
@@ -28,7 +28,6 @@ export default function AccountList(props: Props) {
const currentAccountId = useAccountId()
const address = useStore((s) => s.address)
const { data: accountIds } = useAccountIds(address, true, true)
const [searchParams] = useSearchParams()
useEffect(() => {
if (!currentAccountId) return
@@ -55,7 +54,7 @@ export default function AccountList(props: Props) {
onClick={() => {
if (isActive) return
useStore.setState({ accountDeleteModal: null })
navigate(getRoute(getPage(pathname), searchParams, address, accountId))
navigate(getRoute(getPage(pathname), address, accountId))
}}
title={
<div className={accountCardHeaderClasses} role={!isActive ? 'button' : undefined}>
+1 -1
View File
@@ -2,7 +2,7 @@ import { Suspense } from 'react'
import AccountMenuContent from 'components/Account/AccountMenuContent'
import Loading from 'components/Loading'
import useAccountIds from 'hooks/accounts/useAccountIds'
import useAccountIds from 'hooks/useAccountIds'
import useStore from 'store'
function Content() {
+15 -13
View File
@@ -1,6 +1,6 @@
import classNames from 'classnames'
import { useCallback } from 'react'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { useLocation, useNavigate } from 'react-router-dom'
import AccountCreateFirst from 'components/Account/AccountCreateFirst'
import AccountFund from 'components/Account/AccountFund/AccountFundFullPage'
@@ -10,12 +10,13 @@ import { Account, Plus, PlusCircled } from 'components/Icons'
import Overlay from 'components/Overlay'
import Text from 'components/Text'
import WalletBridges from 'components/Wallet/WalletBridges'
import useAccountIds from 'hooks/accounts/useAccountIds'
import useBaseAsset from 'hooks/assets/useBasetAsset'
import useEnableAutoLendGlobal from 'hooks/localStorage/useEnableAutoLendGlobal'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import useAccountId from 'hooks/useAccountId'
import useAccountIds from 'hooks/useAccountIds'
import useAutoLend from 'hooks/useAutoLend'
import useCurrentWalletBalance from 'hooks/useCurrentWalletBalance'
import useLocalStorage from 'hooks/useLocalStorage'
import useToggle from 'hooks/useToggle'
import useStore from 'store'
import { defaultFee } from 'utils/constants'
@@ -30,16 +31,18 @@ export default function AccountMenuContent() {
const navigate = useNavigate()
const { pathname } = useLocation()
const address = useStore((s) => s.address)
const { data: accountIds } = useAccountIds(address, true, true)
const { data: accountIds } = useAccountIds(address)
const accountId = useAccountId()
const [searchParams] = useSearchParams()
const createAccount = useStore((s) => s.createAccount)
const baseAsset = useBaseAsset()
const baseCurrency = useStore((s) => s.baseCurrency)
const [showMenu, setShowMenu] = useToggle()
const [isCreating, setIsCreating] = useToggle()
const transactionFeeCoinBalance = useCurrentWalletBalance(baseAsset.denom)
const [enableAutoLendGlobal] = useEnableAutoLendGlobal()
const transactionFeeCoinBalance = useCurrentWalletBalance(baseCurrency.denom)
const [lendAssets] = useLocalStorage<boolean>(
LocalStorageKeys.LEND_ASSETS,
DEFAULT_SETTINGS.lendAssets,
)
const { enableAutoLendAccountId } = useAutoLend()
const hasCreditAccounts = !!accountIds?.length
@@ -60,8 +63,8 @@ export default function AccountMenuContent() {
setIsCreating(false)
if (accountId) {
navigate(getRoute(getPage(pathname), searchParams, address, accountId))
if (enableAutoLendGlobal) enableAutoLendAccountId(accountId)
navigate(getRoute(getPage(pathname), address, accountId))
if (lendAssets) enableAutoLendAccountId(accountId)
useStore.setState({
focusComponent: {
component: <AccountFund />,
@@ -77,9 +80,8 @@ export default function AccountMenuContent() {
createAccount,
navigate,
pathname,
searchParams,
address,
enableAutoLendGlobal,
lendAssets,
enableAutoLendAccountId,
])
+7 -9
View File
@@ -14,11 +14,10 @@ import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import { BN_ZERO } from 'constants/math'
import { ORACLE_DENOM } from 'constants/oracle'
import useAllAssets from 'hooks/assets/useAllAssets'
import useLocalStorage from 'hooks/localStorage/useLocalStorage'
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
import useHealthComputer from 'hooks/useHealthComputer'
import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData'
import useLocalStorage from 'hooks/useLocalStorage'
import usePrices from 'hooks/usePrices'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
@@ -35,14 +34,13 @@ export default function AccountSummary(props: Props) {
DEFAULT_SETTINGS.accountSummaryTabs,
)
const { data: prices } = usePrices()
const assets = useAllAssets()
const updatedAccount = useStore((s) => s.updatedAccount)
const accountBalance = useMemo(
() =>
props.account
? calculateAccountBalanceValue(updatedAccount ?? props.account, prices, assets)
? calculateAccountBalanceValue(updatedAccount ?? props.account, prices)
: BN_ZERO,
[props.account, updatedAccount, prices, assets],
[props.account, updatedAccount, prices],
)
const { data } = useBorrowMarketAssetsTableData(false)
const borrowAssetsData = useMemo(() => data?.allAssets || [], [data])
@@ -57,16 +55,16 @@ export default function AccountSummary(props: Props) {
const { health: updatedHealth, healthFactor: updatedHealthFactor } =
useHealthComputer(updatedAccount)
const leverage = useMemo(
() => (props.account ? calculateAccountLeverage(props.account, prices, assets) : BN_ZERO),
[props.account, prices, assets],
() => (props.account ? calculateAccountLeverage(props.account, prices) : BN_ZERO),
[props.account, prices],
)
const updatedLeverage = useMemo(() => {
if (!updatedAccount) return null
const updatedLeverage = calculateAccountLeverage(updatedAccount, prices, assets)
const updatedLeverage = calculateAccountLeverage(updatedAccount, prices)
if (updatedLeverage.eq(leverage)) return null
return updatedLeverage
}, [updatedAccount, prices, assets, leverage])
}, [updatedAccount, prices, leverage])
const handleToggle = useCallback(
(index: number) => {
+1 -1
View File
@@ -5,8 +5,8 @@ import HealthIcon from 'components/Account/Health/HealthIcon'
import HealthTooltip from 'components/Account/Health/HealthTooltip'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import useLocalStorage from 'hooks/localStorage/useLocalStorage'
import useHealthColor from 'hooks/useHealthColor'
import useLocalStorage from 'hooks/useLocalStorage'
import { getHealthIndicatorColors } from 'utils/healthIndicator'
interface Props {
@@ -5,8 +5,8 @@ import HealthIcon from 'components/Account/Health/HealthIcon'
import HealthTooltip from 'components/Account/Health/HealthTooltip'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import useLocalStorage from 'hooks/localStorage/useLocalStorage'
import useHealthColorAndLabel from 'hooks/useHealthColor'
import useLocalStorage from 'hooks/useLocalStorage'
import { computeHealthGaugePercentage } from 'utils/accounts'
import { getHealthIndicatorColors } from 'utils/healthIndicator'
@@ -1,4 +1,4 @@
import { ReactElement, useMemo } from 'react'
import React, { ReactElement, useMemo } from 'react'
import { CircularProgress } from 'components/CircularProgress'
import Text from 'components/Text'
+1 -1
View File
@@ -14,7 +14,7 @@ import Text from 'components/Text'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import { BN_ZERO } from 'constants/math'
import useLocalStorage from 'hooks/localStorage/useLocalStorage'
import useLocalStorage from 'hooks/useLocalStorage'
import { formatValue } from 'utils/formatters'
export const RiskChart = ({ data }: RiskChartProps) => {
-18
View File
@@ -1,18 +0,0 @@
import { FormattedNumber } from 'components/FormattedNumber'
type Props = {
asset: Asset
amount: number
}
export default function AssetAmount(props: Props) {
return (
<FormattedNumber
amount={props.amount}
options={{
decimals: props.asset.decimals,
maxDecimals: props.asset.decimals,
suffix: props.asset.symbol,
}}
/>
)
}
-16
View File
@@ -1,16 +0,0 @@
import Text from 'components/Text'
interface Props {
symbol: string
}
export default function AssetSymbol(props: Props) {
return (
<Text
size='xs'
tag='span'
className='rounded-sm bg-white/10 text-white/50 px-[6px] py-[2px] h-5'
>
{props.symbol}
</Text>
)
}
+37 -9
View File
@@ -1,10 +1,10 @@
import classNames from 'classnames'
import { useEffect, useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import useLocalStorage from 'hooks/localStorage/useLocalStorage'
import useLocalStorage from 'hooks/useLocalStorage'
import useStore from 'store'
import { getPage } from 'utils/route'
@@ -13,13 +13,41 @@ export default function Background() {
LocalStorageKeys.REDUCE_MOTION,
DEFAULT_SETTINGS.reduceMotion,
)
const [backgroundClasses, setBackgroundClasses] = useState<string[]>([
'bg-body',
'bg-orb-primary',
'bg-orb-secondary',
'bg-orb-tertiary',
])
const { pathname } = useLocation()
const page = getPage(pathname)
const isHLS = useMemo(() => page.split('-')[0] === 'hls', [page])
const currentAppSection = useMemo(() => {
switch (page.split('-')[0]) {
case 'hls':
setBackgroundClasses([
'bg-body-hls',
'bg-orb-primary-hls',
'bg-orb-secondary-hls',
'bg-orb-tertiary-hls',
])
return 'hls'
case 'stats':
setBackgroundClasses([
'bg-body',
'bg-orb-primary-stats',
'bg-orb-secondary-stats',
'bg-orb-tertiary-stats',
])
return 'stats'
default:
setBackgroundClasses(['bg-body', 'bg-orb-primary', 'bg-orb-secondary', 'bg-orb-tertiary'])
return 'app'
}
}, [page])
useEffect(() => {
useStore.setState({ isHLS })
}, [isHLS])
useStore.setState({ currentAppSection })
}, [currentAppSection])
return (
<div
@@ -27,7 +55,7 @@ export default function Background() {
'fixed inset-0',
'w-full h-full',
'overflow-hidden pointer-events-none background ',
isHLS ? 'bg-body-hls' : 'bg-body',
backgroundClasses[0],
!reduceMotion && 'transition-bg duration-1000 delay-300',
)}
>
@@ -39,7 +67,7 @@ export default function Background() {
'max-h-[500px] max-w-[500px]',
'left-[-10vw] top-[-10vw]',
'blur-orb-primary',
isHLS ? ' bg-orb-primary-hls' : 'bg-orb-primary',
backgroundClasses[1],
'translate-x-0 translate-y-0 rounded-full opacity-20',
!reduceMotion && 'animate-[float_120s_ease-in-out_infinite_2s]',
!reduceMotion && 'transition-bg duration-1000 delay-300',
@@ -53,7 +81,7 @@ export default function Background() {
'max-h-[1000px] max-w-[1000px]',
'bottom-[-20vw] right-[-10vw]',
'blur-orb-secondary',
isHLS ? ' bg-orb-secondary-hls' : 'bg-orb-secondary',
backgroundClasses[2],
'translate-x-0 translate-y-0 rounded-full opacity-30',
!reduceMotion && 'transition-bg duration-1000 delay-300',
)}
@@ -66,7 +94,7 @@ export default function Background() {
'max-h-[600px] max-w-[600px]',
'right-[-4vw] top-[-10vw]',
'blur-orb-tertiary ',
isHLS ? ' bg-orb-tertiary-hls' : 'bg-orb-tertiary',
backgroundClasses[3],
'translate-x-0 translate-y-0 rounded-full opacity-20',
!reduceMotion && 'animate-[float_180s_ease-in_infinite]',
!reduceMotion && 'transition-bg duration-1000 delay-300',
@@ -3,8 +3,8 @@ import { useCallback } from 'react'
import Button from 'components/Button'
import ActionButton from 'components/Button/ActionButton'
import { HandCoins, Plus } from 'components/Icons'
import useMarketEnabledAssets from 'hooks/assets/useMarketEnabledAssets'
import useStore from 'store'
import { getEnabledMarketAssets } from 'utils/assets'
interface Props {
data: BorrowMarketTableData
@@ -12,7 +12,7 @@ interface Props {
export default function BorrowActionButtons(props: Props) {
const { asset, debt } = props.data
const marketAssets = useMarketEnabledAssets()
const marketAssets = getEnabledMarketAssets()
const currentAsset = marketAssets.find((a) => a.denom === asset.denom)
const borrowHandler = useCallback(() => {
+7 -10
View File
@@ -1,8 +1,10 @@
import React from 'react'
import AvailableBorrowingsTable from 'components/Borrow/Table/AvailableBorrowingsTable'
import DepositedBorrowingsTable from 'components/Borrow/Table/DepositedBorrowingsTable'
import { BN_ZERO } from 'constants/math'
import useBorrowEnabledAssets from 'hooks/assets/useBorrowEnabledAssets'
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
import { getBorrowEnabledAssets } from 'utils/assets'
export default function Borrowings() {
const { data } = useBorrowMarketAssetsTableData()
@@ -19,21 +21,16 @@ export default function Borrowings() {
}
function Fallback() {
const assets = useBorrowEnabledAssets()
const assets = getBorrowEnabledAssets()
const data: BorrowMarketTableData[] = assets.map((asset) => ({
asset,
apy: {
borrow: 0,
deposit: 0,
},
ltv: {
max: 0,
liq: 0,
},
borrowRate: null,
liquidity: null,
marketMaxLtv: 0,
marketDepositAmount: BN_ZERO,
marketLiquidityRate: 0,
marketLiquidityAmount: BN_ZERO,
marketLiquidationThreshold: 0,
}))
return <AvailableBorrowingsTable data={data} isLoading />
@@ -1,11 +1,7 @@
import { FormattedNumber } from 'components/FormattedNumber'
import Loading from 'components/Loading'
export const BORROW_RATE_META = {
accessorKey: 'borrowRate',
header: 'Borrow Rate APY',
meta: { className: 'w-40' },
}
export const BORROW_RATE_META = { accessorKey: 'borrowRate', header: 'Borrow Rate APY' }
interface Props {
borrowRate: number | null
+2 -2
View File
@@ -2,8 +2,8 @@ import { Row } from '@tanstack/react-table'
import AmountAndValue from 'components/AmountAndValue'
import { BN_ZERO } from 'constants/math'
import useMarketEnabledAssets from 'hooks/assets/useMarketEnabledAssets'
import { byDenom } from 'utils/array'
import { getEnabledMarketAssets } from 'utils/assets'
export const DEBT_META = {
accessorKey: 'debt',
@@ -33,7 +33,7 @@ interface Props {
}
export default function Debt(props: Props) {
const marketAssets = useMarketEnabledAssets()
const marketAssets = getEnabledMarketAssets()
const asset = marketAssets.find(byDenom(props.data.asset.denom))
if (!asset) return null
@@ -3,14 +3,14 @@ import { Row } from '@tanstack/react-table'
import AmountAndValue from 'components/AmountAndValue'
import Loading from 'components/Loading'
import { BN_ZERO } from 'constants/math'
import useAsset from 'hooks/assets/useAsset'
import { byDenom } from 'utils/array'
import { getEnabledMarketAssets } from 'utils/assets'
import { demagnify } from 'utils/formatters'
export const LIQUIDITY_META = {
accessorKey: 'liquidity',
header: 'Liquidity Available',
id: 'liquidity',
meta: { className: 'w-40' },
}
export const liquiditySortingFn = (
@@ -31,7 +31,8 @@ interface Props {
export default function Liquidity(props: Props) {
const { liquidity, asset: borrowAsset } = props.data
const asset = useAsset(borrowAsset.denom)
const marketAssets = getEnabledMarketAssets()
const asset = marketAssets.find(byDenom(borrowAsset.denom))
if (!asset) return null
@@ -1,11 +1,8 @@
import React from 'react'
import { ChevronDown, ChevronUp } from 'components/Icons'
export const MANAGE_META = {
accessorKey: 'manage',
enableSorting: false,
header: 'Manage',
meta: { className: 'w-30' },
}
export const MANAGE_META = { accessorKey: 'manage', enableSorting: false, header: 'Manage' }
interface Props {
isExpanded: boolean
@@ -18,7 +18,7 @@ export default function useAvailableColumns() {
},
{
...BORROW_RATE_META,
cell: ({ row }) => <BorrowRate borrowRate={row.original.apy.borrow} />,
cell: ({ row }) => <BorrowRate borrowRate={row.original.borrowRate} />,
},
{
...LIQUIDITY_META,
@@ -24,7 +24,7 @@ export default function useDepositedColumns() {
},
{
...BORROW_RATE_META,
cell: ({ row }) => <BorrowRate borrowRate={row.original.apy.borrow} />,
cell: ({ row }) => <BorrowRate borrowRate={row.original.borrowRate} />,
},
{
...LIQUIDITY_META,
+1 -1
View File
@@ -6,7 +6,7 @@ import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import useLocalStorage from 'hooks/localStorage/useLocalStorage'
import useLocalStorage from 'hooks/useLocalStorage'
interface Props {
balance: number
+1 -1
View File
@@ -5,8 +5,8 @@ import { ACCOUNT_MENU_BUTTON_ID } from 'components/Account/AccountMenuContent'
import Button from 'components/Button'
import { Account, PlusCircled } from 'components/Icons'
import WalletConnectButton from 'components/Wallet/WalletConnectButton'
import useAccountIds from 'hooks/accounts/useAccountIds'
import useAccountId from 'hooks/useAccountId'
import useAccountIds from 'hooks/useAccountIds'
import useStore from 'store'
export default function ActionButton(props: ButtonProps) {
+6 -15
View File
@@ -13,7 +13,7 @@ export default function DropDownButton(props: Props) {
const [isOpen, toggleIsOpen] = useToggle(false)
return (
<Tooltip
content={<DropDown closeMenu={() => toggleIsOpen(false)} {...props} />}
content={<DropDown {...props} />}
type='info'
placement='bottom'
contentClassName='!bg-white/10 border border-white/20 backdrop-blur-xl !p-0'
@@ -34,35 +34,26 @@ export default function DropDownButton(props: Props) {
interface DropDownProps {
items: DropDownItem[]
closeMenu: () => void
}
function DropDown(props: DropDownProps) {
return (
<div>
{props.items.map((item) => (
<DropDownItem key={item.text} item={item} closeMenu={props.closeMenu} />
<DropDownItem key={item.text} {...item} />
))}
</div>
)
}
interface DropDownItemProps {
closeMenu: () => void
item: DropDownItem
}
function DropDownItem(props: DropDownItemProps) {
function DropDownItem(props: DropDownItem) {
return (
<button
onClick={() => {
props.item.onClick()
props.closeMenu()
}}
onClick={props.onClick}
className=' px-4 py-3 flex gap-2 items-center hover:bg-white/5 w-full [&:not(:last-child)]:border-b border-white/10'
>
<div className='flex justify-center w-5 h-5'>{props.item.icon}</div>
<Text size='sm'>{props.item.text}</Text>
<div className='flex justify-center w-5 h-5'>{props.icon}</div>
<Text size='sm'>{props.text}</Text>
</button>
)
}

Some files were not shown because too many files have changed in this diff Show More