[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:
guibescos 2023-01-16 13:14:18 -06:00 committed by GitHub
parent b05845ede5
commit e55a3bbb96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 16408 additions and 7789 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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) => {

View File

@ -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>

View File

@ -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,
}
}

View File

@ -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"
},

View File

@ -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>
</>
)
}

View File

@ -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>

View File

@ -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>
)

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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',

View File

@ -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
}

View File

@ -1,4 +0,0 @@
export enum PriceStatus {
Offline = 0,
Online = 1,
}

View File

@ -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