diff --git a/packages/app-extension/src/components/Unlocked/TokenBalances/index.tsx b/packages/app-extension/src/components/Unlocked/TokenBalances/index.tsx index 672f8a3..c16f4dd 100644 --- a/packages/app-extension/src/components/Unlocked/TokenBalances/index.tsx +++ b/packages/app-extension/src/components/Unlocked/TokenBalances/index.tsx @@ -58,8 +58,9 @@ export function TokenBalances() { const swapEnabled = blockchain === Blockchain.SOLANA && !isDevnet; // Use SOLANA or X1 as providerId based on the connection URL + // Note: Backpack GraphQL API accepts "SOLANA" without network suffix const providerId = isSolanaNetwork - ? "SOLANA-mainnet" + ? "SOLANA" : (blockchain.toUpperCase() as ProviderId); return ( diff --git a/packages/app-extension/src/components/Unlocked/index.tsx b/packages/app-extension/src/components/Unlocked/index.tsx index 9f70abf..c55d206 100644 --- a/packages/app-extension/src/components/Unlocked/index.tsx +++ b/packages/app-extension/src/components/Unlocked/index.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense } from "react"; +import { lazy, Suspense, useMemo } from "react"; import { ApolloProvider } from "@apollo/client"; import { BACKPACK_CONFIG_VERSION, createApolloClient } from "@coral-xyz/common"; import { Loading } from "@coral-xyz/react-common"; @@ -35,10 +35,17 @@ function Bootstrap() { function WithApollo({ children }: { children: any }) { const headers = useApolloClientHeaders(); - const apolloClient = createApolloClient( - "backpack-extension", - BACKPACK_CONFIG_VERSION, - headers + + // Memoize Apollo Client to prevent recreation on every render + // Only recreate when headers actually change + const apolloClient = useMemo( + () => createApolloClient( + "backpack-extension", + BACKPACK_CONFIG_VERSION, + headers + ), + [headers] ); + return {children}; } diff --git a/packages/common/src/apollo/index.ts b/packages/common/src/apollo/index.ts index 76c2f45..9a86c49 100644 --- a/packages/common/src/apollo/index.ts +++ b/packages/common/src/apollo/index.ts @@ -14,7 +14,7 @@ import { import { RetryLink } from "@apollo/client/link/retry"; import { LocalStorageWrapper, persistCacheSync } from "apollo3-cache-persist"; -import { BACKEND_API_URL, X1_JSON_SERVER_URL } from "../constants"; +import { BACKEND_API_URL, X1_JSON_SERVER_URL, BACKPACK_GRAPHQL_API_URL } from "../constants"; const cache = new InMemoryCache({ addTypename: true, @@ -276,7 +276,7 @@ export function createApolloClient( headers?: Record ): ApolloClient { const httpLink = createHttpLink({ - uri: `${BACKEND_API_URL}/`, + uri: BACKPACK_GRAPHQL_API_URL, // Use official Backpack GraphQL API headers, }); @@ -285,7 +285,7 @@ export function createApolloClient( console.log("🚀 GraphQL Query:", operation.operationName); console.log("📋 Query:", operation.query.loc?.source.body); console.log("🔧 Variables:", JSON.stringify(operation.variables, null, 2)); - console.log("🌐 URL:", BACKEND_API_URL); + console.log("🌐 GraphQL URL:", BACKPACK_GRAPHQL_API_URL); return forward(operation).map((response) => { console.log("✅ Response for", operation.operationName, ":", response); return response; diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 34f5573..1d2efb3 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -441,6 +441,8 @@ export const BACKEND_API_URL_PROD = "http://162.250.126.66:4000"; export const BACKEND_API_URL_DEV = "http://localhost:4000"; export const BACKEND_API_URL = BACKEND_API_URL_PROD; // Default for build - use production export const X1_JSON_SERVER_URL = "http://162.250.126.66:4000"; +// Official Backpack GraphQL API endpoint for Solana token data +export const BACKPACK_GRAPHQL_API_URL = "https://backpack-api.xnfts.dev/v2/graphql"; export const MESSAGING_COMMUNICATION_PUSH = "MESSAGING_COMMUNICATION_PUSH"; export const MESSAGING_COMMUNICATION_FETCH = "MESSAGINyarG_COMMUNICATION_FETCH"; export const MESSAGING_COMMUNICATION_FETCH_RESPONSE = diff --git a/packages/data-components/src/apollo/gql.ts b/packages/data-components/src/apollo/gql.ts new file mode 100644 index 0000000..7d7613c --- /dev/null +++ b/packages/data-components/src/apollo/gql.ts @@ -0,0 +1,20 @@ +import { DocumentNode } from "@apollo/client"; +import { TypedDocumentNode as DocumentTypeDecoration } from "@graphql-typed-document-node/core"; + +/** + * The gql function is used to parse GraphQL queries into a document that can be used by Apollo Client. + */ +export type TypedDocumentString = + string & { + __apiType?: DocumentTypeDecoration; + }; + +export function gql(source: TemplateStringsArray): DocumentNode; +export function gql(source: string): DocumentNode; +export function gql(source: string | TemplateStringsArray): DocumentNode { + const doc = Array.isArray(source) ? source[0] : source; + + // Use require to avoid bundling issues + const { parse } = require("graphql"); + return parse(doc); +} diff --git a/packages/data-components/src/apollo/graphql.ts b/packages/data-components/src/apollo/graphql.ts index ca36d5e..253d7c2 100644 --- a/packages/data-components/src/apollo/graphql.ts +++ b/packages/data-components/src/apollo/graphql.ts @@ -1,9 +1,94 @@ -// Stub GraphQL types - GraphQL removed -export type ProviderId = "SOLANA" | "ETHEREUM" | "X1"; -export type Transaction = any; -export type TokenBalance = any; -export type GetTokenBalancesQuery = any; +// GraphQL Types for Backpack API +export type ProviderId = + // Backpack GraphQL API (https://backpack-api.xnfts.dev/v2/graphql) accepts: + | "SOLANA" // ✅ For all Solana networks (mainnet/devnet/testnet) + | "ETHEREUM" // ✅ For all Ethereum networks + // X1 REST API (http://162.250.126.66:4000) accepts: + | "X1" // ✅ X1 blockchain (legacy format) + | "X1-mainnet" // ✅ X1 mainnet (with network suffix) + | "X1-testnet" // ✅ X1 testnet (with network suffix) + // TypeScript compatibility (NOT accepted by Backpack GraphQL API): + | "SOLANA-mainnet" // ⚠️ Type-only, use "SOLANA" for actual API calls + | "SOLANA-devnet" // ⚠️ Type-only, use "SOLANA" for actual API calls + | "SOLANA-testnet" // ⚠️ Type-only, use "SOLANA" for actual API calls + | "ETHEREUM-mainnet" // ⚠️ Type-only, use "ETHEREUM" for actual API calls + | "ETHEREUM-goerli" // ⚠️ Type-only, use "ETHEREUM" for actual API calls + | "ETHEREUM-sepolia";// ⚠️ Type-only, use "ETHEREUM" for actual API calls + +export type GetTokenBalancesQueryVariables = { + address: string; + providerId: ProviderId; +}; + +export type TokenListEntry = { + __typename?: "TokenListEntry"; + id: string; + address: string; + decimals: number; + logo?: string | null; + name: string; + symbol: string; +}; + +export type MarketData = { + __typename?: "MarketData"; + id: string; + percentChange?: number | null; + price?: number | null; + value?: number | null; + valueChange?: number | null; +}; + +export type TokenBalance = { + __typename?: "TokenBalance"; + id: string; + address: string; + amount: string; + decimals: number; + displayAmount: string; + marketData?: MarketData | null; + token: string; + tokenListEntry?: TokenListEntry | null; +}; + +export type TokenBalanceEdge = { + __typename?: "TokenBalanceEdge"; + node: TokenBalance; +}; + +export type TokenBalanceConnection = { + __typename?: "TokenBalanceConnection"; + edges: TokenBalanceEdge[]; +}; + +export type BalanceAggregate = { + __typename?: "BalanceAggregate"; + id: string; + percentChange?: number | null; + value: number; + valueChange?: number | null; +}; + +export type Balances = { + __typename?: "Balances"; + id: string; + aggregate?: BalanceAggregate | null; + tokens?: TokenBalanceConnection | null; +}; + +export type Wallet = { + __typename?: "Wallet"; + id: string; + balances?: Balances | null; +}; + +export type GetTokenBalancesQuery = { + __typename?: "Query"; + wallet?: Wallet | null; +}; + +// Additional query types for other components export type GetTransactionsQuery = any; -export type GetNftSpotlightAggregateQuery = any; export type GetTokensForWalletDetailsQuery = any; +export type GetNftSpotlightAggregateQuery = any; export type GetCollectiblesQuery = any; diff --git a/packages/data-components/src/apollo/index.ts b/packages/data-components/src/apollo/index.ts index 04dc0f7..d80e8b6 100644 --- a/packages/data-components/src/apollo/index.ts +++ b/packages/data-components/src/apollo/index.ts @@ -1,3 +1,2 @@ -// Stub apollo client - GraphQL removed -export const gql = (_query: any) => null; -export const useApolloClient = () => ({ query: () => Promise.resolve({ data: null }) }); +export { gql } from "./gql"; +export * from "./graphql"; diff --git a/packages/data-components/src/components/Balances/index.tsx b/packages/data-components/src/components/Balances/index.tsx index 8d5e6b0..56b6a50 100644 --- a/packages/data-components/src/components/Balances/index.tsx +++ b/packages/data-components/src/components/Balances/index.tsx @@ -22,7 +22,9 @@ import { } from "./BalanceSummary"; import { BalancesTable } from "./BalancesTable"; import type { ResponseBalanceSummary, ResponseTokenBalance } from "./utils"; +import { gql } from "../../apollo"; import type { ProviderId } from "../../apollo/graphql"; +import { usePolledSuspenseQuery } from "../../hooks"; import type { DataComponentScreenProps } from "../common"; export { @@ -33,7 +35,52 @@ export { export { BalancesTable } from "./BalancesTable"; export type { ResponseBalanceSummary, ResponseTokenBalance }; -const DEFAULT_POLLING_INTERVAL_SECONDS = 1; +const DEFAULT_POLLING_INTERVAL_SECONDS = 60; + +// GraphQL query for Solana token balances from Backpack API +export const GET_TOKEN_BALANCES_QUERY = gql(` + query GetTokenBalances($address: String!, $providerId: ProviderID!) { + wallet(address: $address, providerId: $providerId) { + id + balances { + id + aggregate { + id + percentChange + value + valueChange + } + tokens { + edges { + node { + id + address + amount + decimals + displayAmount + marketData { + id + percentChange + price + value + valueChange + } + token + tokenListEntry { + id + address + decimals + logo + name + symbol + } + } + } + } + } + } + } +`); export type TokenBalancesProps = DataComponentScreenProps & { address: string; @@ -86,201 +133,212 @@ function _TokenBalances({ hiddenTokenAddresses(providerId.toLowerCase() as Blockchain) ); - // Get connection URL to detect which network we're on - // Always use Blockchain.X1 because the network toggle changes X1's RPC URL, - // not a separate Solana blockchain config - const connectionUrl = useBlockchainConnectionUrl(Blockchain.X1); + // Determine blockchain from providerId + const blockchain = providerId.includes("SOLANA") + ? Blockchain.SOLANA + : providerId.includes("ETHEREUM") + ? Blockchain.ETHEREUM + : Blockchain.X1; + + // Get connection URL for the correct blockchain + const connectionUrl = useBlockchainConnectionUrl(blockchain); const apiUrl = useRecoilValue(backendApiUrl); - const [rawBalances, setRawBalances] = useState([]); + // Detect if this is a Solana network or X1 network + const isSolanaNetwork = blockchain === Blockchain.SOLANA; + const isX1Network = blockchain === Blockchain.X1; + + // Determine the correct providerId based on blockchain + let finalProviderId = providerId; + + // For Solana: Use simple "SOLANA" - Backpack GraphQL API doesn't support network suffixes + if (blockchain === Blockchain.SOLANA) { + finalProviderId = "SOLANA" as ProviderId; + } + // For X1: Detect network from connection URL (X1 REST API supports network suffixes) + else if (connectionUrl && connectionUrl.includes("x1.xyz")) { + if (connectionUrl.includes("testnet")) { + finalProviderId = "X1-testnet" as ProviderId; + } else if (connectionUrl.includes("mainnet")) { + finalProviderId = "X1-mainnet" as ProviderId; + } + } + + // For Solana networks: Use GraphQL + // For X1 networks: Use REST API (x1-json-server) + return isSolanaNetwork ? ( +