> ## Documentation Index
> Fetch the complete documentation index at: https://docs.socket.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Read Soroban Smart Contract State with SocketFi SDK

> Guide to reading on-chain data from Soroban contracts — balances, metadata, protocol state — with caching, React Query integration, and error handling.

Most of what users see in a wallet-powered application is read-only data — balances, token metadata, staking positions, governance proposals, protocol statistics. SocketFi provides a single method for all of it: `socketfi.readContract()`. Unlike `requestTransaction()`, reads run as simulations: no transaction is submitted, no user signature is required, and nothing on-chain changes. This guide shows you how to use `readContract()` effectively, how to cache results, how to refresh data after a transaction, and how to handle errors.

## readContract() vs requestTransaction()

Use this table to decide which method to call:

| Operation            | Method                 | Requires approval? | Changes state? |
| -------------------- | ---------------------- | ------------------ | -------------- |
| Get token balance    | `readContract()`       | No                 | No             |
| Get token metadata   | `readContract()`       | No                 | No             |
| Read protocol config | `readContract()`       | No                 | No             |
| Transfer tokens      | `requestTransaction()` | **Yes**            | **Yes**        |
| Deposit to vault     | `requestTransaction()` | **Yes**            | **Yes**        |
| Cast a vote          | `requestTransaction()` | **Yes**            | **Yes**        |

If in doubt: reads are free and instant; transactions require a user signature and network fees.

## Basic read example

```typescript theme={null}
const balance = await socketfi.readContract({
  contractId: TOKEN_CONTRACT_ID,
  method: "balance",
  args: [walletAddress],
});
```

The request structure is straightforward:

```typescript theme={null}
type ReadRequest = {
  contractId: string;   // Soroban contract address
  method: string;       // Contract view function name
  args?: unknown[];     // Positional arguments (optional)
};
```

`readContract()` returns `unknown` — cast the result to a typed interface that matches your contract's ABI.

## Multiple read examples

### Token balance

```typescript src/services/token.ts theme={null}
import { socketfi } from "../socketfi/client";

type BalanceResult = { balance: string };

export async function getTokenBalance(walletAddress: string): Promise<string> {
  const result = await socketfi.readContract({
    contractId: import.meta.env.VITE_TOKEN_CONTRACT_ID,
    method: "balance",
    args: [walletAddress],
  }) as BalanceResult;

  return result.balance;
}
```

### Token metadata

```typescript src/services/token.ts theme={null}
type TokenMetadata = {
  name: string;
  symbol: string;
  decimals: number;
};

export async function getTokenMetadata(): Promise<TokenMetadata> {
  return socketfi.readContract({
    contractId: import.meta.env.VITE_TOKEN_CONTRACT_ID,
    method: "metadata",
  }) as Promise<TokenMetadata>;
}
```

### User staking position

```typescript src/services/staking.ts theme={null}
import { socketfi } from "../socketfi/client";

type StakingPosition = {
  stakedAmount: string;
  rewardsAccrued: string;
  unlocksAt: number; // Unix timestamp
};

export async function getStakingPosition(
  walletAddress: string
): Promise<StakingPosition> {
  return socketfi.readContract({
    contractId: import.meta.env.VITE_STAKING_CONTRACT_ID,
    method: "get_position",
    args: [walletAddress],
  }) as Promise<StakingPosition>;
}
```

### Protocol vault state

```typescript src/services/vault.ts theme={null}
import { socketfi } from "../socketfi/client";

type VaultState = {
  totalAssets: string;
  totalShares: string;
  apy: string;
};

export async function getVaultState(): Promise<VaultState> {
  return socketfi.readContract({
    contractId: import.meta.env.VITE_VAULT_CONTRACT_ID,
    method: "get_vault",
  }) as Promise<VaultState>;
}
```

### NFT ownership

```typescript src/services/nft.ts theme={null}
import { socketfi } from "../socketfi/client";

export async function getNftOwner(tokenId: string): Promise<string> {
  const result = await socketfi.readContract({
    contractId: import.meta.env.VITE_NFT_CONTRACT_ID,
    method: "owner",
    args: [tokenId],
  }) as { owner: string };

  return result.owner;
}
```

### Governance proposal

```typescript src/services/governance.ts theme={null}
import { socketfi } from "../socketfi/client";

type Proposal = {
  id: string;
  title: string;
  votesFor: string;
  votesAgainst: string;
  expiresAt: number;
};

export async function getProposal(proposalId: string): Promise<Proposal> {
  return socketfi.readContract({
    contractId: import.meta.env.VITE_GOVERNANCE_CONTRACT_ID,
    method: "proposal",
    args: [proposalId],
  }) as Promise<Proposal>;
}
```

## Caching strategies

### React Query integration

React Query is the recommended caching layer for production web apps. It handles loading state, error state, background refetching, and cache invalidation automatically.

```bash theme={null}
npm install @tanstack/react-query
```

Set up the provider once at your app root:

```typescript src/main.tsx theme={null}
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AuthProvider } from "./auth/provider";
import App from "./App";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 30_000,      // consider data fresh for 30 seconds
      gcTime: 5 * 60_000,     // keep in cache for 5 minutes
      retry: 2,
    },
  },
});

export default function Root() {
  return (
    <QueryClientProvider client={queryClient}>
      <AuthProvider>
        <App />
      </AuthProvider>
    </QueryClientProvider>
  );
}
```

Use it in components:

```typescript src/components/BalanceDisplay.tsx theme={null}
import { useQuery } from "@tanstack/react-query";
import { getTokenBalance } from "../services/token";

export function BalanceDisplay({ walletAddress }: { walletAddress: string }) {
  const { data: balance, isLoading, error } = useQuery({
    queryKey: ["balance", walletAddress],
    queryFn: () => getTokenBalance(walletAddress),
    enabled: !!walletAddress,
    staleTime: 30_000,
  });

  if (isLoading) return <p>Loading balance…</p>;
  if (error) return <p>Could not load balance. <button>Retry</button></p>;

  return <p>Balance: {balance}</p>;
}
```

### Loading multiple data points in parallel

Combine multiple reads into a single dashboard query using `Promise.all` to avoid sequential waterfall requests:

```typescript src/hooks/useDashboard.ts theme={null}
import { useQuery } from "@tanstack/react-query";
import { getTokenBalance } from "../services/token";
import { getStakingPosition } from "../services/staking";
import { getVaultState } from "../services/vault";

export function useDashboard(walletAddress: string) {
  return useQuery({
    queryKey: ["dashboard", walletAddress],
    queryFn: async () => {
      const [balance, staking, vault] = await Promise.all([
        getTokenBalance(walletAddress),
        getStakingPosition(walletAddress),
        getVaultState(),
      ]);
      return { balance, staking, vault };
    },
    enabled: !!walletAddress,
    staleTime: 30_000,
  });
}
```

## Refreshing data after transactions

Prefer **event-driven refreshes** over polling. After a transaction succeeds, invalidate the relevant query keys so React Query immediately re-fetches the affected data.

```typescript src/components/StakeButton.tsx theme={null}
import { useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { stakeTokens } from "../services/staking";
import { useWalletAddress } from "../auth/hooks";

export function StakeButton({ amount }: { amount: bigint }) {
  const walletAddress = useWalletAddress();
  const queryClient = useQueryClient();
  const [loading, setLoading] = useState(false);

  const handleStake = async () => {
    if (!walletAddress) return;
    setLoading(true);
    try {
      await stakeTokens(walletAddress, amount);
      // Invalidate so the balance and staking position refresh immediately
      await queryClient.invalidateQueries({ queryKey: ["balance", walletAddress] });
      await queryClient.invalidateQueries({ queryKey: ["dashboard", walletAddress] });
    } finally {
      setLoading(false);
    }
  };

  return (
    <button onClick={handleStake} disabled={loading}>
      {loading ? "Staking…" : "Stake"}
    </button>
  );
}
```

<Tip>
  Avoid polling with `setInterval` unless you need real-time data (e.g. a live order book). Polling increases RPC load and degrades performance on mobile. Use React Query's `refetchInterval` option only when truly necessary.
</Tip>

## Error handling

Reads can fail — the contract may not exist, the method may be wrong, the arguments may be invalid, or the simulation may time out. Always wrap reads in `try/catch`.

```typescript theme={null}
async function loadBalance(walletAddress: string) {
  try {
    const balance = await socketfi.readContract({
      contractId: TOKEN_CONTRACT_ID,
      method: "balance",
      args: [walletAddress],
    });
    return balance;
  } catch (err: unknown) {
    const message = err instanceof Error ? err.message : "Unknown error";
    console.error(`Failed to read balance: ${message}`);
    throw err; // re-throw so React Query or your UI can handle it
  }
}
```

### Common read errors

| Error              | Likely cause                          | Fix                                     |
| ------------------ | ------------------------------------- | --------------------------------------- |
| Contract not found | Invalid `contractId`                  | Verify the contract address and network |
| Method not found   | Typo in `method` name                 | Check the contract ABI                  |
| Invalid arguments  | Wrong types or wrong number of `args` | Check the function signature            |
| Simulation failure | Contract logic error or stale state   | Retry; contact support if persistent    |

## Service layer pattern

Keep your components clean by moving all `readContract()` calls into a `services/` layer. Components call the service; the service calls the SDK.

```text theme={null}
components/
  └── BalanceDisplay.tsx   → calls useQuery(["balance", …])
hooks/
  └── useDashboard.ts      → composes multiple useQuery calls
services/
  ├── token.ts             → getTokenBalance(), getTokenMetadata()
  ├── staking.ts           → getStakingPosition()
  └── vault.ts             → getVaultState()
```

This separation makes it easy to swap contract addresses, add mocking for tests, and reuse data-fetching logic across multiple components.
