From 7362f17ef455eed217ebe129200908f027f161ef Mon Sep 17 00:00:00 2001 From: stackedPenguin <223498471+stackedPenguin@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:59:26 -0600 Subject: [PATCH 1/5] Add Solana native token pricing via Backpack GraphQL API This commit implements dual data sources for token pricing: - Solana networks: Use official Backpack GraphQL API (https://backpack-api.xnfts.dev/v2/graphql) - X1 networks: Continue using x1-json-server REST API Key changes: 1. Created GraphQL types and gql function for Backpack API integration 2. Implemented network-aware TokenBalances component: - SolanaTokenBalances: Uses GraphQL with Apollo Client (60s polling) - X1TokenBalances: Uses REST API with x1-json-server (60s polling) 3. Added backward-compatible ProviderId types (SOLANA, X1, ETHEREUM) 4. Maintained all existing X1 functionality Benefits: - Real-time pricing for ALL Solana SPL tokens (USDC, BONK, JUP, etc.) - Token metadata (logos, names, symbols) from Backpack's registry - 24h price change percentages for Solana tokens - Apollo Client caching for faster subsequent loads - Automatic retry with exponential backoff (10 attempts) - X1 pricing unchanged ($1.00 for XNT) Files modified: - packages/data-components/src/apollo/gql.ts (NEW) - packages/data-components/src/apollo/graphql.ts - packages/data-components/src/apollo/index.ts - packages/data-components/src/components/Balances/index.tsx - packages/data-components/src/components/Balances/utils.ts --- packages/data-components/src/apollo/gql.ts | 20 + .../data-components/src/apollo/graphql.ts | 94 +++- packages/data-components/src/apollo/index.ts | 5 +- .../src/components/Balances/index.tsx | 476 ++++++++++-------- .../src/components/Balances/utils.ts | 9 +- 5 files changed, 372 insertions(+), 232 deletions(-) create mode 100644 packages/data-components/src/apollo/gql.ts 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..99e9a07 100644 --- a/packages/data-components/src/apollo/graphql.ts +++ b/packages/data-components/src/apollo/graphql.ts @@ -1,9 +1,91 @@ -// 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 = + | "SOLANA-mainnet" + | "SOLANA-devnet" + | "SOLANA-testnet" + | "ETHEREUM-mainnet" + | "ETHEREUM-goerli" + | "ETHEREUM-sepolia" + | "X1-mainnet" + | "X1-testnet" + | "SOLANA" // Legacy format for backward compatibility + | "ETHEREUM" // Legacy format for backward compatibility + | "X1"; // Legacy format for backward compatibility + +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..ac19c11 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; @@ -87,200 +134,204 @@ function _TokenBalances({ ); // 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); const apiUrl = useRecoilValue(backendApiUrl); - const [rawBalances, setRawBalances] = useState([]); + // Detect if this is a Solana network or X1 network + const isSolanaNetwork = connectionUrl?.includes("solana") || false; + const isX1Network = connectionUrl?.includes("x1.xyz") || false; + + // Determine the correct providerId based on connection URL + let finalProviderId = providerId; + if (connectionUrl) { + if (connectionUrl.includes("solana")) { + if ( + connectionUrl.includes("mainnet") || + connectionUrl.includes("solana-mainnet") + ) { + finalProviderId = "SOLANA-mainnet" as ProviderId; + } else if (connectionUrl.includes("devnet")) { + finalProviderId = "SOLANA-devnet" as ProviderId; + } else if (connectionUrl.includes("testnet")) { + finalProviderId = "SOLANA-testnet" as ProviderId; + } + } else if (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 ? ( +