Просмотр исходного кода

fix: update solana client race condition (#267)

* fix: update solana client race condition

* chore: add changeset

Updated versioning for gill and @gillsdk/react to minor.
tobey 1 месяц назад
Родитель
Сommit
459e9a778b

+ 6 - 0
.changeset/cold-apes-work.md

@@ -0,0 +1,6 @@
+---
+"gill": minor
+"@gillsdk/react": minor
+---
+
+fix: update solana client race condition

+ 1 - 0
packages/gill/src/core/create-solana-client.ts

@@ -84,5 +84,6 @@ export function createSolanaClient<TCluster extends ModifiedClusterUrl>({
     }),
     // @ts-ignore
     simulateTransaction: simulateTransactionFactory({ rpc }),
+    urlOrMoniker: urlOrMoniker.toString() as TCluster,
   };
 }

+ 8 - 5
packages/gill/src/types/rpc.ts

@@ -10,27 +10,28 @@ import type {
   SolanaRpcSubscriptionsApi,
   TestnetUrl,
 } from "@solana/kit";
+
 import { SendAndConfirmTransactionWithSignersFunction } from "../core/send-and-confirm-transaction-with-signers";
 import type { SimulateTransactionFunction } from "../core/simulate-transaction";
 
 /** Solana cluster moniker */
-export type SolanaClusterMoniker = "mainnet" | "devnet" | "testnet" | "localnet";
+export type SolanaClusterMoniker = "devnet" | "localnet" | "mainnet" | "testnet";
 
 export type LocalnetUrl = string & { "~cluster": "localnet" };
 
 export type GenericUrl = string & {};
 
-export type ModifiedClusterUrl = MainnetUrl | DevnetUrl | TestnetUrl | LocalnetUrl | GenericUrl;
+export type ModifiedClusterUrl = DevnetUrl | GenericUrl | LocalnetUrl | MainnetUrl | TestnetUrl;
 
-export type SolanaClientUrlOrMoniker = SolanaClusterMoniker | URL | ModifiedClusterUrl;
+export type SolanaClientUrlOrMoniker = ModifiedClusterUrl | SolanaClusterMoniker | URL;
 
 export type CreateSolanaClientArgs<TClusterUrl extends SolanaClientUrlOrMoniker = GenericUrl> = {
-  /** Full RPC URL (for a private RPC endpoint) or the Solana moniker (for a public RPC endpoint) */
-  urlOrMoniker: SolanaClientUrlOrMoniker | TClusterUrl;
   /** Configuration used to create the `rpc` client */
   rpcConfig?: Parameters<typeof createSolanaRpc>[1] & { port?: number };
   /** Configuration used to create the `rpcSubscriptions` client */
   rpcSubscriptionsConfig?: Parameters<typeof createSolanaRpcSubscriptions>[1] & { port?: number };
+  /** Full RPC URL (for a private RPC endpoint) or the Solana moniker (for a public RPC endpoint) */
+  urlOrMoniker: SolanaClientUrlOrMoniker | TClusterUrl;
 };
 
 export type SolanaClient<TClusterUrl extends ModifiedClusterUrl | string = string> = {
@@ -53,4 +54,6 @@ export type SolanaClient<TClusterUrl extends ModifiedClusterUrl | string = strin
    * Simulate a transaction on the network
    */
   simulateTransaction: SimulateTransactionFunction;
+  /** Full RPC URL (for a private RPC endpoint) or the Solana moniker (for a public RPC endpoint) */
+  urlOrMoniker: SolanaClientUrlOrMoniker | TClusterUrl;
 };

+ 6 - 5
packages/react/src/hooks/account.ts

@@ -3,6 +3,7 @@
 import { useQuery } from "@tanstack/react-query";
 import type { Account, Address, Decoder, FetchAccountConfig, Simplify } from "gill";
 import { assertAccountExists, decodeAccount, fetchEncodedAccount } from "gill";
+
 import { GILL_HOOK_CLIENT_KEY } from "../const.js";
 import { useSolanaClient } from "./client.js";
 import type { GillUseRpcHook } from "./types.js";
@@ -24,7 +25,7 @@ type UseAccountInput<
   /**
    * Address of the account to get the balance of
    */
-  address: TAddress | Address;
+  address: Address | TAddress;
   /**
    * Account decoder that can decode the account's `data` byte array value
    */
@@ -40,7 +41,7 @@ export function useAccount<
   TAddress extends string = string,
   TDecodedData extends object = Uint8Array,
 >({ options, config, abortSignal, address, decoder }: UseAccountInput<TConfig, TAddress, TDecodedData>) {
-  const { rpc } = useSolanaClient();
+  const { rpc, urlOrMoniker } = useSolanaClient();
 
   if (abortSignal) {
     // @ts-expect-error we stripped the `abortSignal` from the type but are now adding it back in
@@ -53,14 +54,14 @@ export function useAccount<
   const { data, ...rest } = useQuery({
     networkMode: "offlineFirst",
     ...options,
-    queryKey: [GILL_HOOK_CLIENT_KEY, "getAccountInfo", address],
+    enabled: !!address,
     queryFn: async () => {
       const account = await fetchEncodedAccount(rpc, address as Address, config);
       assertAccountExists(account);
-      if (decoder) return decodeAccount(account, decoder as Decoder<TDecodedData>);
+      if (decoder) return decodeAccount(account, decoder);
       return account;
     },
-    enabled: !!address,
+    queryKey: [GILL_HOOK_CLIENT_KEY, urlOrMoniker, "getAccountInfo", address],
   });
   return {
     ...rest,

+ 3 - 3
packages/react/src/hooks/balance.ts

@@ -2,10 +2,10 @@
 
 import { useQuery } from "@tanstack/react-query";
 import type { Address, GetBalanceApi, Simplify } from "gill";
+
 import { GILL_HOOK_CLIENT_KEY } from "../const.js";
 import { useSolanaClient } from "./client.js";
 import type { GillUseRpcHook } from "./types.js";
-
 type RpcConfig = Simplify<Parameters<GetBalanceApi["getBalance"]>>[1];
 
 type UseBalanceResponse = ReturnType<GetBalanceApi["getBalance"]>["value"];
@@ -27,16 +27,16 @@ export function useBalance<TConfig extends RpcConfig = RpcConfig>({
   abortSignal,
   address,
 }: UseBalanceInput<TConfig>) {
-  const { rpc } = useSolanaClient();
+  const { rpc, urlOrMoniker } = useSolanaClient();
   const { data, ...rest } = useQuery({
     networkMode: "offlineFirst",
     ...options,
     enabled: !!address,
-    queryKey: [GILL_HOOK_CLIENT_KEY, "getBalance", address],
     queryFn: async () => {
       const { value } = await rpc.getBalance(address as Address, config).send({ abortSignal });
       return value;
     },
+    queryKey: [GILL_HOOK_CLIENT_KEY, urlOrMoniker, "getBalance", address],
   });
   return {
     ...rest,

+ 5 - 23
packages/react/src/hooks/client.ts

@@ -1,5 +1,6 @@
 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
 import { createSolanaClient, type SolanaClient } from "gill";
+
 import { GILL_HOOK_CLIENT_KEY } from "../const.js";
 
 /**
@@ -7,13 +8,13 @@ import { GILL_HOOK_CLIENT_KEY } from "../const.js";
  */
 export function useSolanaClient(): SolanaClient {
   const { data: config } = useQuery<SolanaClient>({
-    queryKey: [GILL_HOOK_CLIENT_KEY],
-    staleTime: Infinity,
     // fallback data should not be reached if used within `SolanaProvider`
     // since we set the initial value. but just in case => devnet
     initialData: createSolanaClient({
       urlOrMoniker: "devnet",
     }),
+    queryKey: [GILL_HOOK_CLIENT_KEY],
+    staleTime: Infinity,
   });
   return config;
 }
@@ -24,29 +25,10 @@ export function useSolanaClient(): SolanaClient {
 export function useUpdateSolanaClient() {
   const queryClient = useQueryClient();
   return useMutation({
-    mutationFn: async (newClient: SolanaClient): Promise<SolanaClient> => {
+    mutationFn: async (newClient: SolanaClient): Promise<void> => {
       queryClient.setQueryData([GILL_HOOK_CLIENT_KEY], newClient);
-      return newClient;
-    },
-    onSuccess: () => {
-      // Invalidate any queries that might depend on the Solana client
-      queryClient.invalidateQueries({ queryKey: [GILL_HOOK_CLIENT_KEY] });
-
-      /**
-       * todo: research more here
-       * removing queries here will force the cache to update automatically, but can result in the waterfall of data fetching
-       * but it seems that without it, the client side data does not auto refetch when the SolanaClient is changed :/
-       */
-      // queryClient.removeQueries({
-      //   predicate: (query) => {
-      //     return query.queryKey.length >= 2 && query.queryKey[0] === GILL_HOOK_CLIENT_KEY;
-      //   },
-      // });
 
-      queryClient.prefetchQuery({ queryKey: [GILL_HOOK_CLIENT_KEY] });
-      queryClient.refetchQueries({
-        queryKey: [GILL_HOOK_CLIENT_KEY],
-      });
+      return await Promise.resolve();
     },
   });
 }

+ 3 - 2
packages/react/src/hooks/latest-blockhash.ts

@@ -2,6 +2,7 @@
 
 import { useQuery } from "@tanstack/react-query";
 import type { GetLatestBlockhashApi, Simplify } from "gill";
+
 import { GILL_HOOK_CLIENT_KEY } from "../const.js";
 import { useSolanaClient } from "./client.js";
 import type { GillUseRpcHook } from "./types.js";
@@ -23,14 +24,14 @@ export function useLatestBlockhash<TConfig extends RpcConfig = RpcConfig>({
   config,
   abortSignal,
 }: UseLatestBlockhashInput<TConfig> = {}) {
-  const { rpc } = useSolanaClient();
+  const { rpc, urlOrMoniker } = useSolanaClient();
   const { data, ...rest } = useQuery({
     ...options,
-    queryKey: [GILL_HOOK_CLIENT_KEY, "getLatestBlockhash"],
     queryFn: async () => {
       const { value } = await rpc.getLatestBlockhash(config).send({ abortSignal });
       return value;
     },
+    queryKey: [GILL_HOOK_CLIENT_KEY, urlOrMoniker, "getLatestBlockhash"],
   });
   return {
     ...rest,

+ 4 - 3
packages/react/src/hooks/program-accounts.ts

@@ -12,11 +12,12 @@ import type {
   Simplify,
   SolanaRpcResponse,
 } from "gill";
+
 import { GILL_HOOK_CLIENT_KEY } from "../const.js";
 import { useSolanaClient } from "./client.js";
 import type { GillUseRpcHook } from "./types.js";
 
-type Encoding = "base64" | "jsonParsed" | "base64+zstd";
+type Encoding = "base64" | "base64+zstd" | "jsonParsed";
 
 type RpcConfig = Simplify<
   Parameters<GetProgramAccountsApi["getProgramAccounts"]>[1] &
@@ -61,16 +62,16 @@ export function useProgramAccounts<TConfig extends RpcConfig = RpcConfig>({
   abortSignal,
   program,
 }: UseProgramAccountsInput<TConfig>) {
-  const { rpc } = useSolanaClient();
+  const { rpc, urlOrMoniker } = useSolanaClient();
 
   const { data, ...rest } = useQuery({
     ...options,
     enabled: !!program,
-    queryKey: [GILL_HOOK_CLIENT_KEY, "getProgramAccounts", program],
     queryFn: async () => {
       const accounts = await rpc.getProgramAccounts(program as Address, config).send({ abortSignal });
       return accounts;
     },
+    queryKey: [GILL_HOOK_CLIENT_KEY, urlOrMoniker, "getProgramAccounts", program],
   });
 
   return {

+ 4 - 3
packages/react/src/hooks/recent-prioritization-fees.ts

@@ -2,12 +2,13 @@
 
 import { useQuery } from "@tanstack/react-query";
 import type { GetRecentPrioritizationFeesApi, Simplify } from "gill";
+
 import { GILL_HOOK_CLIENT_KEY } from "../const.js";
 import { useSolanaClient } from "./client.js";
 import type { GillUseRpcHook } from "./types.js";
 
 type UseRecentPrioritizationFeesInput = Simplify<
-  Pick<GillUseRpcHook<{}>, "options" | "abortSignal"> & {
+  Pick<GillUseRpcHook<{}>, "abortSignal" | "options"> & {
     addresses?: Parameters<GetRecentPrioritizationFeesApi["getRecentPrioritizationFees"]>[0];
   }
 >;
@@ -23,15 +24,15 @@ export function useRecentPrioritizationFees({
   abortSignal,
   addresses,
 }: UseRecentPrioritizationFeesInput = {}) {
-  const { rpc } = useSolanaClient();
+  const { rpc, urlOrMoniker } = useSolanaClient();
 
   const { data, ...rest } = useQuery({
     ...options,
-    queryKey: [GILL_HOOK_CLIENT_KEY, "getRecentPrioritizationFees", addresses],
     queryFn: async () => {
       const fees = await rpc.getRecentPrioritizationFees(addresses).send({ abortSignal });
       return fees;
     },
+    queryKey: [GILL_HOOK_CLIENT_KEY, urlOrMoniker, "getRecentPrioritizationFees", addresses],
   });
 
   return {

+ 3 - 2
packages/react/src/hooks/signature-statuses.ts

@@ -2,6 +2,7 @@
 
 import { useQuery } from "@tanstack/react-query";
 import type { GetSignatureStatusesApi, Signature, Simplify } from "gill";
+
 import { GILL_HOOK_CLIENT_KEY } from "../const.js";
 import { useSolanaClient } from "./client.js";
 import type { GillUseRpcHook } from "./types.js";
@@ -28,15 +29,15 @@ export function useSignatureStatuses<TConfig extends RpcConfig = RpcConfig>({
   abortSignal,
   signatures,
 }: UseSignatureStatusesInput<TConfig>) {
-  const { rpc } = useSolanaClient();
+  const { rpc, urlOrMoniker } = useSolanaClient();
   const { data, ...rest } = useQuery({
     ...options,
     enabled: signatures && signatures.length > 0,
-    queryKey: [GILL_HOOK_CLIENT_KEY, "getSignatureStatuses", signatures],
     queryFn: async () => {
       const { value } = await rpc.getSignatureStatuses(signatures as Signature[], config).send({ abortSignal });
       return value;
     },
+    queryKey: [GILL_HOOK_CLIENT_KEY, urlOrMoniker, "getSignatureStatuses", signatures],
   });
   return {
     ...rest,

+ 3 - 2
packages/react/src/hooks/signatures-for-address.ts

@@ -2,6 +2,7 @@
 
 import { useQuery } from "@tanstack/react-query";
 import { Address, GetSignaturesForAddressApi, Simplify } from "gill";
+
 import { GILL_HOOK_CLIENT_KEY } from "../const.js";
 import { useSolanaClient } from "./client.js";
 import { GillUseRpcHook } from "./types.js";
@@ -29,16 +30,16 @@ export function useSignaturesForAddress<TConfig extends RpcConfig = RpcConfig>({
   abortSignal,
   address,
 }: UseSignaturesForAddressInput<TConfig>) {
-  const { rpc } = useSolanaClient();
+  const { rpc, urlOrMoniker } = useSolanaClient();
   const { data, ...rest } = useQuery({
     networkMode: "offlineFirst",
     ...options,
     enabled: !!address,
-    queryKey: [GILL_HOOK_CLIENT_KEY, "getSignaturesForAddress", address],
     queryFn: async () => {
       const signatures = await rpc.getSignaturesForAddress(address as Address, config).send({ abortSignal });
       return signatures;
     },
+    queryKey: [GILL_HOOK_CLIENT_KEY, urlOrMoniker, "getSignaturesForAddress", address],
   });
   return {
     ...rest,

+ 3 - 2
packages/react/src/hooks/slot.ts

@@ -2,6 +2,7 @@
 
 import { useQuery } from "@tanstack/react-query";
 import type { GetSlotApi, Simplify } from "gill";
+
 import { GILL_HOOK_CLIENT_KEY } from "../const.js";
 import { useSolanaClient } from "./client.js";
 import type { GillUseRpcHook } from "./types.js";
@@ -23,15 +24,15 @@ export function useSlot<TConfig extends RpcConfig = RpcConfig>({
   config,
   abortSignal,
 }: UseSlotInput<TConfig> = {}) {
-  const { rpc } = useSolanaClient();
+  const { rpc, urlOrMoniker } = useSolanaClient();
 
   const { data, ...rest } = useQuery({
     ...options,
-    queryKey: [GILL_HOOK_CLIENT_KEY, "getSlot"],
     queryFn: async () => {
       const slot = await rpc.getSlot(config).send({ abortSignal });
       return slot;
     },
+    queryKey: [GILL_HOOK_CLIENT_KEY, urlOrMoniker, "getSlot"],
   });
 
   return {

+ 10 - 9
packages/react/src/hooks/token-account.ts

@@ -1,10 +1,6 @@
 "use client";
 
 import { useQuery } from "@tanstack/react-query";
-import { GILL_HOOK_CLIENT_KEY } from "../const.js";
-import { useSolanaClient } from "./client.js";
-import type { GillUseRpcHook } from "./types.js";
-
 import type { Account, Address, FetchAccountConfig, Simplify } from "gill";
 import { address, assertAccountExists, assertIsAddress, fetchEncodedAccount } from "gill";
 import {
@@ -15,6 +11,10 @@ import {
   type Token,
 } from "gill/programs";
 
+import { GILL_HOOK_CLIENT_KEY } from "../const.js";
+import { useSolanaClient } from "./client.js";
+import type { GillUseRpcHook } from "./types.js";
+
 type RpcConfig = Simplify<Omit<FetchAccountConfig, "abortSignal">>;
 
 type UseTokenAccountResponse<TAddress extends Address = Address> = Simplify<
@@ -31,14 +31,14 @@ type TokenAccountInputWithDeclaredAta<TAddress extends Address = Address> = {
 };
 
 type TokenAccountInputWithDerivedAtaDetails = {
-  /**
-   * Address of the {@link https://solana.com/docs/tokens#token-account | Token Account}'s `owner`
-   */
-  owner: Address;
   /**
    * Address of the {@link https://solana.com/docs/tokens#token-account | Token Account}'s `mint`
    */
   mint: Address;
+  /**
+   * Address of the {@link https://solana.com/docs/tokens#token-account | Token Account}'s `owner`
+   */
+  owner: Address;
   /**
    * The {@link https://solana.com/docs/tokens#token-programs | Token Program} used to create the `mint`
    *
@@ -70,7 +70,7 @@ export function useTokenAccount<TConfig extends RpcConfig = RpcConfig, TAddress
   // tokenProgram,
   ...tokenAccountOptions
 }: UseTokenAccountInput<TConfig, TAddress>) {
-  const { rpc } = useSolanaClient();
+  const { rpc, urlOrMoniker } = useSolanaClient();
 
   if (abortSignal) {
     // @ts-expect-error the `abortSignal` was stripped from the type but is now being added back in
@@ -115,6 +115,7 @@ export function useTokenAccount<TConfig extends RpcConfig = RpcConfig, TAddress
     },
     queryKey: [
       GILL_HOOK_CLIENT_KEY,
+      urlOrMoniker,
       "getTokenAccount",
       hasDeclaredAta(tokenAccountOptions)
         ? [{ ata: tokenAccountOptions.ata }]

+ 7 - 7
packages/react/src/hooks/token-mint.ts

@@ -1,14 +1,14 @@
 "use client";
 
 import { useQuery } from "@tanstack/react-query";
-import { GILL_HOOK_CLIENT_KEY } from "../const.js";
-import { useSolanaClient } from "./client.js";
-import type { GillUseRpcHook } from "./types.js";
-
 import type { Account, Address, FetchAccountConfig, Simplify } from "gill";
 import { assertAccountExists, fetchEncodedAccount } from "gill";
 import { decodeMint, type Mint } from "gill/programs";
 
+import { GILL_HOOK_CLIENT_KEY } from "../const.js";
+import { useSolanaClient } from "./client.js";
+import type { GillUseRpcHook } from "./types.js";
+
 type RpcConfig = Simplify<Omit<FetchAccountConfig, "abortSignal">>;
 
 type UseTokenMintResponse<TAddress extends string = string> = Simplify<
@@ -24,7 +24,7 @@ type UseTokenMintInput<
   /**
    * Address of the Mint account to get and decode
    */
-  mint: TAddress | Address<TAddress>;
+  mint: Address<TAddress> | TAddress;
 };
 
 /**
@@ -36,7 +36,7 @@ export function useTokenMint<TConfig extends RpcConfig = RpcConfig, TAddress ext
   abortSignal,
   mint,
 }: UseTokenMintInput<TConfig, TAddress>) {
-  const { rpc } = useSolanaClient();
+  const { rpc, urlOrMoniker } = useSolanaClient();
 
   if (abortSignal) {
     // @ts-expect-error we stripped the `abortSignal` from the type but are now adding it back in
@@ -50,12 +50,12 @@ export function useTokenMint<TConfig extends RpcConfig = RpcConfig, TAddress ext
     networkMode: "offlineFirst",
     ...options,
     enabled: !!mint,
-    queryKey: [GILL_HOOK_CLIENT_KEY, "getMintAccount", mint],
     queryFn: async () => {
       const account = await fetchEncodedAccount(rpc, mint as Address<TAddress>, config);
       assertAccountExists(account);
       return decodeMint(account);
     },
+    queryKey: [GILL_HOOK_CLIENT_KEY, urlOrMoniker, "getMintAccount", mint],
   });
   return {
     ...rest,

+ 4 - 3
packages/react/src/hooks/transaction.ts

@@ -2,6 +2,7 @@
 
 import { useQuery } from "@tanstack/react-query";
 import type { GetTransactionApi, Signature, Simplify } from "gill";
+
 import { GILL_HOOK_CLIENT_KEY } from "../const.js";
 import { useSolanaClient } from "./client.js";
 import type { GillUseRpcHook } from "./types.js";
@@ -31,23 +32,23 @@ export function useTransaction<TConfig extends RpcConfig = RpcConfig>({
   abortSignal,
   signature,
 }: UseTransactionInput<TConfig>) {
-  const { rpc } = useSolanaClient();
+  const { rpc, urlOrMoniker } = useSolanaClient();
   const { data, ...rest } = useQuery({
     networkMode: "offlineFirst",
     ...options,
     enabled: !!signature,
-    queryKey: [GILL_HOOK_CLIENT_KEY, "getTransaction", signature],
     queryFn: async () => {
       const response = await rpc
         .getTransaction(signature as Signature, {
+          encoding: "json",
           // set default values for better DX
           maxSupportedTransactionVersion: 0,
-          encoding: "json",
           ...(config || {}),
         })
         .send({ abortSignal });
       return response;
     },
+    queryKey: [GILL_HOOK_CLIENT_KEY, urlOrMoniker, "getTransaction", signature],
   });
   return {
     ...rest,