mirror of
https://github.com/cerc-io/mars-interface.git
synced 2026-09-15 03:14:07 +00:00
initiating public sequence
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.button {
|
||||
transition: background-color 0.2s, border 0.2s;
|
||||
color: $fontColorLightPrimary;
|
||||
appearance: none;
|
||||
border: none;
|
||||
border-radius: $borderRadiusXXXL;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// Sizes
|
||||
&.small {
|
||||
@include buttonS;
|
||||
|
||||
svg,
|
||||
.progressIndicator {
|
||||
width: rem-calc(10);
|
||||
height: rem-calc(10);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin-inline-end: space(2);
|
||||
}
|
||||
|
||||
.suffix {
|
||||
margin-inline-start: space(2);
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
@include buttonM;
|
||||
|
||||
svg {
|
||||
width: rem-calc(12);
|
||||
height: rem-calc(12);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin-inline-end: space(3);
|
||||
}
|
||||
|
||||
.suffix {
|
||||
width: rem-calc(12);
|
||||
margin-inline-start: space(3);
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
@include buttonL;
|
||||
svg {
|
||||
width: rem-calc(18);
|
||||
height: rem-calc(18);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin-inline-end: space(4.5);
|
||||
}
|
||||
|
||||
.suffix {
|
||||
margin-inline-start: space(4.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Variants
|
||||
&.solid,
|
||||
&.round {
|
||||
@include buttonSolidPrimary;
|
||||
@include buttonSolidSecondary;
|
||||
@include buttonSolidTertiary;
|
||||
}
|
||||
|
||||
&.round {
|
||||
border-radius: $borderRadiusRound;
|
||||
@include padding(0);
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
@include margin(0);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.small {
|
||||
height: rem-calc(32);
|
||||
width: rem-calc(32);
|
||||
|
||||
svg {
|
||||
width: rem-calc(12);
|
||||
height: rem-calc(12);
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
height: rem-calc(40);
|
||||
width: rem-calc(40);
|
||||
|
||||
svg {
|
||||
width: rem-calc(14);
|
||||
height: rem-calc(14);
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
height: rem-calc(56);
|
||||
width: rem-calc(56);
|
||||
|
||||
svg {
|
||||
width: rem-calc(20);
|
||||
height: rem-calc(20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.transparent {
|
||||
background: none;
|
||||
@include padding(0);
|
||||
height: unset;
|
||||
|
||||
&.primary {
|
||||
color: $colorPrimary;
|
||||
|
||||
* {
|
||||
color: $colorPrimary;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $colorPrimaryHighlight;
|
||||
|
||||
* {
|
||||
color: $colorPrimaryHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
color: $colorPrimaryHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: $colorSecondary;
|
||||
|
||||
* {
|
||||
color: $colorSecondary;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: $colorSecondaryHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
&.tertiary {
|
||||
color: $colorSecondaryDark;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: lighten($colorSecondaryDark, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { CircularProgress } from '@material-ui/core'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import { ButtonStyleOverride } from '../types/components'
|
||||
import styles from './Button.module.scss'
|
||||
|
||||
interface Props {
|
||||
className?: any
|
||||
color?: 'primary' | 'secondary' | 'tertiary'
|
||||
disabled?: boolean
|
||||
externalLink?: string
|
||||
id?: string
|
||||
suffix?: ReactNode
|
||||
prefix?: ReactNode
|
||||
showProgressIndicator?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
styleOverride?: ButtonStyleOverride
|
||||
text?: string | ReactNode
|
||||
variant?: 'solid' | 'transparent' | 'round'
|
||||
onClick?: (e: any) => void
|
||||
}
|
||||
|
||||
const Button = ({
|
||||
className = '',
|
||||
color = 'primary',
|
||||
disabled,
|
||||
externalLink,
|
||||
id = '',
|
||||
suffix,
|
||||
prefix,
|
||||
showProgressIndicator,
|
||||
size = 'small',
|
||||
styleOverride,
|
||||
text,
|
||||
variant = 'solid',
|
||||
onClick,
|
||||
}: Props) => {
|
||||
const Button = () => (
|
||||
<button
|
||||
id={id}
|
||||
onClick={disabled ? () => {} : onClick}
|
||||
style={styleOverride}
|
||||
className={`${styles.button} ${styles[size]} ${styles[color]} ${
|
||||
styles[variant]
|
||||
} ${className} ${disabled ? `${styles.disabled}` : ''}`}
|
||||
>
|
||||
{prefix && !showProgressIndicator && (
|
||||
<div className={styles.prefix}>{prefix}</div>
|
||||
)}
|
||||
{text && (
|
||||
<div className={styles.text}>
|
||||
{showProgressIndicator ? (
|
||||
<CircularProgress
|
||||
color='inherit'
|
||||
size={
|
||||
size === 'small'
|
||||
? '10px'
|
||||
: size === 'medium'
|
||||
? '12px'
|
||||
: '18px'
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{suffix && !showProgressIndicator && (
|
||||
<div className={styles.suffix}>{suffix}</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
|
||||
return externalLink ? (
|
||||
<a
|
||||
href={externalLink}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={styles.link}
|
||||
>
|
||||
{Button()}
|
||||
</a>
|
||||
) : (
|
||||
Button()
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
||||
@@ -0,0 +1,52 @@
|
||||
import { createStyles, Switch, withStyles } from '@material-ui/core'
|
||||
import colors from '../styles/_assets.module.scss'
|
||||
|
||||
interface Props {
|
||||
switchCallback: (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
enabled: boolean
|
||||
) => void
|
||||
checked: boolean
|
||||
}
|
||||
|
||||
const CollateralSwitch = ({ switchCallback, checked }: Props) => {
|
||||
const CustomSwitch = withStyles(() =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: 28,
|
||||
height: 16,
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
},
|
||||
switchBase: {
|
||||
padding: 2,
|
||||
color: colors.grey,
|
||||
'&$checked': {
|
||||
transform: 'translateX(12px)',
|
||||
color: colors.white,
|
||||
'& + $track': {
|
||||
opacity: 1,
|
||||
backgroundColor: colors.primary,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
},
|
||||
},
|
||||
thumb: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
boxShadow: 'none',
|
||||
},
|
||||
track: {
|
||||
border: `1px solid ${colors.grey}`,
|
||||
borderRadius: 16 / 2,
|
||||
opacity: 1,
|
||||
backgroundColor: colors.white,
|
||||
},
|
||||
checked: {},
|
||||
})
|
||||
)(Switch)
|
||||
|
||||
return <CustomSwitch checked={checked} onChange={switchCallback} />
|
||||
}
|
||||
|
||||
export default CollateralSwitch
|
||||
@@ -0,0 +1,70 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.container {
|
||||
@include layoutTooltip;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: rem-calc(184);
|
||||
box-sizing: unset;
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
|
||||
.valueItem {
|
||||
margin-top: space(1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.dot {
|
||||
border: 1px solid $colorWhite;
|
||||
height: rem-calc(8);
|
||||
border-radius: $borderRadiusRound;
|
||||
width: rem-calc(8);
|
||||
margin-inline-end: space(2);
|
||||
margin-top: space(1.5);
|
||||
display: flex;
|
||||
flex: 0 0 rem-calc(8);
|
||||
}
|
||||
|
||||
.subHeadline {
|
||||
@include typoXS;
|
||||
@include margin(0, 0, 2);
|
||||
@include padding(0);
|
||||
text-transform: uppercase;
|
||||
opacity: 0.4;
|
||||
height: rem-calc(16);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex: 1 0 rem-calc(168);
|
||||
flex-direction: column;
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
:first-child {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.titleText {
|
||||
justify-content: start !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.subTitleContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
:first-child {
|
||||
flex: auto;
|
||||
}
|
||||
.subTitleText {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { formatValue } from '../libs/parse'
|
||||
import styles from './CollectionHover.module.scss'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface HoverItem {
|
||||
color?: string
|
||||
name: string
|
||||
amount?: number
|
||||
usdValue?: number
|
||||
negative?: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data?: HoverItem[]
|
||||
title?: string
|
||||
noPercent?: boolean
|
||||
}
|
||||
|
||||
const CollectionHover = ({ data, title, noPercent }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
// -----------------
|
||||
// CALCULATE
|
||||
// -----------------
|
||||
const totalUsdValue = data
|
||||
? data.reduce((total, item) => total + (item.usdValue || 0), 0)
|
||||
: 0
|
||||
|
||||
const producePercentage = (fraction: number, sum: number): string => {
|
||||
if (fraction <= 0.01 || sum <= 0.01) {
|
||||
return '0.00%'
|
||||
} else {
|
||||
return formatValue((fraction / sum) * 100, 2, 2, true, false, '%')
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// PRESENTATION
|
||||
// -----------------
|
||||
|
||||
const produceItem = (item: HoverItem, key: number) => {
|
||||
return (
|
||||
<div className={styles.item} key={key}>
|
||||
{item.usdValue ? (
|
||||
<div className={styles.valueItem}>
|
||||
{item.color && (
|
||||
<div
|
||||
className={styles.dot}
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.content}>
|
||||
<div className={styles.titleContainer}>
|
||||
<span className={`body ${styles.titleText}`}>
|
||||
{item.name}
|
||||
</span>
|
||||
<span className={`body ${styles.titleText}`}>
|
||||
{item.amount
|
||||
? formatValue(item.amount)
|
||||
: formatValue(
|
||||
item.usdValue,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
item.negative ? '$-' : '$'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.subTitleContainer}>
|
||||
<span
|
||||
className={`caption ${styles.subTitleText}`}
|
||||
>
|
||||
{!noPercent &&
|
||||
producePercentage(
|
||||
item.usdValue,
|
||||
totalUsdValue
|
||||
)}
|
||||
</span>
|
||||
{item.amount && (
|
||||
<span
|
||||
className={`caption ${styles.subTitleText}`}
|
||||
>
|
||||
{formatValue(
|
||||
item.usdValue,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
item.negative ? '$-' : '$'
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.subHeadline}>{item.name}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return data ? (
|
||||
<div className={styles.container}>
|
||||
<p className='sub2'>{title ? title : t('common.summary')}</p>
|
||||
{data.map((item: HoverItem, index: number) =>
|
||||
produceItem(item, index)
|
||||
)}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default CollectionHover
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useConnectedWallet, useWallet } from '@terra-money/wallet-provider'
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
import networks from '../networks'
|
||||
import useBlockHeightQuery from '../queries-new/BlockHeightQuery'
|
||||
import useStore from '../store'
|
||||
|
||||
interface CommonContainerProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const CommonContainer = ({ children }: CommonContainerProps) => {
|
||||
/**
|
||||
* Network configurations
|
||||
*/
|
||||
const { network: extNetwork, status } = useWallet()
|
||||
const networkName = extNetwork.name
|
||||
const network = networks[networkName]
|
||||
const connectedWallet = useConnectedWallet()
|
||||
|
||||
const isNetworkLoaded = useStore((s) => s.isNetworkLoaded)
|
||||
const setNetworkInfo = useStore((s) => s.setNetworkInfo)
|
||||
const setUserWalletAddress = useStore((s) => s.setUserWalletAddress)
|
||||
const setNetworkConfig = useStore((s) => s.setNetworkConfig)
|
||||
|
||||
useEffect(() => {
|
||||
setNetworkConfig(network || extNetwork)
|
||||
}, [network, extNetwork, setNetworkConfig])
|
||||
|
||||
useEffect(() => {
|
||||
setUserWalletAddress(connectedWallet?.terraAddress ?? '')
|
||||
}, [setUserWalletAddress, connectedWallet])
|
||||
|
||||
useEffect(() => {
|
||||
setNetworkInfo(networkName, status)
|
||||
}, [networkName, status, setNetworkInfo])
|
||||
|
||||
/**
|
||||
* Blockchain meta data
|
||||
*/
|
||||
useBlockHeightQuery()
|
||||
|
||||
return <>{isNetworkLoaded && children}</>
|
||||
}
|
||||
|
||||
export default CommonContainer
|
||||
@@ -0,0 +1,74 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.network {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 200;
|
||||
position: fixed;
|
||||
margin: -100% 0 0;
|
||||
transition: margin 0.5s;
|
||||
|
||||
&.show {
|
||||
@include margin(0);
|
||||
}
|
||||
|
||||
.container {
|
||||
@include margin(0);
|
||||
@include padding(6, 1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
text-align: center;
|
||||
background-color: $colorAccent;
|
||||
}
|
||||
|
||||
.headline {
|
||||
width: 100%;
|
||||
display: block;
|
||||
@include typoScaps;
|
||||
@include margin(0, 0, 3);
|
||||
}
|
||||
|
||||
p {
|
||||
width: 100%;
|
||||
display: block;
|
||||
letter-spacing: rem-calc(1);
|
||||
@include typoXS;
|
||||
@include margin(0, 0, 3);
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $colorPrimary;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: space(3);
|
||||
top: space(3);
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.5s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
@include padding(0);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: rem-calc(20);
|
||||
width: rem-calc(20);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import styles from './ErrorBanner.module.scss'
|
||||
import { CloseSVG } from './Svg'
|
||||
|
||||
interface ErrorBannerProps {
|
||||
hasNetworkError: boolean
|
||||
hasQueryError: boolean
|
||||
hasServerError: boolean
|
||||
isNetworkSupported: boolean | undefined
|
||||
}
|
||||
|
||||
const ErrorBanner = memo(
|
||||
({
|
||||
hasNetworkError,
|
||||
hasQueryError,
|
||||
hasServerError,
|
||||
isNetworkSupported,
|
||||
}: ErrorBannerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [hideError, setHideError] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasNetworkError || hasQueryError || hasServerError) {
|
||||
setHideError(false)
|
||||
}
|
||||
}, [hasNetworkError, hasQueryError, hasServerError])
|
||||
|
||||
const getHeadline = (): string => {
|
||||
return hasNetworkError
|
||||
? t('common.appearToBeOffline')
|
||||
: hasServerError
|
||||
? t('error.serverOfflineTitle')
|
||||
: t('error.failingRequest')
|
||||
}
|
||||
|
||||
const getBody = (): string => {
|
||||
return hasNetworkError
|
||||
? t('error.youHaveAFailingNetworkRequest')
|
||||
: hasServerError
|
||||
? t('error.serverOfflineBody')
|
||||
: t('error.failingRequestDescription')
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Error banner is disabled for GQL errors currently */}
|
||||
{(hasNetworkError || hasServerError) && isNetworkSupported && (
|
||||
<div
|
||||
className={
|
||||
!hideError
|
||||
? `${styles.network} ${styles.show}`
|
||||
: styles.network
|
||||
}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.close}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setHideError(true)
|
||||
}}
|
||||
>
|
||||
<CloseSVG />
|
||||
</button>
|
||||
</div>
|
||||
<h3 className={styles.headline}>{getHeadline()}</h3>
|
||||
<p>{getBody()}</p>
|
||||
{!hasNetworkError && (
|
||||
<p>
|
||||
<Trans i18nKey={'error.problemPersists'}>
|
||||
text
|
||||
<a
|
||||
className={styles.link}
|
||||
href='https://discord.gg/marsprotocol'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
link
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default ErrorBanner
|
||||
@@ -0,0 +1,54 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.select {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: $fontColorLightPrimary;
|
||||
width: rem-calc(120);
|
||||
height: rem-calc(37);
|
||||
display: inline-block;
|
||||
@include padding(0.5, 0, 0.5, 3);
|
||||
appearance: none;
|
||||
box-sizing: border-box;
|
||||
@include typoM;
|
||||
outline: none;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectWrapper {
|
||||
border: 1px solid $buttonBorder;
|
||||
color: $fontColorLightPrimary;
|
||||
width: rem-calc(120);
|
||||
height: rem-calc(37);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border-radius: $borderRadiusXXS;
|
||||
display: flex;
|
||||
flex: 0 0 rem-calc(37);
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
width: rem-calc(12);
|
||||
height: rem-calc(8);
|
||||
background-color: $fontColorLightPrimary;
|
||||
clip-path: polygon(100% 0%, 0 0%, 50% 100%);
|
||||
justify-self: end;
|
||||
position: absolute;
|
||||
right: rem-calc(8);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import i18n from 'i18next'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import styles from './LanguageSelect.module.scss'
|
||||
|
||||
const LanguageSelect = () => {
|
||||
const [currentLanguage, setCurrentLanguage] = useState('en')
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const lang = i18n.language.substring(0, 2) || 'en'
|
||||
if (currentLanguage !== lang) {
|
||||
setCurrentLanguage(lang)
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[i18n.language, currentLanguage]
|
||||
)
|
||||
|
||||
const changeLanguage = (lng: string) => {
|
||||
setCurrentLanguage(lng)
|
||||
i18n.changeLanguage(lng)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.selectWrapper}>
|
||||
<select
|
||||
className={styles.select}
|
||||
value={currentLanguage}
|
||||
onChange={(e) => changeLanguage(e.target.value)}
|
||||
>
|
||||
<option value='de'>Deutsch</option>
|
||||
<option value='en'>English</option>
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LanguageSelect
|
||||
@@ -0,0 +1,65 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.notification {
|
||||
width: 100%;
|
||||
min-height: rem-calc(48);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include layoutPopover;
|
||||
margin-bottom: space(8) !important;
|
||||
@include padding(2, 3);
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
|
||||
p {
|
||||
@include margin(0);
|
||||
font-weight: $fontWeightSemibold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.info {
|
||||
color: $colorAccent;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $colorInfoWarning;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $colorInfoWarning;
|
||||
}
|
||||
|
||||
&.closeBtnSpace {
|
||||
@include padding(0, 10, 0, 0);
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
@include margin(0, 1);
|
||||
color: $colorSecondary;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.closeNotification {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@include margin(0, 4, 0, 0);
|
||||
border: none;
|
||||
background: transparent;
|
||||
@include padding(0);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: rem-calc(14);
|
||||
height: rem-calc(13);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ReactNode, useMemo, useState } from 'react'
|
||||
|
||||
import styles from './Notification.module.scss'
|
||||
import { SmallCloseSVG } from './Svg'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { NotificationType } from '../types/enums'
|
||||
|
||||
interface Props {
|
||||
showNotification: boolean
|
||||
content: ReactNode
|
||||
type: NotificationType
|
||||
hideCloseBtn?: boolean
|
||||
}
|
||||
|
||||
const Notification = ({
|
||||
showNotification,
|
||||
content,
|
||||
type,
|
||||
hideCloseBtn = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [closeNotification, setCloseNotification] = useState(false)
|
||||
|
||||
const typeClass = useMemo(() => {
|
||||
return type === NotificationType.Warning
|
||||
? styles.warning
|
||||
: type === NotificationType.Error
|
||||
? styles.error
|
||||
: styles.info
|
||||
}, [type])
|
||||
|
||||
return (
|
||||
<>
|
||||
{showNotification && !closeNotification ? (
|
||||
<div
|
||||
className={`${styles.notification} ${typeClass} ${
|
||||
!hideCloseBtn ? styles.closeBtnSpace : null
|
||||
}`}
|
||||
>
|
||||
<p>{content}</p>
|
||||
|
||||
{!hideCloseBtn && (
|
||||
<button
|
||||
title={t('common.close')}
|
||||
className={styles.closeNotification}
|
||||
onClick={() => {
|
||||
setCloseNotification(true)
|
||||
}}
|
||||
>
|
||||
<SmallCloseSVG />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Notification
|
||||
@@ -0,0 +1,78 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.position {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
min-height: rem-calc(61);
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: rem-calc(200);
|
||||
flex: 0 0 rem-calc(200);
|
||||
|
||||
.value {
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
|
||||
h5 {
|
||||
span {
|
||||
@include typoM;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: flex-start;
|
||||
@include margin(2, 0, 0);
|
||||
height: rem-calc(8);
|
||||
flex-wrap: nowrap;
|
||||
|
||||
> .fraction {
|
||||
border-radius: $borderRadiusXXS;
|
||||
display: inline-block;
|
||||
@include margin(0, 0, 0, -1);
|
||||
@include padding(0, 0, 0, 1);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@include margin(0);
|
||||
@include padding(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
box-sizing: unset;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.compact {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpMediumHigh) {
|
||||
.compact {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.position {
|
||||
.container {
|
||||
width: rem-calc(120);
|
||||
flex: 0 0 rem-calc(120);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import Tippy from '@tippyjs/react'
|
||||
import styles from './PositionBar.module.scss'
|
||||
import colors from '../styles/_assets.module.scss'
|
||||
import { formatValue, lookup } from '../libs/parse'
|
||||
import CollectionHover, { HoverItem } from './CollectionHover'
|
||||
import { ReactElement } from 'react'
|
||||
import { UST_DECIMALS, UST_DENOM } from '../constants/appConstants'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
const PositionBar = ({
|
||||
title,
|
||||
value,
|
||||
bars,
|
||||
total,
|
||||
compactView = false,
|
||||
}: StrategyBarProps) => {
|
||||
const { t } = useTranslation()
|
||||
const produceData = (data: StrategyBarItem[]): HoverItem[] => {
|
||||
const items: HoverItem[] = []
|
||||
|
||||
data.forEach((asset: StrategyBarItem) => {
|
||||
if (asset.value !== 0) {
|
||||
items.push({
|
||||
color: asset.color || '',
|
||||
name: t(`strategy.${asset.name}`) || '',
|
||||
usdValue: lookup(asset.value, UST_DENOM, UST_DECIMALS),
|
||||
})
|
||||
}
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
const renderBars = (bars: StrategyBarItem[]): ReactElement[] => {
|
||||
const barParts: ReactElement[] = []
|
||||
bars.forEach((item: StrategyBarItem, index: number) => {
|
||||
barParts.push(
|
||||
<div
|
||||
key={index}
|
||||
className={styles.fraction}
|
||||
style={{
|
||||
width:
|
||||
item.value === 0
|
||||
? '0%'
|
||||
: ((item.value / total) * 100).toFixed(2) + '%',
|
||||
zIndex: 50 - index,
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
return barParts
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
compactView
|
||||
? `${styles.position} ${styles.compact}`
|
||||
: `${styles.position}`
|
||||
}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.value}>
|
||||
<h4>
|
||||
<span>$</span>
|
||||
{formatValue(value)}
|
||||
</h4>
|
||||
</div>
|
||||
<span className={'sub2'}>{title}</span>
|
||||
</div>
|
||||
{bars.length === 0 ? (
|
||||
<div className={styles.bar}>
|
||||
<div
|
||||
className={styles.fraction}
|
||||
style={{
|
||||
width: '33%',
|
||||
zIndex: 50 - 1,
|
||||
backgroundColor: colors.transparentWhite,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!(bars.length === 1 && bars[0].value === 0) && (
|
||||
<Tippy
|
||||
className={styles.box}
|
||||
content={
|
||||
<CollectionHover
|
||||
title={title}
|
||||
data={produceData(bars)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={styles.bar}>{renderBars(bars)}</div>
|
||||
</Tippy>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PositionBar
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
@include margin(8, 0);
|
||||
opacity: 0.6;
|
||||
|
||||
h6 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.horizontalLine {
|
||||
flex: auto;
|
||||
@include devider60;
|
||||
@include margin(0, 0, 3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import styles from './Title.module.scss'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
margin?: string
|
||||
}
|
||||
|
||||
const Title = ({ text, margin }: Props) => {
|
||||
return (
|
||||
<div className={styles.title}>
|
||||
<div className={styles.horizontalLine} />
|
||||
<h6 style={{ margin: margin || '0 40px' }}>{text}</h6>
|
||||
<div className={styles.horizontalLine} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Title
|
||||
@@ -0,0 +1,22 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.txFee {
|
||||
@include padding(0, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.3;
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
@include margin(0, 1.5, 0, 0);
|
||||
}
|
||||
|
||||
svg {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import styles from './TxFee.module.scss'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { formatValue } from '../libs/parse'
|
||||
|
||||
interface Props {
|
||||
txFee: string
|
||||
styleOverride?: any
|
||||
}
|
||||
|
||||
const TxFee = ({ txFee, styleOverride = {} }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className={styles.txFee}>
|
||||
<div className={`overline ${styles.label}`} style={styleOverride}>
|
||||
<span>{t('common.txFee')}</span>
|
||||
</div>
|
||||
<div className={`overline ${styles.value}`} style={styleOverride}>
|
||||
{formatValue(txFee, 2, 2, true, false, ' UST', true)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TxFee
|
||||
@@ -0,0 +1,41 @@
|
||||
import { formatValue } from '../libs/parse'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
gasFeeFormatted: string
|
||||
taxFormatted: string
|
||||
}
|
||||
|
||||
const TxFeeToolTip = ({ gasFeeFormatted, taxFormatted }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ fontSize: '0.8rem' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<span style={{ flex: 'auto', marginRight: '6px' }}>
|
||||
{t('common.gas')}
|
||||
</span>
|
||||
<span>
|
||||
{formatValue(
|
||||
gasFeeFormatted,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
' UST',
|
||||
true
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<span style={{ flex: 'auto', marginRight: '6px' }}>
|
||||
{t('common.stability')}
|
||||
</span>
|
||||
<span>
|
||||
{formatValue(taxFormatted, 2, 2, true, false, ' UST', true)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TxFeeToolTip
|
||||
@@ -0,0 +1,113 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
@include margin(12.5, 25, 4);
|
||||
width: rem-calc(230);
|
||||
height: rem-calc(265);
|
||||
border: 1px solid $graphAxis;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
|
||||
.scale {
|
||||
@include typoXXS;
|
||||
opacity: 0.6;
|
||||
line-height: rem-calc(12);
|
||||
position: absolute;
|
||||
width: rem-calc(95);
|
||||
text-align: end;
|
||||
left: rem-calc(-100);
|
||||
letter-spacing: rem-calc(2);
|
||||
|
||||
&.maxY {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&.minY {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.legend {
|
||||
@include typoXXScaps;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
bottom: rem-calc(-20);
|
||||
|
||||
&.position {
|
||||
left: rem-calc(6);
|
||||
width: rem-calc(120);
|
||||
}
|
||||
|
||||
&.debt {
|
||||
right: rem-calc(-10);
|
||||
width: rem-calc(95);
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
max-height: 100%;
|
||||
width: rem-calc(36);
|
||||
border-radius: $borderRadiusXS;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
&.supply {
|
||||
left: rem-calc(30);
|
||||
}
|
||||
|
||||
&.borrow {
|
||||
left: rem-calc(75);
|
||||
}
|
||||
|
||||
&.debt {
|
||||
left: rem-calc(172);
|
||||
transition: height 1s ease-out;
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
width: rem-calc(34);
|
||||
bottom: rem-calc(12);
|
||||
left: rem-calc(2);
|
||||
text-align: center;
|
||||
@include typoXXScaps;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.liquidation {
|
||||
@include typoXXScaps;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
width: rem-calc(105);
|
||||
margin-bottom: space(-3);
|
||||
text-align: end;
|
||||
left: rem-calc(-110);
|
||||
transition: bottom 1s ease-out;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.liquidationLine {
|
||||
display: block;
|
||||
width: rem-calc(110);
|
||||
height: 1px;
|
||||
border-bottom: 2px dashed $graphLiquidationsLine;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
transition: bottom 1s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import React from 'react'
|
||||
|
||||
import styles from './BarGraph.module.scss'
|
||||
import { formatValue } from '../../libs/parse'
|
||||
|
||||
interface barGraphData {
|
||||
bars: number[]
|
||||
labels: string[]
|
||||
classNames: string[]
|
||||
range: number[]
|
||||
liquidation: number
|
||||
legend: string[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: barGraphData
|
||||
}
|
||||
|
||||
const BarGraph = ({ data }: Props) => {
|
||||
const liquidationPosition = data.liquidation / (data.range[1] / 100)
|
||||
|
||||
const getBarHeightPercentage = (barIndex: number): number => {
|
||||
return Math.floor(
|
||||
(data.bars[barIndex] /
|
||||
Math.max(
|
||||
data.bars[0] || 0,
|
||||
data.bars[1] || 0,
|
||||
data.bars[2] || 0
|
||||
)) *
|
||||
100
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<span className={`${styles.scale} ${styles.maxY}`}>
|
||||
{formatValue(data.range[1], 2, 2, true, '$')}
|
||||
</span>
|
||||
<span className={`${styles.scale} ${styles.minY}`}>
|
||||
{formatValue(data.range[0], 0, 0, true, '$')}
|
||||
</span>
|
||||
<span className={`${styles.legend} ${styles.position}`}>
|
||||
{data.legend[0]}
|
||||
</span>
|
||||
{data.bars[2] > 0 && (
|
||||
<span className={`${styles.legend} ${styles.debt}`}>
|
||||
{data.legend[1]}
|
||||
</span>
|
||||
)}
|
||||
{data.bars[0] > 0 && (
|
||||
<div
|
||||
className={`${styles.bar} ${styles.supply} ${data.classNames[0]}`}
|
||||
style={
|
||||
data.bars[0] === 0
|
||||
? { opacity: 0, height: '0%' }
|
||||
: { height: `${getBarHeightPercentage(0)}%` }
|
||||
}
|
||||
>
|
||||
<span className={styles.label}>{data.labels[0]}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.bars[1] > 0 && (
|
||||
<div
|
||||
className={`${styles.bar} ${styles.borrow} ${data.classNames[1]}`}
|
||||
style={
|
||||
data.bars[1] === 0
|
||||
? { opacity: 0, height: '0%' }
|
||||
: { height: `${getBarHeightPercentage(1)}%` }
|
||||
}
|
||||
>
|
||||
<span className={styles.label}>{data.labels[1]}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.bars[2] > 0 && (
|
||||
<div
|
||||
className={`${styles.bar} ${styles.debt} ${data.classNames[2]}`}
|
||||
style={
|
||||
data.bars[2] === 0
|
||||
? { height: '0%' }
|
||||
: { height: `${getBarHeightPercentage(2)}%` }
|
||||
}
|
||||
>
|
||||
<span className={styles.label}>{data.labels[2]}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.liquidation > 0 && (
|
||||
<>
|
||||
<div
|
||||
className={styles.liquidation}
|
||||
style={{
|
||||
bottom: `${
|
||||
liquidationPosition < 13
|
||||
? 13
|
||||
: liquidationPosition
|
||||
}%`,
|
||||
}}
|
||||
>
|
||||
<span>Liquidation threshold</span>
|
||||
</div>
|
||||
<div
|
||||
className={styles.liquidationLine}
|
||||
style={{
|
||||
bottom: `${liquidationPosition}%`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BarGraph
|
||||
@@ -0,0 +1,118 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.title {
|
||||
@include margin(0, 0, 3);
|
||||
}
|
||||
|
||||
.progressbarContainer {
|
||||
position: relative;
|
||||
height: rem-calc(22);
|
||||
|
||||
.progressbar {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@include bgHatched;
|
||||
box-shadow: $shadowInset;
|
||||
border-radius: $borderRadiusXXXL;
|
||||
}
|
||||
|
||||
.limitLine {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background: $colorInfoWarning;
|
||||
transition: left 2s;
|
||||
box-shadow: $shadowInset;
|
||||
}
|
||||
|
||||
.limit {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background: $colorGreyDark;
|
||||
box-shadow: $shadowInset;
|
||||
border-top-left-radius: $borderRadiusXXXL;
|
||||
border-bottom-left-radius: $borderRadiusXXXL;
|
||||
transition: width 2s;
|
||||
}
|
||||
|
||||
.dot {
|
||||
position: absolute;
|
||||
width: rem-calc(3);
|
||||
height: rem-calc(3);
|
||||
background: $colorInfoWarning;
|
||||
margin-top: space(-1.75);
|
||||
margin-inline-start: space(-0.25);
|
||||
border-radius: $borderRadiusRound;
|
||||
transition: left 2s;
|
||||
}
|
||||
|
||||
.dotGlow {
|
||||
position: absolute;
|
||||
width: rem-calc(7);
|
||||
height: rem-calc(7);
|
||||
background: $colorInfoWarning;
|
||||
margin-top: space(-2.25);
|
||||
margin-inline-start: space(-1.25);
|
||||
border-radius: $borderRadiusXS;
|
||||
@include glowM;
|
||||
transition: left 2s;
|
||||
}
|
||||
|
||||
.ltvContainer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.mask {
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
border-radius: $borderRadiusL;
|
||||
transition: width 2s;
|
||||
color: $fontColorLtv;
|
||||
|
||||
> span {
|
||||
transition: left 2s;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
@include bgLimit;
|
||||
}
|
||||
|
||||
.glow {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
@include bgLimit;
|
||||
opacity: 0.4;
|
||||
transition: width 2s;
|
||||
@include glowXXL;
|
||||
@include margin(-5.5, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.values {
|
||||
display: flex;
|
||||
opacity: 0.6;
|
||||
margin-top: space(1.25);
|
||||
|
||||
.zero {
|
||||
text-align: start;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.limit {
|
||||
flex: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { formatValue, lookup } from '../../libs/parse'
|
||||
import styles from './BorrowLimit.module.scss'
|
||||
import { addDecimals } from '../../libs/math'
|
||||
import { UST_DECIMALS, UST_DENOM } from '../../constants/appConstants'
|
||||
|
||||
interface Props {
|
||||
width: string
|
||||
ltv: number
|
||||
maxLtv: number
|
||||
liquidationThreshold: number
|
||||
barHeight: string
|
||||
showPercentageText: boolean
|
||||
showTitleText: boolean
|
||||
showLegend?: boolean
|
||||
top?: number
|
||||
percentageThreshold?: number
|
||||
percentageOffset?: number
|
||||
title?: string
|
||||
mode?: string
|
||||
criticalIndicator?: number
|
||||
}
|
||||
|
||||
const BorrowLimit = ({
|
||||
width,
|
||||
ltv,
|
||||
maxLtv,
|
||||
liquidationThreshold,
|
||||
barHeight = '22px',
|
||||
showPercentageText = true,
|
||||
showLegend = true,
|
||||
top = 4,
|
||||
showTitleText = true,
|
||||
percentageThreshold = 15,
|
||||
percentageOffset = 45,
|
||||
title = 'Borrowing Capacity',
|
||||
mode = 'default',
|
||||
criticalIndicator,
|
||||
}: Props) => {
|
||||
const ltvPercent =
|
||||
+(((ltv || 0) / (liquidationThreshold || 0)) * 100).toFixed(2) || 0
|
||||
|
||||
const ltvPercentRounded = +(Math.round(ltvPercent * 100) / 100).toFixed(1)
|
||||
const ltvPercentRestrained =
|
||||
ltvPercent > 100 ? 100 : ltvPercent < 0 ? 0 : ltvPercent
|
||||
const ltvPercentMargin =
|
||||
ltvPercent > percentageThreshold
|
||||
? `-${percentageOffset + 15}px`
|
||||
: '10px'
|
||||
const maxBorrowPercent =
|
||||
criticalIndicator || (maxLtv / liquidationThreshold) * 100
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div style={{ width: width }}>
|
||||
{showTitleText ? (
|
||||
<div className={`overline ${styles.title}`}>{title}</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
style={{ height: barHeight }}
|
||||
className={styles.progressbarContainer}
|
||||
>
|
||||
<div
|
||||
style={{ width: width }}
|
||||
className={styles.progressbar}
|
||||
>
|
||||
<div
|
||||
style={{ left: `${maxBorrowPercent}%` }}
|
||||
className={styles.limitLine}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: `${maxBorrowPercent}%`,
|
||||
maxWidth: width,
|
||||
}}
|
||||
className={styles.limit}
|
||||
/>
|
||||
<div
|
||||
style={{ left: `${maxBorrowPercent}%` }}
|
||||
className={styles.dot}
|
||||
/>
|
||||
<div
|
||||
style={{ left: `${maxBorrowPercent}%` }}
|
||||
className={styles.dotGlow}
|
||||
/>
|
||||
|
||||
<div className={styles.ltvContainer}>
|
||||
<div
|
||||
style={{
|
||||
width: `${ltvPercentRestrained}%`,
|
||||
maxWidth: width,
|
||||
}}
|
||||
className={styles.mask}
|
||||
>
|
||||
{showPercentageText ? (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${ltvPercentRestrained}%`,
|
||||
top: `${top}px`,
|
||||
marginLeft: ltvPercentMargin,
|
||||
width: `${percentageOffset + 8}px`,
|
||||
textAlign:
|
||||
ltvPercent > percentageThreshold
|
||||
? 'right'
|
||||
: 'left',
|
||||
}}
|
||||
className='overline'
|
||||
>
|
||||
{ltv < 0 ? (
|
||||
'0%'
|
||||
) : (
|
||||
<>
|
||||
{`${
|
||||
mode === 'default'
|
||||
? ltvPercentRounded
|
||||
: addDecimals(ltv)
|
||||
}%`}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
<div
|
||||
style={{ width: width }}
|
||||
className={styles.indicator}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: `${ltvPercentRestrained}%`,
|
||||
maxWidth: width,
|
||||
}}
|
||||
className={styles.glow}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showLegend && (
|
||||
<div className={`overline ${styles.values}`}>
|
||||
<div className={styles.zero}>
|
||||
{mode === 'default' ? '$0' : '0%'}
|
||||
</div>
|
||||
<div className={styles.limit}>
|
||||
{mode === 'default' ? (
|
||||
<span>
|
||||
{formatValue(
|
||||
lookup(maxLtv, UST_DENOM, UST_DECIMALS),
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
'$'
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
formatValue(
|
||||
liquidationThreshold,
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
'%'
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BorrowLimit
|
||||
@@ -0,0 +1,5 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
@include layoutTile;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ReactNode } from 'react'
|
||||
import styles from './Card.module.scss'
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode
|
||||
styleOverride?: object
|
||||
}
|
||||
|
||||
const Card = ({ children, styleOverride }: Props) => {
|
||||
return (
|
||||
<div style={styleOverride} className={styles.container}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Card
|
||||
@@ -0,0 +1,48 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: 0;
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
|
||||
.innerText {
|
||||
position: absolute;
|
||||
top: 47.17%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 58.66%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: rem-calc(110);
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpMediumHigh) and (min-width: $bpMediumLow) {
|
||||
.container {
|
||||
@include padding(3, 0, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { Doughnut, defaults } from 'react-chartjs-2'
|
||||
|
||||
import styles from './DonutGraph.module.scss'
|
||||
import DonutHover from './charts/DonutHover'
|
||||
import { producePercentData } from '../../libs/assetInfo'
|
||||
import Tippy from '@tippyjs/react'
|
||||
import { formatValue } from '../../libs/parse'
|
||||
defaults.global.defaultFontFamily = 'Inter'
|
||||
|
||||
function hexToRgb(hex: any) {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
// Create a gradient to use on our chart. This is essentially a circular border around the slice(s)
|
||||
function createGrad(
|
||||
ctx: any,
|
||||
canvasWidth: number,
|
||||
canvasHeight: number,
|
||||
colorOne: string,
|
||||
colorTwo = '#1b1d24',
|
||||
opacityTwo = 0.35
|
||||
) {
|
||||
let opacityOne = 1,
|
||||
colorStop = 92
|
||||
var rgbOne = hexToRgb(colorOne)
|
||||
var rgbTwo = hexToRgb(colorTwo)
|
||||
colorStop = colorStop / 100
|
||||
|
||||
var grd = ctx
|
||||
.getContext('2d')
|
||||
.createRadialGradient(
|
||||
canvasWidth / 2,
|
||||
canvasHeight / 2,
|
||||
0.0,
|
||||
canvasWidth / 2,
|
||||
canvasHeight / 2,
|
||||
canvasHeight / 2
|
||||
)
|
||||
|
||||
if (rgbOne == null || rgbTwo == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Add color stops to the gradient
|
||||
grd.addColorStop(
|
||||
0.0,
|
||||
'rgba(' +
|
||||
rgbOne.r +
|
||||
', ' +
|
||||
rgbOne.g +
|
||||
', ' +
|
||||
rgbOne.b +
|
||||
', ' +
|
||||
opacityOne +
|
||||
')'
|
||||
)
|
||||
grd.addColorStop(
|
||||
colorStop,
|
||||
'rgba(' +
|
||||
rgbOne.r +
|
||||
', ' +
|
||||
rgbOne.g +
|
||||
', ' +
|
||||
rgbOne.b +
|
||||
', ' +
|
||||
opacityOne +
|
||||
')'
|
||||
)
|
||||
grd.addColorStop(
|
||||
0.94,
|
||||
'rgba(' +
|
||||
rgbTwo.r +
|
||||
', ' +
|
||||
rgbTwo.g +
|
||||
', ' +
|
||||
rgbTwo.b +
|
||||
', ' +
|
||||
opacityTwo +
|
||||
')'
|
||||
) // required to prevent jagged edges
|
||||
grd.addColorStop(
|
||||
1.0,
|
||||
'rgba(' +
|
||||
rgbTwo.r +
|
||||
', ' +
|
||||
rgbTwo.g +
|
||||
', ' +
|
||||
rgbTwo.b +
|
||||
', ' +
|
||||
opacityTwo +
|
||||
')'
|
||||
)
|
||||
|
||||
return grd
|
||||
}
|
||||
|
||||
function generateColors(mychart: any, defaultColors: any[]) {
|
||||
if (mychart == null) {
|
||||
return defaultColors
|
||||
}
|
||||
|
||||
return defaultColors.map((color) =>
|
||||
createGrad(
|
||||
mychart.chartInstance.ctx.canvas,
|
||||
mychart.chartInstance.width,
|
||||
mychart.chartInstance.height,
|
||||
color,
|
||||
'#1b1d24',
|
||||
0.0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function generateHoverColors(mychart: any, defaultColors: any[]) {
|
||||
if (mychart == null) {
|
||||
return defaultColors
|
||||
}
|
||||
|
||||
return defaultColors.map((color) =>
|
||||
createGrad(
|
||||
mychart.chartInstance.ctx.canvas,
|
||||
mychart.chartInstance.width,
|
||||
mychart.chartInstance.height,
|
||||
color,
|
||||
color
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
placeholderTitle?: string
|
||||
value: number
|
||||
cutoutPercentage: number
|
||||
labels: string[]
|
||||
data: AssetInfo[]
|
||||
colors: string[]
|
||||
expandableOnHover?: boolean
|
||||
labelsOnHover?: boolean
|
||||
styleOverride: Object
|
||||
}
|
||||
|
||||
const DonutGraph = ({
|
||||
title,
|
||||
value,
|
||||
cutoutPercentage,
|
||||
labels,
|
||||
data,
|
||||
colors,
|
||||
expandableOnHover = true,
|
||||
labelsOnHover = true,
|
||||
styleOverride,
|
||||
}: Props) => {
|
||||
const [showToolTip, setShowToolTip] = useState(false)
|
||||
const [myChart, setMyChart] = useState()
|
||||
const _chartRef: any = React.createRef()
|
||||
|
||||
useEffect(() => {
|
||||
setMyChart(_chartRef.current)
|
||||
}, [_chartRef])
|
||||
const colorsToAdd = generateColors(myChart, colors)
|
||||
const hoverColors = generateHoverColors(myChart, colors)
|
||||
|
||||
const percentData = useMemo(() => producePercentData(data), [data])
|
||||
|
||||
// Set up our dataset based on whether we require expandable sections on hover
|
||||
const chartHasData = useMemo(() => data.some((d) => !!d), [data])
|
||||
|
||||
const datasetToAdd = chartHasData
|
||||
? [
|
||||
{
|
||||
data: percentData,
|
||||
backgroundColor: expandableOnHover ? colorsToAdd : colors,
|
||||
hoverBackgroundColor: expandableOnHover
|
||||
? hoverColors
|
||||
: colors,
|
||||
borderWidth: 0,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
data: [100],
|
||||
backgroundColor: expandableOnHover
|
||||
? generateColors(myChart, ['#3a3c49'])
|
||||
: ['#3a3c49'],
|
||||
hoverBackgroundColor: expandableOnHover
|
||||
? generateHoverColors(myChart, ['#3a3c49'])
|
||||
: ['#3a3c49'],
|
||||
borderWidth: 0,
|
||||
},
|
||||
]
|
||||
|
||||
const chartData = {
|
||||
labels: labels,
|
||||
datasets: datasetToAdd,
|
||||
}
|
||||
|
||||
const chartOptions = {
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
custom: (tooltipModel: any) => {
|
||||
if (!chartHasData || !labelsOnHover) return
|
||||
// if chart is not defined, return early
|
||||
var chart: any = _chartRef.current
|
||||
if (!chart) {
|
||||
return
|
||||
}
|
||||
|
||||
// hide the tooltip when chartjs determines you've hovered out
|
||||
if (tooltipModel.opacity <= 0) {
|
||||
setShowToolTip(false)
|
||||
return
|
||||
}
|
||||
setShowToolTip(true)
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
cutoutPercentage: cutoutPercentage,
|
||||
maintainAspectRatio: true,
|
||||
circumference: Math.PI * 2,
|
||||
rotation: 0,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{showToolTip && expandableOnHover && (
|
||||
<Tippy content={<DonutHover title={title} data={data} />}>
|
||||
<div className={styles.overlay} />
|
||||
</Tippy>
|
||||
)}
|
||||
<div style={styleOverride}>
|
||||
<Doughnut
|
||||
data={chartData}
|
||||
options={chartOptions}
|
||||
ref={_chartRef}
|
||||
/>
|
||||
<div className={styles.innerText}>
|
||||
{
|
||||
<div>
|
||||
<span>$</span>
|
||||
<span className={'h4'}>
|
||||
{formatValue(Number(value), 2, 2, true, false)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
<p className={'sub2'}>{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DonutGraph
|
||||
@@ -0,0 +1,5 @@
|
||||
@use '../../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { UST_DENOM, UST_DECIMALS } from '../../../constants/appConstants'
|
||||
import { lookup } from '../../../libs/parse'
|
||||
import CollectionHover, { HoverItem } from '../../CollectionHover'
|
||||
|
||||
interface Props {
|
||||
data: AssetInfo[]
|
||||
title: string
|
||||
}
|
||||
|
||||
const DonutHover = ({ data, title }: Props) => {
|
||||
const produceData = (data: AssetInfo[]): HoverItem[] => {
|
||||
const items: HoverItem[] = []
|
||||
|
||||
data.forEach((asset: AssetInfo) => {
|
||||
if (Number(asset.uusdBalance) > 0) {
|
||||
items.push({
|
||||
color: asset.color || '',
|
||||
name: asset.symbol || '',
|
||||
amount: lookup(
|
||||
Number(asset.balance),
|
||||
asset.denom,
|
||||
asset.decimals
|
||||
),
|
||||
usdValue: lookup(
|
||||
asset.uusdBalance || 0,
|
||||
UST_DENOM,
|
||||
UST_DECIMALS
|
||||
),
|
||||
})
|
||||
}
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
return <CollectionHover title={title} data={produceData(data)} />
|
||||
}
|
||||
|
||||
export default DonutHover
|
||||
@@ -0,0 +1,17 @@
|
||||
interface Props {
|
||||
symbol: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const Asset = ({ symbol, name }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<div>{symbol}</div>
|
||||
<div style={{ opacity: 0.6 }} className='caption'>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Asset
|
||||
@@ -0,0 +1,47 @@
|
||||
import { UST_DECIMALS, UST_DENOM } from '../../constants/appConstants'
|
||||
import { formatValue, lookup } from '../../libs/parse'
|
||||
|
||||
interface Props {
|
||||
denom: string
|
||||
decimals: number
|
||||
amount: number
|
||||
uusdAmount: number
|
||||
}
|
||||
|
||||
const CellAmount = ({ denom, decimals, amount, uusdAmount }: Props) => {
|
||||
const usdAmount = lookup(uusdAmount, UST_DENOM, UST_DECIMALS)
|
||||
const assetAmount = lookup(amount, denom, decimals)
|
||||
return (
|
||||
<div>
|
||||
{denom !== 'uusd'
|
||||
? formatValue(
|
||||
assetAmount > 0 && assetAmount < 0.01
|
||||
? 0.01
|
||||
: assetAmount,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
assetAmount > 0 && assetAmount < 0.01 ? '< ' : false
|
||||
)
|
||||
: null}
|
||||
<div
|
||||
style={{
|
||||
opacity: denom !== 'uusd' ? 0.6 : 1,
|
||||
}}
|
||||
className={denom !== 'uusd' ? 'caption' : ''}
|
||||
>
|
||||
<span>
|
||||
{formatValue(
|
||||
usdAmount > 0 && usdAmount < 0.01 ? 0.01 : usdAmount,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
usdAmount > 0 && usdAmount < 0.01 ? '< $' : '$'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CellAmount
|
||||
@@ -0,0 +1,232 @@
|
||||
import { useTable, useSortBy } from 'react-table'
|
||||
import { useEffect } from 'react'
|
||||
import colors from '../../styles/_assets.module.scss'
|
||||
const Grid = ({
|
||||
columns,
|
||||
data,
|
||||
initialState,
|
||||
hideheader = false,
|
||||
updateSort,
|
||||
}) => {
|
||||
const {
|
||||
columns: instanceColumns,
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
toggleSortBy,
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
initialState,
|
||||
autoResetSortBy: false,
|
||||
disableSortRemove: true,
|
||||
disableMultiSort: true,
|
||||
sortDescFirst: true,
|
||||
},
|
||||
useSortBy
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
instanceColumns.forEach((column) => {
|
||||
if (column.isSorted) {
|
||||
if (
|
||||
initialState.sortBy[0].id !== column.id ||
|
||||
initialState.sortBy[0].desc !== column.isSortedDesc
|
||||
)
|
||||
updateSort({
|
||||
sortBy: [
|
||||
{
|
||||
id: column.id,
|
||||
desc: column.isSortedDesc,
|
||||
},
|
||||
],
|
||||
})
|
||||
return false
|
||||
}
|
||||
})
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[rows]
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
instanceColumns.forEach((column) => {
|
||||
let cleared = false
|
||||
if (column.isSorted) {
|
||||
if (
|
||||
initialState.sortBy[0].id !== column.id ||
|
||||
initialState.sortBy[0].desc !== column.isSortedDesc
|
||||
) {
|
||||
cleared = true
|
||||
column.clearSortBy()
|
||||
}
|
||||
}
|
||||
|
||||
if (cleared) {
|
||||
toggleSortBy(
|
||||
initialState.sortBy[0].id,
|
||||
initialState.sortBy[0].desc,
|
||||
false
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[initialState]
|
||||
)
|
||||
return (
|
||||
<table
|
||||
{...getTableProps()}
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
borderSpacing: '0px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
{headerGroups[0].headers.map((column) => {
|
||||
return (
|
||||
<th
|
||||
{...column.getHeaderProps(
|
||||
column.getSortByToggleProps()
|
||||
)}
|
||||
style={{
|
||||
borderTop: hideheader
|
||||
? 'none'
|
||||
: `1px solid ${colors.tableBorder}`,
|
||||
borderBottom: `1px solid ${colors.tableBorder}`,
|
||||
cursor: column.disableSortBy
|
||||
? 'default'
|
||||
: 'pointer',
|
||||
width: column.width,
|
||||
color: colors.tableHeader, // Overwrite the .caption color to be 50% transparent
|
||||
}}
|
||||
className='caption'
|
||||
>
|
||||
{column.hideHeader || hideheader ? null : (
|
||||
<div
|
||||
style={{
|
||||
padding:
|
||||
column.headingPaddingOverride
|
||||
? column.headingPaddingOverride
|
||||
: '8px',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: 'flex',
|
||||
whiteSpace:
|
||||
column.whiteSpace ||
|
||||
'nowrap',
|
||||
textAlign:
|
||||
column.textAlign ||
|
||||
'inherit',
|
||||
justifyContent:
|
||||
column.textAlign === 'right'
|
||||
? 'flex-end'
|
||||
: '',
|
||||
}}
|
||||
>
|
||||
{column.textAlign !== 'right' &&
|
||||
column.render('Header')}
|
||||
{!column.disableSortBy ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
margin:
|
||||
column.textAlign ===
|
||||
'right'
|
||||
? 'auto 7px auto 0'
|
||||
: 'auto 0 auto 7px',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft:
|
||||
'4px solid transparent',
|
||||
borderRight:
|
||||
'4px solid transparent',
|
||||
borderBottom:
|
||||
!column.disableSortBy
|
||||
? !column.isSortedDesc &&
|
||||
column.isSorted
|
||||
? `4px solid ${colors.tableSortActive}`
|
||||
: `4px solid ${colors.tableSort}`
|
||||
: 'none',
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
marginTop: '3px',
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft:
|
||||
'4px solid transparent',
|
||||
borderRight:
|
||||
'4px solid transparent',
|
||||
borderTop:
|
||||
!column.disableSortBy
|
||||
? column.isSortedDesc &&
|
||||
column.isSorted
|
||||
? `4px solid ${colors.tableSortActive}`
|
||||
: `4px solid ${colors.tableSort}`
|
||||
: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{column.textAlign === 'right' &&
|
||||
column.render('Header')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody {...getTableBodyProps()}>
|
||||
{rows.map((row) => {
|
||||
prepareRow(row)
|
||||
return (
|
||||
<tr {...row.getRowProps()}>
|
||||
{row.cells.map((cell) => {
|
||||
return (
|
||||
<td
|
||||
{...cell.getCellProps()}
|
||||
style={{
|
||||
textAlign: cell.column.textAlign,
|
||||
height: '53px',
|
||||
width: cell.column.width,
|
||||
padding: cell.column.paddingOverride
|
||||
? cell.column.paddingOverride
|
||||
: '0 8px',
|
||||
borderBottom: `1px solid ${colors.tableBorder}`,
|
||||
overflow: cell.column.showOverflow
|
||||
? 'visable'
|
||||
: 'hidden',
|
||||
}}
|
||||
className='body2'
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
export default Grid
|
||||
@@ -0,0 +1,191 @@
|
||||
import OverflowDropdown from '../../components/grid/dropdown/OverflowDropdown'
|
||||
import useActionButtonClickHandler from '../../hooks/useActionButtonClickHandler'
|
||||
import { useRedBank, useAccountBalance } from '../../hooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DropdownItemProps } from '../../types/components'
|
||||
import { useHistory, useLocation } from 'react-router'
|
||||
import { ASTROPORT_URL } from '../../constants/appConstants'
|
||||
import useStore from '../../store'
|
||||
import Button from '../Button'
|
||||
import { ExternalSVG } from '../Svg'
|
||||
import { getRoute } from '../../libs/parse'
|
||||
import { ActionType } from '../../types/enums'
|
||||
|
||||
export enum GridActionType {
|
||||
BorrowAction,
|
||||
DepositAction,
|
||||
WithdrawAction,
|
||||
ManageAction,
|
||||
RepayAction,
|
||||
None,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
denom: string
|
||||
menuItems: DropdownItemProps[]
|
||||
actionType: GridActionType
|
||||
strategy?: StrategyObject
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const GridActions = ({
|
||||
denom,
|
||||
menuItems,
|
||||
actionType,
|
||||
strategy,
|
||||
disabled = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const newMenuItems = [...menuItems]
|
||||
const mainButton = newMenuItems[0]
|
||||
const whitelistedAssets = useStore((s) => s.whitelistedAssets)
|
||||
|
||||
const { find, findDeposit } = useAccountBalance()
|
||||
const { findLiquidity, findMarketInfo } = useRedBank()
|
||||
const provideClickHandler = useActionButtonClickHandler()
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
|
||||
const swapUrl =
|
||||
denom === 'uusd'
|
||||
? `${ASTROPORT_URL}?from=uluna&to=uusd`
|
||||
: denom === 'uluna'
|
||||
? `${ASTROPORT_URL}?from=uusd&to=uluna`
|
||||
: `${ASTROPORT_URL}?from=uusd&to=${
|
||||
whitelistedAssets?.find((asset) => asset.denom === denom)
|
||||
?.contract_addr
|
||||
}`
|
||||
|
||||
const marketInfo = findMarketInfo(denom)
|
||||
|
||||
const buyClickHandler = provideClickHandler(
|
||||
ActionType.ExternalLink,
|
||||
denom,
|
||||
GridActionType.None,
|
||||
swapUrl
|
||||
)
|
||||
|
||||
let dropdownMenuItems = newMenuItems.slice(1, menuItems.length)
|
||||
|
||||
let useBuyButton = false
|
||||
if (actionType === GridActionType.DepositAction) {
|
||||
const assetWallet = find(denom)
|
||||
|
||||
if (Number(assetWallet?.amount || 0) <= 0) useBuyButton = true
|
||||
}
|
||||
|
||||
let disabledDepositButton: boolean = false
|
||||
// If the market has had deposits disabled then disable deposit button
|
||||
if (
|
||||
actionType === GridActionType.DepositAction &&
|
||||
!marketInfo?.deposit_enabled
|
||||
) {
|
||||
disabledDepositButton = true
|
||||
}
|
||||
// Remove deposit from submenu if it's disabled (applicable to My Station's grid)
|
||||
if (
|
||||
actionType === GridActionType.WithdrawAction &&
|
||||
!marketInfo?.deposit_enabled
|
||||
) {
|
||||
dropdownMenuItems = dropdownMenuItems.filter(
|
||||
(item) => item.gridAction !== GridActionType.DepositAction
|
||||
)
|
||||
}
|
||||
|
||||
let disableBorrowButton: boolean = false
|
||||
if (actionType === GridActionType.BorrowAction) {
|
||||
// If the market has had borrows disabled then disable borrow button
|
||||
if (!marketInfo?.borrow_enabled) {
|
||||
disableBorrowButton = true
|
||||
}
|
||||
// If the user has no collateral then disable borrow button
|
||||
if (!disableBorrowButton && whitelistedAssets?.length) {
|
||||
let depositSum: number = 0
|
||||
whitelistedAssets.forEach((asset) => {
|
||||
depositSum += Number(findDeposit(asset.denom)?.amount || 0)
|
||||
})
|
||||
|
||||
if (depositSum <= 0) disableBorrowButton = true
|
||||
}
|
||||
// if the market has no liquidity available then disable borrow button
|
||||
if (!disableBorrowButton) {
|
||||
const assetLiquidity = findLiquidity(denom)
|
||||
|
||||
if (Number(assetLiquidity?.amount || 0) <= 0)
|
||||
disableBorrowButton = true
|
||||
}
|
||||
}
|
||||
// Remove borrow from submenu if it's disabled (applicable to My Station's grid)
|
||||
if (
|
||||
actionType === GridActionType.RepayAction &&
|
||||
!marketInfo?.borrow_enabled
|
||||
) {
|
||||
dropdownMenuItems = dropdownMenuItems.filter(
|
||||
(item) => item.gridAction !== GridActionType.BorrowAction
|
||||
)
|
||||
}
|
||||
|
||||
// Disabled prop is used to disable all actions of a market on the UI, i.e. on the not connected screens where we show a dummie grid
|
||||
// The !marketInfo?.active flag determines if all actions against the market has been disabled on the SC level
|
||||
const isAllActionsDisabled: boolean = disabled || !marketInfo?.active
|
||||
const isDropdownDisabled: boolean =
|
||||
useBuyButton &&
|
||||
dropdownMenuItems.length === 1 &&
|
||||
dropdownMenuItems[0].title === 'Swap'
|
||||
const isPrimaryActionDisabled: boolean =
|
||||
disabledDepositButton || disableBorrowButton
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
{useBuyButton ? (
|
||||
<Button
|
||||
text={t('common.buy')}
|
||||
suffix={<ExternalSVG />}
|
||||
color='secondary'
|
||||
onClick={buyClickHandler}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{strategy ? (
|
||||
<Button
|
||||
text={mainButton?.title || ''}
|
||||
onClick={() => {
|
||||
history.push(`/fields/${actionType}`)
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
text={mainButton?.title || ''}
|
||||
disabled={
|
||||
isAllActionsDisabled || isPrimaryActionDisabled
|
||||
}
|
||||
onClick={() => {
|
||||
history.push(
|
||||
`${location.pathname}/${getRoute(
|
||||
actionType
|
||||
)}/${denom}`
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<OverflowDropdown
|
||||
menuItems={dropdownMenuItems}
|
||||
denom={denom}
|
||||
strategy={strategy}
|
||||
disabled={isAllActionsDisabled || isDropdownDisabled}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GridActions
|
||||
@@ -0,0 +1,13 @@
|
||||
@use '../../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
@include padding(1, 4);
|
||||
display: flex;
|
||||
flex: auto;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: $alphaWhite10;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useHistory } from 'react-router'
|
||||
import useActionButtonClickHandler from '../../../hooks/useActionButtonClickHandler'
|
||||
import { DropdownItemProps } from '../../../types/components'
|
||||
import styles from './DropdownItem.module.scss'
|
||||
|
||||
const DropdownItem = ({
|
||||
icon,
|
||||
title,
|
||||
denom,
|
||||
actionType,
|
||||
gridAction,
|
||||
url,
|
||||
close,
|
||||
strategy,
|
||||
}: DropdownItemProps) => {
|
||||
const provideClickHandler = useActionButtonClickHandler()
|
||||
const history = useHistory()
|
||||
const clickHandler = strategy
|
||||
? () => {
|
||||
history.push(`/fields/strategy/${strategy.key}`)
|
||||
}
|
||||
: provideClickHandler(actionType, denom, gridAction, url)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
onClick={() => {
|
||||
clickHandler()
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
<span className='body2'>{title}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DropdownItem
|
||||
@@ -0,0 +1,9 @@
|
||||
@use '../../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
@include layoutTooltip;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
margin-inline-start: space(1);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useState } from 'react'
|
||||
import Tippy, { TippyProps } from '@tippyjs/react'
|
||||
import DropdownItem from './DropdownItem'
|
||||
import MoreHorizIcon from '@material-ui/icons/MoreHoriz'
|
||||
|
||||
import styles from './OverflowDropdown.module.scss'
|
||||
import { DropdownItemProps } from '../../../types/components'
|
||||
import Button from '../../Button'
|
||||
|
||||
export const DefaultTippyProps: TippyProps = {
|
||||
animation: false,
|
||||
interactive: true,
|
||||
appendTo: document.body,
|
||||
}
|
||||
|
||||
export const DropdownTippyProps: TippyProps = {
|
||||
...DefaultTippyProps,
|
||||
placement: 'bottom-end',
|
||||
}
|
||||
|
||||
interface OverflowDropdownProps {
|
||||
menuItems: DropdownItemProps[]
|
||||
denom: string
|
||||
strategy?: StrategyObject
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const OverflowDropdown = ({
|
||||
menuItems,
|
||||
denom,
|
||||
strategy,
|
||||
disabled = false,
|
||||
}: OverflowDropdownProps) => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const show = () => setVisible(true)
|
||||
const hide = () => setVisible(false)
|
||||
const renderDropdown = () => (
|
||||
<div className={styles.container}>
|
||||
{menuItems.map((menuItem, index) => (
|
||||
<DropdownItem
|
||||
key={index}
|
||||
icon={menuItem.icon}
|
||||
title={menuItem.title}
|
||||
denom={denom}
|
||||
actionType={menuItem.actionType}
|
||||
gridAction={menuItem.gridAction}
|
||||
url={menuItem.url}
|
||||
close={hide}
|
||||
strategy={strategy}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (menuItems.length) {
|
||||
return (
|
||||
<div>
|
||||
<Tippy
|
||||
{...DropdownTippyProps}
|
||||
render={renderDropdown}
|
||||
visible={visible}
|
||||
onClickOutside={hide}
|
||||
>
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
color='tertiary'
|
||||
variant='round'
|
||||
prefix={
|
||||
<MoreHorizIcon
|
||||
style={{
|
||||
width: '1.2rem',
|
||||
height: '1.2rem',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClick={visible ? hide : show}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</Tippy>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default OverflowDropdown
|
||||
@@ -0,0 +1,156 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.button,
|
||||
.disabledButton,
|
||||
.secondary {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-wrap: nowrap;
|
||||
border-radius: $borderRadiusXXL;
|
||||
height: rem-calc(31);
|
||||
@include padding(0.5, 4, 0);
|
||||
@include typoButton;
|
||||
cursor: pointer;
|
||||
color: $colorWhite;
|
||||
border: 1px solid $alphaWhite60;
|
||||
outline: none;
|
||||
background: $alphaBlack10;
|
||||
text-transform: uppercase;
|
||||
|
||||
> span {
|
||||
@include margin(0, 0, 0, 2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $alphaWhite60;
|
||||
text-decoration: none;
|
||||
}
|
||||
&:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
@include margin(-0.5, 0, 0);
|
||||
height: rem-calc(16);
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.disabledButton:hover {
|
||||
background: $alphaWhite30;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: $colorSecondaryHighlight;
|
||||
border: none;
|
||||
@include padding(0.5, 5.5);
|
||||
height: rem-calc(36);
|
||||
|
||||
&:hover {
|
||||
background: $colorSecondary;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
top: rem-calc(38);
|
||||
@include padding(6, 6, 5.5);
|
||||
@include layoutPopover;
|
||||
width: rem-calc(360);
|
||||
max-width: 100vw;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.option {
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
color: $colorAccent;
|
||||
border: none;
|
||||
appearance: none;
|
||||
@include typoL;
|
||||
@include padding(2, 0);
|
||||
font-weight: $fontWeightRegular;
|
||||
text-align: start;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 100%;
|
||||
text-decoration: none;
|
||||
|
||||
.image {
|
||||
opacity: 0.6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: $colorSecondaryHighlight;
|
||||
|
||||
.image {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
@include margin(0, 2, 0, 0);
|
||||
|
||||
svg,
|
||||
img {
|
||||
width: rem-calc(24);
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@include margin(-1.5, 0, 0, 1);
|
||||
|
||||
svg {
|
||||
fill: $colorAccent;
|
||||
opacity: 0.5;
|
||||
align-self: flex-start;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpMediumHigh) {
|
||||
.dropdown {
|
||||
&:not(.centered) {
|
||||
right: 0;
|
||||
transform: none;
|
||||
left: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary {
|
||||
+ .dropdown {
|
||||
right: unset;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.dropdown {
|
||||
max-width: calc(100vw - 24px);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
import styles from './ConnectButton.module.scss'
|
||||
import { WalletSVG } from '../Svg'
|
||||
import { ClickAwayListener } from '@material-ui/core'
|
||||
import { ConnectType, useWallet } from '@terra-money/wallet-provider'
|
||||
import * as rdd from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
textOverride?: string | ReactNode
|
||||
disabled?: boolean
|
||||
color?: string
|
||||
centered?: boolean
|
||||
}
|
||||
|
||||
const ConnectButton = ({
|
||||
textOverride,
|
||||
disabled = false,
|
||||
color,
|
||||
centered = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [openConnectList, setOpenConnectList] = useState(false)
|
||||
|
||||
const { connect, availableConnections, availableInstallations } =
|
||||
useWallet()
|
||||
|
||||
const onClickAway = useCallback(() => {
|
||||
setOpenConnectList(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<button
|
||||
className={
|
||||
disabled
|
||||
? styles.disabledButton
|
||||
: color
|
||||
? styles[color]
|
||||
: styles.button
|
||||
}
|
||||
onClick={() => {
|
||||
rdd.isMobile
|
||||
? connect(ConnectType.WALLETCONNECT)
|
||||
: setOpenConnectList(!openConnectList)
|
||||
}}
|
||||
>
|
||||
<WalletSVG />
|
||||
<span className='overline'>
|
||||
{textOverride || t('common.connectWallet')}
|
||||
</span>
|
||||
</button>
|
||||
{openConnectList && (
|
||||
<ClickAwayListener onClickAway={onClickAway}>
|
||||
<div
|
||||
className={
|
||||
centered
|
||||
? `${styles.dropdown} ${styles.centered}`
|
||||
: styles.dropdown
|
||||
}
|
||||
>
|
||||
{!rdd.isMobile && (
|
||||
<>
|
||||
{availableConnections
|
||||
.filter(
|
||||
({ type }) =>
|
||||
type !== ConnectType.READONLY
|
||||
)
|
||||
.map(({ type, name, identifier }) => {
|
||||
if (
|
||||
(type === ConnectType.EXTENSION &&
|
||||
identifier === 'station') ||
|
||||
type === ConnectType.WALLETCONNECT
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
key={
|
||||
'connection' +
|
||||
type +
|
||||
identifier
|
||||
}
|
||||
className={styles.option}
|
||||
onClick={() => {
|
||||
connect(
|
||||
type,
|
||||
identifier
|
||||
)
|
||||
setOpenConnectList(
|
||||
false
|
||||
)
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
|
||||
{availableInstallations
|
||||
.filter(
|
||||
({ type }) =>
|
||||
type === ConnectType.EXTENSION
|
||||
)
|
||||
.map(({ type, identifier, name, url }) => {
|
||||
if (
|
||||
type === ConnectType.EXTENSION &&
|
||||
identifier === 'station'
|
||||
) {
|
||||
return (
|
||||
<a
|
||||
key={
|
||||
'installation' +
|
||||
type +
|
||||
identifier
|
||||
}
|
||||
className={styles.option}
|
||||
href={url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
onClick={() => {
|
||||
setOpenConnectList(
|
||||
false
|
||||
)
|
||||
}}
|
||||
>
|
||||
{t('common.installName', {
|
||||
name: name,
|
||||
})}
|
||||
</a>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConnectButton
|
||||
@@ -0,0 +1,227 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-wrap: nowrap;
|
||||
border-radius: $borderRadiusXXL;
|
||||
height: rem-calc(31);
|
||||
@include padding(0.5, 2, 0, 3);
|
||||
@include typoS;
|
||||
cursor: pointer;
|
||||
color: $colorWhite;
|
||||
border: 1px solid $alphaWhite60;
|
||||
outline: none;
|
||||
background: $alphaBlack10;
|
||||
|
||||
.walletIcon {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
svg {
|
||||
margin-top: space(-1);
|
||||
height: rem-calc(16);
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.address {
|
||||
margin-inline-start: space(1.5);
|
||||
font-weight: $fontWeightRegular;
|
||||
}
|
||||
|
||||
.balance {
|
||||
font-weight: $fontWeightRegular;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
margin-inline-start: space(2);
|
||||
padding-inline-start: rem-calc(8);
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
bottom: 1px;
|
||||
left: 0;
|
||||
border-left: 1px solid $alphaWhite60;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid $colorWhite;
|
||||
background-color: $alphaWhite10;
|
||||
}
|
||||
|
||||
.circularProgress {
|
||||
margin-inline-end: space(2);
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
top: rem-calc(38);
|
||||
@include padding(6, 6, 5.5);
|
||||
@include layoutPopover;
|
||||
width: rem-calc(420);
|
||||
left: rem-calc(-233);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.detailsHeader {
|
||||
display: flex;
|
||||
flex: 0;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
@include margin(0, 0, 4);
|
||||
}
|
||||
|
||||
.detailsBalance {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: auto;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
@include margin(-1, 4, 0, 0);
|
||||
height: space(6);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
p {
|
||||
@include margin(0);
|
||||
@include typoH4;
|
||||
color: $colorSecondaryDark;
|
||||
}
|
||||
}
|
||||
|
||||
.detailsButton {
|
||||
display: flex;
|
||||
flex: 0 0 rem-calc(116);
|
||||
width: rem-calc(116);
|
||||
}
|
||||
|
||||
.detailsBody {
|
||||
flex: 0;
|
||||
width: 100%;
|
||||
|
||||
.address,
|
||||
.addressMobile,
|
||||
.addressLabel,
|
||||
.tnsAddress {
|
||||
color: $colorSecondaryDark;
|
||||
opacity: 1;
|
||||
@include margin(0, 0, 1);
|
||||
@include typoS;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.addressMobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: rem-calc(16);
|
||||
width: auto;
|
||||
@include margin(-1, 1, 0, 0);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
flex-wrap: wrap;
|
||||
@include padding(1, 0, 0);
|
||||
|
||||
> button {
|
||||
font-weight: $fontWeightRegular;
|
||||
|
||||
&:first-child {
|
||||
width: rem-calc(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
align-items: center;
|
||||
color: $colorSecondaryDark;
|
||||
background: transparent;
|
||||
border: none;
|
||||
@include padding(2, 0);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.network {
|
||||
background-color: $colorSecondaryHighlight;
|
||||
text-transform: uppercase;
|
||||
border-radius: $borderRadiusL;
|
||||
@include padding(0, 2);
|
||||
@include margin(0);
|
||||
@include typoNetwork;
|
||||
position: absolute;
|
||||
top: space(-4);
|
||||
right: space(-4);
|
||||
cursor: default;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpMediumHigh) {
|
||||
.details {
|
||||
right: 0;
|
||||
transform: none;
|
||||
left: unset;
|
||||
}
|
||||
|
||||
.detailsButton {
|
||||
@include margin(4, 0, 0);
|
||||
flex: 1 0 100%;
|
||||
width: 100%;
|
||||
|
||||
> div {
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detailsHeader {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.network {
|
||||
right: rem-calc(-12);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.details {
|
||||
max-width: calc(100vw - 24px);
|
||||
}
|
||||
|
||||
.detailsBody {
|
||||
.addressMobile {
|
||||
display: block;
|
||||
}
|
||||
.address {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { useAccountBalance, useTNS } from '../../hooks'
|
||||
import { truncate } from '../../libs/text'
|
||||
import styles from './ConnectedButton.module.scss'
|
||||
import { formatValue, lookup } from '../../libs/parse'
|
||||
import { useCallback, useState } from 'react'
|
||||
import Button from '../Button'
|
||||
import useClipboard from 'react-use-clipboard'
|
||||
import { CheckSVG, CopySVG, ExternalSVG, WalletSVG } from '../Svg'
|
||||
import colors from '../../styles/_assets.module.scss'
|
||||
import { CircularProgress, ClickAwayListener } from '@material-ui/core'
|
||||
import { useWallet } from '@terra-money/wallet-provider'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
FINDER_URL,
|
||||
UST_DECIMALS,
|
||||
UST_DENOM,
|
||||
} from '../../constants/appConstants'
|
||||
import { State } from '../../types/enums'
|
||||
import useStore from '../../store'
|
||||
|
||||
interface Props {
|
||||
address: string
|
||||
}
|
||||
|
||||
const ConnectedButton = ({ address }: Props) => {
|
||||
const { find, state } = useAccountBalance()
|
||||
const chainID = useStore((s) => s.networkConfig?.chainID)
|
||||
const name = useStore((s) => s.networkConfig?.name)
|
||||
const { disconnect } = useWallet()
|
||||
const uusdAmount = Number(find(UST_DENOM)?.amount || 0)
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
|
||||
const [isCopied, setCopied] = useClipboard(address, {
|
||||
successDuration: 1000 * 5,
|
||||
})
|
||||
|
||||
const viewOnFinder = useCallback(() => {
|
||||
window.open(`${FINDER_URL}/${chainID}/account/${address}`, '_blank')
|
||||
}, [chainID, address])
|
||||
|
||||
const onClickAway = useCallback(() => {
|
||||
setShowDetails(false)
|
||||
}, [])
|
||||
const { t } = useTranslation()
|
||||
|
||||
const walletAddress = useTNS(address, true)
|
||||
const tnsAddress = useTNS(address, false)
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{name !== 'mainnet' && (
|
||||
<span className={styles.network}>{name}</span>
|
||||
)}
|
||||
<button
|
||||
className={styles.button}
|
||||
onClick={() => {
|
||||
setShowDetails(!showDetails)
|
||||
}}
|
||||
>
|
||||
<span className={styles.walletIcon}>
|
||||
<WalletSVG className={styles.walletIcon} />
|
||||
</span>
|
||||
<span className={styles.address}>
|
||||
{walletAddress ? walletAddress : truncate(address, [2, 4])}
|
||||
</span>
|
||||
<div className={styles.balance}>
|
||||
{state === State.READY ? (
|
||||
`${formatValue(
|
||||
lookup(uusdAmount, UST_DENOM, UST_DECIMALS)
|
||||
)}`
|
||||
) : (
|
||||
<CircularProgress
|
||||
color='inherit'
|
||||
size={'0.9rem'}
|
||||
className={styles.circularProgress}
|
||||
/>
|
||||
)}{' '}
|
||||
UST
|
||||
</div>
|
||||
</button>
|
||||
{showDetails && (
|
||||
<ClickAwayListener onClickAway={onClickAway}>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.detailsHeader}>
|
||||
<div className={styles.detailsBalance}>
|
||||
<WalletSVG color={colors.secondaryDark} />
|
||||
<p>
|
||||
{formatValue(
|
||||
lookup(
|
||||
uusdAmount,
|
||||
UST_DENOM,
|
||||
UST_DECIMALS
|
||||
),
|
||||
0
|
||||
)}{' '}
|
||||
UST
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.detailsButton}>
|
||||
<Button
|
||||
text={t('common.disconnect')}
|
||||
color='secondary'
|
||||
onClick={disconnect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.detailsBody}>
|
||||
<p className={`sub2 ${styles.addressLabel}`}>
|
||||
{tnsAddress
|
||||
? tnsAddress
|
||||
: t('common.yourAddress')}
|
||||
</p>
|
||||
<p className={styles.address}>{address}</p>
|
||||
<p className={styles.addressMobile}>
|
||||
{truncate(address, [14, 14])}
|
||||
</p>
|
||||
<div className={styles.buttons}>
|
||||
<button
|
||||
className={styles.copy}
|
||||
onClick={setCopied}
|
||||
>
|
||||
<CopySVG color={colors.secondaryDark} />
|
||||
{isCopied ? (
|
||||
<>
|
||||
{t('common.copied')}{' '}
|
||||
<CheckSVG
|
||||
color={colors.secondaryDark}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>{t('common.copy')}</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className={styles.external}
|
||||
onClick={viewOnFinder}
|
||||
>
|
||||
<ExternalSVG color={colors.secondaryDark} />{' '}
|
||||
{t('common.viewOnFinder')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConnectedButton
|
||||
@@ -0,0 +1,247 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-wrap: nowrap;
|
||||
border-radius: $borderRadiusXXL;
|
||||
height: rem-calc(31);
|
||||
@include padding(0.5, 3, 0);
|
||||
@include margin(0, 2, 0, 0);
|
||||
@include typoS;
|
||||
cursor: pointer;
|
||||
color: $colorWhite;
|
||||
border: 1px solid $alphaWhite60;
|
||||
outline: none;
|
||||
background: $alphaBlack10;
|
||||
|
||||
svg {
|
||||
margin-top: space(-1);
|
||||
height: rem-calc(19);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.balance {
|
||||
font-weight: $fontWeightRegular;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-inline-start: rem-calc(8);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid $colorWhite;
|
||||
background-color: $alphaWhite10;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonHighlight {
|
||||
@include layoutIncentiveButton;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
top: rem-calc(38);
|
||||
width: rem-calc(390);
|
||||
left: rem-calc(-155);
|
||||
z-index: 100;
|
||||
@include layoutPopover;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
top: rem-calc(12);
|
||||
right: rem-calc(16);
|
||||
|
||||
svg {
|
||||
fill: $colorSecondaryDark;
|
||||
}
|
||||
}
|
||||
|
||||
.detailsHeader {
|
||||
display: flex;
|
||||
flex: 0;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
@include padding(4, 0);
|
||||
@include margin(0);
|
||||
position: relative;
|
||||
border-bottom: 1px solid $alphaBlack20;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detailsHead {
|
||||
@include margin(0);
|
||||
@include typoScaps;
|
||||
color: $colorSecondaryDark;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detailsBody {
|
||||
flex: 0;
|
||||
width: 100%;
|
||||
@include padding(4, 8);
|
||||
color: $colorSecondaryDark;
|
||||
|
||||
.successContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.successTitle {
|
||||
text-align: center;
|
||||
color: $colorSecondaryDark;
|
||||
@include margin(0, 0, 4);
|
||||
}
|
||||
|
||||
.succcessTxHash {
|
||||
display: flex;
|
||||
@include margin(0, 0, 4);
|
||||
|
||||
.label {
|
||||
@include margin(0, 2, 0, 0);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container,
|
||||
.total {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
@include padding(4, 0, 0);
|
||||
border-bottom: 1px solid $colorSecondaryDark;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.position {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
flex-wrap: nowrap;
|
||||
@include padding(0, 0, 4);
|
||||
|
||||
.head {
|
||||
@include typoScaps;
|
||||
}
|
||||
|
||||
p {
|
||||
width: 100%;
|
||||
@include margin(0);
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
min-height: rem-calc(12);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.subhead {
|
||||
color: $colorSecondaryDark;
|
||||
opacity: 0.6;
|
||||
@include margin(1, 0);
|
||||
@include typoScaps;
|
||||
}
|
||||
|
||||
.token {
|
||||
@include typoS;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
min-height: rem-calc(12);
|
||||
flex: 0 0 rem-calc(76);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@include margin(0, 0, 0, 3);
|
||||
|
||||
p {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.headline {
|
||||
@include typoXXScaps;
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
|
||||
.tokenAmount {
|
||||
@include typoS;
|
||||
}
|
||||
|
||||
.tokenValue {
|
||||
@include margin(1, 0);
|
||||
@include typoXXS;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.total {
|
||||
border-bottom: none;
|
||||
|
||||
.position {
|
||||
.label {
|
||||
.subhead {
|
||||
opacity: 1;
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.claimButton {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex: 0 0 rem-calc(200);
|
||||
max-width: rem-calc(200);
|
||||
margin: 0 auto;
|
||||
@include padding(6, 0, 0);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
@include padding(2, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpMediumHigh) {
|
||||
.details {
|
||||
right: 0;
|
||||
top: rem-calc(46);
|
||||
transform: none;
|
||||
left: unset;
|
||||
}
|
||||
|
||||
.button {
|
||||
@include margin(2, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.details {
|
||||
max-width: calc(100vw - 24px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveGradient {
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,118 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
width: rem-calc(550);
|
||||
@include margin(-5, -10, 0);
|
||||
|
||||
.glow {
|
||||
width: rem-calc(550);
|
||||
position: absolute;
|
||||
opacity: 0.6;
|
||||
@include glowXL;
|
||||
}
|
||||
|
||||
.dialContainer {
|
||||
width: rem-calc(550);
|
||||
display: flex;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.dotContainer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: space(17.5);
|
||||
|
||||
.dot {
|
||||
position: absolute;
|
||||
width: rem-calc(4);
|
||||
height: rem-calc(4);
|
||||
background: $colorInfoWarning;
|
||||
margin-top: space(13);
|
||||
margin-inline-start: space(59);
|
||||
border-radius: $borderRadiusXS;
|
||||
transition: left 2s;
|
||||
}
|
||||
|
||||
.dotGlow {
|
||||
position: absolute;
|
||||
width: rem-calc(7);
|
||||
height: rem-calc(7);
|
||||
background: $colorInfoWarning;
|
||||
margin-top: space(13);
|
||||
margin-inline-start: space(59);
|
||||
border-radius: $borderRadiusXS;
|
||||
@include glowM;
|
||||
transition: left 2s;
|
||||
}
|
||||
}
|
||||
|
||||
.borrowLimitTextContainer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.percent {
|
||||
margin-bottom: space(-1.25);
|
||||
word-break: normal;
|
||||
word-wrap: unset;
|
||||
}
|
||||
|
||||
.warn {
|
||||
color: $colorInfoWarning;
|
||||
}
|
||||
}
|
||||
|
||||
.maxText {
|
||||
margin-top: space(19);
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.caption {
|
||||
text-transform: uppercase;
|
||||
opacity: 0.4;
|
||||
margin-bottom: space(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpMediumHigh) {
|
||||
.container {
|
||||
@include margin(10, 0);
|
||||
width: rem-calc(360);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
|
||||
.glow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.maxText {
|
||||
width: 100%;
|
||||
@include margin(19, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.container {
|
||||
width: rem-calc(282);
|
||||
|
||||
.dialContainer {
|
||||
@include margin(0);
|
||||
|
||||
+ div {
|
||||
margin-top: space(6) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'create-conical-gradient'
|
||||
import ChartComponent from 'react-chartjs-2'
|
||||
import styles from './RadialGauge.module.scss'
|
||||
import Chart from 'chart.js'
|
||||
|
||||
import produceValueArc, { GradientValue } from './elements/ValueArc'
|
||||
import {
|
||||
setUpDefaults,
|
||||
RadialGaugeController,
|
||||
} from './controllers/RadialGaugeController'
|
||||
import { ReactNode } from 'react'
|
||||
import produceTrackArc from './elements/TrackArc'
|
||||
import { GAUGE_SCALE } from '../../constants/appConstants'
|
||||
import { formatValue } from '../../libs/parse'
|
||||
|
||||
interface Props {
|
||||
currentLtv: number
|
||||
maxLtv: number
|
||||
maxValue: number
|
||||
generateText: (limit: number) => ReactNode
|
||||
showMaxValue: boolean
|
||||
colorStops: Array<GradientValue>
|
||||
showDot: boolean
|
||||
maxTextPrefix?: string
|
||||
maxTextTitle?: string
|
||||
}
|
||||
|
||||
const RadialGauge = ({
|
||||
currentLtv,
|
||||
maxLtv,
|
||||
maxValue,
|
||||
generateText,
|
||||
showMaxValue,
|
||||
colorStops,
|
||||
showDot = true,
|
||||
maxTextPrefix = '$',
|
||||
maxTextTitle = 'MAX',
|
||||
}: Props) => {
|
||||
Chart.defaults.radialGauge = Chart.defaults.doughnut
|
||||
|
||||
setUpDefaults()
|
||||
// @ts-ignore : Chart.elements is not exposed on the d.ts file for chart.js
|
||||
Chart.elements.RoundedOverArc = produceValueArc(colorStops)
|
||||
//@ts-ignore
|
||||
Chart.elements.RoundedArc = produceTrackArc(currentLtv > 0)
|
||||
var controller = RadialGaugeController()
|
||||
|
||||
// todo enable tooltips when we have the finalised specification for them
|
||||
Chart.defaults.global.tooltips.enabled = false
|
||||
Chart.controllers.radialGauge = controller
|
||||
|
||||
const chartData = {
|
||||
datasets: [
|
||||
{
|
||||
// This is the value we want to show
|
||||
data: [(currentLtv > 100 ? 100 : currentLtv) * GAUGE_SCALE],
|
||||
borderWidth: 0,
|
||||
maxLtv: maxLtv,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.glow}>
|
||||
<ChartComponent
|
||||
data={chartData}
|
||||
// We can do this because of https://github.com/jerairrest/react-chartjs-2/blob/master/src/index.js#L25
|
||||
// where it is checking the Chart.js instance for a controller that matches the type.
|
||||
// Typescript will complain however because it is not one of the preconfigured charts in react-chart-js.
|
||||
//@ts-ignore
|
||||
type='radialGauge'
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.dialContainer}>
|
||||
<ChartComponent
|
||||
data={chartData}
|
||||
//@ts-ignore - see comment above
|
||||
type='radialGauge'
|
||||
/>
|
||||
</div>
|
||||
{generateText(currentLtv ?? 0)}
|
||||
|
||||
{showMaxValue ? (
|
||||
<div className={styles.maxText}>
|
||||
<span className={`caption ${styles.caption}`}>
|
||||
{maxTextTitle}
|
||||
</span>
|
||||
<span className={'sub2'} style={{ opacity: '1' }}>
|
||||
{maxTextPrefix}
|
||||
{formatValue(maxValue, 2, 2, true, false, true, true)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RadialGauge
|
||||
@@ -0,0 +1,279 @@
|
||||
import Chart, { helpers } from 'chart.js'
|
||||
import { GAUGE_SCALE } from '../../../constants/appConstants'
|
||||
|
||||
const setUpDefaults = () => {
|
||||
Chart.defaults._set('radialGauge', {
|
||||
animation: {
|
||||
// Boolean - Whether we animate the rotation of the radialGauge
|
||||
animateRotate: true,
|
||||
// Boolean - Whether we animate scaling the radialGauge from the centre
|
||||
animateScale: true,
|
||||
},
|
||||
|
||||
// Todo remove these out of the table
|
||||
// The percentage of the chart that is the center area
|
||||
centerPercentage: 95.5,
|
||||
|
||||
maintainAspectRatio: true,
|
||||
circumference: Math.PI + 2,
|
||||
rotation: -Math.PI - 1,
|
||||
|
||||
// the color of the radial gauge's track
|
||||
trackColor: '#1c1c1c',
|
||||
// whether arc for the gauge should have rounded corners
|
||||
roundedCorners: true,
|
||||
|
||||
hover: {
|
||||
mode: 'disable',
|
||||
},
|
||||
options: {
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
|
||||
// the domain of the metric
|
||||
domain: [0, 100],
|
||||
})
|
||||
}
|
||||
|
||||
const getCustomRadialGaugeController = () => {
|
||||
return Chart.controllers.doughnut.extend({
|
||||
// @ts-ignore
|
||||
dataElementType: Chart.elements.RoundedOverArc,
|
||||
linkScales: helpers.noop,
|
||||
|
||||
draw: function (ease: any) {
|
||||
this.drawTrack()
|
||||
Chart.controllers.doughnut.prototype.draw.call(this, ease)
|
||||
this.drawDot()
|
||||
},
|
||||
|
||||
drawTrack() {
|
||||
//@ts-ignore
|
||||
new Chart.elements.RoundedArc({
|
||||
_view: {
|
||||
backgroundColor: this.chart.options.trackColor,
|
||||
borderColor: this.chart.options.trackColor,
|
||||
borderWidth: 0,
|
||||
startAngle: this.chart.options.rotation,
|
||||
endAngle: Math.PI * 0.33,
|
||||
x: this.centerX,
|
||||
y: this.centerY,
|
||||
innerRadius: this.innerRadius,
|
||||
outerRadius: this.outerRadius,
|
||||
roundedCorners: true,
|
||||
},
|
||||
_chart: this.chart,
|
||||
}).draw()
|
||||
},
|
||||
|
||||
drawDot() {
|
||||
const maxLtv = this.getDataset()['maxLtv']
|
||||
if (!maxLtv || maxLtv <= 0) {
|
||||
return
|
||||
}
|
||||
// Calculate the angle of our borrowing capacity
|
||||
const [domainStart, domainEnd] = this.getDomain() || [0, 100]
|
||||
const value = maxLtv * GAUGE_SCALE
|
||||
const domainSize = domainEnd - domainStart
|
||||
const limitAngle =
|
||||
domainSize > 0
|
||||
? Math.PI *
|
||||
2.0 *
|
||||
(Math.abs(value - domainStart) / domainSize)
|
||||
: 0
|
||||
|
||||
// Get the center of the arc
|
||||
const arcCentre = (this.outerRadius + this.innerRadius) / 2
|
||||
|
||||
// Calculate offset from the centre of our circle
|
||||
const x =
|
||||
arcCentre * Math.cos(limitAngle + this.chart.options.rotation)
|
||||
const y =
|
||||
arcCentre * Math.sin(limitAngle + this.chart.options.rotation)
|
||||
|
||||
// @ts-ignore
|
||||
new Chart.elements.Point({
|
||||
_view: {
|
||||
radius: 2,
|
||||
pointStyle: 'circle',
|
||||
backgroundColor: '#c83333',
|
||||
borderColor: '#c83333',
|
||||
borderWidth: 1,
|
||||
// Hover
|
||||
hitRadius: 1,
|
||||
hoverRadius: 4,
|
||||
hoverBorderWidth: 1,
|
||||
x: this.centerX + x,
|
||||
y: this.centerY + y,
|
||||
},
|
||||
_chart: this.chart,
|
||||
}).draw()
|
||||
},
|
||||
|
||||
update(reset: any) {
|
||||
const chart = this.chart
|
||||
const chartArea = chart.chartArea
|
||||
const opts = chart.options
|
||||
const arcOpts = opts.elements.arc
|
||||
const availableWidth =
|
||||
chartArea.right - chartArea.left - arcOpts.borderWidth
|
||||
const availableHeight =
|
||||
chartArea.bottom - chartArea.top - arcOpts.borderWidth
|
||||
const availableSize = Math.min(availableWidth, availableHeight)
|
||||
|
||||
const meta = this.getMeta()
|
||||
const centerPercentage = opts.centerPercentage
|
||||
|
||||
this.borderWidth = this.getMaxBorderWidth(meta.data)
|
||||
this.outerRadius = Math.max(
|
||||
(availableSize - this.borderWidth) / 2,
|
||||
0
|
||||
)
|
||||
this.innerRadius = Math.max(
|
||||
centerPercentage
|
||||
? (this.outerRadius / 100) * centerPercentage
|
||||
: 0,
|
||||
0
|
||||
)
|
||||
|
||||
meta.total = this.getMetricValue()
|
||||
this.centerX = (chartArea.left + chartArea.right) / 2
|
||||
this.centerY = (chartArea.top + chartArea.bottom) / 2
|
||||
|
||||
helpers.each(meta.data, (arc: any, index: any) => {
|
||||
this.updateElement(arc, index, reset)
|
||||
})
|
||||
},
|
||||
|
||||
updateElement(arc: any, index: any, reset: any) {
|
||||
const chart = this.chart
|
||||
const chartArea = chart.chartArea
|
||||
const opts = chart.options
|
||||
const animationOpts = opts.animation
|
||||
const centerX = (chartArea.left + chartArea.right) / 2
|
||||
const centerY = (chartArea.top + chartArea.bottom) / 2
|
||||
const startAngle = opts.rotation // non reset case handled later
|
||||
const dataset = this.getDataset()
|
||||
const arcAngle =
|
||||
reset && animationOpts.animateRotate
|
||||
? 0
|
||||
: this.calculateArcAngle(dataset.data[index])
|
||||
const value =
|
||||
reset && animationOpts.animateScale ? 0 : this.getMetricValue()
|
||||
const endAngle = startAngle + arcAngle // arcAngle
|
||||
const innerRadius = this.innerRadius
|
||||
const outerRadius = this.outerRadius
|
||||
const valueAtIndexOrDefault = helpers.valueAtIndexOrDefault
|
||||
|
||||
helpers.extend(arc, {
|
||||
// Utility
|
||||
_datasetIndex: this.index,
|
||||
_index: index,
|
||||
|
||||
// Desired view properties
|
||||
_model: {
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
startAngle,
|
||||
endAngle,
|
||||
outerRadius,
|
||||
innerRadius,
|
||||
label: valueAtIndexOrDefault(
|
||||
dataset.label,
|
||||
index,
|
||||
chart.data.labels[index]
|
||||
),
|
||||
roundedCorners: opts.roundedCorners,
|
||||
value,
|
||||
},
|
||||
})
|
||||
|
||||
const model = arc._model
|
||||
|
||||
// Resets the visual styles
|
||||
const custom = arc.custom || {}
|
||||
const valueOrDefault = helpers.valueAtIndexOrDefault
|
||||
const elementOpts = this.chart.options.elements.arc
|
||||
model.backgroundColor = custom.backgroundColor
|
||||
? custom.backgroundColor
|
||||
: valueOrDefault(
|
||||
dataset.backgroundColor,
|
||||
index,
|
||||
elementOpts.backgroundColor
|
||||
)
|
||||
model.borderColor = custom.borderColor
|
||||
? custom.borderColor
|
||||
: valueOrDefault(
|
||||
dataset.borderColor,
|
||||
index,
|
||||
elementOpts.borderColor
|
||||
)
|
||||
model.borderWidth = custom.borderWidth
|
||||
? custom.borderWidth
|
||||
: valueOrDefault(
|
||||
dataset.borderWidth,
|
||||
index,
|
||||
elementOpts.borderWidth
|
||||
)
|
||||
|
||||
arc.pivot()
|
||||
},
|
||||
|
||||
getMetricValue() {
|
||||
let value = this.getDataset().data[0]
|
||||
if (value == null) {
|
||||
value = this.chart.options.domain[0] || 0
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
|
||||
getDomain() {
|
||||
return this.chart.options.domain
|
||||
},
|
||||
|
||||
calculateArcAngle() {
|
||||
const [domainStart, domainEnd] = this.getDomain() || [0, 100]
|
||||
const value = this.getMetricValue()
|
||||
const domainSize = domainEnd - domainStart
|
||||
return domainSize > 0
|
||||
? Math.PI * 2.0 * (Math.abs(value - domainStart) / domainSize)
|
||||
: 0
|
||||
},
|
||||
|
||||
// gets the max border or hover width to properly scale pie charts
|
||||
getMaxBorderWidth(arcs: any) {
|
||||
let max = 0
|
||||
const index = this.index
|
||||
const length = arcs.length
|
||||
let borderWidth
|
||||
let hoverWidth
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
borderWidth = arcs[i]._model ? arcs[i]._model.borderWidth : 0
|
||||
hoverWidth = arcs[i]._chart
|
||||
? arcs[i]._chart.config.data.datasets[index]
|
||||
.hoverBorderWidth
|
||||
: 0
|
||||
|
||||
max = borderWidth > max ? borderWidth : max
|
||||
max = hoverWidth > max ? hoverWidth : max
|
||||
}
|
||||
return max
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
getCustomRadialGaugeController as RadialGaugeController,
|
||||
setUpDefaults,
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import Chart from 'chart.js'
|
||||
|
||||
const produceTrackArc = (hasValue: boolean) => {
|
||||
//@ts-ignore
|
||||
return Chart.elements.Arc.extend({
|
||||
draw() {
|
||||
const noDataTrackColor = '#3a3c49'
|
||||
const ctx = this._chart.ctx
|
||||
const vm = this._view
|
||||
const { startAngle, endAngle } = vm
|
||||
const cornerRadius = (vm.outerRadius - vm.innerRadius) / 2
|
||||
const cornerX = (vm.outerRadius + vm.innerRadius) / 2
|
||||
|
||||
// translate + rotate to make drawing the corners simpler
|
||||
ctx.translate(vm.x, vm.y)
|
||||
ctx.rotate(startAngle)
|
||||
const angle = endAngle - startAngle
|
||||
ctx.beginPath()
|
||||
if (vm.roundedCorners) {
|
||||
ctx.arc(cornerX, 0, cornerRadius, Math.PI, 0)
|
||||
}
|
||||
ctx.arc(0, 0, vm.outerRadius, 0, angle)
|
||||
|
||||
const x = cornerX * Math.cos(angle)
|
||||
const y = cornerX * Math.sin(angle)
|
||||
|
||||
if (vm.roundedCorners) {
|
||||
ctx.arc(x, y, cornerRadius, angle, angle + Math.PI)
|
||||
}
|
||||
|
||||
ctx.arc(0, 0, vm.innerRadius, angle, 0, true)
|
||||
ctx.closePath()
|
||||
ctx.rotate(-startAngle)
|
||||
ctx.translate(-vm.x, -vm.y)
|
||||
ctx.strokeStyle = hasValue ? vm.borderColor : noDataTrackColor
|
||||
ctx.lineWidth = vm.borderWidth
|
||||
ctx.fillStyle = hasValue ? vm.backgroundColor : noDataTrackColor
|
||||
|
||||
ctx.fill()
|
||||
ctx.lineJoin = 'bevel'
|
||||
|
||||
if (vm.borderWidth) {
|
||||
ctx.stroke()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default produceTrackArc
|
||||
@@ -0,0 +1,75 @@
|
||||
export interface GradientValue {
|
||||
color: string
|
||||
value: number
|
||||
}
|
||||
|
||||
const produceValueArc = (colorStops: Array<GradientValue>) => {
|
||||
//@ts-ignore
|
||||
return Chart.elements.Arc.extend({
|
||||
draw() {
|
||||
const ctx = this._chart.ctx
|
||||
const vm = this._view
|
||||
|
||||
const { startAngle, endAngle } = vm
|
||||
if (vm.value === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const cornerRadius = (vm.outerRadius - vm.innerRadius) / 2
|
||||
const cornerX = (vm.outerRadius + vm.innerRadius) / 2
|
||||
|
||||
// translate + rotate to make drawing the corners simpler
|
||||
ctx.translate(vm.x, vm.y)
|
||||
ctx.rotate(startAngle)
|
||||
const angle = endAngle - startAngle
|
||||
ctx.beginPath()
|
||||
if (vm.roundedCorners) {
|
||||
ctx.arc(cornerX, 0, cornerRadius, Math.PI, 0)
|
||||
}
|
||||
ctx.arc(0, 0, vm.outerRadius, 0, angle)
|
||||
|
||||
const x = cornerX * Math.cos(angle)
|
||||
const y = cornerX * Math.sin(angle)
|
||||
|
||||
if (vm.roundedCorners) {
|
||||
ctx.arc(x, y, cornerRadius, angle, angle + Math.PI)
|
||||
}
|
||||
|
||||
ctx.arc(0, 0, vm.innerRadius, angle, 0, true)
|
||||
ctx.closePath()
|
||||
ctx.rotate(-startAngle)
|
||||
ctx.translate(-vm.x, -vm.y)
|
||||
|
||||
ctx.strokeStyle = vm.borderColor
|
||||
ctx.lineWidth = vm.borderWidth
|
||||
ctx.strokeStyle = vm.borderColor
|
||||
// create gradient and fill it
|
||||
const rotationOffSet = Math.PI * 0.5
|
||||
const gradient = ctx.createConicalGradient(
|
||||
vm.x,
|
||||
vm.y - 10,
|
||||
0 + rotationOffSet,
|
||||
Math.PI * 2 + rotationOffSet
|
||||
)
|
||||
|
||||
for (var index = 0; index < colorStops.length; index += 1) {
|
||||
const colorStop = colorStops[index]
|
||||
gradient.addColorStop(colorStop.value, colorStop.color)
|
||||
}
|
||||
|
||||
ctx.fillStyle = gradient.pattern
|
||||
|
||||
ctx.strokeStyle = gradient
|
||||
ctx.fillStyle = gradient
|
||||
|
||||
ctx.fill()
|
||||
ctx.lineJoin = 'bevel'
|
||||
|
||||
if (vm.borderWidth) {
|
||||
ctx.stroke()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default produceValueArc
|
||||
@@ -0,0 +1,50 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
@include layoutTooltip;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: rem-calc(184);
|
||||
.item {
|
||||
@include margin(1, 0, 0);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.dot {
|
||||
border: 1px solid $colorWhite;
|
||||
height: space(2);
|
||||
border-radius: $borderRadiusXXS;
|
||||
width: space(2);
|
||||
@include margin(1.5, 2, 0, 0);
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
width: rem-calc(168);
|
||||
flex-direction: column;
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
:first-child {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.titleText {
|
||||
justify-content: start !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.subTitleContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
:first-child {
|
||||
flex: auto;
|
||||
}
|
||||
.subTitleText {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import styles from './Apr.module.scss'
|
||||
import { formatValue } from '../../libs/parse'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
data: AssetInfo
|
||||
title: string
|
||||
}
|
||||
|
||||
interface HoverItem {
|
||||
color?: string
|
||||
symbol?: string
|
||||
apr?: number
|
||||
subtitle: string
|
||||
}
|
||||
|
||||
const Apr = ({ data }: Props) => {
|
||||
const [aprData, setAprData] = useState<HoverItem[]>([])
|
||||
const { t } = useTranslation()
|
||||
|
||||
const produceData = (data: AssetInfo[]): HoverItem[] => {
|
||||
const items: HoverItem[] = []
|
||||
|
||||
data.forEach((asset: AssetInfo, key: number) => {
|
||||
items.push({
|
||||
color: asset.color,
|
||||
symbol: asset.symbol,
|
||||
apr: asset.apy,
|
||||
subtitle:
|
||||
key === 0 ? t('fields.baseApy') : t('common.incentiveApr'),
|
||||
})
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const baseData = data.incentive ? [data, data.incentive] : [data]
|
||||
setAprData(produceData(baseData))
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[data]
|
||||
)
|
||||
|
||||
const produceItem = (item: HoverItem, key: number) => {
|
||||
return (
|
||||
<div className={styles.item} key={key}>
|
||||
<div
|
||||
className={styles.dot}
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.titleContainer}>
|
||||
<span className={`body ${styles.titleText}`}>
|
||||
{item.symbol}
|
||||
</span>
|
||||
<span className={`body ${styles.titleText}`}>
|
||||
{formatValue(
|
||||
item?.apr || 0,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.subTitleContainer}>
|
||||
<span className={`caption ${styles.subTitleText}`}>
|
||||
{item.subtitle}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p className='sub2'>{t('fields.apyBreakdown')}</p>
|
||||
{aprData.map((item: HoverItem, index: number) =>
|
||||
produceItem(item, index)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Apr
|
||||
@@ -0,0 +1,38 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include layoutTooltip;
|
||||
min-width: rem-calc(184);
|
||||
|
||||
.item {
|
||||
margin-top: space(2);
|
||||
display: flex;
|
||||
width: rem-calc(175);
|
||||
flex-direction: row;
|
||||
@include typoXS;
|
||||
|
||||
&:last-child {
|
||||
@include devider20;
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
flex: 0 0 rem-calc(115);
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 rem-calc(60);
|
||||
}
|
||||
|
||||
&.leverage {
|
||||
@include margin(2, 0, 0);
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import styles from './Apy.module.scss'
|
||||
import { formatValue } from '../../libs/parse'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRedBank } from '../../hooks'
|
||||
|
||||
interface Props {
|
||||
trueApy: number
|
||||
apyData: StrategyRate
|
||||
aprData: StrategyRate
|
||||
token: string
|
||||
leverage?: string
|
||||
borrowDenom?: string
|
||||
}
|
||||
|
||||
const Apy = ({
|
||||
trueApy,
|
||||
apyData,
|
||||
aprData,
|
||||
token,
|
||||
leverage,
|
||||
borrowDenom = 'uusd',
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { findMarketInfo } = useRedBank()
|
||||
const [checkedApyData, setApyData] = useState<StrategyRate>(apyData)
|
||||
const [checkedAprData, setAprData] = useState<StrategyRate>(aprData)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (apyData?.total !== checkedApyData?.total) {
|
||||
setApyData(apyData)
|
||||
}
|
||||
if (aprData?.total !== checkedAprData?.total) {
|
||||
setAprData(aprData)
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[apyData, aprData]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p className='sub2'>{t('fields.apyBreakdown')}</p>
|
||||
{checkedAprData?.trading > 0 && (
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{t('fields.tradingFeesApr')}
|
||||
</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(
|
||||
checkedAprData.trading,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{checkedAprData?.astro > 0 && (
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{t('fields.protocolApr', { protocol: 'ASTRO' })}
|
||||
</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(
|
||||
checkedAprData.astro,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{checkedAprData?.protocol > 0 && (
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{t('fields.protocolApr', { protocol: token })}
|
||||
</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(
|
||||
checkedAprData.protocol,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{checkedApyData?.total > 0 && (
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{t('fields.totalRewardsApy')}
|
||||
</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(
|
||||
checkedApyData.total,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{leverage && Number(leverage) > 0 && trueApy > 0 && (
|
||||
<>
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{t('fields.borrowRateApr')}
|
||||
</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(
|
||||
Number(
|
||||
findMarketInfo(borrowDenom)?.borrow_rate
|
||||
) * 100,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
'-',
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${styles.item} ${styles.leverage}`}>
|
||||
<span className={styles.label}>
|
||||
{`${t('fields.leverage')} ${formatValue(
|
||||
leverage || 0,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
'x',
|
||||
true
|
||||
)}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{t('fields.leveragedApy')}
|
||||
</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(trueApy, 2, 2, true, false, '%')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Apy
|
||||
@@ -0,0 +1,38 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include layoutTooltip;
|
||||
min-width: rem-calc(184);
|
||||
|
||||
.item {
|
||||
margin-top: space(2);
|
||||
display: flex;
|
||||
width: rem-calc(175);
|
||||
flex-direction: row;
|
||||
@include typoXS;
|
||||
|
||||
&:last-child {
|
||||
@include devider20;
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
flex: 0 0 rem-calc(115);
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 rem-calc(60);
|
||||
}
|
||||
|
||||
&.leverage {
|
||||
@include margin(2, 0, 0);
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import styles from './PnL.module.scss'
|
||||
import { formatValue } from '../../libs/parse'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
initial: string
|
||||
current: string
|
||||
pnlValue: number
|
||||
}
|
||||
|
||||
const PnL = ({ initial, current, pnlValue }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p className='sub2'>{t('fields.pnlBreakdown')}</p>
|
||||
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>{t('fields.initialValue')}</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(initial, 2, 2, true, '$')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>{t('fields.currentValue')}</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(current, 2, 2, true, '$')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{pnlValue >= 0 ? t('fields.profit') : t('fields.loss')}
|
||||
</span>
|
||||
<span
|
||||
className={`${styles.value} ${
|
||||
pnlValue > 0
|
||||
? 'colorInfoProfit'
|
||||
: pnlValue < 0
|
||||
? 'colorInfoLoss'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{formatValue(pnlValue, 2, 2, true, '$')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PnL
|
||||
@@ -0,0 +1,8 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.info {
|
||||
color: $tooltipIconColor;
|
||||
fill: $tooltipIconColor !important;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ReactNode } from 'react'
|
||||
import Tippy from '@tippyjs/react'
|
||||
|
||||
import HelpOutlineIcon from '@material-ui/icons/HelpOutline'
|
||||
|
||||
import styles from './Tooltip.module.scss'
|
||||
|
||||
interface TooltipProps {
|
||||
content: string | ReactNode
|
||||
iconWidth: string
|
||||
}
|
||||
|
||||
const Tooltip = ({ content, iconWidth }: TooltipProps) => (
|
||||
<Tippy
|
||||
className='tippyContainer'
|
||||
content={<span className='body2'>{content}</span>}
|
||||
interactive={true}
|
||||
appendTo={() => document.body}
|
||||
>
|
||||
<HelpOutlineIcon style={{ width: iconWidth }} className={styles.info} />
|
||||
</Tippy>
|
||||
)
|
||||
|
||||
export default Tooltip
|
||||
@@ -0,0 +1,86 @@
|
||||
import colors from '../../styles/_assets.module.scss'
|
||||
|
||||
// Native asset images
|
||||
import luna from '../../images/LUNA.svg'
|
||||
import ust from '../../images/UST.svg'
|
||||
|
||||
// CW20 asset images
|
||||
import mars from '../../images/MARS-COLORED.svg'
|
||||
import astro from '../../images/ASTRO.png'
|
||||
import anc from '../../images/ANC.svg'
|
||||
import mir from '../../images/MIR.svg'
|
||||
|
||||
const Assets: Assets = {
|
||||
luna: {
|
||||
symbol: 'LUNA',
|
||||
name: 'Terra',
|
||||
logo: luna,
|
||||
denom: 'uluna',
|
||||
maToken: 'maluna',
|
||||
color: colors.luna,
|
||||
native: true,
|
||||
decimals: 6,
|
||||
hasOraclePrice: true,
|
||||
},
|
||||
ust: {
|
||||
symbol: 'UST',
|
||||
name: 'Terra USD',
|
||||
logo: ust,
|
||||
denom: 'uusd',
|
||||
maToken: 'mausd',
|
||||
color: colors.ust,
|
||||
native: true,
|
||||
decimals: 6,
|
||||
hasOraclePrice: true,
|
||||
},
|
||||
mars: {
|
||||
symbol: 'MARS',
|
||||
name: 'Mars',
|
||||
logo: mars,
|
||||
denom: 'MARS',
|
||||
contract_addr: 'terra12hgwnpupflfpuual532wgrxu2gjp0tcagzgx4n',
|
||||
maToken: 'mamars',
|
||||
color: colors.mars,
|
||||
native: false,
|
||||
decimals: 6,
|
||||
hasOraclePrice: false,
|
||||
},
|
||||
astro: {
|
||||
symbol: 'ASTRO',
|
||||
name: 'Astroport',
|
||||
logo: astro,
|
||||
denom: 'ASTRO',
|
||||
contract_addr: 'terra1xj49zyqrwpv5k928jwfpfy2ha668nwdgkwlrg3',
|
||||
maToken: 'maastro',
|
||||
color: colors.astro,
|
||||
native: false,
|
||||
decimals: 6,
|
||||
hasOraclePrice: false,
|
||||
},
|
||||
anc: {
|
||||
symbol: 'ANC',
|
||||
name: 'Anchor',
|
||||
logo: anc,
|
||||
denom: 'ANC',
|
||||
contract_addr: 'terra14z56l0fp2lsf86zy3hty2z47ezkhnthtr9yq76',
|
||||
maToken: 'maanc',
|
||||
color: colors.anc,
|
||||
native: false,
|
||||
decimals: 6,
|
||||
hasOraclePrice: true,
|
||||
},
|
||||
mir: {
|
||||
symbol: 'MIR',
|
||||
name: 'Mirror',
|
||||
logo: mir,
|
||||
denom: 'MIR',
|
||||
contract_addr: 'terra15gwkyepfc6xgca5t5zefzwy42uts8l2m4g40k6',
|
||||
maToken: 'mamir',
|
||||
color: colors.mir,
|
||||
native: false,
|
||||
decimals: 6,
|
||||
hasOraclePrice: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default Assets
|
||||
@@ -0,0 +1,125 @@
|
||||
import colors from '../../styles/_assets.module.scss'
|
||||
|
||||
// Native asset images
|
||||
import luna from '../../images/LUNA.svg'
|
||||
import ust from '../../images/UST.svg'
|
||||
|
||||
// CW20 asset images
|
||||
import mars from '../../images/MARS-COLORED.svg'
|
||||
import astro from '../../images/ASTRO.png'
|
||||
import anc from '../../images/ANC.svg'
|
||||
import mir from '../../images/MIR.svg'
|
||||
import stLuna from '../../images/STLUNA.svg'
|
||||
// import mine from '../../images/MINE.svg'
|
||||
// import ornb from '../../images/ORNb.svg'
|
||||
|
||||
const Assets: Assets = {
|
||||
luna: {
|
||||
symbol: 'LUNA',
|
||||
name: 'Terra',
|
||||
logo: luna,
|
||||
denom: 'uluna',
|
||||
maToken: 'maluna',
|
||||
color: colors.luna,
|
||||
native: true,
|
||||
decimals: 6,
|
||||
hasOraclePrice: true,
|
||||
},
|
||||
ust: {
|
||||
symbol: 'UST',
|
||||
name: 'Terra USD',
|
||||
logo: ust,
|
||||
denom: 'uusd',
|
||||
maToken: 'mausd',
|
||||
color: colors.ust,
|
||||
native: true,
|
||||
decimals: 6,
|
||||
hasOraclePrice: true,
|
||||
},
|
||||
stLuna: {
|
||||
symbol: 'stLUNA',
|
||||
name: 'Lido Staked LUNA',
|
||||
logo: stLuna,
|
||||
denom: 'stLuna',
|
||||
contract_addr: 'terra1e42d7l5z5u53n7g990ry24tltdphs9vugap8cd',
|
||||
maToken: 'mastluna',
|
||||
color: colors.stLuna,
|
||||
native: false,
|
||||
decimals: 6,
|
||||
hasOraclePrice: true,
|
||||
},
|
||||
mars: {
|
||||
symbol: 'MARS',
|
||||
name: 'Mars',
|
||||
logo: mars,
|
||||
denom: 'MARS',
|
||||
contract_addr: 'terra1qs7h830ud0a4hj72yr8f7jmlppyx7z524f7gw6',
|
||||
maToken: 'mamars',
|
||||
color: colors.mars,
|
||||
native: false,
|
||||
decimals: 6,
|
||||
hasOraclePrice: false,
|
||||
},
|
||||
astro: {
|
||||
symbol: 'ASTRO',
|
||||
name: 'Astroport',
|
||||
logo: astro,
|
||||
denom: 'ASTRO',
|
||||
contract_addr: 'terra1cc2up8erdqn2l7nz37qjgvnqy56sr38aj9vqry',
|
||||
maToken: 'maastro',
|
||||
color: colors.astro,
|
||||
native: false,
|
||||
decimals: 6,
|
||||
hasOraclePrice: false,
|
||||
},
|
||||
anc: {
|
||||
symbol: 'ANC',
|
||||
name: 'Anchor',
|
||||
logo: anc,
|
||||
denom: 'TTN',
|
||||
contract_addr: 'terra1747mad58h0w4y589y3sk84r5efqdev9q4r02pc',
|
||||
maToken: 'mattn',
|
||||
color: colors.anc,
|
||||
native: false,
|
||||
decimals: 6,
|
||||
hasOraclePrice: true,
|
||||
},
|
||||
fanc: {
|
||||
symbol: 'FANC',
|
||||
name: 'Anchor',
|
||||
logo: anc,
|
||||
denom: 'FTTN',
|
||||
contract_addr: 'terra12n6pf6rj3z86s90594d57nmmh65q4pyh502c8z',
|
||||
maToken: 'mafttn',
|
||||
color: colors.anc,
|
||||
native: false,
|
||||
decimals: 6,
|
||||
hasOraclePrice: true,
|
||||
},
|
||||
mir: {
|
||||
symbol: 'MIR',
|
||||
name: 'Mirror',
|
||||
logo: mir,
|
||||
denom: 'MIR',
|
||||
contract_addr: 'terra10llyp6v3j3her8u3ce66ragytu45kcmd9asj3u',
|
||||
maToken: 'mamir',
|
||||
color: colors.mir,
|
||||
native: false,
|
||||
decimals: 6,
|
||||
hasOraclePrice: true,
|
||||
},
|
||||
fmir: {
|
||||
symbol: 'FMIR',
|
||||
name: 'Mirror',
|
||||
logo: mir,
|
||||
denom: 'FMIR',
|
||||
contract_addr: 'terra1swlg8vmfajfw6j0ay57wpa533dywwqz9zqd7cj',
|
||||
maToken: 'mafmir',
|
||||
color: colors.mir,
|
||||
native: false,
|
||||
decimals: 6,
|
||||
hasOraclePrice: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default Assets
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"contracts": {
|
||||
"basecampAddress": "terra1685de0sx5px80d47ec2xjln224phshysqxxeje"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"contracts": {
|
||||
"basecampAddress": "terra1jtdz9fhrrwd8yak6e3z7utmkypvx0qf0n393c6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { StrategyProviders } from '../../constants/strategyProviders'
|
||||
import Assets from '../assets/mainnet'
|
||||
|
||||
const FieldsStrategies: FieldsStrategy[] = [
|
||||
{
|
||||
key: 'lunaBullStrategy',
|
||||
name: 'LUNA-UST LP',
|
||||
description: 'LUNA-UST Bull Leveraged Yield Farm (Max 2x)',
|
||||
minter: 'terra1m6ywlgn6wrjuagcmmezzz2a029gtldhey5k552',
|
||||
logo: Assets.ust.logo,
|
||||
borrow: Assets.ust.symbol,
|
||||
color: Assets.luna.color,
|
||||
astroportGenerator: 'terra1zgrx9jjqrfye8swykfgmd6hpde60j0nszzupp9',
|
||||
contract_addr: 'terra1kztywx50wv38r58unxj9p6k3pgr2ux6w5x68md',
|
||||
lpToken: 'terra1m24f7k4g66gnh9f7uncp32p722v0kyt3q4l3u5',
|
||||
assets: [Assets.luna, Assets.ust],
|
||||
maxLeverage: 2,
|
||||
provider: StrategyProviders.MARS,
|
||||
apyQuery: {
|
||||
url: 'https://api.astroport.fi/graphql',
|
||||
query: `
|
||||
lunaBullStrategy: pool(address:"terra1m6ywlgn6wrjuagcmmezzz2a029gtldhey5k552") {
|
||||
trading_fees {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
astro_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
protocol_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
total_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
}`,
|
||||
target: ['pool', 'total_rewards', 'apy'],
|
||||
apr: true,
|
||||
absolute: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'ancBullStrategy',
|
||||
name: 'ANC-UST LP',
|
||||
description: 'ANC-UST Bull Leveraged Yield Farm (Max 2x)',
|
||||
minter: 'terra1qr2k6yjjd5p2kaewqvg93ag74k6gyjr7re37fs',
|
||||
logo: Assets.ust.logo,
|
||||
borrow: Assets.ust.symbol,
|
||||
color: Assets.anc.color,
|
||||
contract_addr: 'terra1vapq79y9cqghqny7zt72g4qukndz282uvqwtz6',
|
||||
astroportGenerator: 'terra1zgrx9jjqrfye8swykfgmd6hpde60j0nszzupp9',
|
||||
lpToken: 'terra1wmaty65yt7mjw6fjfymkd9zsm6atsq82d9arcd',
|
||||
assets: [Assets.anc, Assets.ust],
|
||||
maxLeverage: 2,
|
||||
provider: StrategyProviders.MARS,
|
||||
apyQuery: {
|
||||
url: 'https://api.astroport.fi/graphql',
|
||||
query: `
|
||||
ancBullStrategy: pool(address:"terra1qr2k6yjjd5p2kaewqvg93ag74k6gyjr7re37fs") {
|
||||
trading_fees {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
astro_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
protocol_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
total_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
}`,
|
||||
target: ['pool', 'total_rewards', 'apy'],
|
||||
apr: true,
|
||||
absolute: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'mirBullStrategy',
|
||||
name: 'MIR-UST LP',
|
||||
description: 'MIR-UST Bull Leveraged Yield Farm (Max 2x)',
|
||||
minter: 'terra143xxfw5xf62d5m32k3t4eu9s82ccw80lcprzl9',
|
||||
logo: Assets.ust.logo,
|
||||
borrow: Assets.ust.symbol,
|
||||
color: Assets.mir.color,
|
||||
contract_addr: 'terra12dq4wmfcsnz6ycep6ek4umtuaj6luhfp256hyu',
|
||||
astroportGenerator: 'terra1zgrx9jjqrfye8swykfgmd6hpde60j0nszzupp9',
|
||||
lpToken: 'terra17trxzqjetl0q6xxep0s2w743dhw2cay0x47puc',
|
||||
assets: [Assets.mir, Assets.ust],
|
||||
maxLeverage: 2,
|
||||
provider: StrategyProviders.MARS,
|
||||
apyQuery: {
|
||||
url: 'https://api.astroport.fi/graphql',
|
||||
query: `
|
||||
mirBullStrategy: pool(address:"terra143xxfw5xf62d5m32k3t4eu9s82ccw80lcprzl9") {
|
||||
trading_fees {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
astro_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
protocol_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
total_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
}`,
|
||||
target: ['pool', 'total_rewards', 'apy'],
|
||||
apr: true,
|
||||
absolute: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default FieldsStrategies
|
||||
@@ -0,0 +1,239 @@
|
||||
import { StrategyProviders } from '../../constants/strategyProviders'
|
||||
import Assets from '../assets/testnet'
|
||||
|
||||
const FieldsStrategies: FieldsStrategy[] = [
|
||||
{
|
||||
key: 'apolloAncBullStrategy',
|
||||
externalLink: 'https://app.apollo.farm/',
|
||||
minter: 'terra13r3vngakfw457dwhw9ef36mc8w6agggefe70d9',
|
||||
logo: Assets.ust.logo,
|
||||
borrow: Assets.ust.symbol,
|
||||
color: Assets.luna.color,
|
||||
astroportGenerator: 'terra1gjm7d9nmewn27qzrvqyhda8zsfl40aya7tvaw5',
|
||||
contract_addr: 'terra1lanxgewats337t49mnkaeh5g098j2g7e87k87s',
|
||||
lpToken: 'terra1agu2qllktlmf0jdkuhcheqtchnkppzrl4759y6',
|
||||
assets: [Assets.anc, Assets.ust],
|
||||
maxLeverage: 2.5,
|
||||
provider: StrategyProviders.APOLLO,
|
||||
apyQuery: {
|
||||
url: 'https://api.astroport.fi/graphql',
|
||||
query: `
|
||||
apolloAncBullStrategy: pool(address:"terra1qr2k6yjjd5p2kaewqvg93ag74k6gyjr7re37fs") {
|
||||
trading_fees {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
astro_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
protocol_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
total_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
}`,
|
||||
target: ['pool', 'total_rewards', 'apy'],
|
||||
apr: true,
|
||||
absolute: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'lunaBullStrategy',
|
||||
minter: 'terra1e49fv4xm3c2znzpxmxs0z2z6y74xlwxspxt38s',
|
||||
logo: Assets.ust.logo,
|
||||
borrow: Assets.ust.symbol,
|
||||
color: Assets.luna.color,
|
||||
astroportGenerator: 'terra1gjm7d9nmewn27qzrvqyhda8zsfl40aya7tvaw5',
|
||||
contract_addr: 'terra1pkpgcqy38gyr978xfh9fx0ttq0jllzyfl05k4f',
|
||||
lpToken: 'terra1dqjpcqej9nxej80u0p56rhkrzlr6w8tp7txkmj',
|
||||
assets: [Assets.luna, Assets.ust],
|
||||
maxLeverage: 2,
|
||||
provider: StrategyProviders.MARS,
|
||||
apyQuery: {
|
||||
url: 'https://api.astroport.fi/graphql',
|
||||
query: `
|
||||
lunaBullStrategy: pool(address:"terra1m6ywlgn6wrjuagcmmezzz2a029gtldhey5k552") {
|
||||
trading_fees {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
astro_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
protocol_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
total_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
}`,
|
||||
target: ['pool', 'total_rewards', 'apy'],
|
||||
apr: true,
|
||||
absolute: false,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'ancBullStrategy',
|
||||
minter: 'terra13r3vngakfw457dwhw9ef36mc8w6agggefe70d9',
|
||||
logo: Assets.ust.logo,
|
||||
borrow: Assets.ust.symbol,
|
||||
color: Assets.anc.color,
|
||||
contract_addr: 'terra1x3tu0tgsa3wuz97w2nm29fvhnhjnag00nxsgmy',
|
||||
astroportGenerator: 'terra1gjm7d9nmewn27qzrvqyhda8zsfl40aya7tvaw5',
|
||||
lpToken: 'terra1agu2qllktlmf0jdkuhcheqtchnkppzrl4759y6',
|
||||
assets: [Assets.anc, Assets.ust],
|
||||
maxLeverage: 2,
|
||||
provider: StrategyProviders.MARS,
|
||||
apyQuery: {
|
||||
url: 'https://api.astroport.fi/graphql',
|
||||
query: `
|
||||
ancBullStrategy: pool(address:"terra1qr2k6yjjd5p2kaewqvg93ag74k6gyjr7re37fs") {
|
||||
trading_fees {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
astro_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
protocol_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
total_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
}`,
|
||||
target: ['pool', 'total_rewards', 'apy'],
|
||||
apr: true,
|
||||
absolute: false,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'mirBullStrategy',
|
||||
minter: 'terra1xrt4j56mkefvhnyqqd5pgk7pfxullnkvsje7wx',
|
||||
logo: Assets.ust.logo,
|
||||
borrow: Assets.ust.symbol,
|
||||
color: Assets.mir.color,
|
||||
contract_addr: 'terra1wrj7lrrxzdmcmpask6y48eudq7huvu8eylssjs',
|
||||
astroportGenerator: 'terra1gjm7d9nmewn27qzrvqyhda8zsfl40aya7tvaw5',
|
||||
lpToken: 'terra1efmcf22aweaj3zzjhzgyghv88dda0yk4j9jp29',
|
||||
assets: [Assets.mir, Assets.ust],
|
||||
maxLeverage: 2,
|
||||
provider: StrategyProviders.MARS,
|
||||
apyQuery: {
|
||||
url: 'https://api.astroport.fi/graphql',
|
||||
query: `
|
||||
mirBullStrategy: pool(address:"terra143xxfw5xf62d5m32k3t4eu9s82ccw80lcprzl9") {
|
||||
trading_fees {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
astro_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
protocol_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
total_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
}`,
|
||||
target: ['pool', 'total_rewards', 'apy'],
|
||||
apr: true,
|
||||
absolute: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'fakeMirStrat',
|
||||
minter: 'terra1408najmjzueu9ktg4rw7a8yge3z280d5yp9nay',
|
||||
logo: Assets.ust.logo,
|
||||
borrow: Assets.ust.symbol,
|
||||
color: Assets.fmir.color,
|
||||
contract_addr: 'terra16htwvqxqkygazqxmgt9wpqr0vtf6jekm6mf44n',
|
||||
astroportGenerator: 'terra1f7awhwwqcjkxlk83qvgzrpnnnzm7577u5ffsx7',
|
||||
lpToken: 'terra1r5q4cepddwn9sklkm4x0jhspv09w8rwsfszx5u',
|
||||
assets: [Assets.fmir, Assets.ust],
|
||||
maxLeverage: 2,
|
||||
provider: StrategyProviders.MARS,
|
||||
apyQuery: {
|
||||
url: 'https://api.astroport.fi/graphql',
|
||||
query: `
|
||||
fakeMirStrat: pool(address:"terra1m6ywlgn6wrjuagcmmezzz2a029gtldhey5k552") {
|
||||
trading_fees {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
astro_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
protocol_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
total_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
}`,
|
||||
target: ['pool', 'total_rewards', 'apy'],
|
||||
apr: true,
|
||||
absolute: false,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'fancBullStrategy',
|
||||
minter: 'terra1q2kv38ldy03g8ql6l8vfhc0sf5a2ypur7ee50c',
|
||||
logo: Assets.ust.logo,
|
||||
borrow: Assets.ust.symbol,
|
||||
color: Assets.fanc.color,
|
||||
contract_addr: 'terra1gq03knyygm3v0fu2cvc3nptdhndl9cr4vekxkv',
|
||||
astroportGenerator: 'terra1f7awhwwqcjkxlk83qvgzrpnnnzm7577u5ffsx7',
|
||||
lpToken: 'terra1llys4hxu8wkt0msnm0nz328ymyrk3j3nz65rkx',
|
||||
assets: [Assets.fanc, Assets.ust],
|
||||
maxLeverage: 2,
|
||||
provider: StrategyProviders.MARS,
|
||||
apyQuery: {
|
||||
url: 'https://api.astroport.fi/graphql',
|
||||
query: `
|
||||
fancBullStrategy: pool(address:"terra1qr2k6yjjd5p2kaewqvg93ag74k6gyjr7re37fs") {
|
||||
trading_fees {
|
||||
apr
|
||||
apy
|
||||
}
|
||||
astro_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
protocol_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
total_rewards {
|
||||
apy
|
||||
apr
|
||||
}
|
||||
}`,
|
||||
target: ['pool', 'total_rewards', 'apy'],
|
||||
apr: true,
|
||||
absolute: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default FieldsStrategies
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"contracts": {
|
||||
"redBankContractAddress": "terra19dtgj9j5j7kyf3pmejqv8vzfpxtejaypgzkz5u"
|
||||
},
|
||||
"whitelist": {
|
||||
"terra1dj9jfrrjc0yckz8l5g3mmqd86e67msvfh3q30f": {
|
||||
"denom": "ANC",
|
||||
"decimals": 6
|
||||
},
|
||||
"terra1x4rrkxx5pyuce32wsdn8ypqnpx8n27klnegv0d": {
|
||||
"denom": "uluna",
|
||||
"decimals": 6
|
||||
},
|
||||
"terra1cuku0vggplpgfxegdrenp302km26symjk4xxaf": {
|
||||
"denom": "uusd",
|
||||
"decimals": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"contracts": {
|
||||
"redBankContractAddress": "terra1avkm5w0gzwm92h0dlxymsdhx4l2rm7k0lxnwq7"
|
||||
},
|
||||
"whitelist": {
|
||||
"terra1l7dzcwhz24prg3dsuzrl9s7l6cpc2l72lesacd": {
|
||||
"denom": "stLuna",
|
||||
"decimals": 6
|
||||
},
|
||||
"terra1gwl5srdgdy2r24dxpdzn7m2t7509yhnzjyc36w": {
|
||||
"denom": "uluna",
|
||||
"decimals": 6
|
||||
},
|
||||
"terra1xmx2e3eyj9d0mcycpak4fa95z0wdvykp7zesqt": {
|
||||
"denom": "uusd",
|
||||
"decimals": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"lockdropAddress": "terra1n38982txtv2yygtcfv3e9wp2ktmjyxl6z88rma",
|
||||
"airdropAddress": "terra106htgurux879drvjy5tzp8l7ydcnytmhpj7yr3",
|
||||
"auctionAddress": "terra1hgyamk2kcy3stqx82wrnsklw9aq7rask5dxfds",
|
||||
"astroportMarsUstPoolAddress": "terra19wauh79y42u5vt62c5adt2g5h4exgh26t3rpds"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"lockdropAddress": "terra1eqpcgsa7750r8rjqhxzcmpzvw46fn7gquywzfe",
|
||||
"airdropAddress": "terra1xn9079ly7qfxf3mspf3pny3w4q4hl6agxwlnps",
|
||||
"auctionAddress": "terra1myp6y5869dngaczu943pqej56r8at3as2ehqje"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"contracts": {
|
||||
"astroportFactoryAddress": "terra1fnywlw4edny3vw44x04xd67uzkdqluymgreu7g"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"contracts": {
|
||||
"astroportFactoryAddress": "terra15jsahkaf9p0qu8ye873p0u5z6g07wdad0tdq43"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import Assets from '../assets/mainnet'
|
||||
|
||||
const nativeAssetWhitelist: WhitelistAsset[] = [Assets.luna, Assets.ust]
|
||||
|
||||
const cw20AssetWhitelist: WhitelistAsset[] = [Assets.anc]
|
||||
|
||||
// These assets are supported by the red bank and should use the Mars oracle
|
||||
export const Whitelist: WhitelistAsset[] = [
|
||||
...nativeAssetWhitelist,
|
||||
...cw20AssetWhitelist,
|
||||
]
|
||||
|
||||
// These assets are not supported by red bank but will be displayed in the app where they need an exchange rate
|
||||
export const Other: OtherAsset[] = [Assets.mars, Assets.astro, Assets.mir]
|
||||
@@ -0,0 +1,25 @@
|
||||
import Assets from '../assets/testnet'
|
||||
|
||||
const nativeAssetWhitelist: WhitelistAsset[] = [
|
||||
Assets.luna,
|
||||
Assets.ust,
|
||||
Assets.stLuna,
|
||||
]
|
||||
|
||||
const cw20AssetWhitelist: WhitelistAsset[] = []
|
||||
|
||||
// These assets are supported by the red bank and should use the Mars oracle
|
||||
export const Whitelist: WhitelistAsset[] = [
|
||||
...nativeAssetWhitelist,
|
||||
...cw20AssetWhitelist,
|
||||
]
|
||||
|
||||
// These assets are not supported by red bank but will be displayed in the app where they need an exchange rate
|
||||
export const Other: OtherAsset[] = [
|
||||
Assets.mars,
|
||||
Assets.astro,
|
||||
Assets.mir,
|
||||
Assets.anc,
|
||||
Assets.fanc,
|
||||
Assets.fmir,
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
/* terra:network */
|
||||
export const FINDER_URL = 'https://terrascope.info/'
|
||||
export const EXTENSION = 'https://terra.money/extension'
|
||||
export const CHROME = 'https://google.com/chrome'
|
||||
export const ASTROPORT_URL = 'https://app.astroport.fi/swap'
|
||||
export const FORUM_URL = 'https://forum.marsprotocol.io/'
|
||||
|
||||
/* contract deploy block heights */
|
||||
export const STAKING_CONTRACT_DEPLOY_HEIGHT = 6531019
|
||||
export const MARS_CONTRACT_DEPLOY_HEIGHT = 6530880
|
||||
export const XMARS_CONTRACT_DEPLOY_HEIGHT = 6531023
|
||||
export const AUCTION_CONTRACT_END_TIMESTAMP = 1646046000 + 432000 + 172800 // init_timestamp + ust_deposit_window + withdrawal_window
|
||||
export const AUCTION_LP_TOKENS_VESTING_DURATION =
|
||||
AUCTION_CONTRACT_END_TIMESTAMP + 7776000 // lp_tokens_vesting_duration
|
||||
|
||||
/* mars:unit */
|
||||
export const MARS_DENOM = 'MARS'
|
||||
export const MARS_DECIMALS = 6
|
||||
export const XMARS_DENOM = 'XMARS'
|
||||
export const XMARS_DECIMALS = 6
|
||||
export const ASTRO_DENOM = 'ASTRO'
|
||||
export const ASTRO_DECIMALS = 6
|
||||
export const PROPOSAL_LIMIT = 5
|
||||
export const UST_DENOM = 'uusd'
|
||||
export const UST_DECIMALS = 6
|
||||
|
||||
export const BLOCK_TIME = 7500
|
||||
export const BLOCKS_PER_DAY = (86400 * 1000) / BLOCK_TIME
|
||||
export const COOLDOWN_BUFFER = BLOCK_TIME * 2
|
||||
|
||||
/* borrowLimit */
|
||||
export const GAUGE_SCALE = 0.825
|
||||
export const DEFAULT_SLIPPAGE = 0.01
|
||||
|
||||
/* other */
|
||||
export const VOLATILITY_THRESHOLD = 0.05
|
||||
|
||||
/* feature flags */
|
||||
export const FIELDS_FEATURE: Boolean = true
|
||||
export const MY_LOCKDROP_FEATURE: Boolean = true
|
||||
export const PROPOSAL_ACTION_BUTTONS_FEATURE: Boolean = false
|
||||
export const AIRDROP_CLAIM_FEATURE: Boolean = true
|
||||
@@ -0,0 +1,15 @@
|
||||
import mars from '../images/MARS-COLORED.svg'
|
||||
import apollo from '../images/Apollo.svg'
|
||||
|
||||
import { StrategyProvider } from '../types/interfaces/strategyProvider'
|
||||
|
||||
export const StrategyProviders: StrategyProvider = {
|
||||
MARS: {
|
||||
logo: mars,
|
||||
name: 'Mars',
|
||||
},
|
||||
APOLLO: {
|
||||
logo: apollo,
|
||||
name: 'Apollo',
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const DAY_IN_SECONDS = 60 * 60 * 24
|
||||
export const HOUR_IN_SECONDS = 60 * 60
|
||||
export const MINUTE_IN_SECONDS = 60
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,16 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
const CreateContext = <A>(name: string) => {
|
||||
const ctx = createContext<A | undefined>(undefined)
|
||||
|
||||
const useCtx = () => {
|
||||
const c = useContext(ctx)
|
||||
if (!c)
|
||||
throw new Error(`${name} must be inside a Provider with a value`)
|
||||
return c
|
||||
}
|
||||
|
||||
return [useCtx, ctx.Provider] as const
|
||||
}
|
||||
|
||||
export default CreateContext
|
||||
@@ -0,0 +1,26 @@
|
||||
export { default as useLocalStorage } from './useLocalStorage'
|
||||
export { useAccountBalance } from './useAccountBalance'
|
||||
export { default as useNewContractMsg } from './useNewContractMsg'
|
||||
export { useInterval } from './useInterval'
|
||||
export { useRedBank } from './useRedBank'
|
||||
export { useAstroportSpotOracle } from './useAstroportSpotOracle'
|
||||
export { useMarsOracle } from './useMarsOracle'
|
||||
export { useExchangeRate } from './useExchangeRate'
|
||||
export { useAddressProvider } from './useAddressProvider'
|
||||
export { useSimulateSwap } from './useSimulateSwap'
|
||||
export { useContract } from './useContract'
|
||||
export { useIncentives } from './useIncentives'
|
||||
export { useStaking } from './useStaking'
|
||||
export { default as useErrorMessage } from './useErrorMessage'
|
||||
export { useMarsBalance } from './useMarsBalance'
|
||||
export { useFields } from './useFields'
|
||||
export { useAssetGrid } from './useAssetGrid'
|
||||
export { useAirdrop } from './lockdrop/useAirdrop'
|
||||
export { useLockdropUserInfo } from './lockdrop/useLockdropUserInfo'
|
||||
export { useLockdropLockupPositions } from './lockdrop/useLockdropLockupPositions'
|
||||
export { useAuctionUserInfo } from './lockdrop/useAuctionUserInfo'
|
||||
export { useMarsLpAssetRate } from './lockdrop/useMarsLpAssetRate'
|
||||
export { useErrors } from './useErrors'
|
||||
export { useBasecamp } from './useBasecamp'
|
||||
export { useVesting } from './useVesting'
|
||||
export { useTNS } from './useTNS'
|
||||
@@ -0,0 +1,168 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import createContext from '../createContext'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import gql from 'graphql-tag'
|
||||
import { AIRDROP_CLAIM_FEATURE } from '../../constants/appConstants'
|
||||
import useStore from '../../store'
|
||||
|
||||
export interface AirdropConfig {
|
||||
/// Account who can update config
|
||||
owner: string
|
||||
/// MARS token address
|
||||
mars_token_address: string
|
||||
/// Merkle roots used to verify is a terra user is eligible for the airdrop
|
||||
merkle_roots: string[]
|
||||
/// Timestamp since which MARS airdrops can be delegated to bootstrap auction contract
|
||||
from_timestamp: number
|
||||
/// Timestamp till which MARS airdrops can be claimed
|
||||
to_timestamp: number
|
||||
/// Bootstrap auction contract address
|
||||
auction_contract_address?: string
|
||||
/// Boolean value indicating if the users can withdraw their MARS airdrop tokens or not
|
||||
/// This value is updated in the same Tx in which Liquidity is added to the LP Pool
|
||||
are_claims_allowed: boolean
|
||||
}
|
||||
|
||||
export interface AirdropState {
|
||||
/// Total MARS issuance used as airdrop incentives
|
||||
total_airdrop_size: string
|
||||
/// Total MARS tokens that have been delegated to the bootstrap auction pool
|
||||
total_delegated_amount: string
|
||||
/// Total MARS tokens that are yet to be claimed by the users
|
||||
unclaimed_tokens: string
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
/// Total MARS airdrop tokens claimable by the user
|
||||
airdrop_amount: string
|
||||
/// MARS tokens delegated to the bootstrap auction contract to add to the user's position
|
||||
delegated_amount: string
|
||||
/// Boolean value indicating if the user has withdrawn the remaining MARS tokens
|
||||
tokens_withdrawn: boolean
|
||||
}
|
||||
|
||||
export interface AirdropData {
|
||||
address: string
|
||||
amount: string
|
||||
merkle_proof: string[]
|
||||
index: number
|
||||
}
|
||||
|
||||
export interface Airdrop {
|
||||
config: AirdropConfig | undefined
|
||||
state: AirdropState | undefined
|
||||
userInfo: UserInfo | undefined
|
||||
airdropData: AirdropData | undefined
|
||||
initialised: boolean
|
||||
refetch: () => void
|
||||
getAirdropBalance: () => number
|
||||
}
|
||||
|
||||
export const [useAirdrop, AirdropProvider] =
|
||||
createContext<Airdrop>('useAirdrop')
|
||||
|
||||
export const useAirdropState = (address: string): Airdrop => {
|
||||
const [config, setConfig] = useState<AirdropConfig | undefined>()
|
||||
const [state, setState] = useState<AirdropState | undefined>()
|
||||
const [userInfo, setUserInfo] = useState<UserInfo | undefined>()
|
||||
const [airdropData, setAirdropData] = useState<AirdropData | undefined>()
|
||||
const [initialisedContractQueries, setInitialisedContractQueries] =
|
||||
useState(false)
|
||||
const [initialisedAirdropData, setInitialisedAirdropData] = useState(false)
|
||||
const [rawData, setRawData] = useState<object>()
|
||||
const lockdropAddresses = useStore((s) => s.lockdropAddresses)
|
||||
const networkConfig = useStore((s) => s.networkConfig)
|
||||
|
||||
const wasmKey = 'airdropWasm'
|
||||
const query = gql`
|
||||
query AirdropQuery(
|
||||
$airdropAddress: String!,
|
||||
$address: String!) {
|
||||
${wasmKey}: wasm {
|
||||
config : contractQuery(contractAddress: $airdropAddress, query: { config : {} })
|
||||
state :contractQuery(contractAddress: $airdropAddress, query: { state: {} })
|
||||
userInfo :contractQuery(contractAddress: $airdropAddress, query: { user_info: { address: $address } })
|
||||
}
|
||||
}`
|
||||
|
||||
const { data, loading, error, refetch } = useQuery(query, {
|
||||
variables: {
|
||||
airdropAddress: lockdropAddresses?.airdropAddress,
|
||||
address: address,
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
notifyOnNetworkStatusChange: true,
|
||||
pollInterval: 10000, // small and many people will be claiming still
|
||||
skip: !lockdropAddresses?.airdropAddress || !address,
|
||||
})
|
||||
|
||||
const getAirdropData = async (address: string): Promise<AirdropData> => {
|
||||
const result = await fetch(
|
||||
`${networkConfig!.airdropWebServiceURL}/${address}`
|
||||
)
|
||||
|
||||
return result.status === 200
|
||||
? await result.json()
|
||||
: {
|
||||
address,
|
||||
amount: '0',
|
||||
merkle_proof: '',
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const initialise = async () => {
|
||||
if (!address || !networkConfig) return
|
||||
if (AIRDROP_CLAIM_FEATURE) {
|
||||
setAirdropData(await getAirdropData(address))
|
||||
}
|
||||
setInitialisedAirdropData(true)
|
||||
}
|
||||
initialise()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [address, networkConfig])
|
||||
|
||||
useEffect(() => {
|
||||
const initialise = () => {
|
||||
if (!error && !loading && data !== rawData) {
|
||||
if (!data || !data[wasmKey]) return
|
||||
const wasmData = data[wasmKey]
|
||||
setRawData(data)
|
||||
setConfig(wasmData.config)
|
||||
setState(wasmData.state)
|
||||
setUserInfo(wasmData.userInfo)
|
||||
setInitialisedContractQueries(true)
|
||||
}
|
||||
}
|
||||
initialise()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data])
|
||||
|
||||
const getAirdropBalance = (): number => {
|
||||
if (
|
||||
!airdropData ||
|
||||
!userInfo ||
|
||||
!(initialisedContractQueries && initialisedAirdropData) ||
|
||||
userInfo.tokens_withdrawn
|
||||
)
|
||||
return 0
|
||||
|
||||
if (Number(userInfo.airdrop_amount) === 0)
|
||||
return Number(airdropData.amount)
|
||||
|
||||
return (
|
||||
Number(userInfo.airdrop_amount) - Number(userInfo.delegated_amount)
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
state,
|
||||
userInfo,
|
||||
airdropData,
|
||||
initialised: initialisedContractQueries && initialisedAirdropData,
|
||||
refetch,
|
||||
getAirdropBalance,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import createContext from '../createContext'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import gql from 'graphql-tag'
|
||||
import useStore from '../../store'
|
||||
|
||||
export interface UserInfo {
|
||||
mars_deposited: string
|
||||
ust_deposited: string
|
||||
ust_withdrawn_flag: boolean
|
||||
lp_shares: string
|
||||
withdrawn_lp_shares: string
|
||||
withdrawable_lp_shares: string
|
||||
total_auction_incentives: string
|
||||
withdrawn_auction_incentives: string
|
||||
withdrawable_auction_incentives: string
|
||||
mars_reward_index: number
|
||||
withdrawable_mars_incentives: string
|
||||
withdrawn_mars_incentives: string
|
||||
astro_reward_index: number
|
||||
withdrawable_astro_incentives: string
|
||||
withdrawn_astro_incentives: string
|
||||
}
|
||||
|
||||
export interface AuctionUserInfo {
|
||||
userInfo: UserInfo | undefined
|
||||
initialised: boolean
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
export const [useAuctionUserInfo, AuctionUserInfoProvider] =
|
||||
createContext<AuctionUserInfo>('useAuctionUserInfo')
|
||||
|
||||
export const useAuctionUserInfoState = (address: string): AuctionUserInfo => {
|
||||
const [userInfo, setUserInfo] = useState<UserInfo | undefined>()
|
||||
const [initialised, setIntialised] = useState(false)
|
||||
const [rawData, setRawData] = useState<object>()
|
||||
const lockdropAddresses = useStore((s) => s.lockdropAddresses)
|
||||
|
||||
const wasmKey = 'auctionUserInfoWasm'
|
||||
const query = gql`
|
||||
query AuctionUserInfoQuery(
|
||||
$auctionAddress: String!,
|
||||
$address: String!) {
|
||||
${wasmKey}: wasm {
|
||||
userInfo :contractQuery(contractAddress: $auctionAddress, query: { user_info: { address: $address } })
|
||||
}
|
||||
}`
|
||||
|
||||
const { data, loading, error, refetch } = useQuery(query, {
|
||||
variables: {
|
||||
auctionAddress: lockdropAddresses?.auctionAddress,
|
||||
address: address,
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
notifyOnNetworkStatusChange: true,
|
||||
pollInterval: 60000,
|
||||
skip: !lockdropAddresses?.auctionAddress || !address,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const initialise = () => {
|
||||
if (!error && !loading && data !== rawData) {
|
||||
if (!data || !data[wasmKey]) return
|
||||
const wasmData = data[wasmKey]
|
||||
setRawData(data)
|
||||
setUserInfo(wasmData.userInfo)
|
||||
setIntialised(true)
|
||||
}
|
||||
}
|
||||
initialise()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data])
|
||||
|
||||
useEffect(() => {
|
||||
if (!address) {
|
||||
setUserInfo(undefined)
|
||||
setIntialised(false)
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [address])
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
initialised,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import createContext from '../createContext'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { contractQuery } from '../../queries/contractQuery'
|
||||
import gql from 'graphql-tag'
|
||||
import { State } from '../../types/enums'
|
||||
import useStore from '../../store'
|
||||
|
||||
interface LockDropPositionObject {
|
||||
/// Lockup Duration
|
||||
duration: number
|
||||
/// UST locked as part of this lockup position
|
||||
ust_locked: string
|
||||
/// MA-UST share
|
||||
maust_balance: string
|
||||
/// Lockdrop incentive distributed to this position
|
||||
lockdrop_reward: string
|
||||
/// Timestamp beyond which this position can be unlocked
|
||||
unlock_timestamp: number
|
||||
/// Boolean value indicating if the user's has withdrawn funds post the only 1 withdrawal limit cutoff
|
||||
withdrawal_flag: boolean
|
||||
}
|
||||
|
||||
interface LockdropLockupPositions {
|
||||
lockupPositions: LockDropPositionObject[] | undefined
|
||||
state: State
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
export const [useLockdropLockupPositions, LockdropLockupPositionsProvider] =
|
||||
createContext<LockdropLockupPositions>('useLockdropLockupPositions')
|
||||
|
||||
export const useLockdropLockupPositionsState = (
|
||||
address: string,
|
||||
lockupPositionIds: string[] | undefined
|
||||
): LockdropLockupPositions => {
|
||||
const [lockupPositions, setLockupPositions] = useState<
|
||||
LockDropPositionObject[] | undefined
|
||||
>()
|
||||
const [state, setState] = useState(State.INITIALISING)
|
||||
const [rawData, setRawData] = useState<object>()
|
||||
const lockdropAddresses = useStore((s) => s.lockdropAddresses)
|
||||
|
||||
const positionQuery = (positionId: string) => {
|
||||
return `
|
||||
{
|
||||
lockup_info_with_id: {
|
||||
lockup_id: "${positionId}"
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
const buildQuery = () => {
|
||||
if (!lockdropAddresses?.lockdropAddress || !lockupPositionIds?.length)
|
||||
return
|
||||
|
||||
const queries: string[] = lockupPositionIds?.map((positionId) => {
|
||||
return contractQuery(
|
||||
positionId,
|
||||
lockdropAddresses?.lockdropAddress,
|
||||
positionQuery(positionId)
|
||||
)
|
||||
})
|
||||
|
||||
return queries
|
||||
}
|
||||
|
||||
const query = buildQuery()
|
||||
const { data, loading, error, refetch } = useQuery(
|
||||
gql`
|
||||
query LockdropLockupPositionsQuery { lockdropLockupPositionsWasm: wasm { ${query} } }
|
||||
`,
|
||||
{
|
||||
variables: {
|
||||
lockdropAddress: lockdropAddresses?.lockdropAddress,
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
notifyOnNetworkStatusChange: true,
|
||||
pollInterval: 60000,
|
||||
skip:
|
||||
!lockdropAddresses?.lockdropAddress ||
|
||||
!lockupPositionIds?.length,
|
||||
}
|
||||
)
|
||||
|
||||
if (error && state !== State.ERROR) {
|
||||
setState(State.ERROR)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const initialise = () => {
|
||||
if (!error && !loading && data && data !== rawData) {
|
||||
if (!data?.lockdropLockupPositionsWasm) return
|
||||
|
||||
const userPositions: LockDropPositionObject[] = []
|
||||
lockupPositionIds?.forEach((positionId) => {
|
||||
const position =
|
||||
data?.lockdropLockupPositionsWasm[positionId]
|
||||
if (position?.lockup_info) {
|
||||
userPositions.push(position?.lockup_info)
|
||||
}
|
||||
})
|
||||
|
||||
setRawData(data)
|
||||
setLockupPositions(userPositions)
|
||||
setState(State.READY)
|
||||
} else if (!lockupPositionIds?.length && lockupPositions?.length) {
|
||||
setLockupPositions([])
|
||||
}
|
||||
}
|
||||
initialise()
|
||||
}, [data, error, loading, lockupPositionIds, rawData, lockupPositions])
|
||||
|
||||
useEffect(() => {
|
||||
if (!address) {
|
||||
setLockupPositions(undefined)
|
||||
setState(State.ERROR)
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [address])
|
||||
|
||||
return { lockupPositions, state, refetch }
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import createContext from '../createContext'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import gql from 'graphql-tag'
|
||||
import useStore from '../../store'
|
||||
|
||||
export interface UserInfo {
|
||||
total_ust_locked: string
|
||||
total_maust_share: string
|
||||
lockup_position_ids: string[]
|
||||
total_mars_incentives: string
|
||||
delegated_mars_incentives: string
|
||||
is_lockdrop_claimed: boolean
|
||||
reward_index: number
|
||||
total_xmars_claimed: string
|
||||
pending_xmars_to_claim: string
|
||||
}
|
||||
|
||||
export interface LockdropUserInfo {
|
||||
userInfo: UserInfo | undefined
|
||||
initialised: boolean
|
||||
getLockdropBalance: () => number
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
export const [useLockdropUserInfo, LockdropUserInfoProvider] =
|
||||
createContext<LockdropUserInfo>('useLockdropUserInfo')
|
||||
|
||||
export const useLockdropUserInfoState = (address: string): LockdropUserInfo => {
|
||||
const [userInfo, setUserInfo] = useState<UserInfo | undefined>()
|
||||
const [initialised, setIntialised] = useState(false)
|
||||
const [rawData, setRawData] = useState<object>()
|
||||
const lockdropAddresses = useStore((s) => s.lockdropAddresses)
|
||||
|
||||
const wasmKey = 'lockdropUserInfoWasm'
|
||||
const query = gql`
|
||||
query LockdropUserInfoQuery(
|
||||
$lockdropAddress: String!,
|
||||
$address: String!) {
|
||||
${wasmKey}: wasm {
|
||||
userInfo :contractQuery(contractAddress: $lockdropAddress, query: { user_info: { address: $address } })
|
||||
}
|
||||
}`
|
||||
|
||||
const { data, loading, error, refetch } = useQuery(query, {
|
||||
variables: {
|
||||
lockdropAddress: lockdropAddresses?.lockdropAddress,
|
||||
address: address,
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
notifyOnNetworkStatusChange: true,
|
||||
pollInterval: 60000,
|
||||
skip: !lockdropAddresses?.lockdropAddress || !address,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const initialise = () => {
|
||||
if (!error && !loading && data !== rawData) {
|
||||
if (!data || !data[wasmKey]) return
|
||||
const wasmData = data[wasmKey]
|
||||
setRawData(data)
|
||||
setUserInfo(wasmData.userInfo)
|
||||
setIntialised(true)
|
||||
}
|
||||
}
|
||||
initialise()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data])
|
||||
|
||||
useEffect(() => {
|
||||
if (!address) {
|
||||
setUserInfo(undefined)
|
||||
setIntialised(false)
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [address])
|
||||
|
||||
const getLockdropBalance = (): number => {
|
||||
if (userInfo?.is_lockdrop_claimed) return 0
|
||||
|
||||
return (
|
||||
(Number(userInfo?.total_mars_incentives) || 0) -
|
||||
(Number(userInfo?.delegated_mars_incentives) || 0)
|
||||
)
|
||||
}
|
||||
|
||||
return { userInfo, initialised, getLockdropBalance, refetch }
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import createContext from '../createContext'
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
import { contractQuery } from '../../queries/contractQuery'
|
||||
import { State } from '../../types/enums'
|
||||
import useStore from '../../store'
|
||||
|
||||
export interface MarsLpAssetRates {
|
||||
state: State
|
||||
marsLpToAssets: (marsLpAmount: number) => LpToAssetsResponse
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
interface Token {
|
||||
contract_addr: string
|
||||
}
|
||||
|
||||
interface NativeToken {
|
||||
denom: string
|
||||
}
|
||||
|
||||
interface AssetInfo {
|
||||
token?: Token
|
||||
native_token?: NativeToken
|
||||
}
|
||||
|
||||
interface Asset {
|
||||
info: AssetInfo
|
||||
amount: number
|
||||
}
|
||||
|
||||
interface PoolQueryResponse {
|
||||
assets: Asset[]
|
||||
total_share: number
|
||||
}
|
||||
|
||||
export interface LpToAssetsResponse {
|
||||
mars: number
|
||||
uusd: number
|
||||
}
|
||||
|
||||
export const [useMarsLpAssetRate, MarsLpAssetRateProvider] =
|
||||
createContext<MarsLpAssetRates>('useMarsLpAssetRate')
|
||||
|
||||
export const useMarsLpAssetRateState = (): MarsLpAssetRates => {
|
||||
const lockdropAddresses = useStore((s) => s.lockdropAddresses)
|
||||
const [marsAssetInfo, setMarsAssetInfo] = useState<Asset>()
|
||||
const [uusdAssetInfo, setUusdAssetInfo] = useState<Asset>()
|
||||
const [lpTokensInCirculation, setLpTokensInCirculation] = useState<number>()
|
||||
const [state, setState] = useState<State>(State.INITIALISING)
|
||||
const wasmKey = 'marsLpAssetRateWasm'
|
||||
|
||||
const producePoolQuery = () => {
|
||||
if (!lockdropAddresses?.astroportMarsUstPoolAddress) return 'ping'
|
||||
const query = contractQuery(
|
||||
'poolQueryResponse',
|
||||
lockdropAddresses?.astroportMarsUstPoolAddress,
|
||||
'{ pool : {} }'
|
||||
)
|
||||
|
||||
return `${wasmKey}: wasm {
|
||||
${query}
|
||||
}`
|
||||
}
|
||||
|
||||
const poolQuery = gql`query MarsLpAssetRateQuery {
|
||||
${producePoolQuery()}
|
||||
}`
|
||||
const { data, loading, error, refetch } = useQuery(poolQuery, {
|
||||
fetchPolicy: 'no-cache',
|
||||
notifyOnNetworkStatusChange: true,
|
||||
pollInterval: 60000,
|
||||
skip: !lockdropAddresses?.astroportMarsUstPoolAddress,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (error || loading || !data) return
|
||||
|
||||
const response: PoolQueryResponse = data[wasmKey]?.poolQueryResponse
|
||||
if (response) {
|
||||
const uusd = response.assets[0].info.native_token
|
||||
? response.assets[0]
|
||||
: response.assets[1]
|
||||
const mars = response.assets[0].info.native_token
|
||||
? response.assets[1]
|
||||
: response.assets[0]
|
||||
|
||||
setUusdAssetInfo(uusd)
|
||||
setMarsAssetInfo(mars)
|
||||
setLpTokensInCirculation(response.total_share)
|
||||
}
|
||||
|
||||
setState(State.READY)
|
||||
}, [data, error, loading])
|
||||
|
||||
const marsLpToAssets = (marsLpAmount: number): LpToAssetsResponse => {
|
||||
if (!marsLpAmount || state !== State.READY || !lpTokensInCirculation)
|
||||
return { mars: 0, uusd: 0 }
|
||||
|
||||
const lpRatio =
|
||||
lpTokensInCirculation === 0
|
||||
? 0
|
||||
: marsLpAmount / lpTokensInCirculation
|
||||
const marsFromLpPosition = lpRatio * (marsAssetInfo?.amount || 0)
|
||||
const uusdFromLpPosition = lpRatio * (uusdAssetInfo?.amount || 0)
|
||||
|
||||
return { mars: marsFromLpPosition, uusd: uusdFromLpPosition }
|
||||
}
|
||||
|
||||
return { state, marsLpToAssets, refetch }
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Coin } from '@terra-money/terra.js'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
AUCTION_LP_TOKENS_VESTING_DURATION,
|
||||
MARS_DENOM,
|
||||
UST_DECIMALS,
|
||||
UST_DENOM,
|
||||
} from '../../constants/appConstants'
|
||||
import createContext from './../createContext'
|
||||
import {
|
||||
useLockdropLockupPositions,
|
||||
useAccountBalance,
|
||||
useAuctionUserInfo,
|
||||
useMarsLpAssetRate,
|
||||
useExchangeRate,
|
||||
} from '..'
|
||||
import moment from 'moment'
|
||||
import ust from '../../images/UST.svg'
|
||||
import mars from '../../images/MARS-COLORED.svg'
|
||||
|
||||
export interface MyLockupGrid {
|
||||
lockdropPositions: lockdropPosition[]
|
||||
}
|
||||
|
||||
export const [useMyLockupGrid, MyLockupGridProvider] =
|
||||
createContext<MyLockupGrid>('useMyLockupGrid')
|
||||
|
||||
export const useMyLockupGridState = (): MyLockupGrid => {
|
||||
const { convertMaTokenToUnderlying } = useAccountBalance()
|
||||
const { lockupPositions } = useLockdropLockupPositions()
|
||||
const { userInfo: auctionUserInfo } = useAuctionUserInfo()
|
||||
const { marsLpToAssets } = useMarsLpAssetRate()
|
||||
const { exchangeToUusd } = useExchangeRate()
|
||||
|
||||
const [lockdropPositions, setLockdropPositions] = useState<
|
||||
lockdropPosition[]
|
||||
>([])
|
||||
|
||||
useEffect(() => {
|
||||
let positions: lockdropPosition[] = []
|
||||
|
||||
if (lockupPositions?.length) {
|
||||
positions.push(
|
||||
...lockupPositions.map((position) => {
|
||||
return {
|
||||
denom: UST_DENOM,
|
||||
decimals: UST_DECIMALS,
|
||||
logos: [ust],
|
||||
position: 'UST',
|
||||
name: 'Terra USD',
|
||||
liquidity: convertMaTokenToUnderlying(
|
||||
UST_DENOM,
|
||||
Number(position?.maust_balance) || 0
|
||||
),
|
||||
apy: 0,
|
||||
rewards: Number(position?.lockdrop_reward) || 0,
|
||||
unlocked:
|
||||
moment().unix() >= position?.unlock_timestamp
|
||||
? convertMaTokenToUnderlying(
|
||||
UST_DENOM,
|
||||
Number(position?.maust_balance) || 0
|
||||
)
|
||||
: 0,
|
||||
timestamp: position?.unlock_timestamp || 0,
|
||||
duration: position?.duration,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
auctionUserInfo &&
|
||||
Number(auctionUserInfo?.total_auction_incentives) > 0
|
||||
) {
|
||||
const lockedTokens =
|
||||
(Number(auctionUserInfo?.lp_shares) || 0) -
|
||||
(Number(auctionUserInfo?.withdrawn_lp_shares) || 0)
|
||||
const underlyingTokensLocked = marsLpToAssets(lockedTokens)
|
||||
const underlyingTokensUnlocked = marsLpToAssets(
|
||||
Number(auctionUserInfo?.withdrawable_lp_shares)
|
||||
)
|
||||
const uusdValueLocked =
|
||||
underlyingTokensLocked.uusd +
|
||||
exchangeToUusd(
|
||||
new Coin(MARS_DENOM, underlyingTokensLocked.mars)
|
||||
)
|
||||
const uusdValueUnlocked =
|
||||
underlyingTokensUnlocked.uusd +
|
||||
exchangeToUusd(
|
||||
new Coin(MARS_DENOM, underlyingTokensUnlocked.mars)
|
||||
)
|
||||
|
||||
const phase2Position: lockdropPosition = {
|
||||
denom: UST_DENOM,
|
||||
decimals: UST_DECIMALS,
|
||||
logos: [mars, ust],
|
||||
position: 'MARS-UST',
|
||||
name: '',
|
||||
liquidity: uusdValueLocked,
|
||||
apy: 0,
|
||||
rewards: Number(auctionUserInfo.total_auction_incentives),
|
||||
unlocked: uusdValueUnlocked,
|
||||
timestamp: AUCTION_LP_TOKENS_VESTING_DURATION,
|
||||
duration: 0,
|
||||
}
|
||||
|
||||
positions.push(phase2Position)
|
||||
}
|
||||
|
||||
setLockdropPositions(positions)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [lockupPositions, auctionUserInfo])
|
||||
|
||||
return { lockdropPositions }
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Coin, Denom } from '@terra-money/terra.js'
|
||||
|
||||
import createContext from './createContext'
|
||||
import { MarketQuery } from './useRedBank'
|
||||
import gql from 'graphql-tag'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import { accountBalanceQuery } from '../queries/accountBalanceQuery'
|
||||
import { State } from '../types/enums'
|
||||
import useStore from '../store'
|
||||
|
||||
export interface AccountBalance {
|
||||
coins: Coin[] | undefined
|
||||
debts: Coin[] | undefined
|
||||
deposits: Coin[] | undefined
|
||||
state: State
|
||||
find: (key: Denom) => Coin | undefined
|
||||
findDebt: (key: Denom) => Coin | undefined
|
||||
findDeposit: (key: Denom) => Coin | undefined
|
||||
convertMaTokenToUnderlying: (key: Denom, maTokenAmount: number) => number
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
interface DebtQueryResponse {
|
||||
debts: [
|
||||
{
|
||||
denom: string
|
||||
amount_scaled: string
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const [useAccountBalance, AccountBalanceProvider] =
|
||||
createContext<AccountBalance>('useAccountBalance')
|
||||
|
||||
export const useAccountBalanceState = (
|
||||
address: string,
|
||||
findMarketInfo: (key: Denom) => MarketQuery | undefined
|
||||
): AccountBalance => {
|
||||
const [coins, setCoins] = useState<Coin[] | undefined>(undefined)
|
||||
const [deposits, setDeposits] = useState<Coin[] | undefined>(undefined)
|
||||
const [debts, setDebts] = useState<Coin[] | undefined>(undefined)
|
||||
const [state, setState] = useState(State.INITIALISING)
|
||||
const [fetchedData, setFetchedData] = useState<object>()
|
||||
const [refetchRequired, setRefetchRequired] = useState(true)
|
||||
const lcd = useStore((s) => s.networkConfig?.lcd)
|
||||
const chainID = useStore((s) => s.networkConfig?.chainID)
|
||||
const networkAddresses = useStore((s) => s.networkAddresses)
|
||||
const whitelistedAssets = useStore((s) => s.whitelistedAssets)
|
||||
const otherAssets = useStore((s) => s.otherAssets)
|
||||
const allAssets = [...(whitelistedAssets || []), ...(otherAssets || [])]
|
||||
|
||||
const wasmKey = 'accountBalanceWasm'
|
||||
const { data, loading, error, refetch } = useQuery(
|
||||
gql`
|
||||
query ${accountBalanceQuery(
|
||||
address,
|
||||
wasmKey,
|
||||
allAssets || [],
|
||||
networkAddresses
|
||||
)}
|
||||
`,
|
||||
{
|
||||
fetchPolicy: 'network-only',
|
||||
pollInterval: 30000,
|
||||
nextFetchPolicy: 'no-cache',
|
||||
skip: !address || (!allAssets?.length && !networkAddresses),
|
||||
}
|
||||
)
|
||||
|
||||
if (error && state !== State.ERROR && !data) {
|
||||
setState(State.ERROR)
|
||||
}
|
||||
|
||||
if (!loading && !error && data && data !== fetchedData) {
|
||||
setFetchedData(data)
|
||||
const wasmQueryResults = data[wasmKey]
|
||||
const bankQueryResults = data.balance
|
||||
const newDeposits: Coin[] = []
|
||||
const rawBalances: Coin[] = bankQueryResults.balance
|
||||
const newCoins: Coin[] = rawBalances.map(
|
||||
(coin) => new Coin(coin.denom, coin.amount)
|
||||
)
|
||||
|
||||
allAssets?.forEach((asset: WhitelistAsset) => {
|
||||
const denom = asset.denom
|
||||
if (denom) {
|
||||
if (asset.contract_addr) {
|
||||
const newCoin: Coin = new Coin(
|
||||
denom,
|
||||
wasmQueryResults[denom]?.balance || 0
|
||||
)
|
||||
newCoins.push(newCoin)
|
||||
}
|
||||
|
||||
// Only whitelisted assets will have red bank deposit balences
|
||||
if (whitelistedAssets?.find((d) => d.denom === denom)) {
|
||||
const deposit = wasmQueryResults[`${denom}Deposit`]
|
||||
if (deposit) {
|
||||
newDeposits.push(
|
||||
new Coin(
|
||||
denom,
|
||||
wasmQueryResults[`${denom}Deposit`].balance
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const debtsResponse: DebtQueryResponse = wasmQueryResults?.debts
|
||||
if (debtsResponse?.debts) {
|
||||
setDebts(
|
||||
debtsResponse.debts.map((debt) => {
|
||||
return new Coin(debt.denom, debt.amount_scaled)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
setCoins(newCoins)
|
||||
setDeposits(newDeposits)
|
||||
setState(State.READY)
|
||||
setRefetchRequired(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!address) {
|
||||
setDebts([])
|
||||
setCoins([])
|
||||
setState(State.ERROR)
|
||||
return
|
||||
}
|
||||
|
||||
if (!networkAddresses) {
|
||||
setCoins([])
|
||||
setState(State.READY)
|
||||
setRefetchRequired(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!refetchRequired ||
|
||||
!lcd ||
|
||||
!chainID ||
|
||||
!whitelistedAssets ||
|
||||
!otherAssets
|
||||
)
|
||||
return
|
||||
|
||||
// eslint-disable-next-line
|
||||
}, [
|
||||
address,
|
||||
lcd,
|
||||
chainID,
|
||||
networkAddresses,
|
||||
whitelistedAssets,
|
||||
otherAssets,
|
||||
refetchRequired,
|
||||
])
|
||||
|
||||
// If a new wallet connects we need to refetch balances
|
||||
useEffect(() => {
|
||||
if (address) {
|
||||
setRefetchRequired(true)
|
||||
}
|
||||
}, [address])
|
||||
|
||||
const find = (denom: Denom) => {
|
||||
return coins && coins.find((coin) => coin.denom === denom)
|
||||
}
|
||||
|
||||
const findDeposit = (denom: Denom) => {
|
||||
const asset = (allAssets || []).find(
|
||||
(maAsset) => maAsset.denom === denom
|
||||
)
|
||||
const deposit =
|
||||
deposits && deposits.find((coin) => coin.denom === asset?.denom)
|
||||
const scaledAmount = convertMaTokenToUnderlying(
|
||||
denom,
|
||||
Number(deposit?.amount) || 0
|
||||
)
|
||||
return new Coin(asset?.denom || '', scaledAmount)
|
||||
}
|
||||
|
||||
const convertMaTokenToUnderlying = (
|
||||
denom: Denom,
|
||||
maTokenAmount: number
|
||||
) => {
|
||||
const marketInfo = findMarketInfo(denom)
|
||||
// Mars contracts scale/multiply everything by 1e6 to add accuracy, so we need to do the opposite here and divide by 1e6
|
||||
const scaledAmount =
|
||||
(maTokenAmount / 1e6) * (marketInfo?.liquidity_index || 1)
|
||||
return scaledAmount
|
||||
}
|
||||
|
||||
const findDebt = (denom: Denom) => {
|
||||
const marketInfo = findMarketInfo(denom)
|
||||
|
||||
// scale our debt by the index to get the correct amount
|
||||
const debt = debts && debts.find((debt) => debt.denom === denom)
|
||||
// Mars contracts scale/multiply everything by 1e6 to add accuracy, so we need to do the opposite here and divide by 1e6
|
||||
const scaledAmount =
|
||||
(Number(debt?.amount) / 1e6) * (marketInfo?.borrow_index || 1)
|
||||
|
||||
return new Coin(denom, scaledAmount)
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
coins,
|
||||
debts,
|
||||
deposits,
|
||||
find,
|
||||
findDebt,
|
||||
refetch,
|
||||
findDeposit,
|
||||
convertMaTokenToUnderlying,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useHistory, useLocation } from 'react-router'
|
||||
import { GridActionType } from '../components/grid/GridActions'
|
||||
import { getRoute } from '../libs/parse'
|
||||
import { ActionType } from '../types/enums'
|
||||
|
||||
const useActionButtonClickHandler = () => {
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
action: ActionType,
|
||||
denom: string,
|
||||
gridAction: GridActionType,
|
||||
url: string
|
||||
) => {
|
||||
return gridAction !== GridActionType.None
|
||||
? () => {
|
||||
history.push(
|
||||
`${location.pathname}/${getRoute(gridAction)}/${denom}`
|
||||
)
|
||||
}
|
||||
: action === ActionType.ExternalLink
|
||||
? () => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
: () => {
|
||||
alert(`open external link with url : ${url}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default useActionButtonClickHandler
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import createContext from './createContext'
|
||||
import { BasecampConfig } from './useBasecamp'
|
||||
import { useContract } from './useContract'
|
||||
import useStore from '../store'
|
||||
|
||||
export interface AddressProviderConfig {
|
||||
owner: string
|
||||
council_address: string
|
||||
incentives_address: string
|
||||
safety_fund_address: string
|
||||
mars_token_address: string
|
||||
oracle_address: string
|
||||
red_bank_address: string
|
||||
staking_address: string
|
||||
treasury_address: string
|
||||
xmars_token_address: string
|
||||
protocol_admin: string
|
||||
vesting_address: string
|
||||
}
|
||||
|
||||
export interface AddressProvider {
|
||||
config: AddressProviderConfig | undefined
|
||||
initialised: boolean
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
export const [useAddressProvider, AddressProviderProvider] =
|
||||
createContext<AddressProvider>('useAddressProvider')
|
||||
|
||||
export const useAddressProviderState = (
|
||||
basecampConfig: BasecampConfig | undefined
|
||||
): AddressProvider => {
|
||||
const [config, setConfig] = useState<AddressProviderConfig | undefined>()
|
||||
const [initialised, setIntialised] = useState(false)
|
||||
const [refetchRequired, setRefetchRequired] = useState(true)
|
||||
const lcd = useStore((s) => s.networkConfig?.lcd)
|
||||
const chainID = useStore((s) => s.networkConfig?.chainID)
|
||||
const { query } = useContract()
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const getConfig = async () => {
|
||||
if (!refetchRequired || !basecampConfig) return
|
||||
|
||||
const addressProviderConfig =
|
||||
await query<AddressProviderConfig>(
|
||||
basecampConfig.address_provider_address,
|
||||
{ config: {} }
|
||||
)
|
||||
|
||||
setConfig(addressProviderConfig)
|
||||
setIntialised(true)
|
||||
setRefetchRequired(false)
|
||||
}
|
||||
getConfig()
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
[lcd, chainID, basecampConfig, refetchRequired]
|
||||
)
|
||||
|
||||
const refetch = () => setRefetchRequired(true)
|
||||
|
||||
return { config, initialised, refetch }
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { Coin } from '@terra-money/terra.js'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { MARS_DENOM, UST_DECIMALS, UST_DENOM } from '../constants/appConstants'
|
||||
import { lookup, lookupDecimals } from '../libs/parse'
|
||||
import useStore from '../store'
|
||||
import { State } from '../types/enums'
|
||||
import createContext from './createContext'
|
||||
import { AccountBalance } from './useAccountBalance'
|
||||
import { ExchangeRates } from './useExchangeRate'
|
||||
import { MarketIncentiveQuery, RedBankState } from './useRedBank'
|
||||
|
||||
export interface AssetGrid {
|
||||
supplyMarketsGridData: AssetInfo[]
|
||||
borrowMarketsGridData: AssetInfo[]
|
||||
}
|
||||
|
||||
export const [useAssetGrid, AssetGridProvider] =
|
||||
createContext<AssetGrid>('useAssetGrid')
|
||||
|
||||
export const useAssetGridState = (
|
||||
accountBalanceHook: AccountBalance,
|
||||
redbankHook: RedBankState,
|
||||
exchangeHook: ExchangeRates
|
||||
): AssetGrid => {
|
||||
const [supplyMarketsGridData, setSupplyData] = useState<AssetInfo[]>([])
|
||||
const [borrowMarketsGridData, setBorrowData] = useState<AssetInfo[]>([])
|
||||
const [marsAsset, setMarsAsset] = useState<WhitelistAsset>()
|
||||
const whitelistedAssets = useStore((s) => s.whitelistedAssets)
|
||||
const name = useStore((s) => s.networkConfig?.name)
|
||||
const setIsNetworkSupported = useStore((s) => s.setIsNetworkSupported)
|
||||
|
||||
useEffect(() => {
|
||||
const getMarsAssetInfo = async () => {
|
||||
try {
|
||||
const assets = await import(`../configs/assets/${name}.ts`)
|
||||
setMarsAsset(assets.default.mars)
|
||||
} catch {
|
||||
setIsNetworkSupported(false)
|
||||
}
|
||||
}
|
||||
getMarsAssetInfo()
|
||||
}, [name, setIsNetworkSupported])
|
||||
|
||||
const calculateIncentiveAssetInfo = (
|
||||
incentive: MarketIncentiveQuery | undefined,
|
||||
marketTotalLiquidity: Coin | undefined
|
||||
): AssetInfo | undefined => {
|
||||
if (
|
||||
!incentive?.asset_incentive ||
|
||||
!marketTotalLiquidity ||
|
||||
!whitelistedAssets
|
||||
)
|
||||
return
|
||||
|
||||
const secondsInAYear = 31540000
|
||||
const anualEmission =
|
||||
Number(incentive.asset_incentive.emission_per_second) *
|
||||
secondsInAYear
|
||||
const anualEmissionUSTVaule = exchangeHook.exchangeToUusd(
|
||||
new Coin(MARS_DENOM, anualEmission)
|
||||
)
|
||||
const liquidityUSTValue =
|
||||
exchangeHook.exchangeToUusd(marketTotalLiquidity)
|
||||
const incentiveApr = anualEmissionUSTVaule / liquidityUSTValue
|
||||
|
||||
return {
|
||||
denom: MARS_DENOM,
|
||||
decimals: lookupDecimals(MARS_DENOM, whitelistedAssets),
|
||||
symbol: marsAsset?.symbol || 'MARS',
|
||||
color: marsAsset?.color || '#ea2941',
|
||||
apy: incentiveApr * 100,
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (
|
||||
exchangeHook.state !== State.READY ||
|
||||
accountBalanceHook.state !== State.READY ||
|
||||
redbankHook.state !== State.READY
|
||||
)
|
||||
return
|
||||
let supplyData: AssetInfo[] = []
|
||||
let borrowData: AssetInfo[] = []
|
||||
if (!whitelistedAssets?.length) return
|
||||
whitelistedAssets.forEach((asset) => {
|
||||
const assetWallet = accountBalanceHook.find(asset.denom)
|
||||
const uusdWallet = exchangeHook.exchangeToUusd(assetWallet)
|
||||
const supplyAssetBalance = accountBalanceHook.findDeposit(
|
||||
asset.denom
|
||||
)
|
||||
const borrowAssetBalance = accountBalanceHook.findDebt(
|
||||
asset.denom
|
||||
)
|
||||
const reserveInfo = redbankHook.findMarketInfo(asset.denom)
|
||||
const depositApy = reserveInfo?.liquidity_rate || 0
|
||||
const liquidity = redbankHook.findLiquidity(asset.denom)
|
||||
const incentive = calculateIncentiveAssetInfo(
|
||||
redbankHook.findMarketIncentiveInfo(asset.denom),
|
||||
redbankHook.findMarketTotalLiquidity(asset.denom)
|
||||
)
|
||||
|
||||
const supplyAssetBalanceUusd = lookup(
|
||||
exchangeHook.exchangeToUusd(supplyAssetBalance),
|
||||
UST_DENOM,
|
||||
UST_DECIMALS
|
||||
)
|
||||
const combinedDepositApy =
|
||||
Number(depositApy) + Number(incentive?.apy || 0) / 100
|
||||
const dlyIncome = incentive
|
||||
? (supplyAssetBalanceUusd * combinedDepositApy) / 365
|
||||
: (supplyAssetBalanceUusd * combinedDepositApy) / 365
|
||||
|
||||
const borrowApy = reserveInfo?.borrow_rate || 0
|
||||
const dlyExpense =
|
||||
(lookup(
|
||||
exchangeHook.exchangeToUusd(borrowAssetBalance),
|
||||
UST_DENOM,
|
||||
UST_DECIMALS
|
||||
) *
|
||||
borrowApy) /
|
||||
365
|
||||
supplyData.push({
|
||||
...asset,
|
||||
wallet: assetWallet?.amount.toString(),
|
||||
uusdWallet,
|
||||
balance: supplyAssetBalance?.amount.toString(),
|
||||
uusdBalance:
|
||||
exchangeHook.exchangeToUusd(supplyAssetBalance) >= 1000
|
||||
? exchangeHook.exchangeToUusd(supplyAssetBalance)
|
||||
: 0,
|
||||
apy: depositApy * 100 >= 0.01 ? depositApy * 100 : 0.0,
|
||||
incomeOrExpense: Number(dlyIncome.toFixed(2)) || 0,
|
||||
incentive,
|
||||
})
|
||||
|
||||
borrowData.push({
|
||||
...asset,
|
||||
wallet: assetWallet?.amount.toString(),
|
||||
uusdWallet,
|
||||
balance: borrowAssetBalance?.amount.toString(),
|
||||
uusdBalance:
|
||||
exchangeHook.exchangeToUusd(borrowAssetBalance) >= 1000
|
||||
? exchangeHook.exchangeToUusd(borrowAssetBalance)
|
||||
: 0,
|
||||
apy: borrowApy * 100 >= 0.01 ? borrowApy * 100 : 0.0,
|
||||
incomeOrExpense: Number(dlyExpense.toFixed(2)) || 0,
|
||||
liquidity: liquidity?.amount.toString(),
|
||||
uusdLiquidity: exchangeHook.exchangeToUusd(liquidity),
|
||||
})
|
||||
})
|
||||
|
||||
setSupplyData(supplyData)
|
||||
setBorrowData(borrowData)
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
[
|
||||
exchangeHook.state,
|
||||
exchangeHook.uusdExchangeRates,
|
||||
redbankHook.state,
|
||||
redbankHook.assetLiquidity,
|
||||
redbankHook.marketInfo,
|
||||
redbankHook.marketIncentiveInfo,
|
||||
accountBalanceHook.state,
|
||||
accountBalanceHook.coins,
|
||||
accountBalanceHook.debts,
|
||||
accountBalanceHook.deposits,
|
||||
whitelistedAssets,
|
||||
marsAsset,
|
||||
]
|
||||
)
|
||||
|
||||
return { supplyMarketsGridData, borrowMarketsGridData }
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Coin } from '@terra-money/terra.js'
|
||||
|
||||
import createContext from './createContext'
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
import { State } from '../types/enums'
|
||||
import useStore from '../store'
|
||||
|
||||
export interface AstroportSpotOracle {
|
||||
state: State
|
||||
uusdExchangeRates: Coin[] | undefined
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
interface AssetPairInfo {
|
||||
denom: string
|
||||
contract_addr: string
|
||||
}
|
||||
|
||||
interface PairQueryResponse {
|
||||
contract_addr: string
|
||||
}
|
||||
|
||||
interface Token {
|
||||
contract_addr: string
|
||||
}
|
||||
|
||||
interface NativeToken {
|
||||
denom: string
|
||||
}
|
||||
|
||||
interface AssetInfo {
|
||||
token?: Token
|
||||
native_token?: NativeToken
|
||||
}
|
||||
|
||||
interface Asset {
|
||||
info: AssetInfo
|
||||
amount: number
|
||||
}
|
||||
|
||||
interface PoolQueryResponse {
|
||||
assets: Asset[]
|
||||
total_share: number
|
||||
}
|
||||
|
||||
export const [useAstroportSpotOracle, AstroportSpotOracleProvider] =
|
||||
createContext<AstroportSpotOracle>('useAstroportSpotOracle')
|
||||
|
||||
export const useAstroportSpotOracleState = (): AstroportSpotOracle => {
|
||||
const otherAssets = useStore((s) => s.otherAssets)
|
||||
const oracleAddresses = useStore((s) => s.oracleAddresses)
|
||||
const [assetPairInfos, setAssetPairInfos] = useState<AssetPairInfo[]>()
|
||||
const [uusdExchangeRates, setuusdExchangeRates] = useState<Coin[]>()
|
||||
const [state, setState] = useState(State.INITIALISING)
|
||||
|
||||
const produceWasmPairQuery = (
|
||||
astroportFactoryAddress: string,
|
||||
wasmPairKey: string
|
||||
) => {
|
||||
let queries = ``
|
||||
if (!otherAssets || !astroportFactoryAddress || !wasmPairKey)
|
||||
return 'error'
|
||||
|
||||
otherAssets
|
||||
.filter((otherAsset: OtherAsset) => !!otherAsset.contract_addr)
|
||||
.forEach((otherAsset: OtherAsset) => {
|
||||
const denom = otherAsset.denom
|
||||
const contract_addr = otherAsset.contract_addr
|
||||
|
||||
const pairQuery = `{
|
||||
pair: {
|
||||
asset_infos: [
|
||||
{
|
||||
token: {
|
||||
contract_addr: "${contract_addr}"
|
||||
},
|
||||
},
|
||||
{
|
||||
native_token: {
|
||||
denom: "uusd",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}`
|
||||
|
||||
let querySegment = `
|
||||
${denom}: contractQuery(contractAddress: "${astroportFactoryAddress}", query: ${pairQuery})
|
||||
`
|
||||
|
||||
queries += querySegment
|
||||
})
|
||||
return `${wasmPairKey}: wasm {
|
||||
${queries}
|
||||
}`
|
||||
}
|
||||
|
||||
const astroportFactoryAddress =
|
||||
oracleAddresses?.contracts?.astroportFactoryAddress || ''
|
||||
const wasmPairKey = 'astroportPairWasmQuery'
|
||||
const wasmPairQuery = produceWasmPairQuery(
|
||||
astroportFactoryAddress,
|
||||
wasmPairKey
|
||||
)
|
||||
|
||||
const pairQuery = gql` query AstroPortPairQuery{
|
||||
${wasmPairQuery}
|
||||
}`
|
||||
|
||||
const {
|
||||
loading: pairQueryLoading,
|
||||
data: pairQueryData,
|
||||
error: pairQueryError,
|
||||
} = useQuery(pairQuery, {
|
||||
fetchPolicy: 'no-cache',
|
||||
notifyOnNetworkStatusChange: true,
|
||||
skip: !otherAssets || !astroportFactoryAddress,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const initialise = () => {
|
||||
if (pairQueryData && !pairQueryLoading && !pairQueryError) {
|
||||
const wasmQueryResults = pairQueryData[wasmPairKey]
|
||||
const infos: AssetPairInfo[] = []
|
||||
|
||||
otherAssets
|
||||
?.filter(
|
||||
(otherAsset: OtherAsset) =>
|
||||
!!otherAsset.denom && !!otherAsset.contract_addr
|
||||
)
|
||||
.forEach((otherAsset: OtherAsset) => {
|
||||
const denom = otherAsset.denom
|
||||
const pairQueryResponse: PairQueryResponse =
|
||||
wasmQueryResults[`${denom}`]
|
||||
if (!pairQueryResponse?.contract_addr) return
|
||||
infos.push({
|
||||
denom: denom,
|
||||
contract_addr: pairQueryResponse.contract_addr,
|
||||
})
|
||||
})
|
||||
setAssetPairInfos(infos)
|
||||
}
|
||||
}
|
||||
initialise()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pairQueryData])
|
||||
|
||||
useEffect(() => {
|
||||
const initialise = () => {
|
||||
if (pairQueryError && state !== State.ERROR && !pairQueryData) {
|
||||
setState(State.ERROR)
|
||||
}
|
||||
}
|
||||
initialise()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pairQueryError])
|
||||
|
||||
const produceWasmPoolQuery = (
|
||||
astroportFactoryAddress: string,
|
||||
wasmPoolKey: string
|
||||
) => {
|
||||
let queries = ``
|
||||
if (
|
||||
!otherAssets ||
|
||||
!astroportFactoryAddress ||
|
||||
!wasmPoolKey ||
|
||||
!assetPairInfos?.length
|
||||
)
|
||||
return 'error'
|
||||
|
||||
otherAssets
|
||||
.filter((otherAsset: OtherAsset) => !!otherAsset.contract_addr)
|
||||
.forEach((otherAsset: OtherAsset) => {
|
||||
const denom = otherAsset.denom
|
||||
const pair_contract_addr = assetPairInfos.find(
|
||||
(asset: AssetPairInfo) => asset.denom === denom
|
||||
)?.contract_addr
|
||||
|
||||
// Didn't find a pair contract address on astroport for this asset? can't execute the the pool query then.
|
||||
if (!pair_contract_addr) {
|
||||
return
|
||||
} else {
|
||||
const poolQuery = `{ pool: {} }`
|
||||
|
||||
let querySegment = `
|
||||
${denom}: contractQuery(contractAddress: "${pair_contract_addr}", query: ${poolQuery})
|
||||
`
|
||||
|
||||
queries += querySegment
|
||||
}
|
||||
})
|
||||
return `${wasmPoolKey}: wasm {
|
||||
${queries}
|
||||
}`
|
||||
}
|
||||
|
||||
const wasmPoolKey = 'astroportPoolWasmQuery'
|
||||
const wasmPoolQuery = produceWasmPoolQuery(
|
||||
astroportFactoryAddress,
|
||||
wasmPoolKey
|
||||
)
|
||||
|
||||
const poolQuery = gql` query AstroportPoolQuery{
|
||||
${wasmPoolQuery}
|
||||
}`
|
||||
|
||||
const {
|
||||
loading: poolQueryLoading,
|
||||
data: poolQueryData,
|
||||
error: poolQueryError,
|
||||
refetch: poolQueryRefetch,
|
||||
} = useQuery(poolQuery, {
|
||||
fetchPolicy: 'no-cache',
|
||||
notifyOnNetworkStatusChange: true,
|
||||
pollInterval: 10000,
|
||||
skip:
|
||||
!otherAssets || !astroportFactoryAddress || !assetPairInfos?.length,
|
||||
})
|
||||
|
||||
if (poolQueryError && state !== State.ERROR && !poolQueryData) {
|
||||
setState(State.ERROR)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const initialise = () => {
|
||||
if (poolQueryError && state !== State.ERROR && !poolQueryData) {
|
||||
setState(State.ERROR)
|
||||
}
|
||||
}
|
||||
initialise()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [poolQueryError])
|
||||
|
||||
useEffect(() => {
|
||||
const initialise = () => {
|
||||
if (poolQueryData && !poolQueryLoading && !poolQueryError) {
|
||||
const wasmQueryResults = poolQueryData[wasmPoolKey]
|
||||
const exchangeRates: Coin[] = []
|
||||
|
||||
otherAssets
|
||||
?.filter(
|
||||
(otherAsset: OtherAsset) =>
|
||||
!!otherAsset.denom && !!otherAsset.contract_addr
|
||||
)
|
||||
.forEach((otherAsset: OtherAsset) => {
|
||||
const denom = otherAsset.denom
|
||||
const poolQueryResponse: PoolQueryResponse =
|
||||
wasmQueryResults[`${denom}`]
|
||||
|
||||
if (
|
||||
!poolQueryResponse ||
|
||||
!poolQueryResponse.assets.length
|
||||
)
|
||||
return
|
||||
|
||||
const asset0 = poolQueryResponse.assets[0].info
|
||||
.native_token
|
||||
? poolQueryResponse.assets[0]
|
||||
: poolQueryResponse.assets[1]
|
||||
const asset1 = poolQueryResponse.assets[0].info
|
||||
.native_token
|
||||
? poolQueryResponse.assets[1]
|
||||
: poolQueryResponse.assets[0]
|
||||
|
||||
const exchangeRate = asset0.amount / asset1.amount || 0
|
||||
|
||||
exchangeRates.push(new Coin(denom, exchangeRate))
|
||||
})
|
||||
setuusdExchangeRates(exchangeRates)
|
||||
setState(State.READY)
|
||||
}
|
||||
}
|
||||
initialise()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [poolQueryData])
|
||||
|
||||
return { state, uusdExchangeRates, refetch: poolQueryRefetch }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import createContext from './createContext'
|
||||
import { useContract } from './useContract'
|
||||
import useStore from '../store'
|
||||
|
||||
export interface BasecampConfig {
|
||||
// Contract that holds all other contract addresses in the Mars design
|
||||
address_provider_address: string
|
||||
// Blocks during which a proposal is active since being submitted
|
||||
proposal_voting_period: number
|
||||
// Blocks that need to pass since a proposal succeeds in order for it to be available to be executed
|
||||
proposal_effective_delay: number
|
||||
// Blocks after the effective_delay during which a successful proposal can be activated before it expires
|
||||
proposal_expiration_period: number
|
||||
// Number of Mars needed to make a proposal. Will be returned if successful. Will be
|
||||
// distributed between stakers if proposal is not executed.
|
||||
proposal_required_deposit: number
|
||||
// % of total voting power required to participate in the proposal in order to consider it successful
|
||||
proposal_required_quorum: string // Decimal
|
||||
// % of for votes required in order to consider the proposal successful
|
||||
proposal_required_threshold: string // Decimal
|
||||
}
|
||||
|
||||
export interface Basecamp {
|
||||
config: BasecampConfig | undefined
|
||||
initialised: boolean
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
export const [useBasecamp, BasecampProvider] =
|
||||
createContext<Basecamp>('useBasecamp')
|
||||
|
||||
export const useBasecampState = (): Basecamp => {
|
||||
const [config, setConfig] = useState<BasecampConfig | undefined>()
|
||||
const [initialised, setIntialised] = useState(false)
|
||||
const [refetchRequired, setRefetchRequired] = useState(true)
|
||||
const lcd = useStore((s) => s.networkConfig?.lcd)
|
||||
const chainID = useStore((s) => s.networkConfig?.chainID)
|
||||
const basecampAddresses = useStore((s) => s.basecampAddresses)
|
||||
const { query } = useContract()
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const getConfig = async () => {
|
||||
if (!refetchRequired || !basecampAddresses) return
|
||||
|
||||
const basecampConfig = await query<BasecampConfig>(
|
||||
basecampAddresses.contracts.basecampAddress,
|
||||
{ config: {} }
|
||||
)
|
||||
|
||||
setConfig(basecampConfig)
|
||||
setIntialised(true)
|
||||
setRefetchRequired(false)
|
||||
}
|
||||
getConfig()
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
[lcd, chainID, basecampAddresses, refetchRequired]
|
||||
)
|
||||
|
||||
const refetch = () => setRefetchRequired(true)
|
||||
|
||||
return { config, initialised, refetch }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import createContext from './createContext'
|
||||
import { useContract } from './useContract'
|
||||
|
||||
export interface ContractQueryResponse {
|
||||
balance: string
|
||||
}
|
||||
|
||||
export interface CW20 {
|
||||
findSpecificAddressBalance: (
|
||||
address: string,
|
||||
cw20Address: string
|
||||
) => Promise<string>
|
||||
}
|
||||
|
||||
export const [useCW20, CW20Provider] = createContext<CW20>('useCW20')
|
||||
|
||||
export const useCW20State = (): CW20 => {
|
||||
const { query } = useContract()
|
||||
|
||||
const findSpecificAddressBalance = async (
|
||||
address: string,
|
||||
cw20Address: string
|
||||
) => {
|
||||
const result = await query<ContractQueryResponse>(cw20Address, {
|
||||
balance: { address: address },
|
||||
})
|
||||
return result?.balance || '0'
|
||||
}
|
||||
|
||||
return { findSpecificAddressBalance }
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { CreateTxOptions, LCDClient, TxAPI } from '@terra-money/terra.js'
|
||||
import { TxResult, useWallet } from '@terra-money/wallet-provider'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import useStore from '../store'
|
||||
import createContext from './createContext'
|
||||
|
||||
interface Contract {
|
||||
query: <T>(
|
||||
contractAddress: string,
|
||||
queryMsg: object,
|
||||
retries?: number,
|
||||
ignoreFailures?: boolean
|
||||
) => Promise<T | undefined>
|
||||
post: (
|
||||
options: CreateTxOptions,
|
||||
retries?: number
|
||||
) => Promise<TxResult | undefined>
|
||||
estimateFee: (
|
||||
options: CreateTxOptions,
|
||||
sourceAddress?: string,
|
||||
retries?: number,
|
||||
ignoreFailures?: boolean
|
||||
) => Promise<Fee>
|
||||
}
|
||||
|
||||
export const [useContract, ContractProvider] =
|
||||
createContext<Contract>('useContract')
|
||||
|
||||
export const useContractState = (
|
||||
lcd: string,
|
||||
chainID: string,
|
||||
gasPriceUrl: string
|
||||
): Contract => {
|
||||
const { post: postTx } = useWallet()
|
||||
const userWalletAddress = useStore((s) => s.userWalletAddress)
|
||||
// const { setQueryError } = useErrors()
|
||||
|
||||
const getLcd = () => {
|
||||
const terra = new LCDClient({
|
||||
URL: lcd,
|
||||
chainID: chainID,
|
||||
})
|
||||
|
||||
return terra
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for terra.js contract query. This handles the request with retries upon failure until the set amount (default is 3).
|
||||
* If the request continually fails it will set a failed state on the network hook.
|
||||
*
|
||||
* @param contractAddress The address of the contract we are querying
|
||||
* @param queryMsg Query msg, in object form (e.g {'token_info' : {}})
|
||||
* @param retries The max amount of retries before setting an errored state and returning null.
|
||||
* @returns The reponse from the query, or null if the query fails
|
||||
*/
|
||||
const query = async <T>(
|
||||
contractAddress: string,
|
||||
queryMsg: object,
|
||||
retries: number = 3,
|
||||
ignoreFailures: boolean = false
|
||||
) => {
|
||||
let attempts = 0
|
||||
while (attempts < retries) {
|
||||
if (!lcd || !chainID) return
|
||||
const terra = getLcd()
|
||||
try {
|
||||
const res = await terra.wasm.contractQuery<Promise<T>>(
|
||||
contractAddress,
|
||||
queryMsg
|
||||
)
|
||||
// setQueryError('contractQuery', false)
|
||||
return res
|
||||
} catch (exception: any) {
|
||||
} finally {
|
||||
attempts += 1
|
||||
}
|
||||
if (attempts === retries && !ignoreFailures) {
|
||||
// setQueryError('contractQuery', true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getGasPrice = async (): Promise<number> => {
|
||||
return (await fetch(gasPriceUrl)).json().then((json) => json['uusd'])
|
||||
}
|
||||
|
||||
const estimateFee = async (
|
||||
options: CreateTxOptions,
|
||||
sourceAddress: string = userWalletAddress,
|
||||
retries: number = 3,
|
||||
ignoreFailures: boolean = false
|
||||
): Promise<Fee> => {
|
||||
if (!sourceAddress) {
|
||||
// return default object
|
||||
return {
|
||||
gas: 0,
|
||||
gasPrice: 0,
|
||||
amount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const terra = new LCDClient({
|
||||
URL: lcd,
|
||||
chainID: chainID,
|
||||
gasAdjustment: 1.6,
|
||||
})
|
||||
const txApi = new TxAPI(terra)
|
||||
// Estimates are not accurate and do not account for things such as loops, so we add an adjustment to ensure it is enough.
|
||||
options.feeDenoms = ['uusd']
|
||||
|
||||
let attempts = 0
|
||||
|
||||
while (attempts < retries) {
|
||||
try {
|
||||
const feeEstimate = await txApi.create(
|
||||
[{ address: sourceAddress }],
|
||||
options
|
||||
)
|
||||
const gasPrice = await getGasPrice()
|
||||
const amount = new BigNumber(
|
||||
// gas_limit is the total estimated gas units used to execute this tx
|
||||
feeEstimate.auth_info.fee.gas_limit
|
||||
)
|
||||
// gas price is what we should multiple the gas limit by to get the gas amount
|
||||
// we are only paying for gas in uusd, so gasPrice here refers to gas price in uusd
|
||||
// however gasPrice could be in any native asset, and can change over time
|
||||
.multipliedBy(gasPrice)
|
||||
// It appears the network nodes validate gasPrice via amount / gas rather than looking
|
||||
// at the gasPrice supplied, because of precision issues we need to use BigNumber lib
|
||||
// to round ceil otherwise we can come in short with the gasPrice e.g.
|
||||
// - Network has gasPrice set at 0.15uusd
|
||||
// - simulate fee returns gas limit of 6219537 (after gas adjustment)
|
||||
// - amount = 6219537 * 0.15 = 932930.55uusd which will be truncated to 932930uusd
|
||||
// - network node calculates gasPrice via 932930 / 6219537 = 0.149999... = less than 0.15
|
||||
.integerValue(BigNumber.ROUND_CEIL)
|
||||
.toNumber()
|
||||
|
||||
// setQueryError('estimateFee', false)
|
||||
|
||||
return {
|
||||
gas: feeEstimate.auth_info.fee.gas_limit,
|
||||
gasPrice: gasPrice,
|
||||
amount: amount,
|
||||
}
|
||||
} catch (exception: any) {
|
||||
if (
|
||||
exception?.response?.status &&
|
||||
(exception.response.status === 400 ||
|
||||
exception.response.status === 429)
|
||||
) {
|
||||
break
|
||||
}
|
||||
} finally {
|
||||
attempts += 1
|
||||
}
|
||||
}
|
||||
if (!ignoreFailures) {
|
||||
// if we reach here we failed 3 times
|
||||
// setQueryError('estimateFee', true)
|
||||
}
|
||||
|
||||
// return default object
|
||||
return {
|
||||
gas: 0,
|
||||
gasPrice: 0,
|
||||
amount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const post = async (
|
||||
options: CreateTxOptions,
|
||||
retries = 1
|
||||
): Promise<TxResult | undefined> => {
|
||||
let attempts = 0
|
||||
while (attempts < retries) {
|
||||
try {
|
||||
return postTx(options)
|
||||
} catch (exception) {
|
||||
console.debug(exception)
|
||||
} finally {
|
||||
attempts += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { query, post, estimateFee }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
const ZERO_ERROR_LUNA = 'Error: This transaction requires a LUNA amount > 0'
|
||||
const ZERO_ERROR_UST = 'Error: This transaction requires a UST amount > 0'
|
||||
const INSUFFICIENT_FUNDS =
|
||||
'Error: You do not have enough funds in your account for this transaction'
|
||||
const INSUFFICIENT_GAS =
|
||||
'Error: You do not have enough UST to pay for this transaction'
|
||||
|
||||
const useErrorMessage = (message: string): string => {
|
||||
if (message.startsWith('insufficient funds: insufficient account funds')) {
|
||||
message = 'insufficient funds: insufficient account funds'
|
||||
}
|
||||
if (
|
||||
message.startsWith(
|
||||
'insufficient funds: insufficient funds to pay for fees'
|
||||
)
|
||||
) {
|
||||
message = 'insufficient funds: insufficient funds to pay for fees'
|
||||
}
|
||||
|
||||
switch (message) {
|
||||
case 'invalid coins: 0uluna':
|
||||
return ZERO_ERROR_LUNA
|
||||
case 'invalid coins: 0uusd':
|
||||
return ZERO_ERROR_UST
|
||||
case 'insufficient funds: insufficient account funds':
|
||||
return INSUFFICIENT_FUNDS
|
||||
case 'insufficient funds: insufficient funds to pay for fees':
|
||||
return INSUFFICIENT_GAS
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
export default useErrorMessage
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useState } from 'react'
|
||||
import createContext from './createContext'
|
||||
|
||||
interface Errors {
|
||||
/** Whether errors are present */
|
||||
errors: {
|
||||
network: boolean
|
||||
query: boolean
|
||||
server: boolean
|
||||
}
|
||||
/** List of queries that are failing */
|
||||
queryErrors: string[]
|
||||
setNetworkError: (isError: boolean) => void
|
||||
setQueryError: (name: string, isError: boolean) => void
|
||||
setServerError: (isError: boolean) => void
|
||||
}
|
||||
|
||||
export const [useErrors, ErrorsProvider] = createContext<Errors>('useErrors')
|
||||
|
||||
export const UseErrorsState = (): Errors => {
|
||||
const [errors, setErrors] = useState<Errors['errors']>({
|
||||
query: false,
|
||||
network: false,
|
||||
server: false,
|
||||
})
|
||||
const [queryErrors, setQueryErrors] = useState<string[]>([])
|
||||
|
||||
const setNetworkError = (isError: boolean) => {
|
||||
if (isError !== errors.network) {
|
||||
errors.network = isError
|
||||
setErrors({ ...errors })
|
||||
}
|
||||
}
|
||||
|
||||
const setServerError = (isError: boolean) => {
|
||||
if (isError !== errors.server) {
|
||||
errors.server = isError
|
||||
setErrors({ ...errors })
|
||||
}
|
||||
}
|
||||
|
||||
const setQueryError = (name: string, isError: boolean) => {
|
||||
let queryErrorsCopy = [...queryErrors]
|
||||
|
||||
if (isError && !queryErrorsCopy.includes(name)) {
|
||||
queryErrorsCopy.push(name)
|
||||
errors.query = true
|
||||
setErrors({ ...errors })
|
||||
setQueryErrors(queryErrorsCopy)
|
||||
} else if (!isError && queryErrorsCopy.includes(name)) {
|
||||
const idx = queryErrorsCopy.indexOf(name)
|
||||
queryErrorsCopy.splice(idx, 1)
|
||||
errors.query = !!queryErrorsCopy.length
|
||||
setErrors({ ...errors })
|
||||
setQueryErrors(queryErrorsCopy)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
queryErrors,
|
||||
setNetworkError,
|
||||
setQueryError,
|
||||
setServerError,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Coin } from '@terra-money/terra.js'
|
||||
|
||||
import createContext from './createContext'
|
||||
import { MarsOracle } from '../hooks/useMarsOracle'
|
||||
import { AstroportSpotOracle } from '../hooks/useAstroportSpotOracle'
|
||||
import { lookupDecimals } from '../libs/parse'
|
||||
import { UST_DECIMALS } from '../constants/appConstants'
|
||||
import { State } from '../types/enums'
|
||||
import { useEffect, useState } from 'react'
|
||||
import useStore from '../store'
|
||||
|
||||
export interface ExchangeRates {
|
||||
state: State
|
||||
uusdExchangeRates: Coin[] | undefined
|
||||
exchangeToUusd: (coin: Coin | undefined) => number
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
export const [useExchangeRate, ExchangeRateProvider] =
|
||||
createContext<ExchangeRates>('useExchangeRate')
|
||||
|
||||
export const useExchangeRateState = (
|
||||
marsOracle: MarsOracle,
|
||||
astroSpotOracle: AstroportSpotOracle
|
||||
): ExchangeRates => {
|
||||
const whitelistedAssets = useStore((s) => s.whitelistedAssets)
|
||||
const otherAssets = useStore((s) => s.otherAssets)
|
||||
const [uusdExchangeRates, setUusdExchangeRates] = useState<Coin[]>([])
|
||||
const [state, setState] = useState<State>(State.INITIALISING)
|
||||
|
||||
const refetch = () => {
|
||||
marsOracle.refetch()
|
||||
astroSpotOracle.refetch()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setUusdExchangeRates([
|
||||
// Spread the marsOracle Coins first. find() is used on the array, which matches the first element,
|
||||
// so marsOracle takes preference over astrSpotOracle
|
||||
...(marsOracle?.uusdExchangeRates || []),
|
||||
...(astroSpotOracle?.uusdExchangeRates || []),
|
||||
])
|
||||
}, [marsOracle.uusdExchangeRates, astroSpotOracle.uusdExchangeRates])
|
||||
|
||||
useEffect(() => {
|
||||
setState(
|
||||
marsOracle.state === State.READY &&
|
||||
astroSpotOracle.state === State.READY
|
||||
? State.READY
|
||||
: marsOracle.state === State.ERROR ||
|
||||
astroSpotOracle.state === State.ERROR
|
||||
? State.ERROR
|
||||
: State.INITIALISING
|
||||
)
|
||||
}, [marsOracle.state, astroSpotOracle.state])
|
||||
|
||||
const exchangeToUusd = (coin: Coin | undefined): number => {
|
||||
if (!coin || !uusdExchangeRates || !whitelistedAssets || !otherAssets)
|
||||
return 0
|
||||
|
||||
const exchangeRate: Coin | undefined = uusdExchangeRates.find(
|
||||
(exchangeRate) => exchangeRate.denom === coin.denom
|
||||
)
|
||||
|
||||
const allAssets = [...whitelistedAssets, ...otherAssets]
|
||||
let uusdAmount = coin.amount.toNumber()
|
||||
if (exchangeRate) {
|
||||
// First we need to convert the coin from it's minor to it's major representation to take into account dp discrepancies
|
||||
const coinMajorAmount =
|
||||
coin.amount.toNumber() /
|
||||
10 ** lookupDecimals(coin.denom, allAssets)
|
||||
// Then we convert major to UST value
|
||||
const ustAmount = coinMajorAmount * exchangeRate.amount.toNumber()
|
||||
// Once converted to UST we convert back to uusd
|
||||
uusdAmount = ustAmount * 10 ** UST_DECIMALS
|
||||
|
||||
return uusdAmount > 0.009 ? uusdAmount : 0
|
||||
}
|
||||
|
||||
return uusdAmount
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
uusdExchangeRates,
|
||||
exchangeToUusd,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,585 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Coin } from '@terra-money/terra.js'
|
||||
|
||||
import { useExchangeRate } from './useExchangeRate'
|
||||
import { contractQuery } from '../queries/contractQuery'
|
||||
import { configQuery } from '../queries/configQuery'
|
||||
import { stateQuery } from '../queries/stateQuery'
|
||||
import { debtQuery } from '../queries/debtQuery'
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
import { useAddressProvider, useRedBank } from '.'
|
||||
import { deposit } from '../queries/astroDepositQuery'
|
||||
import {
|
||||
uncollaterisedLoanLimitQuery,
|
||||
uncollaterisedNativeLoanLimitQuery,
|
||||
} from '../queries/uncollateralisedLoanLimitQuery'
|
||||
import createContext from './createContext'
|
||||
import useStore from '../store'
|
||||
import { convertAprToApy } from '../libs/parse'
|
||||
|
||||
interface QueryObject {
|
||||
standardQuery: string
|
||||
apyQuery: StrategyApyQuery | undefined
|
||||
}
|
||||
|
||||
interface AstroPoolQueryResponse {
|
||||
trading_fees: {
|
||||
apr: number
|
||||
apy: number
|
||||
}
|
||||
astro_rewards: {
|
||||
apy: number
|
||||
apr: number
|
||||
}
|
||||
protocol_rewards: {
|
||||
apy: number
|
||||
apr: number
|
||||
}
|
||||
total_rewards: {
|
||||
apy: number
|
||||
apr: number
|
||||
}
|
||||
}
|
||||
|
||||
const produceQuery = (queryType: string, userAddress: string) => {
|
||||
return `{
|
||||
${queryType}: {
|
||||
user: "${userAddress}"
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
const poolQuery = () => {
|
||||
return `{pool: { }}`
|
||||
}
|
||||
|
||||
export const [useFields, FieldsProvider] =
|
||||
createContext<FieldsState>('useFields')
|
||||
|
||||
export const useFieldsState = (): FieldsState => {
|
||||
const fieldsStrategies = useStore((s) => s.fieldsStrategies)
|
||||
const isNetworkLoaded = useStore((s) => s.isNetworkLoaded)
|
||||
const { exchangeToUusd } = useExchangeRate()
|
||||
const { findMarketInfo } = useRedBank()
|
||||
const addresses = useAddressProvider()
|
||||
const [strategies, setStrategies] = useState<StrategyObject[] | undefined>()
|
||||
const [initQuery, setInitQuery] = useState<String>('{ping}') // placeholder
|
||||
const [apyLoaded, setApyLoaded] = useState(false)
|
||||
const [queriesReady, setQueriesReady] = useState(false)
|
||||
const [apyLoadingError, setApyLoadingError] = useState(false)
|
||||
const [netWorth, setNetWorth] = useState(0)
|
||||
const userWalletAddress = useStore((s) => s.userWalletAddress)
|
||||
const CONFIG = 'config'
|
||||
const STATE = 'state'
|
||||
const POSITION = 'position'
|
||||
const HEALTH = 'health'
|
||||
const POOL = 'pool'
|
||||
const SNAPSHOT = 'snapshot'
|
||||
const LP_DEPOSIT = 'lpdeposit'
|
||||
const UNCOLLATERISEDLOANLIMIT = 'uncollaterisedLoanLimit'
|
||||
const STRATEGY_CURRENT_DEBT = 'strategyTotalDebt'
|
||||
const dailyCompoundingPeriod = 365 // dailyCompounding
|
||||
|
||||
// build the request
|
||||
const buildRequest = (): QueryObject[] => {
|
||||
if (
|
||||
!isNetworkLoaded ||
|
||||
!fieldsStrategies?.length ||
|
||||
!addresses.config?.red_bank_address
|
||||
)
|
||||
return []
|
||||
const queries: QueryObject[] = fieldsStrategies?.map(
|
||||
(strategy: FieldsStrategy) => {
|
||||
// build apy query for astroport
|
||||
const apyQuery = strategy.apyQuery
|
||||
let standardQuery =
|
||||
contractQuery(
|
||||
`${strategy.key}${CONFIG}`,
|
||||
strategy.contract_addr,
|
||||
configQuery()
|
||||
) +
|
||||
contractQuery(
|
||||
`${strategy.key}${STATE}`,
|
||||
strategy.contract_addr,
|
||||
stateQuery()
|
||||
) +
|
||||
contractQuery(
|
||||
`${strategy.key}${POSITION}`,
|
||||
strategy.contract_addr,
|
||||
produceQuery(POSITION, userWalletAddress)
|
||||
) +
|
||||
contractQuery(
|
||||
`${strategy.key}${SNAPSHOT}`,
|
||||
strategy.contract_addr,
|
||||
produceQuery(SNAPSHOT, userWalletAddress)
|
||||
) +
|
||||
contractQuery(
|
||||
`${strategy.key}${HEALTH}`,
|
||||
strategy.contract_addr,
|
||||
produceQuery(HEALTH, userWalletAddress)
|
||||
) +
|
||||
contractQuery(
|
||||
`${strategy.key}${POOL}`,
|
||||
strategy.minter,
|
||||
poolQuery()
|
||||
) +
|
||||
contractQuery(
|
||||
`${strategy.key}${LP_DEPOSIT}`,
|
||||
strategy.astroportGenerator,
|
||||
deposit(strategy.contract_addr, strategy.lpToken)
|
||||
) +
|
||||
contractQuery(
|
||||
`${strategy.key}${UNCOLLATERISEDLOANLIMIT}`,
|
||||
addresses.config?.red_bank_address!,
|
||||
strategy.borrow === 'UST' || strategy.borrow === 'LUNA'
|
||||
? uncollaterisedNativeLoanLimitQuery(
|
||||
strategy.contract_addr,
|
||||
getBorrowDenomFromStrategy(strategy)
|
||||
)
|
||||
: uncollaterisedLoanLimitQuery(
|
||||
strategy.contract_addr,
|
||||
getBorrowDenomFromStrategy(strategy)
|
||||
)
|
||||
) +
|
||||
contractQuery(
|
||||
`${strategy.key}${STRATEGY_CURRENT_DEBT}`,
|
||||
addresses.config?.red_bank_address!,
|
||||
debtQuery(strategy.contract_addr)
|
||||
)
|
||||
|
||||
const result: QueryObject = { standardQuery, apyQuery }
|
||||
return result
|
||||
}
|
||||
)
|
||||
|
||||
return queries
|
||||
}
|
||||
|
||||
const getBorrowDenomFromStrategy = (strategy: FieldsStrategy): string => {
|
||||
const borrowSymbol = strategy.borrow
|
||||
const asset = strategy.assets.find(
|
||||
(asset) => asset.symbol === borrowSymbol
|
||||
)!
|
||||
return asset.denom
|
||||
}
|
||||
|
||||
const defaultRates = {
|
||||
trading: 0,
|
||||
astro: 0,
|
||||
protocol: 0,
|
||||
total: 0,
|
||||
leverage: 0,
|
||||
}
|
||||
const queryList = buildRequest()
|
||||
|
||||
const getAprs = (poolDetails: AstroPoolQueryResponse) => {
|
||||
const tradingFeesApy = Number(poolDetails.trading_fees.apy)
|
||||
const astroRewardsApr = Number(poolDetails.astro_rewards.apr)
|
||||
const protocolRewardsApr = Number(poolDetails.protocol_rewards.apr)
|
||||
const totalRewardsApr =
|
||||
tradingFeesApy + astroRewardsApr + protocolRewardsApr
|
||||
return {
|
||||
tradingFeesApr: tradingFeesApy,
|
||||
astroRewardsApr,
|
||||
protocolRewardsApr,
|
||||
totalRewardsApr,
|
||||
}
|
||||
}
|
||||
|
||||
const produceApy = (poolDetails: AstroPoolQueryResponse): StrategyRate => {
|
||||
const { tradingFeesApr, protocolRewardsApr, astroRewardsApr } =
|
||||
getAprs(poolDetails)
|
||||
|
||||
return poolDetails !== null
|
||||
? {
|
||||
trading: poolDetails.trading_fees.apy * 100 || 0,
|
||||
astro: poolDetails.astro_rewards.apy * 100 || 0,
|
||||
protocol: poolDetails.protocol_rewards.apy * 100 || 0,
|
||||
total:
|
||||
tradingFeesApr * 100 +
|
||||
convertAprToApy(
|
||||
(protocolRewardsApr + astroRewardsApr) * 100 || 0,
|
||||
dailyCompoundingPeriod
|
||||
),
|
||||
leverage: 0,
|
||||
}
|
||||
: defaultRates
|
||||
}
|
||||
|
||||
const produceApr = (poolDetails: AstroPoolQueryResponse): StrategyRate => {
|
||||
const {
|
||||
totalRewardsApr,
|
||||
tradingFeesApr,
|
||||
astroRewardsApr,
|
||||
protocolRewardsApr,
|
||||
} = getAprs(poolDetails)
|
||||
return poolDetails !== null
|
||||
? {
|
||||
trading: tradingFeesApr * 100 || 0,
|
||||
astro: astroRewardsApr * 100 || 0,
|
||||
protocol: protocolRewardsApr * 100 || 0,
|
||||
total: totalRewardsApr * 100 || 0,
|
||||
leverage: 0,
|
||||
}
|
||||
: defaultRates
|
||||
}
|
||||
|
||||
const handleAstroGraphError = () => {
|
||||
// We wait for the apy loading attempt (which comes from astroport graph) before attempting to build our strategy details
|
||||
// If this request fails (perhaps astroport graph is down) we still want to be able to use the app
|
||||
setApyLoaded(true)
|
||||
setApyLoadingError(true)
|
||||
console.error(
|
||||
'error loading from astroport graph, continuing without apy'
|
||||
)
|
||||
}
|
||||
|
||||
const fetchApy = async (query: string) => {
|
||||
fetch('https://api.astroport.fi/graphql', {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
variables: null,
|
||||
}),
|
||||
method: 'POST',
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
// if this network request fails, we want to continue loading the application anyway so that users can
|
||||
// still edit their positions
|
||||
handleAstroGraphError()
|
||||
}
|
||||
response.json().then((data) => {
|
||||
fieldsStrategies?.map((strategy: FieldsStrategy) => {
|
||||
strategy.apy = produceApy(data.data[`${strategy.key}`])
|
||||
strategy.apr = produceApr(data.data[`${strategy.key}`])
|
||||
|
||||
return strategy
|
||||
})
|
||||
setApyLoadingError(false)
|
||||
setApyLoaded(true)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
handleAstroGraphError()
|
||||
})
|
||||
}
|
||||
|
||||
const { data, error, refetch, loading } = useQuery(
|
||||
gql`
|
||||
${initQuery}
|
||||
`,
|
||||
{
|
||||
fetchPolicy: 'no-cache',
|
||||
notifyOnNetworkStatusChange: true,
|
||||
pollInterval: 60000,
|
||||
errorPolicy: 'ignore',
|
||||
skip:
|
||||
!isNetworkLoaded || !fieldsStrategies?.length || !queriesReady,
|
||||
}
|
||||
)
|
||||
|
||||
if (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
const getStrategyObject = (
|
||||
results: any,
|
||||
strategy: FieldsStrategy,
|
||||
strategySnapshot: StrategySnapshot
|
||||
): StrategyObject => {
|
||||
const strategyObject = {
|
||||
position: results[`${strategy.key}${POSITION}`],
|
||||
health: results[`${strategy.key}${HEALTH}`],
|
||||
snapshot: results[`${strategy.key}${SNAPSHOT}`],
|
||||
pool_info: results[`${strategy.key}${POOL}`],
|
||||
minter_address: strategy.minter,
|
||||
primarySupplyRatio: undefined,
|
||||
secondarySupplyRatio: undefined,
|
||||
...strategy,
|
||||
...results[`${strategy.key}${HEALTH}`],
|
||||
...results[`${strategy.key}${CONFIG}`],
|
||||
...results[`${strategy.key}${STATE}`],
|
||||
uncollaterisedLoanLimit: Number(
|
||||
results[`${strategy.key}${UNCOLLATERISEDLOANLIMIT}`]
|
||||
),
|
||||
strategyTotalDebt: results[
|
||||
`${strategy.key}${STRATEGY_CURRENT_DEBT}`
|
||||
].debts.find(
|
||||
(asset: AssetInfo) => asset.denom === strategy.assets[1].denom
|
||||
).amount,
|
||||
}
|
||||
|
||||
const primaryIsNative = strategyObject.primary_asset_info.native
|
||||
const primaryPoolIndex = primaryIsNative
|
||||
? strategyObject.pool_info?.assets[0].info.native_token?.denom ===
|
||||
strategyObject.primary_asset_info.native
|
||||
? 0
|
||||
: 1
|
||||
: strategyObject.pool_info.assets[0].info.token?.contract_addr ===
|
||||
strategyObject.primary_asset_info.cw20
|
||||
? 0
|
||||
: 1
|
||||
const secondaryPoolIndex = primaryPoolIndex === 0 ? 1 : 0
|
||||
strategyObject.bondedShares = results[`${strategy.key}${LP_DEPOSIT}`]
|
||||
strategyObject.primaryAssetIndex = primaryPoolIndex
|
||||
strategyObject.secondaryAssetIndex = secondaryPoolIndex
|
||||
|
||||
const primarySupplyRatio =
|
||||
Number(
|
||||
strategyObject.pool_info?.assets[primaryPoolIndex].amount || 1
|
||||
) / Number(strategyObject.pool_info?.total_share || 1)
|
||||
const secondarySupplyRatio =
|
||||
Number(
|
||||
strategyObject.pool_info?.assets[secondaryPoolIndex].amount || 1
|
||||
) / Number(strategyObject.pool_info?.total_share || 1)
|
||||
|
||||
strategyObject.primarySupplyRatio = primarySupplyRatio
|
||||
strategyObject.secondarySupplyRatio = secondarySupplyRatio
|
||||
return strategyObject
|
||||
}
|
||||
|
||||
const calculateUnlock = (
|
||||
bondUnitsToUnlock: number,
|
||||
strategy: StrategyObject
|
||||
): UnlockedAssets => {
|
||||
const primaryDepth = Number(
|
||||
strategy?.pool_info?.assets[strategy.primaryAssetIndex].amount
|
||||
)
|
||||
const secondaryDepth =
|
||||
strategy?.pool_info?.assets[
|
||||
strategy.primaryAssetIndex === 0 ? 1 : 0
|
||||
].amount
|
||||
const totalShares = Number(strategy?.pool_info?.total_share)
|
||||
|
||||
const usersBondedShares = Math.floor(
|
||||
(Number(strategy?.bondedShares) * Number(bondUnitsToUnlock)) /
|
||||
Number(strategy?.total_bond_units)
|
||||
)
|
||||
|
||||
const primaryUnlocked = Math.floor(
|
||||
(primaryDepth * usersBondedShares) / totalShares
|
||||
)
|
||||
|
||||
const secondaryUnlocked: number = Math.floor(
|
||||
(Number(secondaryDepth) * usersBondedShares) / totalShares
|
||||
)
|
||||
|
||||
return {
|
||||
primaryAssetUnlocked: primaryUnlocked,
|
||||
secondaryAssetUnlocked: secondaryUnlocked,
|
||||
}
|
||||
}
|
||||
|
||||
const processData = () => {
|
||||
const results = data?.useFieldsWasm
|
||||
let netWorthValue = 0
|
||||
if (!results) return
|
||||
const newStrategies = fieldsStrategies?.map(
|
||||
(strategy: FieldsStrategy) => {
|
||||
let strategySnapshot = {
|
||||
height: '0',
|
||||
time: '0',
|
||||
position: {
|
||||
bond_units: '0',
|
||||
debt_units: '0',
|
||||
},
|
||||
health: {
|
||||
bond_value: '0',
|
||||
debt_value: '0',
|
||||
ltv: '0',
|
||||
},
|
||||
}
|
||||
// get the indexes of the assets
|
||||
const strategyObject = getStrategyObject(
|
||||
results,
|
||||
strategy,
|
||||
strategySnapshot
|
||||
)
|
||||
|
||||
// MATH SECTION
|
||||
if (strategyObject.position) {
|
||||
const position = {
|
||||
bond_units: strategyObject.position?.bond_units ?? '0',
|
||||
debt_units: strategyObject.position?.debt_units ?? '0',
|
||||
apy: 0,
|
||||
poolApr: 0,
|
||||
trueApy: 0,
|
||||
net_worth: 0,
|
||||
pnl: 0,
|
||||
leverage: 0,
|
||||
liquidation_price: 0,
|
||||
daily_return: 0,
|
||||
primarySupplyUnits: 0,
|
||||
primarySupplyRate: 0,
|
||||
secondarySupplyUnits: 0,
|
||||
secondarySupplyRate: 0,
|
||||
denom: 'uusd',
|
||||
decimals: 6,
|
||||
primaryAssetAvailable: 0,
|
||||
secondaryAssetAvailable: 0,
|
||||
}
|
||||
const health = {
|
||||
bond_value: strategyObject.health?.bond_value ?? '0',
|
||||
debt_value: strategyObject.health?.debt_value ?? '0',
|
||||
ltv: strategyObject.health?.ltv ?? '0',
|
||||
}
|
||||
|
||||
const { apy, apr } =
|
||||
!apyLoadingError && strategyObject
|
||||
? {
|
||||
apy: Number(strategyObject.apy?.total),
|
||||
apr: Number(strategyObject.apr?.total),
|
||||
}
|
||||
: { apy: 0, apr: 0 }
|
||||
|
||||
const primaryPrice =
|
||||
Number(strategyObject?.secondarySupplyRatio || 1) /
|
||||
Number(strategyObject?.primarySupplyRatio || 1)
|
||||
|
||||
const secondaryAsset = strategy.assets[1]
|
||||
const secondaryPrice =
|
||||
secondaryAsset?.denom === 'uusd'
|
||||
? 1
|
||||
: exchangeToUusd(
|
||||
new Coin(
|
||||
secondaryAsset?.denom || '',
|
||||
1 || '0'
|
||||
)
|
||||
)
|
||||
|
||||
const { primaryAssetUnlocked, secondaryAssetUnlocked } =
|
||||
calculateUnlock(
|
||||
Number(strategyObject.position.bond_units),
|
||||
strategyObject
|
||||
)
|
||||
|
||||
const primaryValue = primaryPrice * primaryAssetUnlocked
|
||||
const secondaryValue =
|
||||
secondaryAssetUnlocked * secondaryPrice
|
||||
|
||||
const totalValue = primaryValue + secondaryValue
|
||||
|
||||
// debt value it's fine to use contract value
|
||||
const debtValue = Number(strategyObject.health?.debt_value)
|
||||
const leverage =
|
||||
1 + debtValue / (totalValue - debtValue) || 1
|
||||
const netWorth = totalValue - debtValue
|
||||
const currentLtv =
|
||||
Number(strategyObject.health?.debt_value) /
|
||||
Number(totalValue)
|
||||
|
||||
// contract uses oracle, so replace contract values with our own
|
||||
health.ltv = currentLtv.toString()
|
||||
const liquidationPrice = Number(
|
||||
primaryPrice *
|
||||
Math.pow(
|
||||
currentLtv /
|
||||
Number(strategyObject?.max_ltv || 0),
|
||||
2
|
||||
) || 0
|
||||
)
|
||||
|
||||
const snapShotBondValue =
|
||||
strategyObject.snapshot?.health.bond_value || '0'
|
||||
const snapShotDebtValue =
|
||||
strategyObject.snapshot?.health.debt_value || '0'
|
||||
|
||||
// bond value of our position according to the mars oracle, not the underlying pool
|
||||
const oraclePositionBondValue = Number(
|
||||
strategyObject.health?.bond_value
|
||||
)
|
||||
|
||||
const pnl =
|
||||
oraclePositionBondValue -
|
||||
Number(health.debt_value) -
|
||||
(Number(snapShotBondValue) - Number(snapShotDebtValue))
|
||||
|
||||
const borrowApr =
|
||||
Number(findMarketInfo('uusd')?.borrow_rate) * 100
|
||||
|
||||
// temp fix for apy issues.
|
||||
const trueApy = apy * leverage
|
||||
// const trueApy = calculateStrategyRate(
|
||||
// leverage,
|
||||
// apr / 100,
|
||||
// borrowApr / 100,
|
||||
// totalValue,
|
||||
// debtValue,
|
||||
// dailyCompoundingPeriod
|
||||
// )
|
||||
|
||||
position.trueApy = trueApy
|
||||
const dailyReturn =
|
||||
(totalValue * (apr / 100) -
|
||||
debtValue * (borrowApr / 100)) /
|
||||
dailyCompoundingPeriod
|
||||
position.pnl = pnl
|
||||
position.poolApr = apr
|
||||
position.apy = apy
|
||||
// override the bond value from the contract
|
||||
health.bond_value = totalValue.toFixed(0)
|
||||
position.primaryAssetAvailable = primaryAssetUnlocked
|
||||
position.secondaryAssetAvailable = secondaryAssetUnlocked
|
||||
position.leverage =
|
||||
leverage === 1 || leverage === 2
|
||||
? leverage
|
||||
: Number(leverage)
|
||||
position.liquidation_price = liquidationPrice
|
||||
position.net_worth = netWorth
|
||||
position.denom = strategy.assets[1].denom
|
||||
position.daily_return = dailyReturn
|
||||
position.debt_units =
|
||||
strategyObject.position?.debt_units || '0'
|
||||
position.primarySupplyRate =
|
||||
strategyObject.primarySupplyRatio || 0
|
||||
position.secondarySupplyRate =
|
||||
strategyObject.secondarySupplyRatio || 0
|
||||
strategyObject.position = position
|
||||
strategyObject.health = health
|
||||
netWorthValue += netWorth || 0
|
||||
}
|
||||
|
||||
return strategyObject
|
||||
}
|
||||
)
|
||||
|
||||
setNetWorth(netWorthValue)
|
||||
return newStrategies
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (queryList.length === 0) return
|
||||
|
||||
if (!apyLoaded) {
|
||||
fetchApy(
|
||||
`query {${queryList.map(
|
||||
(queryObject: QueryObject) => queryObject.apyQuery?.query
|
||||
)}}`
|
||||
)
|
||||
}
|
||||
|
||||
setInitQuery(`query UseFieldsQuery { useFieldsWasm: wasm {
|
||||
${queryList.map(
|
||||
(queryObject: QueryObject) => queryObject.standardQuery
|
||||
)}}}`)
|
||||
|
||||
setQueriesReady(true)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queryList])
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && data && apyLoaded) setStrategies(processData())
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data, loading, apyLoaded])
|
||||
|
||||
return {
|
||||
refetch,
|
||||
netWorth,
|
||||
strategies,
|
||||
calculateUnlock,
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user