[xc-admin] rework pyth hook (#490)
* Rework pyth hooks * Delete unused files * Checkpoint * xc admin/add min pub page (#492) * add min pubs page * add wallet * update connect wallet button * fix header * add ClusterSwitch * fix header css * Cleanup Co-authored-by: Daniel Chew <cctdaniel@outlook.com>
This commit is contained in:
parent
b05845ede5
commit
e55a3bbb96
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,104 @@
|
|||
import { Menu, Transition } from '@headlessui/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Fragment, useCallback, useContext, useEffect } from 'react'
|
||||
import { ClusterContext, DEFAULT_CLUSTER } from '../contexts/ClusterContext'
|
||||
import Arrow from '../images/icons/down.inline.svg'
|
||||
|
||||
const ClusterSwitch = ({ light }: { light?: boolean | null }) => {
|
||||
const router = useRouter()
|
||||
|
||||
const { cluster, setCluster } = useContext(ClusterContext)
|
||||
const handleChange = useCallback(
|
||||
(event: any) => {
|
||||
if (event.target.value) {
|
||||
router.query.cluster = event.target.value
|
||||
setCluster(event.target.value)
|
||||
router.push(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: router.query,
|
||||
},
|
||||
undefined,
|
||||
{ scroll: false }
|
||||
)
|
||||
}
|
||||
},
|
||||
[setCluster, router]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
router.query && router.query.cluster
|
||||
? setCluster(router.query.cluster)
|
||||
: setCluster(DEFAULT_CLUSTER)
|
||||
}, [setCluster, router])
|
||||
|
||||
const clusters = [
|
||||
{
|
||||
value: 'pythnet',
|
||||
name: 'pythnet',
|
||||
},
|
||||
{
|
||||
value: 'mainnet-beta',
|
||||
name: 'mainnet-beta',
|
||||
},
|
||||
{
|
||||
value: 'testnet',
|
||||
name: 'testnet',
|
||||
},
|
||||
{
|
||||
value: 'devnet',
|
||||
name: 'devnet',
|
||||
},
|
||||
// hide pythtest as its broken
|
||||
// {
|
||||
// value: 'pythtest',
|
||||
// name: 'pythtest',
|
||||
// },
|
||||
]
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative z-[2] block w-[180px] text-left">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Menu.Button
|
||||
className={`inline-flex w-full items-center justify-between py-3 px-6 text-sm outline-0 ${
|
||||
light ? 'bg-beige2' : 'bg-darkGray2'
|
||||
}`}
|
||||
>
|
||||
<span className="mr-3">{cluster}</span>
|
||||
<Arrow className={`${open && 'rotate-180'}`} />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 mt-2 w-full origin-top-right">
|
||||
{clusters.map((c) => (
|
||||
<Menu.Item key={c.name}>
|
||||
<button
|
||||
className={`block w-full py-3 px-6 text-left text-sm ${
|
||||
light
|
||||
? 'bg-beige2 hover:bg-beige3'
|
||||
: 'bg-darkGray hover:bg-darkGray2'
|
||||
} `}
|
||||
value={c.value}
|
||||
onClick={handleChange}
|
||||
>
|
||||
{c.name}
|
||||
</button>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default ClusterSwitch
|
|
@ -1,13 +0,0 @@
|
|||
function Main() {
|
||||
return (
|
||||
<div className="pt-15 relative lg:pt-20">
|
||||
<div className="container z-10 flex flex-col items-center justify-between pt-32 lg:flex-row ">
|
||||
<div className="mb-10 w-full max-w-lg text-center lg:mb-0 lg:w-1/2 lg:max-w-none lg:text-left">
|
||||
<h1 className="h1 mb-3">Governance Dashboard</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Main
|
|
@ -0,0 +1,75 @@
|
|||
import { usePythContext } from '../contexts/PythContext'
|
||||
import ClusterSwitch from './ClusterSwitch'
|
||||
import Loadbar from './loaders/Loadbar'
|
||||
|
||||
function MinPublishers() {
|
||||
const { rawConfig, dataIsLoading } = usePythContext()
|
||||
|
||||
return (
|
||||
<div className="pt-15 relative lg:pt-20">
|
||||
<div className="container flex flex-col items-center justify-between pt-32 lg:flex-row ">
|
||||
<div className="mb-10 w-full text-left lg:mb-0">
|
||||
<h1 className="h1 mb-3">Min Publishers</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container">
|
||||
<div className="mb-4 md:mb-0">
|
||||
<ClusterSwitch />
|
||||
</div>
|
||||
<div className="table-responsive relative mt-6">
|
||||
{dataIsLoading ? (
|
||||
<div className="mt-3">
|
||||
<Loadbar theme="light" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-responsive mb-10">
|
||||
<table className="w-full bg-darkGray text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="base16 pt-8 pb-6 pl-4 pr-2 font-semibold opacity-60 lg:pl-14">
|
||||
Symbol
|
||||
</th>
|
||||
<th className="base16 pt-8 pb-6 pl-1 pr-2 font-semibold opacity-60 lg:pl-14">
|
||||
Minimum Publishers
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rawConfig.mappingAccounts.length ? (
|
||||
rawConfig.mappingAccounts[0].products.map((product) =>
|
||||
product.priceAccounts.map((priceAccount) => {
|
||||
return (
|
||||
<tr
|
||||
key={product.metadata.symbol}
|
||||
className="border-t border-beige-300"
|
||||
>
|
||||
<td className="py-3 pl-4 pr-2 lg:pl-14">
|
||||
{product.metadata.symbol}
|
||||
</td>
|
||||
<td className="py-3 pl-1 lg:pl-14">
|
||||
<span className="mr-2">
|
||||
{priceAccount.minPub}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)
|
||||
) : (
|
||||
<tr className="border-t border-beige-300">
|
||||
<td className="py-3 pl-1 lg:pl-14" colSpan={2}>
|
||||
No mapping accounts found.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MinPublishers
|
|
@ -1,3 +1,4 @@
|
|||
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
|
@ -66,50 +67,37 @@ function Header() {
|
|||
<>
|
||||
<header
|
||||
className={`left-0 top-0 z-40 w-full px-1 transition-all lg:px-10
|
||||
${isSticky || headerState.opened ? 'fixed ' : 'absolute'}
|
||||
${isSticky || headerState.opened ? 'fixed' : 'absolute'}
|
||||
${isSticky && !headerState.opened ? 'bg-darkGray shadow-black' : ''}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`relative flex items-center justify-between ${
|
||||
isSticky ? 'lg:py-4' : 'before:gradient-border md:py-6'
|
||||
}
|
||||
${
|
||||
!headerState.opened
|
||||
? 'px-4 py-3 lg:px-10 lg:py-6'
|
||||
: 'sm:px-4 sm:py-3 sm:lg:px-10 sm:lg:py-6'
|
||||
}
|
||||
`}
|
||||
} px-4 py-3 lg:px-10 lg:py-6`}
|
||||
>
|
||||
<Link href="/">
|
||||
<a
|
||||
className={`basis-7 ${
|
||||
headerState.opened &&
|
||||
'fixed left-5 top-3 sm:relative sm:left-0 sm:top-0'
|
||||
}`}
|
||||
className={`flex min-h-[45px] basis-[160px] cursor-pointer items-center`}
|
||||
>
|
||||
<Pyth />
|
||||
</a>
|
||||
</Link>
|
||||
<nav>
|
||||
<ul
|
||||
className={`hidden list-none lg:flex ${
|
||||
headerState.opened && 'hidden'
|
||||
className={`list-none space-x-10 ${
|
||||
headerState.opened ? 'hidden' : 'hidden lg:flex'
|
||||
}`}
|
||||
>
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link href={item.href}>
|
||||
<a
|
||||
className={`px-6 text-sm leading-none tracking-wide transition-colors hover:text-white lg:px-6 xl:px-8 ${
|
||||
router.pathname === item.href
|
||||
? 'text-white'
|
||||
: 'text-light'
|
||||
}`}
|
||||
aria-current={
|
||||
router.pathname === item.href ? 'page' : undefined
|
||||
className={
|
||||
router.pathname == item.href
|
||||
? 'nav-link font-bold'
|
||||
: 'nav-link'
|
||||
}
|
||||
target={item.target}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
|
@ -118,34 +106,41 @@ function Header() {
|
|||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div
|
||||
className={`basis-7 ${
|
||||
headerState.opened &&
|
||||
'fixed right-5 top-[20px] sm:relative sm:left-0 sm:top-0'
|
||||
}`}
|
||||
onClick={handleToggleMenu}
|
||||
>
|
||||
<button className="group ml-auto block lg:hidden">
|
||||
<span
|
||||
className={`ml-auto block h-0.5 w-3.5 rounded-sm bg-light transition-all lg:group-hover:w-5 ${
|
||||
headerState.opened
|
||||
? 'mb-0 w-5 translate-y-1 rotate-45'
|
||||
: 'mb-1'
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
{headerState.opened ? null : (
|
||||
<WalletMultiButton className="primary-btn pt-0.5" />
|
||||
)}
|
||||
<div
|
||||
className={`relative top-0 right-5 left-0 basis-7
|
||||
`}
|
||||
onClick={handleToggleMenu}
|
||||
>
|
||||
<button
|
||||
className={`group ml-auto align-middle ${
|
||||
headerState.opened ? 'block' : 'lg:hidden'
|
||||
}`}
|
||||
></span>
|
||||
<span
|
||||
className={`mb-1 block h-0.5 w-5 rounded-sm bg-light transition-all ${
|
||||
headerState.opened && 'opacity-0'
|
||||
}`}
|
||||
></span>
|
||||
<span
|
||||
className={`ml-auto block h-0.5 w-3.5 rounded-sm bg-light transition-all lg:group-hover:w-5 ${
|
||||
headerState.opened
|
||||
? 'mb-0 w-5 -translate-y-1 -rotate-45'
|
||||
: 'mb-1'
|
||||
}`}
|
||||
></span>
|
||||
</button>
|
||||
>
|
||||
<span
|
||||
className={`ml-auto block h-0.5 w-3.5 rounded-sm bg-light transition-all lg:group-hover:w-5 ${
|
||||
headerState.opened
|
||||
? 'mb-0 w-5 translate-y-1 rotate-45'
|
||||
: 'mb-1'
|
||||
}`}
|
||||
></span>
|
||||
<span
|
||||
className={`mb-1 block h-0.5 w-5 rounded-sm bg-light transition-all ${
|
||||
headerState.opened && 'opacity-0'
|
||||
}`}
|
||||
></span>
|
||||
<span
|
||||
className={`ml-auto block h-0.5 w-3.5 rounded-sm bg-light transition-all lg:group-hover:w-5 ${
|
||||
headerState.opened
|
||||
? 'mb-0 w-5 -translate-y-1 -rotate-45'
|
||||
: 'mb-1'
|
||||
}`}
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react'
|
||||
|
||||
interface LoadbarProps {
|
||||
theme?: string
|
||||
width?: string | null
|
||||
}
|
||||
|
||||
const Loadbar: React.FC<LoadbarProps> = ({ theme, width }) => {
|
||||
let color = 'bg-dark-300'
|
||||
if (theme == 'light') {
|
||||
color = 'bg-beige-300'
|
||||
}
|
||||
return (
|
||||
<div className=" animate-pulse">
|
||||
{width ? (
|
||||
<div className="w-full">
|
||||
<div className={`h-3 ${color} w-${width} mb-2.5`}></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full">
|
||||
<div className={`h-3 ${color} mb-2.5 w-48`}></div>
|
||||
<div className={`h-3 ${color} mb-2.5 max-w-[480px]`}></div>
|
||||
<div className={`h-3 ${color} mb-2.5`}></div>
|
||||
<div className={`h-3 ${color} mb-2.5 max-w-[840px]`}></div>
|
||||
<div className={`h-3 ${color} mb-2.5 max-w-[760px]`}></div>
|
||||
<div className={`h-3 ${color} max-w-[560px]`}></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loadbar
|
|
@ -1,15 +1,14 @@
|
|||
import { PythCluster } from '@pythnetwork/client/lib/cluster'
|
||||
import { createContext, useMemo, useState } from 'react'
|
||||
import { isValidCluster } from '../utils/isValidCluster'
|
||||
|
||||
export const DEFAULT_CLUSTER: PythCluster = 'pythnet'
|
||||
export const DEFAULT_CLUSTER: PythCluster = 'mainnet-beta'
|
||||
|
||||
export const ClusterContext = createContext<{
|
||||
cluster: PythCluster
|
||||
setCluster: any
|
||||
}>({
|
||||
cluster: DEFAULT_CLUSTER,
|
||||
setCluster: (cluster: PythCluster) => {},
|
||||
setCluster: {},
|
||||
})
|
||||
|
||||
export const ClusterProvider = (props: any) => {
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import React, { createContext, useContext, useEffect, useMemo } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import React, { createContext, useContext, useMemo } from 'react'
|
||||
import usePyth from '../hooks/usePyth'
|
||||
import { PythData } from '../types'
|
||||
import { RawConfig } from '../hooks/usePyth'
|
||||
|
||||
// TODO: fix any
|
||||
interface PythContextProps {
|
||||
data: PythData
|
||||
rawConfig: RawConfig
|
||||
dataIsLoading: boolean
|
||||
error: any
|
||||
connection: any
|
||||
}
|
||||
|
||||
const PythContext = createContext<PythContextProps>({
|
||||
data: {},
|
||||
rawConfig: { mappingAccounts: [] },
|
||||
dataIsLoading: true,
|
||||
error: null,
|
||||
connection: null,
|
||||
|
@ -28,35 +27,17 @@ interface PythContextProviderProps {
|
|||
|
||||
export const PythContextProvider: React.FC<PythContextProviderProps> = ({
|
||||
children,
|
||||
symbols,
|
||||
raw,
|
||||
}) => {
|
||||
const { symbolMap, isLoading, error, connection } = usePyth(symbols)
|
||||
const [debouncedSymbolMap, { flush: flushSymbolMap }] = useDebounce(
|
||||
symbolMap,
|
||||
500,
|
||||
{ maxWait: 500 }
|
||||
)
|
||||
const data = raw ? symbolMap : debouncedSymbolMap
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(symbolMap).length == 0) flushSymbolMap()
|
||||
}, [symbolMap, flushSymbolMap])
|
||||
|
||||
// const {
|
||||
// data: historicalData,
|
||||
// loading: historicalLoading,
|
||||
// error: historicalError,
|
||||
// } = useHistoricalData()
|
||||
const { isLoading, error, connection, rawConfig } = usePyth()
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
data,
|
||||
rawConfig,
|
||||
dataIsLoading: isLoading,
|
||||
error,
|
||||
connection,
|
||||
}),
|
||||
[data, isLoading, error, connection]
|
||||
[rawConfig, isLoading, error, connection]
|
||||
)
|
||||
|
||||
return <PythContext.Provider value={value}>{children}</PythContext.Provider>
|
||||
|
|
|
@ -1,201 +1,66 @@
|
|||
import {
|
||||
AccountType,
|
||||
getPythProgramKeyForCluster,
|
||||
parseBaseData,
|
||||
parseMappingData,
|
||||
parsePermissionData,
|
||||
parsePriceData,
|
||||
parseProductData,
|
||||
PriceData,
|
||||
ProductData,
|
||||
PermissionData,
|
||||
Product,
|
||||
} from '@pythnetwork/client'
|
||||
import { AccountInfo, Commitment, Connection, PublicKey } from '@solana/web3.js'
|
||||
import { Buffer } from 'buffer'
|
||||
import { SetStateAction, useContext, useEffect, useRef, useState } from 'react'
|
||||
import { Connection, PublicKey } from '@solana/web3.js'
|
||||
import assert from 'assert'
|
||||
import { useContext, useEffect, useRef, useState } from 'react'
|
||||
import { ClusterContext } from '../contexts/ClusterContext'
|
||||
import { pythClusterApiUrls } from '../utils/pythClusterApiUrl'
|
||||
|
||||
const ONES = '11111111111111111111111111111111'
|
||||
|
||||
function chunks<T>(array: T[], size: number): T[][] {
|
||||
return Array.apply(0, new Array(Math.ceil(array.length / size))).map(
|
||||
(_, index) => array.slice(index * size, (index + 1) * size)
|
||||
)
|
||||
}
|
||||
|
||||
const getMultipleAccountsCore = async (
|
||||
connection: Connection,
|
||||
keys: string[],
|
||||
commitment: string
|
||||
) => {
|
||||
//keys are initially base58 encoded
|
||||
const pubkeyTransform = keys.map((x) => new PublicKey(x))
|
||||
const resultArray = await connection.getMultipleAccountsInfo(
|
||||
pubkeyTransform,
|
||||
commitment as Commitment
|
||||
)
|
||||
|
||||
return { keys, array: resultArray }
|
||||
}
|
||||
|
||||
const getMultipleAccounts = async (
|
||||
connection: Connection,
|
||||
keys: string[],
|
||||
commitment: string
|
||||
) => {
|
||||
const result = await Promise.all(
|
||||
chunks(keys, 99).map((chunk) =>
|
||||
getMultipleAccountsCore(connection, chunk, commitment)
|
||||
)
|
||||
)
|
||||
|
||||
const array = result
|
||||
.map(
|
||||
(a) =>
|
||||
a.array
|
||||
.map((acc) => {
|
||||
if (!acc) {
|
||||
return undefined
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
})
|
||||
.filter((_) => _) as AccountInfo<Buffer>[]
|
||||
)
|
||||
.flat()
|
||||
return { keys, array }
|
||||
}
|
||||
|
||||
export const ORACLE_PUBLIC_KEYS: { [key: string]: string } = {
|
||||
devnet: 'BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2',
|
||||
testnet: 'AFmdnt9ng1uVxqCmqwQJDAYC5cKTkw8gJKSM5PnzuF6z',
|
||||
'mainnet-beta': 'AHtgzX45WTKfkPG53L6WYhGEXwQkN1BVknET3sVsLL8J',
|
||||
pythtest: 'AFmdnt9ng1uVxqCmqwQJDAYC5cKTkw8gJKSM5PnzuF6z',
|
||||
pythnet: 'AHtgzX45WTKfkPG53L6WYhGEXwQkN1BVknET3sVsLL8J',
|
||||
}
|
||||
|
||||
export const BAD_SYMBOLS = [undefined]
|
||||
|
||||
const createSetSymbolMapUpdater =
|
||||
(
|
||||
symbol: string | number,
|
||||
product: ProductData,
|
||||
price: PriceData,
|
||||
productAccountKey: any,
|
||||
priceAccountKey: any
|
||||
) =>
|
||||
(prev: { [x: string]: { price: { [x: string]: number } } }) =>
|
||||
!prev[symbol] || prev[symbol].price['validSlot'] < price.validSlot
|
||||
? {
|
||||
...prev,
|
||||
[symbol]: {
|
||||
product,
|
||||
price,
|
||||
productAccountKey,
|
||||
priceAccountKey,
|
||||
},
|
||||
}
|
||||
: prev
|
||||
|
||||
const handlePriceInfo = (
|
||||
symbol: string,
|
||||
product: ProductData,
|
||||
accountInfo: {
|
||||
executable?: boolean
|
||||
owner?: PublicKey
|
||||
lamports?: number
|
||||
data: Buffer
|
||||
rentEpoch?: number | undefined
|
||||
},
|
||||
setSymbolMap: {
|
||||
(value: SetStateAction<{}>): void
|
||||
(value: SetStateAction<{}>): void
|
||||
(
|
||||
arg0: (prev: { [x: string]: { price: { [x: string]: number } } }) => {
|
||||
[x: string]:
|
||||
| { price: { [x: string]: number } }
|
||||
| {
|
||||
product: ProductData
|
||||
price: PriceData
|
||||
productAccountKey: number
|
||||
priceAccountKey: number
|
||||
}
|
||||
}
|
||||
): void
|
||||
},
|
||||
productAccountKey: string,
|
||||
priceAccountKey: PublicKey,
|
||||
setPriceAccounts: {
|
||||
(value: SetStateAction<{}>): void
|
||||
(value: SetStateAction<{}>): void
|
||||
(arg0: (o: any) => any): void
|
||||
}
|
||||
) => {
|
||||
if (!accountInfo || !accountInfo.data) return
|
||||
const price = parsePriceData(accountInfo.data)
|
||||
setPriceAccounts((o) => ({
|
||||
...o,
|
||||
[priceAccountKey.toString()]: {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
price,
|
||||
},
|
||||
}))
|
||||
if (price.priceType !== 1)
|
||||
console.log(symbol, price.priceType, price.nextPriceAccountKey!.toString)
|
||||
setSymbolMap(
|
||||
createSetSymbolMapUpdater(
|
||||
symbol,
|
||||
product,
|
||||
price,
|
||||
productAccountKey,
|
||||
priceAccountKey
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
interface IProductAccount {
|
||||
isLoading: boolean
|
||||
error: any // TODO: fix any
|
||||
product: any // TODO: fix any
|
||||
}
|
||||
|
||||
interface PythHookData {
|
||||
isLoading: boolean
|
||||
error: any // TODO: fix any
|
||||
version: number | null
|
||||
numProducts: number
|
||||
productAccounts: { [key: string]: IProductAccount }
|
||||
priceAccounts: any // TODO: fix any
|
||||
symbolMap: any // TODO: fix any
|
||||
rawConfig: RawConfig
|
||||
connection?: Connection
|
||||
}
|
||||
|
||||
const usePyth = (
|
||||
symbolFilter?: Array<String>,
|
||||
subscribe = true
|
||||
): PythHookData => {
|
||||
export type RawConfig = {
|
||||
mappingAccounts: MappingRawConfig[]
|
||||
permissionAccount?: PermissionData
|
||||
}
|
||||
export type MappingRawConfig = {
|
||||
address: PublicKey
|
||||
next: PublicKey | null
|
||||
products: ProductRawConfig[]
|
||||
}
|
||||
export type ProductRawConfig = {
|
||||
address: PublicKey
|
||||
priceAccounts: PriceRawConfig[]
|
||||
metadata: Product
|
||||
}
|
||||
export type PriceRawConfig = {
|
||||
next: PublicKey | null
|
||||
address: PublicKey
|
||||
expo: number
|
||||
minPub: number
|
||||
publishers: PublicKey[]
|
||||
}
|
||||
|
||||
const usePyth = (): PythHookData => {
|
||||
const connectionRef = useRef<Connection>()
|
||||
const { cluster } = useContext(ClusterContext)
|
||||
const oraclePublicKey = ORACLE_PUBLIC_KEYS[cluster]
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
const [version, setVersion] = useState<number | null>(null)
|
||||
const [rawConfig, setRawConfig] = useState<RawConfig>({ mappingAccounts: [] })
|
||||
const [urlsIndex, setUrlsIndex] = useState(0)
|
||||
const [numProducts, setNumProducts] = useState(0)
|
||||
const [productAccounts, setProductAccounts] = useState({})
|
||||
const [priceAccounts, setPriceAccounts] = useState({})
|
||||
const [symbolMap, setSymbolMap] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
setVersion(null)
|
||||
setNumProducts(0)
|
||||
setProductAccounts({})
|
||||
setPriceAccounts({})
|
||||
setSymbolMap({})
|
||||
}, [urlsIndex, oraclePublicKey, cluster])
|
||||
}, [urlsIndex, cluster])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
const subscriptionIds: number[] = []
|
||||
const urls = pythClusterApiUrls(cluster)
|
||||
const connection = new Connection(urls[urlsIndex].rpcUrl, {
|
||||
commitment: 'confirmed',
|
||||
|
@ -204,123 +69,116 @@ const usePyth = (
|
|||
|
||||
connectionRef.current = connection
|
||||
;(async () => {
|
||||
// read mapping account
|
||||
const publicKey = new PublicKey(oraclePublicKey)
|
||||
try {
|
||||
const accountInfo = await connection.getAccountInfo(publicKey)
|
||||
if (cancelled) return
|
||||
if (!accountInfo || !accountInfo.data) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
const { productAccountKeys, version, nextMappingAccount } =
|
||||
parseMappingData(accountInfo.data)
|
||||
let allProductAccountKeys = [...productAccountKeys]
|
||||
let anotherMappingAccount = nextMappingAccount
|
||||
while (anotherMappingAccount) {
|
||||
const accountInfo = await connection.getAccountInfo(
|
||||
anotherMappingAccount
|
||||
)
|
||||
if (cancelled) return
|
||||
if (!accountInfo || !accountInfo.data) {
|
||||
anotherMappingAccount = null
|
||||
} else {
|
||||
const { productAccountKeys, nextMappingAccount } = parseMappingData(
|
||||
accountInfo.data
|
||||
)
|
||||
allProductAccountKeys = [
|
||||
...allProductAccountKeys,
|
||||
...productAccountKeys,
|
||||
]
|
||||
anotherMappingAccount = nextMappingAccount
|
||||
const allPythAccounts = await connection.getProgramAccounts(
|
||||
getPythProgramKeyForCluster(cluster)
|
||||
)
|
||||
const priceRawConfigs: { [key: string]: PriceRawConfig } = {}
|
||||
|
||||
/// First pass, price accounts
|
||||
let i = 0
|
||||
while (i < allPythAccounts.length) {
|
||||
const base = parseBaseData(allPythAccounts[i].account.data)
|
||||
switch (base?.type) {
|
||||
case AccountType.Price:
|
||||
const parsed = parsePriceData(allPythAccounts[i].account.data)
|
||||
priceRawConfigs[allPythAccounts[i].pubkey.toBase58()] = {
|
||||
next: parsed.nextPriceAccountKey,
|
||||
address: allPythAccounts[i].pubkey,
|
||||
publishers: parsed.priceComponents.map((x) => {
|
||||
return x.publisher!
|
||||
}),
|
||||
expo: parsed.exponent,
|
||||
minPub: parsed.minPublishers,
|
||||
}
|
||||
allPythAccounts[i] = allPythAccounts[allPythAccounts.length - 1]
|
||||
allPythAccounts.pop()
|
||||
break
|
||||
default:
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
setIsLoading(false)
|
||||
setVersion(version)
|
||||
setNumProducts(allProductAccountKeys.length)
|
||||
setProductAccounts(
|
||||
allProductAccountKeys.reduce((o, p) => {
|
||||
// @ts-ignore
|
||||
o[p.toString()] = { isLoading: true, error: null, product: null }
|
||||
return o
|
||||
}, {})
|
||||
)
|
||||
const productsInfos = await getMultipleAccounts(
|
||||
connection,
|
||||
allProductAccountKeys.map((p) => p.toBase58()),
|
||||
'confirmed'
|
||||
)
|
||||
if (cancelled) return
|
||||
const productsData = productsInfos.array.map((p) =>
|
||||
parseProductData(p.data)
|
||||
)
|
||||
const priceInfos = await getMultipleAccounts(
|
||||
connection,
|
||||
productsData
|
||||
.filter((x) => x.priceAccountKey.toString() !== ONES)
|
||||
.map((p) => p.priceAccountKey.toBase58()),
|
||||
'confirmed'
|
||||
)
|
||||
if (cancelled) return
|
||||
|
||||
for (let i = 0; i < productsInfos.keys.length; i++) {
|
||||
const productAccountKey = productsInfos.keys[i]
|
||||
const product = productsData[i]
|
||||
const symbol = product.product.symbol
|
||||
const priceAccountKey = product.priceAccountKey
|
||||
const priceInfo = priceInfos.array[i]
|
||||
/// Second pass, product accounts
|
||||
i = 0
|
||||
const productRawConfigs: { [key: string]: ProductRawConfig } = {}
|
||||
while (i < allPythAccounts.length) {
|
||||
const base = parseBaseData(allPythAccounts[i].account.data)
|
||||
switch (base?.type) {
|
||||
case AccountType.Product:
|
||||
const parsed = parseProductData(allPythAccounts[i].account.data)
|
||||
|
||||
setProductAccounts((o) => ({
|
||||
...o,
|
||||
[productAccountKey.toString()]: {
|
||||
isLoading: false,
|
||||
error: null,
|
||||
product,
|
||||
},
|
||||
}))
|
||||
if (
|
||||
priceAccountKey.toString() !== ONES &&
|
||||
(!symbolFilter || symbolFilter.includes(symbol)) &&
|
||||
// @ts-ignore
|
||||
!BAD_SYMBOLS.includes(symbol)
|
||||
) {
|
||||
// TODO: we can add product info here and update the price later
|
||||
setPriceAccounts((o) => ({
|
||||
...o,
|
||||
[priceAccountKey.toString()]: {
|
||||
isLoading: true,
|
||||
error: null,
|
||||
price: null,
|
||||
},
|
||||
}))
|
||||
handlePriceInfo(
|
||||
symbol,
|
||||
product,
|
||||
priceInfo,
|
||||
setSymbolMap,
|
||||
productAccountKey,
|
||||
priceAccountKey,
|
||||
setPriceAccounts
|
||||
)
|
||||
if (subscribe) {
|
||||
subscriptionIds.push(
|
||||
connection.onAccountChange(priceAccountKey, (accountInfo) => {
|
||||
if (cancelled) return
|
||||
handlePriceInfo(
|
||||
symbol,
|
||||
product,
|
||||
accountInfo,
|
||||
setSymbolMap,
|
||||
productAccountKey,
|
||||
priceAccountKey,
|
||||
setPriceAccounts
|
||||
)
|
||||
})
|
||||
if (parsed.priceAccountKey.toBase58() == ONES) {
|
||||
productRawConfigs[allPythAccounts[i].pubkey.toBase58()] = {
|
||||
priceAccounts: [],
|
||||
metadata: parsed.product,
|
||||
address: allPythAccounts[i].pubkey,
|
||||
}
|
||||
} else {
|
||||
let priceAccountKey: string | null =
|
||||
parsed.priceAccountKey.toBase58()
|
||||
let priceAccounts = []
|
||||
while (priceAccountKey) {
|
||||
const toAdd: PriceRawConfig = priceRawConfigs[priceAccountKey]
|
||||
priceAccounts.push(toAdd)
|
||||
delete priceRawConfigs[priceAccountKey]
|
||||
priceAccountKey = toAdd.next ? toAdd.next.toBase58() : null
|
||||
}
|
||||
productRawConfigs[allPythAccounts[i].pubkey.toBase58()] = {
|
||||
priceAccounts,
|
||||
metadata: parsed.product,
|
||||
address: allPythAccounts[i].pubkey,
|
||||
}
|
||||
}
|
||||
allPythAccounts[i] = allPythAccounts[allPythAccounts.length - 1]
|
||||
allPythAccounts.pop()
|
||||
break
|
||||
default:
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
const rawConfig: RawConfig = { mappingAccounts: [] }
|
||||
/// Third pass, mapping accounts
|
||||
i = 0
|
||||
while (i < allPythAccounts.length) {
|
||||
const base = parseBaseData(allPythAccounts[i].account.data)
|
||||
switch (base?.type) {
|
||||
case AccountType.Mapping:
|
||||
const parsed = parseMappingData(allPythAccounts[i].account.data)
|
||||
rawConfig.mappingAccounts.push({
|
||||
next: parsed.nextMappingAccount,
|
||||
address: allPythAccounts[i].pubkey,
|
||||
products: parsed.productAccountKeys.map((key) => {
|
||||
const toAdd = productRawConfigs[key.toBase58()]
|
||||
delete productRawConfigs[key.toBase58()]
|
||||
return toAdd
|
||||
}),
|
||||
})
|
||||
allPythAccounts[i] = allPythAccounts[allPythAccounts.length - 1]
|
||||
allPythAccounts.pop()
|
||||
break
|
||||
case AccountType.Permission:
|
||||
rawConfig.permissionAccount = parsePermissionData(
|
||||
allPythAccounts[i].account.data
|
||||
)
|
||||
}
|
||||
allPythAccounts[i] = allPythAccounts[allPythAccounts.length - 1]
|
||||
allPythAccounts.pop()
|
||||
break
|
||||
default:
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
assert(
|
||||
allPythAccounts.every(
|
||||
(x) =>
|
||||
!parseBaseData(x.account.data) ||
|
||||
parseBaseData(x.account.data)?.type == AccountType.Test
|
||||
)
|
||||
)
|
||||
|
||||
setRawConfig(rawConfig)
|
||||
setIsLoading(false)
|
||||
} catch (e) {
|
||||
if (cancelled) return
|
||||
|
@ -328,36 +186,21 @@ const usePyth = (
|
|||
// @ts-ignore
|
||||
setError(e)
|
||||
setIsLoading(false)
|
||||
console.warn(
|
||||
`Failed to fetch mapping info for ${publicKey.toString()}`
|
||||
)
|
||||
console.warn(`Failed to fetch accounts`)
|
||||
} else if (urlsIndex < urls.length - 1) {
|
||||
setUrlsIndex((urlsIndex) => urlsIndex + 1)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
for (const subscriptionId of subscriptionIds) {
|
||||
connection.removeAccountChangeListener(subscriptionId).catch(() => {
|
||||
console.warn(
|
||||
`Unsuccessfully attempted to remove listener for subscription id ${subscriptionId}`
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [symbolFilter, urlsIndex, oraclePublicKey, cluster, subscribe])
|
||||
return () => {}
|
||||
}, [urlsIndex, cluster])
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
version,
|
||||
numProducts,
|
||||
productAccounts,
|
||||
priceAccounts,
|
||||
symbolMap,
|
||||
connection: connectionRef.current,
|
||||
rawConfig,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@coral-xyz/anchor": "^0.26.0",
|
||||
"@headlessui/react": "^1.7.7",
|
||||
"@pythnetwork/client": "^2.9.0",
|
||||
"@solana/wallet-adapter-base": "^0.9.20",
|
||||
"@solana/wallet-adapter-react-ui": "^0.9.27",
|
||||
"@solana/wallet-adapter-wallets": "^0.19.10",
|
||||
"@solana/web3.js": "^1.73.0",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/react": "18.0.26",
|
||||
|
@ -20,6 +24,7 @@
|
|||
"next-seo": "^5.15.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"typescript": "4.9.4",
|
||||
"use-debounce": "^9.0.2"
|
||||
},
|
||||
|
|
|
@ -1,23 +1,82 @@
|
|||
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base'
|
||||
import {
|
||||
ConnectionProvider,
|
||||
WalletProvider,
|
||||
} from '@solana/wallet-adapter-react'
|
||||
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'
|
||||
import '@solana/wallet-adapter-react-ui/styles.css'
|
||||
import {
|
||||
GlowWalletAdapter,
|
||||
LedgerWalletAdapter,
|
||||
PhantomWalletAdapter,
|
||||
SolflareWalletAdapter,
|
||||
SolletExtensionWalletAdapter,
|
||||
SolletWalletAdapter,
|
||||
TorusWalletAdapter,
|
||||
} from '@solana/wallet-adapter-wallets'
|
||||
import { clusterApiUrl } from '@solana/web3.js'
|
||||
import { DefaultSeo } from 'next-seo'
|
||||
import type { AppProps } from 'next/app'
|
||||
import Head from 'next/head'
|
||||
import { useMemo } from 'react'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { ClusterProvider } from '../contexts/ClusterContext'
|
||||
import SEO from '../next-seo.config'
|
||||
import '../styles/globals.css'
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
// Can be set to 'devnet', 'testnet', or 'mainnet-beta'
|
||||
// const network = WalletAdapterNetwork.Devnet
|
||||
|
||||
// You can also provide a custom RPC endpoint
|
||||
// const endpoint = useMemo(() => clusterApiUrl(network), [network])
|
||||
|
||||
const endpoint = process.env.ENDPOINT
|
||||
// @solana/wallet-adapter-wallets includes all the adapters but supports tree shaking and lazy loading --
|
||||
// Only the wallets you configure here will be compiled into your application, and only the dependencies
|
||||
// of wallets that your users connect to will be loaded
|
||||
const wallets = useMemo(
|
||||
() => [
|
||||
new PhantomWalletAdapter(),
|
||||
new GlowWalletAdapter(),
|
||||
new SolflareWalletAdapter(),
|
||||
new TorusWalletAdapter(),
|
||||
new LedgerWalletAdapter(),
|
||||
new SolletWalletAdapter(),
|
||||
new SolletExtensionWalletAdapter(),
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ClusterProvider>
|
||||
<Head>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
|
||||
/>
|
||||
</Head>
|
||||
<DefaultSeo {...SEO} />
|
||||
<Component {...pageProps} />
|
||||
</ClusterProvider>
|
||||
<ConnectionProvider
|
||||
endpoint={endpoint || clusterApiUrl(WalletAdapterNetwork.Devnet)}
|
||||
>
|
||||
<WalletProvider wallets={wallets} autoConnect>
|
||||
<WalletModalProvider>
|
||||
<ClusterProvider>
|
||||
<Head>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
|
||||
/>
|
||||
</Head>
|
||||
<DefaultSeo {...SEO} />
|
||||
<Component {...pageProps} />
|
||||
<Toaster
|
||||
position="bottom-left"
|
||||
toastOptions={{
|
||||
style: {
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
}}
|
||||
reverseOrder={false}
|
||||
/>
|
||||
</ClusterProvider>
|
||||
</WalletModalProvider>
|
||||
</WalletProvider>
|
||||
</ConnectionProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ export default function Document() {
|
|||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#242235" />
|
||||
<meta name="msapplication-TileColor" content="#242235" />
|
||||
<meta name="theme-color" content="#242235"></meta>
|
||||
|
|
|
@ -1,21 +1,13 @@
|
|||
import type { NextPage } from 'next'
|
||||
import { useContext, useEffect } from 'react'
|
||||
import Layout from '../components/layout/Layout'
|
||||
import Main from '../components/Main'
|
||||
import MinPublishers from '../components/MinPublishers'
|
||||
import { PythContextProvider } from '../contexts/PythContext'
|
||||
import { ClusterContext, DEFAULT_CLUSTER } from './../contexts/ClusterContext'
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const { setCluster } = useContext(ClusterContext)
|
||||
|
||||
useEffect(() => {
|
||||
setCluster(DEFAULT_CLUSTER)
|
||||
}, [setCluster])
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<PythContextProvider>
|
||||
<Main />
|
||||
<MinPublishers />
|
||||
</PythContextProvider>
|
||||
</Layout>
|
||||
)
|
||||
|
|
|
@ -1,278 +0,0 @@
|
|||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: inherit;
|
||||
justify-content: inherit;
|
||||
align-items: inherit;
|
||||
font-size: 0.85rem;
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.description a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.description p {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
background-color: rgba(var(--callout-rgb), 0.5);
|
||||
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.code {
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(25%, auto));
|
||||
width: var(--max-width);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
background: rgba(var(--card-rgb), 0);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0);
|
||||
transition: background 200ms, border 200ms;
|
||||
}
|
||||
|
||||
.card span {
|
||||
display: inline-block;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
opacity: 0.6;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
max-width: 30ch;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
background: var(--secondary-glow);
|
||||
border-radius: 50%;
|
||||
width: 480px;
|
||||
height: 360px;
|
||||
margin-left: -400px;
|
||||
}
|
||||
|
||||
.center::after {
|
||||
background: var(--primary-glow);
|
||||
width: 240px;
|
||||
height: 180px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.center::before,
|
||||
.center::after {
|
||||
content: '';
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
filter: blur(45px);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.logo,
|
||||
.thirteen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.thirteen {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
padding: 25px 10px;
|
||||
margin-left: 16px;
|
||||
transform: translateZ(0);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 2px 8px -1px #0000001a;
|
||||
}
|
||||
|
||||
.thirteen::before,
|
||||
.thirteen::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Conic Gradient Animation */
|
||||
.thirteen::before {
|
||||
animation: 6s rotate linear infinite;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: var(--tile-border);
|
||||
}
|
||||
|
||||
/* Inner Square */
|
||||
.thirteen::after {
|
||||
inset: 0;
|
||||
padding: 1px;
|
||||
border-radius: var(--border-radius);
|
||||
background: linear-gradient(
|
||||
to bottom right,
|
||||
rgba(var(--tile-start-rgb), 1),
|
||||
rgba(var(--tile-end-rgb), 1)
|
||||
);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.card:hover {
|
||||
background: rgba(var(--card-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0.15);
|
||||
}
|
||||
|
||||
.card:hover span {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.thirteen::before {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.card:hover span {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 700px) {
|
||||
.content {
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
margin-bottom: 120px;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 2.5rem;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.center {
|
||||
padding: 8rem 0 6rem;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
transform: none;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.description a {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.description p,
|
||||
.description div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description p {
|
||||
align-items: center;
|
||||
inset: 0 0 auto;
|
||||
padding: 2rem 1rem 1.4rem;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--background-start-rgb), 1),
|
||||
rgba(var(--callout-rgb), 0.5)
|
||||
);
|
||||
background-clip: padding-box;
|
||||
backdrop-filter: blur(24px);
|
||||
}
|
||||
|
||||
.description div {
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
inset: auto 0 0;
|
||||
padding: 2rem;
|
||||
height: 200px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
rgb(var(--background-end-rgb)) 40%
|
||||
);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet and Smaller Desktop */
|
||||
@media (min-width: 701px) and (max-width: 1120px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.vercelLogo {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.logo,
|
||||
.thirteen img {
|
||||
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
|
@ -223,3 +223,48 @@
|
|||
background: rgba(241, 234, 234, 0.6);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
@apply text-sm text-light hover:text-white;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
@apply h-[45px] whitespace-nowrap rounded-full border border-solid border-pythPurple bg-pythPurple px-8 font-mono text-xs uppercase;
|
||||
}
|
||||
|
||||
.input-no-spin::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.wallet-adapter-dropdown-list {
|
||||
@apply gap-1 border border-darkGray3 bg-darkGray2 text-xs;
|
||||
}
|
||||
|
||||
.wallet-adapter-modal-wrapper {
|
||||
@apply max-w-[660px] rounded-3xl bg-darkGray2;
|
||||
}
|
||||
|
||||
.wallet-adapter-modal-title {
|
||||
@apply max-w-[480px] font-body text-[32px] leading-[1.1] text-light md:text-[44px];
|
||||
}
|
||||
|
||||
.wallet-adapter-modal-list {
|
||||
@apply mx-auto max-w-[380px] text-light;
|
||||
}
|
||||
|
||||
.wallet-adapter-modal-list-more {
|
||||
@apply mx-auto font-mono text-xs font-semibold uppercase;
|
||||
}
|
||||
.wallet-adapter-dropdown-list .wallet-adapter-dropdown-list-item:hover,
|
||||
.wallet-adapter-modal-list .wallet-adapter-button:hover {
|
||||
background-color: #413e53;
|
||||
}
|
||||
|
||||
.wallet-adapter-modal-button-close {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
.wallet-adapter-button.secondary-btn:hover {
|
||||
@apply hover:bg-pythPurple;
|
||||
}
|
||||
|
|
|
@ -45,8 +45,13 @@ module.exports = {
|
|||
hoverable: { raw: '(hover: hover)' },
|
||||
},
|
||||
fontFamily: {
|
||||
body: ["'Urbanist'", 'sans-serif'],
|
||||
mono: ["'IBM Plex Mono'", 'monospace'],
|
||||
arboria: 'arboria, sans-serif',
|
||||
roboto: 'roboto, sans-serif',
|
||||
robotoMono: 'roboto-mono, monospace',
|
||||
inter: 'inter, sans-serif',
|
||||
poppins: 'poppins, sans-serif',
|
||||
body: 'Urbanist, sans-serif',
|
||||
mono: 'IBM Plex Mono, monospace',
|
||||
},
|
||||
extend: {
|
||||
fontSize: {
|
||||
|
@ -69,6 +74,7 @@ module.exports = {
|
|||
darkGray2: '#312F47',
|
||||
darkGray3: '#2F2C4F',
|
||||
darkGray4: '#413E53',
|
||||
hoverGray: 'rgba(255, 255, 255, 0.08)',
|
||||
beige: '#F1EAEA',
|
||||
'beige-300': 'rgba(229, 231, 235, .3)',
|
||||
beige2: '#E4DADB',
|
||||
|
@ -76,6 +82,7 @@ module.exports = {
|
|||
green: '#15AE6E',
|
||||
lightPurple: '#7731EA',
|
||||
offPurple: '#745E9D',
|
||||
pythPurple: '#7142CF',
|
||||
},
|
||||
letterSpacing: {
|
||||
wide: '.02em',
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
import { PriceComponent, PriceType, Product } from '@pythnetwork/client'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { PriceStatus } from '../utils/PriceStatus'
|
||||
|
||||
export interface PythPriceData {
|
||||
aggregate?: {
|
||||
status: PriceStatus
|
||||
price: number
|
||||
confidence: number
|
||||
publishSlot: number
|
||||
priceComponent: number
|
||||
confidenceComponent: number
|
||||
}
|
||||
emaPrice: {
|
||||
value: number
|
||||
valueComponent: number
|
||||
}
|
||||
emaConfidence: {
|
||||
value: number
|
||||
valueComponent: number
|
||||
}
|
||||
validSlot: number
|
||||
minPublishers: number
|
||||
priceComponents: PriceComponent[]
|
||||
priceType: PriceType
|
||||
exponent: number
|
||||
numComponentPrices: number
|
||||
numQuoters: number
|
||||
lastSlot: number
|
||||
previousTimestamp: BigInt
|
||||
timestamp: BigInt
|
||||
}
|
||||
|
||||
export interface PythSymbolData {
|
||||
productAccountKey: PublicKey
|
||||
priceAccountKey: PublicKey
|
||||
price: PythPriceData
|
||||
product: {
|
||||
product: Product
|
||||
}
|
||||
}
|
||||
|
||||
export interface PythData {
|
||||
[key: string]: PythSymbolData
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export enum PriceStatus {
|
||||
Offline = 0,
|
||||
Online = 1,
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { PythCluster } from '../contexts/ClusterContext'
|
||||
|
||||
export const isValidCluster = (str: string): str is PythCluster =>
|
||||
['devnet', 'testnet', 'mainnet-beta', 'pythtest', 'pythnet'].indexOf(str) !==
|
||||
-1
|
Loading…
Reference in New Issue