Skip to content

Commit 322fdb4

Browse files
committed
feat(programs): implement placeholder PythLazerAdapter and update adapter factory
1 parent fb71b8a commit 322fdb4

File tree

4 files changed

+148
-135
lines changed

4 files changed

+148
-135
lines changed

governance/xc_admin/packages/xc_admin_common/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@ export { default as lazerIdl } from "./multisig_transaction/idl/lazer.json";
1919
export * from "./programs/program_adapter";
2020
export * from "./programs/types";
2121
export * from "./programs/adapter_factory";
22+
export * from "./programs/core/core_adapter";
23+
export * from "./programs/lazer/lazer_adapter";

governance/xc_admin/packages/xc_admin_common/src/programs/adapter_factory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ProgramAdapter } from "./program_adapter";
22
import { ProgramType } from "./types";
33
import { PythCoreAdapter } from "./core/core_adapter";
4+
import { PythLazerAdapter } from "./lazer/lazer_adapter";
45

56
/**
67
* Factory function to get the appropriate program adapter based on program type.
@@ -14,8 +15,7 @@ export function getProgramAdapter(type: ProgramType): ProgramAdapter {
1415
case ProgramType.PYTH_CORE:
1516
return new PythCoreAdapter();
1617
case ProgramType.PYTH_LAZER:
17-
// Will be implemented in a future commit
18-
throw new Error("Pyth Lazer adapter not yet implemented");
18+
return new PythLazerAdapter();
1919
default:
2020
const exhaustiveCheck: never = type;
2121
throw new Error(`Unknown program type: ${exhaustiveCheck}`);
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
2+
import { PythCluster } from "@pythnetwork/client";
3+
import { ProgramAdapter } from "../program_adapter";
4+
import { ProgramType } from "../types";
5+
6+
/**
7+
* Adapter for the next-generation Pyth Lazer oracle program
8+
*/
9+
export class PythLazerAdapter implements ProgramAdapter {
10+
readonly name = "Pyth Lazer";
11+
readonly description = "Next-generation Pyth oracle program";
12+
readonly type = ProgramType.PYTH_LAZER;
13+
14+
private readonly LAZER_PROGRAM_ID = new PublicKey(
15+
"pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt",
16+
);
17+
18+
/**
19+
* Get the program address for the given cluster
20+
*
21+
* @param cluster The Pyth cluster to get the address for
22+
* @returns The program address
23+
*/
24+
getProgramAddress(cluster: PythCluster): PublicKey {
25+
// Currently using the same mock address for all clusters
26+
// Will be updated with actual addresses for each cluster when available
27+
return this.LAZER_PROGRAM_ID;
28+
}
29+
30+
/**
31+
* Check if the Pyth Lazer program is available on the specified cluster
32+
*
33+
* @param cluster The Pyth cluster to check
34+
* @returns True if the program is available on the cluster
35+
*/
36+
isAvailableOnCluster(cluster: PythCluster): boolean {
37+
return (
38+
cluster === "pythnet" ||
39+
cluster === "mainnet-beta" ||
40+
cluster === "devnet" ||
41+
cluster === "testnet"
42+
);
43+
}
44+
45+
/**
46+
* Parse raw on-chain accounts into a configuration object
47+
*
48+
* @param accounts Array of account data from the blockchain
49+
* @param cluster The Pyth cluster where the accounts were fetched from
50+
* @returns Lazer-specific configuration object
51+
*/
52+
getConfigFromRawAccounts(accounts: any[], cluster: PythCluster): any {
53+
// Not implemented yet - minimal placeholder
54+
return {
55+
programType: this.type,
56+
cluster,
57+
feeds: [],
58+
};
59+
}
60+
61+
/**
62+
* Format the configuration for downloading as a JSON file
63+
*
64+
* @param config The program's configuration object
65+
* @returns Configuration formatted for download
66+
*/
67+
getDownloadableConfig(config: any): any {
68+
// For now, just return an empty config
69+
return {};
70+
}
71+
72+
/**
73+
* Validate an uploaded configuration against the current configuration
74+
*
75+
* @param existingConfig Current configuration
76+
* @param uploadedConfig Configuration from an uploaded file
77+
* @param cluster The Pyth cluster the configuration is for
78+
* @returns Object with validation result and optional error message
79+
*/
80+
validateUploadedConfig(
81+
existingConfig: any,
82+
uploadedConfig: any,
83+
cluster: PythCluster,
84+
): {
85+
isValid: boolean;
86+
error?: string;
87+
changes?: any;
88+
} {
89+
// Not implemented yet - return error
90+
return {
91+
isValid: false,
92+
error: "Uploading configuration for Pyth Lazer is not yet supported",
93+
};
94+
}
95+
96+
/**
97+
* Generate the necessary instructions to apply configuration changes
98+
*
99+
* @param changes Configuration changes to apply
100+
* @param cluster The Pyth cluster where the changes will be applied
101+
* @param accounts Additional context needed for generating instructions
102+
* @returns Promise resolving to an array of TransactionInstructions
103+
*/
104+
async generateInstructions(
105+
changes: any,
106+
cluster: PythCluster,
107+
accounts: {
108+
fundingAccount: PublicKey;
109+
[key: string]: any;
110+
},
111+
): Promise<TransactionInstruction[]> {
112+
// Not implemented yet - return empty array
113+
return [];
114+
}
115+
}
Lines changed: 29 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,34 @@
11
import {
22
AccountType,
3+
PythCluster,
34
getPythProgramKeyForCluster,
45
parseBaseData,
5-
parseMappingData,
6-
parsePermissionData,
7-
parsePriceData,
8-
parseProductData,
9-
PermissionData,
10-
Product,
116
} from '@pythnetwork/client'
12-
import { Connection, PublicKey } from '@solana/web3.js'
13-
import assert from 'assert'
7+
import { Connection } from '@solana/web3.js'
148
import { useContext, useEffect, useRef, useState } from 'react'
159
import { ClusterContext } from '../contexts/ClusterContext'
1610
import { deriveWsUrl, pythClusterApiUrls } from '../utils/pythClusterApiUrl'
11+
import { getProgramAdapter, ProgramType } from '@pythnetwork/xc-admin-common'
12+
import {
13+
RawConfig,
14+
MappingRawConfig,
15+
ProductRawConfig,
16+
PriceRawConfig,
17+
} from '@pythnetwork/xc-admin-common/src/programs/core/core_adapter'
1718

1819
interface PythHookData {
1920
isLoading: boolean
2021
rawConfig: RawConfig
2122
connection?: Connection
2223
}
2324

24-
export type RawConfig = {
25-
mappingAccounts: MappingRawConfig[]
26-
permissionAccount?: PermissionData
27-
}
28-
export type MappingRawConfig = {
29-
address: PublicKey
30-
next: PublicKey | null
31-
products: ProductRawConfig[]
32-
}
33-
export type ProductRawConfig = {
34-
address: PublicKey
35-
priceAccounts: PriceRawConfig[]
36-
metadata: Product
37-
}
38-
export type PriceRawConfig = {
39-
next: PublicKey | null
40-
address: PublicKey
41-
expo: number
42-
minPub: number
43-
maxLatency: number
44-
publishers: PublicKey[]
45-
}
46-
4725
export const usePyth = (): PythHookData => {
4826
const connectionRef = useRef<Connection | undefined>(undefined)
4927
const { cluster } = useContext(ClusterContext)
5028
const [isLoading, setIsLoading] = useState(true)
5129
const [rawConfig, setRawConfig] = useState<RawConfig>({ mappingAccounts: [] })
5230
const [urlsIndex, setUrlsIndex] = useState(0)
31+
const pythAdapter = useRef(getProgramAdapter(ProgramType.PYTH_CORE))
5332

5433
useEffect(() => {
5534
setIsLoading(true)
@@ -72,116 +51,30 @@ export const usePyth = (): PythHookData => {
7251
try {
7352
const allPythAccounts = [
7453
...(await connection.getProgramAccounts(
75-
getPythProgramKeyForCluster(cluster)
54+
getPythProgramKeyForCluster(cluster as PythCluster)
7655
)),
7756
]
7857
if (cancelled) return
79-
const priceRawConfigs: { [key: string]: PriceRawConfig } = {}
80-
81-
/// First pass, price accounts
82-
let i = 0
83-
while (i < allPythAccounts.length) {
84-
const base = parseBaseData(allPythAccounts[i].account.data)
85-
switch (base?.type) {
86-
case AccountType.Price:
87-
const parsed = parsePriceData(allPythAccounts[i].account.data)
88-
priceRawConfigs[allPythAccounts[i].pubkey.toBase58()] = {
89-
next: parsed.nextPriceAccountKey,
90-
address: allPythAccounts[i].pubkey,
91-
publishers: parsed.priceComponents.map((x) => {
92-
return x.publisher!
93-
}),
94-
expo: parsed.exponent,
95-
minPub: parsed.minPublishers,
96-
maxLatency: parsed.maxLatency,
97-
}
98-
allPythAccounts[i] = allPythAccounts[allPythAccounts.length - 1]
99-
allPythAccounts.pop()
100-
break
101-
default:
102-
i += 1
103-
}
104-
}
10558

106-
if (cancelled) return
107-
/// Second pass, product accounts
108-
i = 0
109-
const productRawConfigs: { [key: string]: ProductRawConfig } = {}
110-
while (i < allPythAccounts.length) {
111-
const base = parseBaseData(allPythAccounts[i].account.data)
112-
switch (base?.type) {
113-
case AccountType.Product:
114-
const parsed = parseProductData(allPythAccounts[i].account.data)
115-
if (parsed.priceAccountKey) {
116-
let priceAccountKey: string | undefined =
117-
parsed.priceAccountKey.toBase58()
118-
const priceAccounts = []
119-
while (priceAccountKey) {
120-
const toAdd: PriceRawConfig = priceRawConfigs[priceAccountKey]
121-
priceAccounts.push(toAdd)
122-
delete priceRawConfigs[priceAccountKey]
123-
priceAccountKey = toAdd.next
124-
? toAdd.next.toBase58()
125-
: undefined
126-
}
127-
productRawConfigs[allPythAccounts[i].pubkey.toBase58()] = {
128-
priceAccounts,
129-
metadata: parsed.product,
130-
address: allPythAccounts[i].pubkey,
131-
}
132-
}
133-
allPythAccounts[i] = allPythAccounts[allPythAccounts.length - 1]
134-
allPythAccounts.pop()
135-
break
136-
default:
137-
i += 1
138-
}
139-
}
59+
// Use the adapter to parse the accounts
60+
const parsedConfig = pythAdapter.current.getConfigFromRawAccounts(
61+
allPythAccounts,
62+
cluster as PythCluster
63+
)
14064

141-
const rawConfig: RawConfig = { mappingAccounts: [] }
142-
if (cancelled) return
143-
/// Third pass, mapping accounts
144-
i = 0
145-
while (i < allPythAccounts.length) {
146-
const base = parseBaseData(allPythAccounts[i].account.data)
147-
switch (base?.type) {
148-
case AccountType.Mapping:
149-
const parsed = parseMappingData(allPythAccounts[i].account.data)
150-
rawConfig.mappingAccounts.push({
151-
next: parsed.nextMappingAccount,
152-
address: allPythAccounts[i].pubkey,
153-
products: parsed.productAccountKeys
154-
.filter((key) => productRawConfigs[key.toBase58()])
155-
.map((key) => {
156-
const toAdd = productRawConfigs[key.toBase58()]
157-
delete productRawConfigs[key.toBase58()]
158-
return toAdd
159-
}),
160-
})
161-
allPythAccounts[i] = allPythAccounts[allPythAccounts.length - 1]
162-
allPythAccounts.pop()
163-
break
164-
case AccountType.Permission:
165-
rawConfig.permissionAccount = parsePermissionData(
166-
allPythAccounts[i].account.data
167-
)
168-
allPythAccounts[i] = allPythAccounts[allPythAccounts.length - 1]
169-
allPythAccounts.pop()
170-
break
171-
default:
172-
i += 1
173-
}
174-
}
65+
// Verify all accounts were processed
66+
const remainingAccounts = allPythAccounts.filter((account) => {
67+
const base = parseBaseData(account.account.data)
68+
return base && base.type !== AccountType.Test
69+
})
17570

176-
assert(
177-
allPythAccounts.every(
178-
(x) =>
179-
!parseBaseData(x.account.data) ||
180-
parseBaseData(x.account.data)?.type == AccountType.Test
71+
if (remainingAccounts.length > 0) {
72+
console.warn(
73+
`${remainingAccounts.length} accounts were not processed`
18174
)
182-
)
75+
}
18376

184-
setRawConfig(rawConfig)
77+
setRawConfig(parsedConfig)
18578
setIsLoading(false)
18679
} catch (e) {
18780
if (cancelled) return
@@ -205,3 +98,6 @@ export const usePyth = (): PythHookData => {
20598
rawConfig,
20699
}
207100
}
101+
102+
// Re-export the types for compatibility
103+
export type { RawConfig, MappingRawConfig, ProductRawConfig, PriceRawConfig }

0 commit comments

Comments
 (0)