Oracle update button (#606)
* feat: added a resync button to the header * fix: updated the text * fix: remove pulsing
This commit is contained in:
parent
c69dd9e172
commit
03ce931687
20
src/api/prices/getPriceData.ts
Normal file
20
src/api/prices/getPriceData.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import fetchPythPriceData from 'api/prices/getPythPriceData'
|
||||||
|
import { getPythAssets } from 'utils/assets'
|
||||||
|
|
||||||
|
export default async function getPricesData(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const assetsWithPythPriceFeedId = getPythAssets()
|
||||||
|
const pythAndOraclePriceData = await requestPythPriceData(assetsWithPythPriceFeedId)
|
||||||
|
return pythAndOraclePriceData
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex)
|
||||||
|
throw ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestPythPriceData(assets: Asset[]): Promise<string[]> {
|
||||||
|
if (!assets.length) return []
|
||||||
|
|
||||||
|
const priceFeedIds = assets.map((a) => a.pythPriceFeedId) as string[]
|
||||||
|
return await fetchPythPriceData(...priceFeedIds)
|
||||||
|
}
|
@ -2,6 +2,7 @@ import getOraclePrices from 'api/prices/getOraclePrices'
|
|||||||
import getPoolPrice from 'api/prices/getPoolPrice'
|
import getPoolPrice from 'api/prices/getPoolPrice'
|
||||||
import fetchPythPrices from 'api/prices/getPythPrices'
|
import fetchPythPrices from 'api/prices/getPythPrices'
|
||||||
import { ENV } from 'constants/env'
|
import { ENV } from 'constants/env'
|
||||||
|
import useStore from 'store'
|
||||||
import { BNCoin } from 'types/classes/BNCoin'
|
import { BNCoin } from 'types/classes/BNCoin'
|
||||||
import { NETWORK } from 'types/enums/network'
|
import { NETWORK } from 'types/enums/network'
|
||||||
import { partition } from 'utils/array'
|
import { partition } from 'utils/array'
|
||||||
@ -22,9 +23,16 @@ export default async function getPrices(): Promise<BNCoin[]> {
|
|||||||
).flat()
|
).flat()
|
||||||
const poolPrices = await requestPoolPrices(assetsWithPoolIds, pythAndOraclePrices)
|
const poolPrices = await requestPoolPrices(assetsWithPoolIds, pythAndOraclePrices)
|
||||||
|
|
||||||
|
useStore.setState({ isOracleStale: false })
|
||||||
|
|
||||||
return [...pythAndOraclePrices, ...poolPrices, usdPrice]
|
return [...pythAndOraclePrices, ...poolPrices, usdPrice]
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
console.error(ex)
|
console.error(ex)
|
||||||
|
let message = 'Unknown Error'
|
||||||
|
if (ex instanceof Error) message = ex.message
|
||||||
|
if (message.includes('price publish time is too old'))
|
||||||
|
useStore.setState({ isOracleStale: true })
|
||||||
|
|
||||||
throw ex
|
throw ex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
src/api/prices/getPythPriceData.ts
Normal file
19
src/api/prices/getPythPriceData.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { cacheFn, pythPriceCache } from 'api/cache'
|
||||||
|
import { ENV } from 'constants/env'
|
||||||
|
|
||||||
|
export default async function fetchPythPriceData(...priceFeedIds: string[]) {
|
||||||
|
try {
|
||||||
|
const pricesUrl = new URL(`${ENV.PYTH_ENDPOINT}/latest_vaas`)
|
||||||
|
priceFeedIds.forEach((id) => pricesUrl.searchParams.append('ids[]', id))
|
||||||
|
|
||||||
|
const pythDataResponse: string[] = await cacheFn(
|
||||||
|
() => fetch(pricesUrl).then((res) => res.json()),
|
||||||
|
pythPriceCache,
|
||||||
|
`pythPricData/${priceFeedIds.flat().join('-')}`,
|
||||||
|
30,
|
||||||
|
)
|
||||||
|
return pythDataResponse
|
||||||
|
} catch (ex) {
|
||||||
|
throw ex
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { isDesktop } from 'react-device-detect'
|
|||||||
|
|
||||||
import AccountMenu from 'components/Account/AccountMenu'
|
import AccountMenu from 'components/Account/AccountMenu'
|
||||||
import EscButton from 'components/Button/EscButton'
|
import EscButton from 'components/Button/EscButton'
|
||||||
|
import OracleResyncButton from 'components/Header/OracleResyncButton'
|
||||||
import DesktopNavigation from 'components/Navigation/DesktopNavigation'
|
import DesktopNavigation from 'components/Navigation/DesktopNavigation'
|
||||||
import RewardsCenter from 'components/RewardsCenter'
|
import RewardsCenter from 'components/RewardsCenter'
|
||||||
import Settings from 'components/Settings'
|
import Settings from 'components/Settings'
|
||||||
@ -22,6 +23,7 @@ export const menuTree: { pages: Page[]; label: string }[] = [
|
|||||||
export default function DesktopHeader() {
|
export default function DesktopHeader() {
|
||||||
const address = useStore((s) => s.address)
|
const address = useStore((s) => s.address)
|
||||||
const focusComponent = useStore((s) => s.focusComponent)
|
const focusComponent = useStore((s) => s.focusComponent)
|
||||||
|
const isOracleStale = useStore((s) => s.isOracleStale)
|
||||||
const accountId = useAccountId()
|
const accountId = useAccountId()
|
||||||
|
|
||||||
function handleCloseFocusMode() {
|
function handleCloseFocusMode() {
|
||||||
@ -46,6 +48,7 @@ export default function DesktopHeader() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DesktopNavigation />
|
<DesktopNavigation />
|
||||||
|
|
||||||
{focusComponent ? (
|
{focusComponent ? (
|
||||||
<div className='flex justify-between w-full'>
|
<div className='flex justify-between w-full'>
|
||||||
<div className='flex h-5 w-13' />
|
<div className='flex h-5 w-13' />
|
||||||
@ -54,6 +57,7 @@ export default function DesktopHeader() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='flex gap-4'>
|
<div className='flex gap-4'>
|
||||||
|
{isOracleStale && <OracleResyncButton />}
|
||||||
{accountId && <RewardsCenter />}
|
{accountId && <RewardsCenter />}
|
||||||
{address && <AccountMenu />}
|
{address && <AccountMenu />}
|
||||||
<Wallet />
|
<Wallet />
|
||||||
|
30
src/components/Header/OracleResyncButton.tsx
Normal file
30
src/components/Header/OracleResyncButton.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
import Button from 'components/Button'
|
||||||
|
import { ExclamationMarkCircled } from 'components/Icons'
|
||||||
|
import { Tooltip } from 'components/Tooltip'
|
||||||
|
import usePricesData from 'hooks/usePriceData'
|
||||||
|
import useStore from 'store'
|
||||||
|
|
||||||
|
export default function OracleResyncButton() {
|
||||||
|
const { data: pricesData } = usePricesData()
|
||||||
|
const updateOracle = useStore((s) => s.updateOracle)
|
||||||
|
|
||||||
|
const updatePythOracle = useCallback(() => updateOracle(pricesData), [pricesData])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
type='warning'
|
||||||
|
content='The on-chain Pyth oracle prices are too old/stale. Update them by executing a resync transaction.'
|
||||||
|
hideArrow={true}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
leftIcon={<ExclamationMarkCircled className='w-4' />}
|
||||||
|
text='Resync Oracle'
|
||||||
|
className='!text-warning !border-warning hover:bg-warning/20 active:bg-warning/20 focus:bg-warning/20'
|
||||||
|
color='secondary'
|
||||||
|
onClick={updatePythOracle}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
@ -89,7 +89,7 @@ export default function Toaster() {
|
|||||||
|
|
||||||
const handleResponse = (toast: ToastResponse, details?: boolean) => {
|
const handleResponse = (toast: ToastResponse, details?: boolean) => {
|
||||||
const isError = toast?.isError
|
const isError = toast?.isError
|
||||||
if (!isError) addTransaction(toast)
|
if (!isError && toast.accountId) addTransaction(toast)
|
||||||
const generalMessage = isError ? 'Transaction failed!' : 'Transaction completed successfully!'
|
const generalMessage = isError ? 'Transaction failed!' : 'Transaction completed successfully!'
|
||||||
const showDetailElement = !!(!details && toast.hash)
|
const showDetailElement = !!(!details && toast.hash)
|
||||||
const Msg = () => (
|
const Msg = () => (
|
||||||
|
@ -4,6 +4,7 @@ interface EnvironmentVariables {
|
|||||||
ADDRESS_INCENTIVES: string
|
ADDRESS_INCENTIVES: string
|
||||||
ADDRESS_ORACLE: string
|
ADDRESS_ORACLE: string
|
||||||
ADDRESS_PARAMS: string
|
ADDRESS_PARAMS: string
|
||||||
|
ADDRESS_PYTH: string
|
||||||
ADDRESS_RED_BANK: string
|
ADDRESS_RED_BANK: string
|
||||||
ADDRESS_SWAPPER: string
|
ADDRESS_SWAPPER: string
|
||||||
ADDRESS_ZAPPER: string
|
ADDRESS_ZAPPER: string
|
||||||
@ -26,6 +27,7 @@ export const ENV: EnvironmentVariables = {
|
|||||||
ADDRESS_INCENTIVES: process.env.NEXT_PUBLIC_INCENTIVES || '',
|
ADDRESS_INCENTIVES: process.env.NEXT_PUBLIC_INCENTIVES || '',
|
||||||
ADDRESS_ORACLE: process.env.NEXT_PUBLIC_ORACLE || '',
|
ADDRESS_ORACLE: process.env.NEXT_PUBLIC_ORACLE || '',
|
||||||
ADDRESS_PARAMS: process.env.NEXT_PUBLIC_PARAMS || '',
|
ADDRESS_PARAMS: process.env.NEXT_PUBLIC_PARAMS || '',
|
||||||
|
ADDRESS_PYTH: process.env.NEXT_PUBLIC_PYTH || '',
|
||||||
ADDRESS_RED_BANK: process.env.NEXT_PUBLIC_RED_BANK || '',
|
ADDRESS_RED_BANK: process.env.NEXT_PUBLIC_RED_BANK || '',
|
||||||
ADDRESS_SWAPPER: process.env.NEXT_PUBLIC_SWAPPER || '',
|
ADDRESS_SWAPPER: process.env.NEXT_PUBLIC_SWAPPER || '',
|
||||||
ADDRESS_ZAPPER: process.env.NEXT_PUBLIC_ZAPPER || '',
|
ADDRESS_ZAPPER: process.env.NEXT_PUBLIC_ZAPPER || '',
|
||||||
|
11
src/hooks/usePriceData.tsx
Normal file
11
src/hooks/usePriceData.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import useSWR from 'swr'
|
||||||
|
|
||||||
|
import getPricesData from 'api/prices/getPriceData'
|
||||||
|
|
||||||
|
export default function usePricesData() {
|
||||||
|
return useSWR('pricesData', getPricesData, {
|
||||||
|
fallbackData: [],
|
||||||
|
refreshInterval: 30_000,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
})
|
||||||
|
}
|
@ -14,7 +14,7 @@ import {
|
|||||||
ExecuteMsg as CreditManagerExecuteMsg,
|
ExecuteMsg as CreditManagerExecuteMsg,
|
||||||
} from 'types/generated/mars-credit-manager/MarsCreditManager.types'
|
} from 'types/generated/mars-credit-manager/MarsCreditManager.types'
|
||||||
import { AccountKind } from 'types/generated/mars-rover-health-types/MarsRoverHealthTypes.types'
|
import { AccountKind } from 'types/generated/mars-rover-health-types/MarsRoverHealthTypes.types'
|
||||||
import { getAssetByDenom, getAssetBySymbol } from 'utils/assets'
|
import { getAssetByDenom, getAssetBySymbol, getPythAssets } from 'utils/assets'
|
||||||
import { generateErrorMessage, getSingleValueFromBroadcastResult } from 'utils/broadcast'
|
import { generateErrorMessage, getSingleValueFromBroadcastResult } from 'utils/broadcast'
|
||||||
import checkAutoLendEnabled from 'utils/checkAutoLendEnabled'
|
import checkAutoLendEnabled from 'utils/checkAutoLendEnabled'
|
||||||
import { defaultFee } from 'utils/constants'
|
import { defaultFee } from 'utils/constants'
|
||||||
@ -26,7 +26,7 @@ import { getVaultDepositCoinsFromActions } from 'utils/vaults'
|
|||||||
function generateExecutionMessage(
|
function generateExecutionMessage(
|
||||||
sender: string | undefined = '',
|
sender: string | undefined = '',
|
||||||
contract: string,
|
contract: string,
|
||||||
msg: CreditManagerExecuteMsg | AccountNftExecuteMsg,
|
msg: CreditManagerExecuteMsg | AccountNftExecuteMsg | PythUpdateExecuteMsg,
|
||||||
funds: Coin[],
|
funds: Coin[],
|
||||||
) {
|
) {
|
||||||
return new MsgExecuteContract({
|
return new MsgExecuteContract({
|
||||||
@ -643,6 +643,27 @@ export default function createBroadcastSlice(
|
|||||||
|
|
||||||
return { estimateFee, execute }
|
return { estimateFee, execute }
|
||||||
},
|
},
|
||||||
|
updateOracle: async (pricesData: string[]) => {
|
||||||
|
const msg: PythUpdateExecuteMsg = { update_price_feeds: { data: pricesData } }
|
||||||
|
const pythAssets = getPythAssets()
|
||||||
|
const response = get().executeMsg({
|
||||||
|
messages: [
|
||||||
|
generateExecutionMessage(get().address, ENV.ADDRESS_PYTH, msg, [
|
||||||
|
{ denom: get().baseCurrency.denom, amount: String(pythAssets.length) },
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
get().setToast({
|
||||||
|
response,
|
||||||
|
options: {
|
||||||
|
action: 'oracle',
|
||||||
|
message: 'Oracle updated successfully!',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.then((response) => !!response.result)
|
||||||
|
},
|
||||||
setToast: (toast: ToastObject) => {
|
setToast: (toast: ToastObject) => {
|
||||||
const id = moment().unix()
|
const id = moment().unix()
|
||||||
set({
|
set({
|
||||||
|
@ -13,5 +13,6 @@ export default function createCommonSlice(set: SetState<CommonSlice>, get: GetSt
|
|||||||
migrationBanner: true,
|
migrationBanner: true,
|
||||||
tutorial: true,
|
tutorial: true,
|
||||||
useMargin: true,
|
useMargin: true,
|
||||||
|
isOracleStale: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
src/types/interfaces/pyth.d.ts
vendored
4
src/types/interfaces/pyth.d.ts
vendored
@ -10,3 +10,7 @@ interface PythConfidenceData {
|
|||||||
price: string
|
price: string
|
||||||
publish_time: number
|
publish_time: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PythUpdateExecuteMsg {
|
||||||
|
update_price_feeds: { data: string[] }
|
||||||
|
}
|
||||||
|
2
src/types/interfaces/store/broadcast.d.ts
vendored
2
src/types/interfaces/store/broadcast.d.ts
vendored
@ -70,6 +70,7 @@ interface HandleResponseProps {
|
|||||||
| 'claim'
|
| 'claim'
|
||||||
| 'unlock'
|
| 'unlock'
|
||||||
| 'swap'
|
| 'swap'
|
||||||
|
| 'oracle'
|
||||||
lend?: boolean
|
lend?: boolean
|
||||||
accountId?: string
|
accountId?: string
|
||||||
changes?: { debts?: BNCoin[]; deposits?: BNCoin[]; lends?: BNCoin[] }
|
changes?: { debts?: BNCoin[]; deposits?: BNCoin[]; lends?: BNCoin[] }
|
||||||
@ -121,6 +122,7 @@ interface BroadcastSlice {
|
|||||||
vault: DepositedVault
|
vault: DepositedVault
|
||||||
amount: string
|
amount: string
|
||||||
}) => Promise<boolean>
|
}) => Promise<boolean>
|
||||||
|
updateOracle: (pricesData: string[]) => Promise<boolean>
|
||||||
withdrawFromVaults: (options: {
|
withdrawFromVaults: (options: {
|
||||||
accountId: string
|
accountId: string
|
||||||
vaults: DepositedVault[]
|
vaults: DepositedVault[]
|
||||||
|
1
src/types/interfaces/store/common.d.ts
vendored
1
src/types/interfaces/store/common.d.ts
vendored
@ -12,6 +12,7 @@ interface CommonSlice {
|
|||||||
migrationBanner: boolean
|
migrationBanner: boolean
|
||||||
tutorial: boolean
|
tutorial: boolean
|
||||||
useMargin: boolean
|
useMargin: boolean
|
||||||
|
isOracleStale: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FocusComponent {
|
interface FocusComponent {
|
||||||
|
@ -16,6 +16,10 @@ export function getAssetsMustHavePriceInfo(): Asset[] {
|
|||||||
return ASSETS.filter((asset) => (asset.isEnabled && asset.isMarket) || asset.forceFetchPrice)
|
return ASSETS.filter((asset) => (asset.isEnabled && asset.isMarket) || asset.forceFetchPrice)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPythAssets(): Asset[] {
|
||||||
|
return ASSETS.filter((asset) => !!asset.pythPriceFeedId)
|
||||||
|
}
|
||||||
|
|
||||||
export function getBaseAsset() {
|
export function getBaseAsset() {
|
||||||
return ASSETS.find((asset) => asset.denom === 'uosmo')!
|
return ASSETS.find((asset) => asset.denom === 'uosmo')!
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user