From 4d714524f575800a1ca380596dd8ac5cfe7b951e Mon Sep 17 00:00:00 2001 From: Yuru Shao Date: Wed, 8 Jan 2025 08:39:39 -0800 Subject: [PATCH] pg: devnet support (#361) Easier to test vault creation and acls etc using devnet. --- anchor/src/client/base.ts | 48 +++++++++++++------ anchor/src/client/marinade.ts | 2 +- anchor/src/client/staking.ts | 2 +- anchor/src/clientConfig.ts | 12 +++-- anchor/src/glamExports.ts | 7 ++- .../src/react/cluster-provider.tsx | 40 ++++++++-------- anchor/src/react/glam.tsx | 13 +++-- anchor/src/react/index.tsx | 1 + cli/src/main.ts | 2 +- cli/src/utils.ts | 23 +-------- .../(playground)/playground/cluster/page.tsx | 2 +- playground/src/app/(shared)/settings/page.tsx | 26 +++++----- playground/src/app/layout.tsx | 3 +- playground/src/components/ExplorerLink.tsx | 2 +- playground/src/components/wallet-provider.tsx | 10 ++-- 15 files changed, 96 insertions(+), 97 deletions(-) rename playground/src/components/solana-cluster-provider.tsx => anchor/src/react/cluster-provider.tsx (86%) diff --git a/anchor/src/client/base.ts b/anchor/src/client/base.ts index 74703969..f09a089a 100644 --- a/anchor/src/client/base.ts +++ b/anchor/src/client/base.ts @@ -29,8 +29,14 @@ import { } from "@solana/spl-token"; import { WSOL, USDC } from "../constants"; -import { GlamIDL, GlamProgram, getGlamProgramId } from "../glamExports"; -import { ClusterOrCustom, GlamClientConfig } from "../clientConfig"; +import { + Glam, + GlamIDL, + GlamIDLJson, + GlamProgram, + getGlamProgramId, +} from "../glamExports"; +import { ClusterNetwork, GlamClientConfig } from "../clientConfig"; import { FundAccount, FundMetadataAccount, @@ -72,17 +78,15 @@ export type TokenAccount = { }; export class BaseClient { - cluster: ClusterOrCustom; + cluster: ClusterNetwork; provider: anchor.Provider; program: GlamProgram; - programId: PublicKey; jupiterApi: string; blockhashWithCache: BlockhashWithCache; public constructor(config?: GlamClientConfig) { if (config?.provider) { this.provider = config?.provider; - this.program = new Program(GlamIDL, this.provider) as GlamProgram; } else { const defaultProvider = anchor.AnchorProvider.env(); const url = defaultProvider.connection.rpcEndpoint; @@ -100,17 +104,29 @@ export class BaseClient { }, ); anchor.setProvider(this.provider); - this.program = anchor.workspace.Glam as GlamProgram; } // autodetect mainnet const defaultCluster = this.provider.connection.rpcEndpoint.includes( "mainnet", ) - ? "mainnet-beta" - : "devnet"; + ? ClusterNetwork.Mainnet + : ClusterNetwork.Devnet; this.cluster = config?.cluster || defaultCluster; - this.programId = getGlamProgramId(this.cluster); + + if (this.cluster === ClusterNetwork.Mainnet) { + this.program = new Program(GlamIDL, this.provider) as GlamProgram; + } else { + const GlamIDLDevnet = { ...GlamIDLJson }; + GlamIDLDevnet.address = getGlamProgramId( + ClusterNetwork.Devnet, + ).toBase58(); + this.program = new Program( + GlamIDLDevnet as Glam, + this.provider, + ) as GlamProgram; + } + this.jupiterApi = config?.jupiterApi || JUPITER_API_DEFAULT; this.blockhashWithCache = new BlockhashWithCache( this.provider, @@ -119,7 +135,7 @@ export class BaseClient { } isMainnet(): boolean { - return this.cluster === "mainnet-beta"; + return this.cluster === ClusterNetwork.Mainnet; } isPhantom(): boolean { @@ -350,7 +366,7 @@ export class BaseClient { manager.toBuffer(), Uint8Array.from(createdKey), ], - this.programId, + this.program.programId, ); return pda; } @@ -358,7 +374,7 @@ export class BaseClient { getVaultPda(fundPDA: PublicKey): PublicKey { const [pda, _bump] = PublicKey.findProgramAddressSync( [Buffer.from("treasury"), fundPDA.toBuffer()], - this.programId, + this.program.programId, ); return pda; } @@ -449,7 +465,11 @@ export class BaseClient { } getOpenfundsPDA(fundPDA: PublicKey): PublicKey { - return FundModel.openfundsPda(fundPDA); + const [pda, _] = PublicKey.findProgramAddressSync( + [Buffer.from("openfunds"), fundPDA.toBuffer()], + this.program.programId, + ); + return pda; } getShareClassPDA(fundPDA: PublicKey, shareId: number = 0): PublicKey { @@ -560,7 +580,7 @@ export class BaseClient { 0x31, 0x68, 0xa8, 0xd6, 0x86, 0xb4, 0xad, 0x9a, ]); const accounts = await this.provider.connection.getParsedProgramAccounts( - this.programId, + this.program.programId, { filters: [{ memcmp: { offset: 0, bytes: bs58.encode(bytes) } }], }, diff --git a/anchor/src/client/marinade.ts b/anchor/src/client/marinade.ts index 55c72822..06fde612 100644 --- a/anchor/src/client/marinade.ts +++ b/anchor/src/client/marinade.ts @@ -74,7 +74,7 @@ export class MarinadeClient { ): [PublicKey, number] { return PublicKey.findProgramAddressSync( [Buffer.from("ticket"), Buffer.from(ticketId), fundPDA.toBuffer()], - this.base.programId, + this.base.program.programId, ); } diff --git a/anchor/src/client/staking.ts b/anchor/src/client/staking.ts index 4b3c06ae..34680cbe 100644 --- a/anchor/src/client/staking.ts +++ b/anchor/src/client/staking.ts @@ -193,7 +193,7 @@ export class StakingClient { Buffer.from(accountId), fundPDA.toBuffer(), ], - this.base.programId, + this.base.program.programId, ); } diff --git a/anchor/src/clientConfig.ts b/anchor/src/clientConfig.ts index a940cc6d..79279a1d 100644 --- a/anchor/src/clientConfig.ts +++ b/anchor/src/clientConfig.ts @@ -1,12 +1,14 @@ import { Provider, Wallet } from "@coral-xyz/anchor"; -import { Cluster } from "@solana/web3.js"; - -export type ClusterOrCustom = Cluster | "custom"; +export enum ClusterNetwork { + Mainnet = "mainnet-beta", + Testnet = "testnet", + Devnet = "devnet", + Custom = "custom", +} export type GlamClientConfig = { - mainnet?: boolean; provider?: Provider; wallet?: Wallet; - cluster?: ClusterOrCustom; + cluster?: ClusterNetwork; jupiterApi?: string; }; diff --git a/anchor/src/glamExports.ts b/anchor/src/glamExports.ts index ac593086..df496e62 100644 --- a/anchor/src/glamExports.ts +++ b/anchor/src/glamExports.ts @@ -1,11 +1,10 @@ -// Here we export some useful types and functions for interacting with the Anchor program. import { Program } from "@coral-xyz/anchor"; import { PublicKey } from "@solana/web3.js"; + +import type { ClusterNetwork } from "./clientConfig"; import type { Glam } from "../target/types/glam"; import GlamIDLJson from "../target/idl/glam.json"; -import type { ClusterOrCustom } from "./clientConfig"; - const GlamIDL = GlamIDLJson as Glam; export { Glam, GlamIDL, GlamIDLJson }; export type GlamProgram = Program; @@ -17,7 +16,7 @@ export const GLAM_PROGRAM_ID_MAINNET = new PublicKey( "GLAMpLuXu78TA4ao3DPZvT1zQ7woxoQ8ahdYbhnqY9mP", ); -export function getGlamProgramId(cluster: ClusterOrCustom) { +export function getGlamProgramId(cluster: ClusterNetwork) { switch (cluster) { case "mainnet-beta": return GLAM_PROGRAM_ID_MAINNET; diff --git a/playground/src/components/solana-cluster-provider.tsx b/anchor/src/react/cluster-provider.tsx similarity index 86% rename from playground/src/components/solana-cluster-provider.tsx rename to anchor/src/react/cluster-provider.tsx index f8200122..cd66e306 100644 --- a/playground/src/components/solana-cluster-provider.tsx +++ b/anchor/src/react/cluster-provider.tsx @@ -4,7 +4,7 @@ import { clusterApiUrl, Connection } from "@solana/web3.js"; import { atom, useAtomValue, useSetAtom } from "jotai"; import { atomWithStorage } from "jotai/utils"; import { createContext, ReactNode, useContext } from "react"; -import toast from "react-hot-toast"; +import { ClusterNetwork } from "../clientConfig"; interface Cluster { name: string; @@ -13,13 +13,6 @@ interface Cluster { active?: boolean; } -export enum ClusterNetwork { - Mainnet = "mainnet-beta", - Testnet = "testnet", - Devnet = "devnet", - Custom = "custom", -} - const defaultClusters: Cluster[] = [ { name: "mainnet-beta", @@ -29,28 +22,33 @@ const defaultClusters: Cluster[] = [ }, { name: "devnet", - endpoint: clusterApiUrl("devnet"), + endpoint: + process.env.NEXT_PUBLIC_SOLANA_RPC?.replace("mainnet", "devnet") || + clusterApiUrl("devnet"), network: ClusterNetwork.Devnet, }, - { + // { + // name: "testnet", + // endpoint: clusterApiUrl("testnet"), + // network: ClusterNetwork.Testnet, + // }, +]; + +if (process.env.NODE_ENV === "development") { + defaultClusters.push({ name: "localnet", endpoint: "http://localhost:8899", network: ClusterNetwork.Custom, - }, - { - name: "testnet", - endpoint: clusterApiUrl("testnet"), - network: ClusterNetwork.Testnet, - }, -]; + }); +} const clusterAtom = atomWithStorage( "solana-cluster", - defaultClusters[0] + defaultClusters[0], ); const clustersAtom = atomWithStorage( "solana-clusters", - defaultClusters + defaultClusters, ); const activeClustersAtom = atom((get) => { @@ -78,7 +76,7 @@ interface ClusterProviderContext { } const Context = createContext( - {} as ClusterProviderContext + {} as ClusterProviderContext, ); export function ClusterProvider({ children }: { children: ReactNode }) { @@ -95,7 +93,7 @@ export function ClusterProvider({ children }: { children: ReactNode }) { new Connection(cluster.endpoint); setClusters([...clusters, cluster]); } catch (err) { - toast.error(`${err}`); + throw err; } }, deleteCluster: (cluster: Cluster) => { diff --git a/anchor/src/react/glam.tsx b/anchor/src/react/glam.tsx index 1f7dcb2a..45b7c5d5 100644 --- a/anchor/src/react/glam.tsx +++ b/anchor/src/react/glam.tsx @@ -22,6 +22,7 @@ import { } from "@solana/spl-token"; import { DriftMarketConfigs, GlamDriftUser } from "../client/drift"; import { TokenAccount } from "../client/base"; +import { useCluster } from "./cluster-provider"; export interface JupTokenListItem { address: string; @@ -134,9 +135,7 @@ const fetchBalances = async (glamClient: GlamClient, owner: PublicKey) => { export function GlamProvider({ children, -}: Readonly<{ - children: React.ReactNode; -}>) { +}: Readonly<{ children: React.ReactNode }>) { const setActiveFund = useSetAtom(fundAtom); const setFundsList = useSetAtom(fundsListAtom); @@ -144,6 +143,7 @@ export function GlamProvider({ const [userWallet, setUserWallet] = useState({} as UserWallet); const wallet = useWallet(); const { connection } = useConnection(); + const { cluster } = useCluster(); const glamClient = useMemo( () => @@ -151,8 +151,9 @@ export function GlamProvider({ provider: new AnchorProvider(connection, wallet as AnchorWallet, { commitment: "confirmed", }), + cluster: cluster.network, }), - [connection, wallet], + [connection, wallet, cluster], ); const [allFunds, setAllFunds] = useState([] as FundModel[]); const [jupTokenList, setJupTokenList] = useState([] as JupTokenListItem[]); @@ -238,13 +239,14 @@ export function GlamProvider({ } refreshTreasury(); - }, [allFundsData, activeFund, wallet]); + }, [allFundsData, activeFund, wallet, cluster]); // // Fetch token prices https://station.jup.ag/docs/apis/price-api-v2 // const { data: jupTokenPricesData } = useQuery({ queryKey: ["/jup-token-prices", treasury?.pubkey], + enabled: cluster.network === "mainnet-beta", refetchInterval: 10_000, queryFn: () => { const tokenMints = new Set([] as string[]); @@ -332,6 +334,7 @@ export function GlamProvider({ // const { data: marketConfigs } = useQuery({ queryKey: ["drift-market-configs"], + enabled: cluster.network === "mainnet-beta", queryFn: async () => { const response = await fetch( "https://api.glam.systems/v0/drift/market_configs/", diff --git a/anchor/src/react/index.tsx b/anchor/src/react/index.tsx index 148da2d0..771ba706 100644 --- a/anchor/src/react/index.tsx +++ b/anchor/src/react/index.tsx @@ -1,2 +1,3 @@ export * from "../index"; export * from "./glam"; +export * from "./cluster-provider"; diff --git a/cli/src/main.ts b/cli/src/main.ts index 2a728c11..a6e71e8e 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -31,7 +31,7 @@ try { const config = fs.readFileSync(configPath, "utf8"); const { keypair_path, helius_api_key, priority_fee_level, fund } = JSON.parse(config); - process.env.ANCHOR_PROVIDER_URL = `https://mainnet.helius-rpc.com/?api-key=${helius_api_key}`; + process.env.ANCHOR_PROVIDER_URL = `https://devnet.helius-rpc.com/?api-key=${helius_api_key}`; process.env.ANCHOR_WALLET = keypair_path; if (fund) { fundPDA = new PublicKey(fund); diff --git a/cli/src/utils.ts b/cli/src/utils.ts index 5321fb70..d79a65f7 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -1,29 +1,10 @@ import fs from "fs"; import * as anchor from "@coral-xyz/anchor"; import { Connection } from "@solana/web3.js"; -import { GlamClient } from "@glam/anchor"; +import { ClusterNetwork, GlamClient } from "@glam/anchor"; export const getGlamClient = () => { - const defaultProvider = anchor.AnchorProvider.env(); - const url = defaultProvider.connection.rpcEndpoint; - const connection = new Connection(url, { - commitment: "confirmed", - }); - const provider = new anchor.AnchorProvider( - connection, - defaultProvider.wallet, - { - ...defaultProvider.opts, - commitment: "confirmed", - preflightCommitment: "confirmed", - } - ); - anchor.setProvider(provider); - - return new GlamClient({ - provider, - cluster: "mainnet-beta", - }); + return new GlamClient(); }; export const setFundToConfig = (fund, path) => { diff --git a/playground/src/app/(playground)/playground/cluster/page.tsx b/playground/src/app/(playground)/playground/cluster/page.tsx index 6d2b6e65..805ec212 100644 --- a/playground/src/app/(playground)/playground/cluster/page.tsx +++ b/playground/src/app/(playground)/playground/cluster/page.tsx @@ -3,8 +3,8 @@ import { Label } from "@/components/ui/label"; import React from "react"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { useCluster } from "@/components/solana-cluster-provider"; import { toast } from "@/components/ui/use-toast"; +import { useCluster } from "@glam/anchor/react"; export default function Cluster() { const { cluster, clusters, setCluster } = useCluster(); diff --git a/playground/src/app/(shared)/settings/page.tsx b/playground/src/app/(shared)/settings/page.tsx index 94d83d8a..1b616f5a 100644 --- a/playground/src/app/(shared)/settings/page.tsx +++ b/playground/src/app/(shared)/settings/page.tsx @@ -30,7 +30,6 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { useCluster } from "@/components/solana-cluster-provider"; import { toast } from "@/components/ui/use-toast"; import { PlusIcon, ResetIcon } from "@radix-ui/react-icons"; import { Label } from "@/components/ui/label"; @@ -47,6 +46,7 @@ import { useQuery } from "@tanstack/react-query"; import { getPriorityFeeEstimate, GLAM_PROGRAM_ID_MAINNET, + useCluster, } from "@glam/anchor/react"; import { LAMPORTS_PER_SOL } from "@solana/web3.js"; @@ -90,7 +90,7 @@ type FormKey = keyof typeof PERSISTED_FIELDS; function usePersistedForm( formKey: FormKey, schema: T, - defaultValues: z.infer + defaultValues: z.infer, ): UseFormReturn> { const form = useForm>({ resolver: zodResolver(schema), @@ -186,7 +186,7 @@ const SettingsPage: React.FC = () => { const priorityFeeForm = usePersistedForm( "priorityFee", priorityFeeFormSchema, - PRIORITY_FEE_FORM_DEFAULT_VALUES + PRIORITY_FEE_FORM_DEFAULT_VALUES, ); const feeOption = priorityFeeForm.watch("option"); @@ -201,7 +201,7 @@ const SettingsPage: React.FC = () => { process.env.NEXT_PUBLIC_HELIUS_API_KEY!, undefined, [GLAM_PROGRAM_ID_MAINNET.toBase58()], - "High" + "High", ), }); useEffect(() => { @@ -252,7 +252,7 @@ const SettingsPage: React.FC = () => { label: capitalizeWords(ce.label), url: truncateUrl(ce.endpoint), isCustom: true, - }) + }), ) : []; @@ -270,7 +270,7 @@ const SettingsPage: React.FC = () => { setEndpoints(updatedEndpoints); localStorage.setItem( "customEndpoints", - JSON.stringify(updatedEndpoints.filter((e) => e.isCustom)) + JSON.stringify(updatedEndpoints.filter((e) => e.isCustom)), ); // Automatically set the new endpoint as active @@ -301,12 +301,12 @@ const SettingsPage: React.FC = () => { const deleteCustomEndpoint = (endpointToDelete: string) => { const updatedEndpoints = endpoints.filter( - (e) => e.value !== endpointToDelete + (e) => e.value !== endpointToDelete, ); setEndpoints(updatedEndpoints); localStorage.setItem( "customEndpoints", - JSON.stringify(updatedEndpoints.filter((e) => e.isCustom)) + JSON.stringify(updatedEndpoints.filter((e) => e.isCustom)), ); if (rpcForm.getValues("activeEndpoint") === endpointToDelete) { @@ -343,7 +343,7 @@ const SettingsPage: React.FC = () => { > {field.value ? endpoints.find( - (endpoint) => endpoint.value === field.value + (endpoint) => endpoint.value === field.value, )?.label : "Select endpoint..."} @@ -367,7 +367,7 @@ const SettingsPage: React.FC = () => { handleEndpointChange( currentValue === field.value ? "" - : currentValue + : currentValue, ); setOpen(false); }} @@ -379,7 +379,7 @@ const SettingsPage: React.FC = () => { "mr-2 h-4 w-4", field.value === endpoint.value ? "opacity-100" - : "opacity-0" + : "opacity-0", )} />
@@ -533,7 +533,7 @@ const SettingsPage: React.FC = () => {
@@ -559,7 +559,7 @@ const SettingsPage: React.FC = () => { field.onChange(val.toString()); priorityFeeForm.setValue( "multiplier", - val.toString() + val.toString(), ); }} value={field.value} diff --git a/playground/src/app/layout.tsx b/playground/src/app/layout.tsx index 89c4c3a4..91a8def5 100644 --- a/playground/src/app/layout.tsx +++ b/playground/src/app/layout.tsx @@ -6,8 +6,7 @@ import { ReactQueryProvider } from "./react-query-provider"; import React from "react"; import MobileOverlay from "@/components/MobileOverlay"; import dynamic from "next/dynamic"; -import { ClusterProvider } from "@/components/solana-cluster-provider"; -import { GlamProvider } from "@glam/anchor/react"; +import { ClusterProvider, GlamProvider } from "@glam/anchor/react"; const inter = Inter({ subsets: ["latin"] }); diff --git a/playground/src/components/ExplorerLink.tsx b/playground/src/components/ExplorerLink.tsx index e2006a15..5b458eba 100644 --- a/playground/src/components/ExplorerLink.tsx +++ b/playground/src/components/ExplorerLink.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCluster } from "@/components/solana-cluster-provider"; +import { useCluster } from "@glam/anchor/react"; export function ellipsify(str = "", len = 4) { if (str.length > 30) { diff --git a/playground/src/components/wallet-provider.tsx b/playground/src/components/wallet-provider.tsx index f07672db..db3ac7e4 100644 --- a/playground/src/components/wallet-provider.tsx +++ b/playground/src/components/wallet-provider.tsx @@ -8,17 +8,13 @@ import { import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"; -import { - ClusterNetwork, - useCluster, -} from "@/components/solana-cluster-provider"; - // To use default styles: // import "@solana/wallet-adapter-react-ui/styles.css"; import "./wallet-styles.css"; +import { ClusterNetwork, useCluster } from "@glam/anchor/react"; function toWalletAdapterNetwork( - cluster?: ClusterNetwork + cluster?: ClusterNetwork, ): WalletAdapterNetwork | undefined { switch (cluster) { case ClusterNetwork.Mainnet: @@ -46,7 +42,7 @@ export default function AppWalletProvider({ // manually add any legacy wallet adapters here // new UnsafeBurnerWalletAdapter(), ], - [walletAdapterNetwork] + [walletAdapterNetwork], ); return (