initiating public sequence

This commit is contained in:
Linkie Link
2022-04-29 23:45:14 +02:00
commit 2ecadb938f
384 changed files with 79043 additions and 0 deletions
+191
View File
@@ -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;
}
+88
View File
@@ -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
+52
View File
@@ -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;
}
}
}
}
}
+112
View File
@@ -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
+45
View File
@@ -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
+74
View File
@@ -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);
}
}
}
}
+91
View File
@@ -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
+54
View File
@@ -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;
}
}
+39
View File
@@ -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
+65
View File
@@ -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);
}
}
}
+59
View File
@@ -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
+78
View File
@@ -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);
}
}
}
+103
View File
@@ -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
+17
View File
@@ -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);
}
}
+18
View File
@@ -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
+22
View File
@@ -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;
}
}
}
+24
View File
@@ -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
+41
View File
@@ -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;
}
}
+112
View File
@@ -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;
}
}
}
+172
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
@use '../../styles/master' as *;
.container {
@include layoutTile;
}
+17
View File
@@ -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);
}
}
+266
View File
@@ -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
+17
View File
@@ -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
+47
View File
@@ -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
+232
View File
@@ -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
+191
View File
@@ -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);
}
}
+146
View File
@@ -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;
}
}
}
+150
View File
@@ -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
+50
View File
@@ -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;
}
}
}
}
}
+90
View File
@@ -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
+38
View File
@@ -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;
}
}
}
+159
View File
@@ -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
+38
View File
@@ -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;
}
}
}
+52
View File
@@ -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;
}
+24
View File
@@ -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
+86
View File
@@ -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
+125
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
{
"contracts": {
"basecampAddress": "terra1685de0sx5px80d47ec2xjln224phshysqxxeje"
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"contracts": {
"basecampAddress": "terra1jtdz9fhrrwd8yak6e3z7utmkypvx0qf0n393c6"
}
}
+127
View File
@@ -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
+239
View File
@@ -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
+19
View File
@@ -0,0 +1,19 @@
{
"contracts": {
"redBankContractAddress": "terra19dtgj9j5j7kyf3pmejqv8vzfpxtejaypgzkz5u"
},
"whitelist": {
"terra1dj9jfrrjc0yckz8l5g3mmqd86e67msvfh3q30f": {
"denom": "ANC",
"decimals": 6
},
"terra1x4rrkxx5pyuce32wsdn8ypqnpx8n27klnegv0d": {
"denom": "uluna",
"decimals": 6
},
"terra1cuku0vggplpgfxegdrenp302km26symjk4xxaf": {
"denom": "uusd",
"decimals": 6
}
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"contracts": {
"redBankContractAddress": "terra1avkm5w0gzwm92h0dlxymsdhx4l2rm7k0lxnwq7"
},
"whitelist": {
"terra1l7dzcwhz24prg3dsuzrl9s7l6cpc2l72lesacd": {
"denom": "stLuna",
"decimals": 6
},
"terra1gwl5srdgdy2r24dxpdzn7m2t7509yhnzjyc36w": {
"denom": "uluna",
"decimals": 6
},
"terra1xmx2e3eyj9d0mcycpak4fa95z0wdvykp7zesqt": {
"denom": "uusd",
"decimals": 6
}
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"lockdropAddress": "terra1n38982txtv2yygtcfv3e9wp2ktmjyxl6z88rma",
"airdropAddress": "terra106htgurux879drvjy5tzp8l7ydcnytmhpj7yr3",
"auctionAddress": "terra1hgyamk2kcy3stqx82wrnsklw9aq7rask5dxfds",
"astroportMarsUstPoolAddress": "terra19wauh79y42u5vt62c5adt2g5h4exgh26t3rpds"
}
+5
View File
@@ -0,0 +1,5 @@
{
"lockdropAddress": "terra1eqpcgsa7750r8rjqhxzcmpzvw46fn7gquywzfe",
"airdropAddress": "terra1xn9079ly7qfxf3mspf3pny3w4q4hl6agxwlnps",
"auctionAddress": "terra1myp6y5869dngaczu943pqej56r8at3as2ehqje"
}
+5
View File
@@ -0,0 +1,5 @@
{
"contracts": {
"astroportFactoryAddress": "terra1fnywlw4edny3vw44x04xd67uzkdqluymgreu7g"
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"contracts": {
"astroportFactoryAddress": "terra15jsahkaf9p0qu8ye873p0u5z6g07wdad0tdq43"
}
}
+14
View File
@@ -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]
+25
View File
@@ -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,
]
+42
View File
@@ -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
+15
View File
@@ -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',
},
}
+3
View File
@@ -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.
+16
View File
@@ -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
+26
View File
@@ -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'
+168
View File
@@ -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,
}
}
+90
View File
@@ -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 }
}
+90
View File
@@ -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 }
}
+112
View File
@@ -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 }
}
+115
View File
@@ -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 }
}
+219
View File
@@ -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,
}
}
+32
View File
@@ -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
+66
View File
@@ -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 }
}
+174
View File
@@ -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 }
}
+279
View File
@@ -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 }
}
+66
View File
@@ -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 }
}
+31
View File
@@ -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 }
}
+187
View File
@@ -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 }
}
+33
View File
@@ -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
+66
View File
@@ -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,
}
}
+89
View File
@@ -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,
}
}
+585
View File
@@ -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