Custom Pyth Endpoint & Price Data on Tx's (#736)

* env: remove testing library

* fix: use pyth over oracle

* fix: fix the endpoints

* fix: fix build

* tidy: refactor

* fix: fixed account fetching

* fix: made all queries chain agnostic

* fix: fixed the chart position

* feat: added basic auth

* fix: added env validation

* fix: added ‘no-cors’ and included credentials

* fix: removed ‘no-cors’ mode

* fix: change the isLessThanACent logic

* tidy: console log

* fix: added fallback api

* feat: progress on pythVaas

* fix: getting prices

* fix: try catch

* feat: add pythPriceData to transactions based on setting

* fix: disable Pyth Update when user uses Ledger

* tidy: copy
This commit is contained in:
Linkie Link 2024-01-18 09:03:32 +01:00 committed by GitHub
parent 68206ae14c
commit fd924c885e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 85 additions and 62 deletions

View File

@ -12,4 +12,5 @@ CHARTING_LIBRARY_USERNAME=git_username
CHARTING_LIBRARY_ACCESS_TOKEN=access_token
CHARTING_LIBRARY_REPOSITORY=github.com/tradingview/charting_library/
NEXT_PUBLIC_PYTH_API=https://mars.rpc.p2p.world/api

View File

@ -1,18 +0,0 @@
import fetchPythPriceData from 'api/prices/getPythPriceData'
export default async function getPricesData(assets: Asset[]): Promise<string[]> {
try {
const assetsWithPythPriceFeedId = assets.filter((asset) => !!asset.pythPriceFeedId)
return await requestPythPriceData(assetsWithPythPriceFeedId)
} 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)
}

View File

@ -1,7 +1,7 @@
import { cacheFn, pythPriceCache } from 'api/cache'
import { pythEndpoints } from 'constants/pyth'
export default async function fetchPythPriceData(priceFeedIds: string[]) {
export default async function getPythPriceData(priceFeedIds: string[]) {
try {
const pricesUrl = new URL(`${pythEndpoints.api}/latest_vaas`)
priceFeedIds.forEach((id) => pricesUrl.searchParams.append('ids[]', id))
@ -14,6 +14,7 @@ export default async function fetchPythPriceData(priceFeedIds: string[]) {
)
return pythDataResponse
} catch (ex) {
throw ex
console.log(ex)
return []
}
}

View File

@ -1,6 +1,7 @@
import { aprsCache, aprsCacheResponse, cacheFn } from 'api/cache'
export default async function getAprs(chainConfig: ChainConfig) {
if (!chainConfig.farm) return []
try {
const response = await cacheFn(
() => fetch(chainConfig.endpoints.aprs.vaults),

View File

@ -1,16 +1,16 @@
import classNames from 'classnames'
import { useCallback, useMemo, useState } from 'react'
import AssetImage from 'components/common/assets/AssetImage'
import Button from 'components/common/Button'
import { ArrowCircle, Enter } from 'components/common/Icons'
import Modal from 'components/Modals/Modal'
import SettingsOptions from 'components/Modals/Settings/SettingsOptions'
import SettingsSwitch from 'components/Modals/Settings/SettingsSwitch'
import Button from 'components/common/Button'
import { ArrowCircle, Enter } from 'components/common/Icons'
import NumberInput from 'components/common/NumberInput'
import Select from 'components/common/Select'
import Text from 'components/common/Text'
import { TextLink } from 'components/common/TextLink'
import AssetImage from 'components/common/assets/AssetImage'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import { BN_ZERO } from 'constants/math'
@ -48,6 +48,10 @@ export default function SettingsModal() {
LocalStorageKeys.SLIPPAGE,
DEFAULT_SETTINGS.slippage,
)
const [updateOracle, setUpdateOracle] = useLocalStorage<boolean>(
LocalStorageKeys.UPDATE_ORACLE,
DEFAULT_SETTINGS.updateOracle,
)
const displayCurrenciesOptions = useMemo(
() =>
@ -148,6 +152,13 @@ export default function SettingsModal() {
handleLendAssets(DEFAULT_SETTINGS.enableAutoLendGlobal)
}, [handleDisplayCurrency, handleReduceMotion, handleLendAssets, handleSlippage])
const handleUpdateOracle = useCallback(
(value: boolean) => {
setUpdateOracle(value)
},
[setUpdateOracle],
)
const showResetModal = useCallback(() => {
showResetDialog({
icon: (
@ -196,6 +207,15 @@ export default function SettingsModal() {
description='By turning this on you will automatically lend out all the assets you deposit into your Credit Accounts to earn yield.'
withStatus
/>
<SettingsSwitch
onChange={handleUpdateOracle}
name='updateOracle'
value={updateOracle}
label='Update Pyth Oracles on transactions'
description={`When this setting is on, your Mars transactions will automatically trigger an update of Pyth's price oracles. This increases the accuracy of the prices shown in the UI. In some instances, this could cause your transactions to fail when using a Ledger hardware wallet. If you encounter transaction failures with Ledger, please turn this setting off.`}
withStatus
/>
<SettingsSwitch
onChange={handleReduceMotion}
name='reduceMotion'

View File

@ -24,7 +24,6 @@ export const valueSortingFn = (a: Row<AccountBalanceRow>, b: Row<AccountBalanceR
export default function Value(props: Props) {
const { amountChange, type, value } = props
const color = getAmountChangeColor(type, amountChange)
const allowZero = !amountChange.isZero()
const coin = new BNCoin({
denom: ORACLE_DENOM,
amount: value,
@ -34,7 +33,7 @@ export default function Value(props: Props) {
<DisplayCurrency
coin={coin}
className={classNames('text-xs text-right', color)}
showZero={!allowZero}
showZero={true}
/>
)
}

View File

@ -52,10 +52,12 @@ export default function DisplayCurrency(props: Props) {
return [amount, Math.abs(amount)]
}, [assets, displayCurrency, displayCurrencyAsset.decimals, prices, props.coin])
const isLessThanACent =
(isUSD && absoluteAmount < 0.01 && absoluteAmount > 0) ||
(absoluteAmount === 0 && props.showZero)
const smallerThanPrefix = isLessThanACent ? '< ' : ''
const isLessThanACent = useMemo(
() => isUSD && absoluteAmount < 0.01 && absoluteAmount > 0,
[absoluteAmount, isUSD],
)
const smallerThanPrefix = isLessThanACent && !props.showZero ? '< ' : ''
const prefix = isUSD
? `${props.isApproximation ? '~ ' : smallerThanPrefix}$`

View File

@ -3,14 +3,11 @@ import { useCallback } from 'react'
import Button from 'components/common/Button'
import { ExclamationMarkCircled } from 'components/common/Icons'
import { Tooltip } from 'components/common/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, updateOracle])
const updatePythOracle = useCallback(() => updateOracle(), [])
return (
<Tooltip

View File

@ -21,4 +21,5 @@ export const DEFAULT_SETTINGS: Settings = {
tutorial: true,
migrationBanner: true,
perpsAsset: '',
updateOracle: true,
}

View File

@ -15,4 +15,5 @@ export enum LocalStorageKeys {
HLS_INFORMATION = 'hlsInformation',
CURRENT_CHAIN_ID = 'currentChainId',
PERPS_ASSET = 'perpsAsset',
UPDATE_ORACLE = 'updateOracle',
}

View File

@ -1,4 +1,4 @@
export const pythEndpoints = {
api: 'https://hermes.pyth.network/api',
api: process.env.NEXT_PUBLIC_PYTH_API ?? 'https://hermes.pyth.network/api',
candles: 'https://benchmarks.pyth.network/v1/shims/tradingview',
}

View File

@ -1,15 +0,0 @@
import useSWR from 'swr'
import getPricesData from 'api/prices/getPriceData'
import useChainConfig from 'hooks/useChainConfig'
import useStore from 'store'
export default function usePricesData() {
const assets = useStore((s) => s.chainConfig.assets)
const chainConfig = useChainConfig()
return useSWR(`chains/${chainConfig.id}/pricesData`, () => getPricesData(assets), {
fallbackData: [],
refreshInterval: 30_000,
revalidateOnFocus: false,
})
}

View File

@ -4,6 +4,7 @@ import moment from 'moment'
import { isMobile } from 'react-device-detect'
import { GetState, SetState } from 'zustand'
import getPythPriceData from 'api/prices/getPythPriceData'
import { BN_ZERO } from 'constants/math'
import { Store } from 'store'
import { BNCoin } from 'types/classes/BNCoin'
@ -19,6 +20,7 @@ import { AccountKind } from 'types/generated/mars-rover-health-types/MarsRoverHe
import { byDenom, bySymbol } from 'utils/array'
import { generateErrorMessage, getSingleValueFromBroadcastResult } from 'utils/broadcast'
import checkAutoLendEnabled from 'utils/checkAutoLendEnabled'
import checkPythUpdateEnabled from 'utils/checkPythUpdateEnabled'
import { defaultFee } from 'utils/constants'
import { formatAmountWithSymbol } from 'utils/formatters'
import getTokenOutFromSwapResponse from 'utils/getTokenOutFromSwapResponse'
@ -832,16 +834,10 @@ export default function createBroadcastSlice(
return { estimateFee, execute }
},
updateOracle: async (pricesData: string[]) => {
const msg: PythUpdateExecuteMsg = { update_price_feeds: { data: pricesData } }
const pythAssets = get().chainConfig.assets.filter((asset) => !!asset.pythPriceFeedId)
const pythContract = get().chainConfig.contracts.pyth
updateOracle: async () => {
const response = get().executeMsg({
messages: [
generateExecutionMessage(get().address, pythContract, msg, [
{ denom: get().chainConfig.assets[0].denom, amount: String(pythAssets.length) },
]),
],
messages: [],
isPythUpdate: true,
})
get().setToast({
@ -901,10 +897,19 @@ export default function createBroadcastSlice(
})
})
},
executeMsg: async (options: { messages: MsgExecuteContract[] }): Promise<BroadcastResult> => {
executeMsg: async (options: {
messages: MsgExecuteContract[]
isPythUpdate?: boolean
}): Promise<BroadcastResult> => {
try {
const client = get().client
if (!client) return { error: 'no client detected' }
const isLedger = client?.connectedWallet?.account.isLedger
if (!client)
return { result: undefined, error: 'No client detected. Please reconnect your wallet.' }
if ((checkPythUpdateEnabled() || options.isPythUpdate) && !isLedger) {
const pythUpdateMsg = await get().getPythVaas()
options.messages.unshift(pythUpdateMsg)
}
const fee = await getEstimatedFee(options.messages)
const broadcastOptions = {
messages: options.messages,
@ -989,5 +994,18 @@ export default function createBroadcastSlice(
return response.then((response) => !!response.result)
},
getPythVaas: async () => {
const priceFeedIds = get()
.chainConfig.assets.filter((asset) => !!asset.pythPriceFeedId)
.map((asset) => asset.pythPriceFeedId as string)
const pricesData = await getPythPriceData(priceFeedIds)
const msg: PythUpdateExecuteMsg = { update_price_feeds: { data: pricesData } }
const pythAssets = get().chainConfig.assets.filter((asset) => !!asset.pythPriceFeedId)
const pythContract = get().chainConfig.contracts.pyth
return generateExecutionMessage(get().address, pythContract, msg, [
{ denom: get().chainConfig.assets[0].denom, amount: String(pythAssets.length) },
])
},
}
}

View File

@ -118,7 +118,10 @@ interface BroadcastSlice {
kind: import('types/generated/mars-rover-health-types/MarsRoverHealthTypes.types').AccountKind
}) => Promise<boolean>
execute: (contract: string, msg: ExecuteMsg, funds: Coin[]) => Promise<BroadcastResult>
executeMsg: (options: { messages: MsgExecuteContract[] }) => Promise<BroadcastResult>
executeMsg: (options: {
messages: MsgExecuteContract[]
isPythUpdate?: boolean
}) => Promise<BroadcastResult>
lend: (options: { accountId: string; coin: BNCoin; isMax?: boolean }) => Promise<boolean>
closePerpPosition: (options: { accountId: string; denom: string }) => Promise<boolean>
openPerpPosition: (options: { accountId: string; coin: BNCoin }) => Promise<boolean>
@ -147,7 +150,8 @@ interface BroadcastSlice {
vault: DepositedVault
amount: string
}) => Promise<boolean>
updateOracle: (pricesData: string[]) => Promise<boolean>
updateOracle: () => Promise<boolean>
getPythVaas: () => Promise<import('@delphi-labs/shuttle-react').MsgExecuteContract>
withdrawFromVaults: (options: {
accountId: string
vaults: DepositedVault[]

View File

@ -9,4 +9,5 @@ interface Settings {
slippage: number
tutorial: boolean
migrationBanner: boolean
updateOracle: boolean
}

View File

@ -0,0 +1,5 @@
import { LocalStorageKeys } from 'constants/localStorageKeys'
export default function checkPythUpdateEnabled() {
return localStorage.getItem(LocalStorageKeys.UPDATE_ORACLE) === 'true'
}

View File

@ -4148,6 +4148,11 @@ balanced-match@^1.0.0:
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base-64@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a"
integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==
base-x@^3.0.2:
version "3.0.9"
resolved "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz"