[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 Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useContext, useEffect, useState } from 'react'
|
import { useContext, useEffect, useState } from 'react'
|
||||||
|
@ -73,43 +74,30 @@ function Header() {
|
||||||
<div
|
<div
|
||||||
className={`relative flex items-center justify-between ${
|
className={`relative flex items-center justify-between ${
|
||||||
isSticky ? 'lg:py-4' : 'before:gradient-border md:py-6'
|
isSticky ? 'lg:py-4' : 'before:gradient-border md:py-6'
|
||||||
}
|
} px-4 py-3 lg:px-10 lg: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'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a
|
<a
|
||||||
className={`basis-7 ${
|
className={`flex min-h-[45px] basis-[160px] cursor-pointer items-center`}
|
||||||
headerState.opened &&
|
|
||||||
'fixed left-5 top-3 sm:relative sm:left-0 sm:top-0'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Pyth />
|
<Pyth />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<nav>
|
<nav>
|
||||||
<ul
|
<ul
|
||||||
className={`hidden list-none lg:flex ${
|
className={`list-none space-x-10 ${
|
||||||
headerState.opened && 'hidden'
|
headerState.opened ? 'hidden' : 'hidden lg:flex'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{navigation.map((item) => (
|
{navigation.map((item) => (
|
||||||
<li key={item.name}>
|
<li key={item.name}>
|
||||||
<Link href={item.href}>
|
<Link href={item.href}>
|
||||||
<a
|
<a
|
||||||
className={`px-6 text-sm leading-none tracking-wide transition-colors hover:text-white lg:px-6 xl:px-8 ${
|
className={
|
||||||
router.pathname === item.href
|
router.pathname == item.href
|
||||||
? 'text-white'
|
? 'nav-link font-bold'
|
||||||
: 'text-light'
|
: 'nav-link'
|
||||||
}`}
|
|
||||||
aria-current={
|
|
||||||
router.pathname === item.href ? 'page' : undefined
|
|
||||||
}
|
}
|
||||||
target={item.target}
|
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</a>
|
</a>
|
||||||
|
@ -118,14 +106,20 @@ function Header() {
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div className="flex items-center justify-end space-x-2">
|
||||||
|
{headerState.opened ? null : (
|
||||||
|
<WalletMultiButton className="primary-btn pt-0.5" />
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={`basis-7 ${
|
className={`relative top-0 right-5 left-0 basis-7
|
||||||
headerState.opened &&
|
`}
|
||||||
'fixed right-5 top-[20px] sm:relative sm:left-0 sm:top-0'
|
|
||||||
}`}
|
|
||||||
onClick={handleToggleMenu}
|
onClick={handleToggleMenu}
|
||||||
>
|
>
|
||||||
<button className="group ml-auto block lg:hidden">
|
<button
|
||||||
|
className={`group ml-auto align-middle ${
|
||||||
|
headerState.opened ? 'block' : 'lg:hidden'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
className={`ml-auto block h-0.5 w-3.5 rounded-sm bg-light transition-all lg:group-hover:w-5 ${
|
className={`ml-auto block h-0.5 w-3.5 rounded-sm bg-light transition-all lg:group-hover:w-5 ${
|
||||||
headerState.opened
|
headerState.opened
|
||||||
|
@ -148,6 +142,7 @@ function Header() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<MobileMenu headerState={headerState} setHeaderState={setHeaderState} />
|
<MobileMenu headerState={headerState} setHeaderState={setHeaderState} />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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 { PythCluster } from '@pythnetwork/client/lib/cluster'
|
||||||
import { createContext, useMemo, useState } from 'react'
|
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<{
|
export const ClusterContext = createContext<{
|
||||||
cluster: PythCluster
|
cluster: PythCluster
|
||||||
setCluster: any
|
setCluster: any
|
||||||
}>({
|
}>({
|
||||||
cluster: DEFAULT_CLUSTER,
|
cluster: DEFAULT_CLUSTER,
|
||||||
setCluster: (cluster: PythCluster) => {},
|
setCluster: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ClusterProvider = (props: any) => {
|
export const ClusterProvider = (props: any) => {
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
import React, { createContext, useContext, useEffect, useMemo } from 'react'
|
import React, { createContext, useContext, useMemo } from 'react'
|
||||||
import { useDebounce } from 'use-debounce'
|
|
||||||
import usePyth from '../hooks/usePyth'
|
import usePyth from '../hooks/usePyth'
|
||||||
import { PythData } from '../types'
|
import { RawConfig } from '../hooks/usePyth'
|
||||||
|
|
||||||
// TODO: fix any
|
// TODO: fix any
|
||||||
interface PythContextProps {
|
interface PythContextProps {
|
||||||
data: PythData
|
rawConfig: RawConfig
|
||||||
dataIsLoading: boolean
|
dataIsLoading: boolean
|
||||||
error: any
|
error: any
|
||||||
connection: any
|
connection: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const PythContext = createContext<PythContextProps>({
|
const PythContext = createContext<PythContextProps>({
|
||||||
data: {},
|
rawConfig: { mappingAccounts: [] },
|
||||||
dataIsLoading: true,
|
dataIsLoading: true,
|
||||||
error: null,
|
error: null,
|
||||||
connection: null,
|
connection: null,
|
||||||
|
@ -28,35 +27,17 @@ interface PythContextProviderProps {
|
||||||
|
|
||||||
export const PythContextProvider: React.FC<PythContextProviderProps> = ({
|
export const PythContextProvider: React.FC<PythContextProviderProps> = ({
|
||||||
children,
|
children,
|
||||||
symbols,
|
|
||||||
raw,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { symbolMap, isLoading, error, connection } = usePyth(symbols)
|
const { isLoading, error, connection, rawConfig } = usePyth()
|
||||||
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 value = useMemo(
|
const value = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
data,
|
rawConfig,
|
||||||
dataIsLoading: isLoading,
|
dataIsLoading: isLoading,
|
||||||
error,
|
error,
|
||||||
connection,
|
connection,
|
||||||
}),
|
}),
|
||||||
[data, isLoading, error, connection]
|
[rawConfig, isLoading, error, connection]
|
||||||
)
|
)
|
||||||
|
|
||||||
return <PythContext.Provider value={value}>{children}</PythContext.Provider>
|
return <PythContext.Provider value={value}>{children}</PythContext.Provider>
|
||||||
|
|
|
@ -1,201 +1,66 @@
|
||||||
import {
|
import {
|
||||||
|
AccountType,
|
||||||
|
getPythProgramKeyForCluster,
|
||||||
|
parseBaseData,
|
||||||
parseMappingData,
|
parseMappingData,
|
||||||
|
parsePermissionData,
|
||||||
parsePriceData,
|
parsePriceData,
|
||||||
parseProductData,
|
parseProductData,
|
||||||
PriceData,
|
PermissionData,
|
||||||
ProductData,
|
Product,
|
||||||
} from '@pythnetwork/client'
|
} from '@pythnetwork/client'
|
||||||
import { AccountInfo, Commitment, Connection, PublicKey } from '@solana/web3.js'
|
import { Connection, PublicKey } from '@solana/web3.js'
|
||||||
import { Buffer } from 'buffer'
|
import assert from 'assert'
|
||||||
import { SetStateAction, useContext, useEffect, useRef, useState } from 'react'
|
import { useContext, useEffect, useRef, useState } from 'react'
|
||||||
import { ClusterContext } from '../contexts/ClusterContext'
|
import { ClusterContext } from '../contexts/ClusterContext'
|
||||||
import { pythClusterApiUrls } from '../utils/pythClusterApiUrl'
|
import { pythClusterApiUrls } from '../utils/pythClusterApiUrl'
|
||||||
|
|
||||||
const ONES = '11111111111111111111111111111111'
|
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 {
|
interface PythHookData {
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
error: any // TODO: fix any
|
error: any // TODO: fix any
|
||||||
version: number | null
|
rawConfig: RawConfig
|
||||||
numProducts: number
|
|
||||||
productAccounts: { [key: string]: IProductAccount }
|
|
||||||
priceAccounts: any // TODO: fix any
|
|
||||||
symbolMap: any // TODO: fix any
|
|
||||||
connection?: Connection
|
connection?: Connection
|
||||||
}
|
}
|
||||||
|
|
||||||
const usePyth = (
|
export type RawConfig = {
|
||||||
symbolFilter?: Array<String>,
|
mappingAccounts: MappingRawConfig[]
|
||||||
subscribe = true
|
permissionAccount?: PermissionData
|
||||||
): PythHookData => {
|
}
|
||||||
|
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 connectionRef = useRef<Connection>()
|
||||||
const { cluster } = useContext(ClusterContext)
|
const { cluster } = useContext(ClusterContext)
|
||||||
const oraclePublicKey = ORACLE_PUBLIC_KEYS[cluster]
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [version, setVersion] = useState<number | null>(null)
|
const [rawConfig, setRawConfig] = useState<RawConfig>({ mappingAccounts: [] })
|
||||||
const [urlsIndex, setUrlsIndex] = useState(0)
|
const [urlsIndex, setUrlsIndex] = useState(0)
|
||||||
const [numProducts, setNumProducts] = useState(0)
|
|
||||||
const [productAccounts, setProductAccounts] = useState({})
|
|
||||||
const [priceAccounts, setPriceAccounts] = useState({})
|
|
||||||
const [symbolMap, setSymbolMap] = useState({})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
setVersion(null)
|
}, [urlsIndex, cluster])
|
||||||
setNumProducts(0)
|
|
||||||
setProductAccounts({})
|
|
||||||
setPriceAccounts({})
|
|
||||||
setSymbolMap({})
|
|
||||||
}, [urlsIndex, oraclePublicKey, cluster])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
const subscriptionIds: number[] = []
|
|
||||||
const urls = pythClusterApiUrls(cluster)
|
const urls = pythClusterApiUrls(cluster)
|
||||||
const connection = new Connection(urls[urlsIndex].rpcUrl, {
|
const connection = new Connection(urls[urlsIndex].rpcUrl, {
|
||||||
commitment: 'confirmed',
|
commitment: 'confirmed',
|
||||||
|
@ -204,123 +69,116 @@ const usePyth = (
|
||||||
|
|
||||||
connectionRef.current = connection
|
connectionRef.current = connection
|
||||||
;(async () => {
|
;(async () => {
|
||||||
// read mapping account
|
|
||||||
const publicKey = new PublicKey(oraclePublicKey)
|
|
||||||
try {
|
try {
|
||||||
const accountInfo = await connection.getAccountInfo(publicKey)
|
const allPythAccounts = await connection.getProgramAccounts(
|
||||||
if (cancelled) return
|
getPythProgramKeyForCluster(cluster)
|
||||||
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
|
const priceRawConfigs: { [key: string]: PriceRawConfig } = {}
|
||||||
if (!accountInfo || !accountInfo.data) {
|
|
||||||
anotherMappingAccount = null
|
/// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
|
||||||
|
if (parsed.priceAccountKey.toBase58() == ONES) {
|
||||||
|
productRawConfigs[allPythAccounts[i].pubkey.toBase58()] = {
|
||||||
|
priceAccounts: [],
|
||||||
|
metadata: parsed.product,
|
||||||
|
address: allPythAccounts[i].pubkey,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const { productAccountKeys, nextMappingAccount } = parseMappingData(
|
let priceAccountKey: string | null =
|
||||||
accountInfo.data
|
parsed.priceAccountKey.toBase58()
|
||||||
)
|
let priceAccounts = []
|
||||||
allProductAccountKeys = [
|
while (priceAccountKey) {
|
||||||
...allProductAccountKeys,
|
const toAdd: PriceRawConfig = priceRawConfigs[priceAccountKey]
|
||||||
...productAccountKeys,
|
priceAccounts.push(toAdd)
|
||||||
]
|
delete priceRawConfigs[priceAccountKey]
|
||||||
anotherMappingAccount = nextMappingAccount
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 rawConfig: RawConfig = { mappingAccounts: [] }
|
||||||
const productAccountKey = productsInfos.keys[i]
|
/// Third pass, mapping accounts
|
||||||
const product = productsData[i]
|
i = 0
|
||||||
const symbol = product.product.symbol
|
while (i < allPythAccounts.length) {
|
||||||
const priceAccountKey = product.priceAccountKey
|
const base = parseBaseData(allPythAccounts[i].account.data)
|
||||||
const priceInfo = priceInfos.array[i]
|
switch (base?.type) {
|
||||||
|
case AccountType.Mapping:
|
||||||
setProductAccounts((o) => ({
|
const parsed = parseMappingData(allPythAccounts[i].account.data)
|
||||||
...o,
|
rawConfig.mappingAccounts.push({
|
||||||
[productAccountKey.toString()]: {
|
next: parsed.nextMappingAccount,
|
||||||
isLoading: false,
|
address: allPythAccounts[i].pubkey,
|
||||||
error: null,
|
products: parsed.productAccountKeys.map((key) => {
|
||||||
product,
|
const toAdd = productRawConfigs[key.toBase58()]
|
||||||
},
|
delete productRawConfigs[key.toBase58()]
|
||||||
}))
|
return toAdd
|
||||||
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
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
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)
|
setIsLoading(false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
|
@ -328,36 +186,21 @@ const usePyth = (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
setError(e)
|
setError(e)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
console.warn(
|
console.warn(`Failed to fetch accounts`)
|
||||||
`Failed to fetch mapping info for ${publicKey.toString()}`
|
|
||||||
)
|
|
||||||
} else if (urlsIndex < urls.length - 1) {
|
} else if (urlsIndex < urls.length - 1) {
|
||||||
setUrlsIndex((urlsIndex) => urlsIndex + 1)
|
setUrlsIndex((urlsIndex) => urlsIndex + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
return () => {
|
return () => {}
|
||||||
cancelled = true
|
}, [urlsIndex, cluster])
|
||||||
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 {
|
return {
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
version,
|
|
||||||
numProducts,
|
|
||||||
productAccounts,
|
|
||||||
priceAccounts,
|
|
||||||
symbolMap,
|
|
||||||
connection: connectionRef.current,
|
connection: connectionRef.current,
|
||||||
|
rawConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,11 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coral-xyz/anchor": "^0.26.0",
|
"@coral-xyz/anchor": "^0.26.0",
|
||||||
|
"@headlessui/react": "^1.7.7",
|
||||||
"@pythnetwork/client": "^2.9.0",
|
"@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",
|
"@solana/web3.js": "^1.73.0",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.11.18",
|
||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.26",
|
||||||
|
@ -20,6 +24,7 @@
|
||||||
"next-seo": "^5.15.0",
|
"next-seo": "^5.15.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-hot-toast": "^2.4.0",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
"use-debounce": "^9.0.2"
|
"use-debounce": "^9.0.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,13 +1,60 @@
|
||||||
|
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 { DefaultSeo } from 'next-seo'
|
||||||
import type { AppProps } from 'next/app'
|
import type { AppProps } from 'next/app'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { Toaster } from 'react-hot-toast'
|
||||||
import { ClusterProvider } from '../contexts/ClusterContext'
|
import { ClusterProvider } from '../contexts/ClusterContext'
|
||||||
import SEO from '../next-seo.config'
|
import SEO from '../next-seo.config'
|
||||||
import '../styles/globals.css'
|
import '../styles/globals.css'
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppProps) {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<ConnectionProvider
|
||||||
|
endpoint={endpoint || clusterApiUrl(WalletAdapterNetwork.Devnet)}
|
||||||
|
>
|
||||||
|
<WalletProvider wallets={wallets} autoConnect>
|
||||||
|
<WalletModalProvider>
|
||||||
<ClusterProvider>
|
<ClusterProvider>
|
||||||
<Head>
|
<Head>
|
||||||
<meta
|
<meta
|
||||||
|
@ -17,7 +64,19 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||||
</Head>
|
</Head>
|
||||||
<DefaultSeo {...SEO} />
|
<DefaultSeo {...SEO} />
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
<Toaster
|
||||||
|
position="bottom-left"
|
||||||
|
toastOptions={{
|
||||||
|
style: {
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
reverseOrder={false}
|
||||||
|
/>
|
||||||
</ClusterProvider>
|
</ClusterProvider>
|
||||||
|
</WalletModalProvider>
|
||||||
|
</WalletProvider>
|
||||||
|
</ConnectionProvider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,6 @@ export default function Document() {
|
||||||
sizes="16x16"
|
sizes="16x16"
|
||||||
href="/favicon-16x16.png"
|
href="/favicon-16x16.png"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#242235" />
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#242235" />
|
||||||
<meta name="msapplication-TileColor" content="#242235" />
|
<meta name="msapplication-TileColor" content="#242235" />
|
||||||
<meta name="theme-color" content="#242235"></meta>
|
<meta name="theme-color" content="#242235"></meta>
|
||||||
|
|
|
@ -1,21 +1,13 @@
|
||||||
import type { NextPage } from 'next'
|
import type { NextPage } from 'next'
|
||||||
import { useContext, useEffect } from 'react'
|
|
||||||
import Layout from '../components/layout/Layout'
|
import Layout from '../components/layout/Layout'
|
||||||
import Main from '../components/Main'
|
import MinPublishers from '../components/MinPublishers'
|
||||||
import { PythContextProvider } from '../contexts/PythContext'
|
import { PythContextProvider } from '../contexts/PythContext'
|
||||||
import { ClusterContext, DEFAULT_CLUSTER } from './../contexts/ClusterContext'
|
|
||||||
|
|
||||||
const Home: NextPage = () => {
|
const Home: NextPage = () => {
|
||||||
const { setCluster } = useContext(ClusterContext)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCluster(DEFAULT_CLUSTER)
|
|
||||||
}, [setCluster])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<PythContextProvider>
|
<PythContextProvider>
|
||||||
<Main />
|
<MinPublishers />
|
||||||
</PythContextProvider>
|
</PythContextProvider>
|
||||||
</Layout>
|
</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);
|
background: rgba(241, 234, 234, 0.6);
|
||||||
border-radius: 5px;
|
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)' },
|
hoverable: { raw: '(hover: hover)' },
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
body: ["'Urbanist'", 'sans-serif'],
|
arboria: 'arboria, sans-serif',
|
||||||
mono: ["'IBM Plex Mono'", 'monospace'],
|
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: {
|
extend: {
|
||||||
fontSize: {
|
fontSize: {
|
||||||
|
@ -69,6 +74,7 @@ module.exports = {
|
||||||
darkGray2: '#312F47',
|
darkGray2: '#312F47',
|
||||||
darkGray3: '#2F2C4F',
|
darkGray3: '#2F2C4F',
|
||||||
darkGray4: '#413E53',
|
darkGray4: '#413E53',
|
||||||
|
hoverGray: 'rgba(255, 255, 255, 0.08)',
|
||||||
beige: '#F1EAEA',
|
beige: '#F1EAEA',
|
||||||
'beige-300': 'rgba(229, 231, 235, .3)',
|
'beige-300': 'rgba(229, 231, 235, .3)',
|
||||||
beige2: '#E4DADB',
|
beige2: '#E4DADB',
|
||||||
|
@ -76,6 +82,7 @@ module.exports = {
|
||||||
green: '#15AE6E',
|
green: '#15AE6E',
|
||||||
lightPurple: '#7731EA',
|
lightPurple: '#7731EA',
|
||||||
offPurple: '#745E9D',
|
offPurple: '#745E9D',
|
||||||
|
pythPurple: '#7142CF',
|
||||||
},
|
},
|
||||||
letterSpacing: {
|
letterSpacing: {
|
||||||
wide: '.02em',
|
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