Chapter 6: Integrating The Graph with Frontend (React + Ethers.js)

Chapter 6: Integrating The Graph with Frontend (React + Ethers.js)

Integrating The Graph with a frontend using React and Ethers.js involves querying a subgraph to fetch decentralized data and displaying it in a user interface.

6.1 Setting Up the Project

Ensure you have a React project set up. Install necessary dependencies:

bash

npm create vite@latest my-dapp -- --template react
cd my-dapp
npm install ethers @apollo/client graphql
  • Ethers.js: Interacts with Ethereum blockchain.
  • @apollo/client: Queries The Graph’s subgraph via GraphQL.

6.2 Understanding The Graph

The Graph is a decentralized protocol for indexing and querying blockchain data. You’ll need a deployed subgraph (created via The Graph’s hosted service or a decentralized network). For this example, assume you have a subgraph for a dApp tracking token transfers, with a schema like:

graphql

type Transfer @entity {
  id: ID!
  from: Bytes!
  to: Bytes!
  amount: BigInt!
  timestamp: BigInt!
}

The subgraph exposes a GraphQL endpoint, e.g., https://api.thegraph.com/subgraphs/name/your-username/your-subgraph.

6.3 Configuring Apollo Client

Set up Apollo Client to query the subgraph. Create a file src/apollo/client.js:

javascript

import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://api.thegraph.com/subgraphs/name/your-username/your-subgraph',
  cache: new InMemoryCache(),
});

export default client;

6.4 Connecting to Ethereum with Ethers.js

Use Ethers.js to interact with the user’s wallet (e.g., MetaMask) and smart contracts. Create a utility file src/utils/web3.js:

javascript

import { ethers } from 'ethers';

export const connectWallet = async () => {
  if (window.ethereum) {
    const provider = new ethers.BrowserProvider(window.ethereum);
    await provider.send('eth_requestAccounts', []);
    const signer = await provider.getSigner();
    const address = await signer.getAddress();
    return { provider, signer, address };
  } else {
    throw new Error('Please install MetaMask');
  }
};

6.5 Querying the Subgraph

Write a GraphQL query to fetch data. Create src/queries/transfers.js:

javascript

import { gql } from '@apollo/client';

export const GET_TRANSFERS = gql`
  query GetTransfers($first: Int, $orderBy: String, $orderDirection: String) {
    transfers(first: $first, orderBy: $orderBy, orderDirection: $orderDirection) {
      id
      from
      to
      amount
      timestamp
    }
  }
`;

6.6 Building the React Component

Create a component to display transfer data and interact with the wallet. Edit src/App.jsx:

javascript

import { useState, useEffect } from 'react';
import { useQuery } from '@apollo/client';
import { GET_TRANSFERS } from './queries/transfers';
import { connectWallet } from './utils/web3';
import './App.css';

function App() {
  const [wallet, setWallet] = useState(null);
  const { loading, error, data } = useQuery(GET_TRANSFERS, {
    variables: { first: 10, orderBy: 'timestamp', orderDirection: 'desc' },
  });

  const handleConnect = async () => {
    try {
      const { address } = await connectWallet();
      setWallet(address);
    } catch (err) {
      console.error(err);
      alert(err.message);
    }
  };

  return (
    <div>
      <h1>Token Transfers</h1>
      {wallet ? (
        <p>Connected: {wallet}</p>
      ) : (
        <button onClick={handleConnect}>Connect Wallet</button>
      )}
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error.message}</p>}
      {data && (
        <table>
          <thead>
            <tr>
              <th>ID</th>
              <th>From</th>
              <th>To</th>
              <th>Amount</th>
              <th>Timestamp</th>
            </tr>
          </thead>
          <tbody>
            {data.transfers.map((transfer) => (
              <tr key={transfer.id}>
                <td>{transfer.id}</td>
                <td>{transfer.from}</td>
                <td>{transfer.to}</td>
                <td>{transfer.amount}</td>
                <td>{new Date(Number(transfer.timestamp) * 1000).toLocaleString()}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

export default App;

6.7 Styling (Optional)

Add basic styles in src/App.css:

css

table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 20px;
}
th, td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}
th {
  background-color: #f2f2f2;
}
button {
  padding: 10px 20px;
  font-size: 16px;
}

6.8 Running the App

Start the development server:

bash

npm run dev

Visit http://localhost:5173 to see the app. It will:

  • Allow wallet connection via MetaMask.
  • Fetch and display the latest 10 token transfers from the subgraph.
  • Show loading or error states as needed.

6.9 Advanced Integration (Optional)

  • Filtering: Add input fields to filter transfers by from or to address.javascript
const [filter, setFilter] = useState('');
const { data } = useQuery(GET_TRANSFERS, {
  variables: {
    first: 10,
    orderBy: 'timestamp',
    orderDirection: 'desc',
    where: filter ? { from: filter } : {},
  },
});
  • Real-Time Updates: Use Apollo’s subscribeToMore for live updates if the subgraph supports subscriptions.
  • Contract Interaction: Add a form to call a smart contract function (e.g., initiate a transfer) using Ethers.js.

6.10 Best Practices

  • Error Handling: Gracefully handle network errors or missing MetaMask.
  • Security: Validate user inputs and sanitize data.
  • Optimization: Use pagination (skip, first) for large datasets.
  • Environment Variables: Store subgraph URLs in .env: bash
VITE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/your-username/your-subgraph

6.11 Troubleshooting

  • Subgraph Errors: Verify the GraphQL endpoint and schema in The Graph’s playground.
  • Wallet Issues: Ensure MetaMask is installed and connected to the correct network.
  • CORS: If the subgraph rejects queries, check the hosted service status or use a decentralized endpoint.

This setup provides a solid foundation for integrating The Graph with a React frontend using Ethers.js.

Apollo Client setup in React

Setting up Apollo Client in a React project to query a GraphQL API (like The Graph) is straightforward.

1. Install Dependencies

Ensure your React project is set up, then install the required packages:

bash

npm install @apollo/client graphql
  • @apollo/client: Provides tools for querying GraphQL APIs.
  • graphql: Parses GraphQL queries.

2. Configure Apollo Client

Create a file to initialize Apollo Client, e.g., src/apollo/client.js:

javascript

import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://api.thegraph.com/subgraphs/name/your-username/your-subgraph', // Replace with your GraphQL endpoint
  cache: new InMemoryCache(),
});

export default client;
  • uri: The GraphQL API endpoint (e.g., a subgraph URL).
  • cache: InMemoryCache stores query results to optimize performance.

3. Integrate with React

Wrap your app with ApolloProvider to make the client available to all components. Update src/main.jsx (or src/index.js for older setups):

javascript

import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import App from './App';
import client from './apollo/client';

ReactDOM.createRoot(document.getElementById('root')).render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);
  • ApolloProvider: Passes the Apollo Client instance to React’s context, enabling hooks like useQuery.

4. Example: Querying Data

Create a component to fetch data using Apollo Client. For example, querying a subgraph for token transfers. First, define the query in src/queries/transfers.js:

javascript

import { gql } from '@apollo/client';

export const GET_TRANSFERS = gql`
  query GetTransfers($first: Int) {
    transfers(first: $first, orderBy: timestamp, orderDirection: desc) {
      id
      from
      to
      amount
      timestamp
    }
  }
`;

Then, use it in a component, e.g., src/App.jsx:

javascript

import { useQuery } from '@apollo/client';
import { GET_TRANSFERS } from './queries/transfers';

function App() {
  const { loading, error, data } = useQuery(GET_TRANSFERS, {
    variables: { first: 5 },
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h1>Token Transfers</h1>
      <ul>
        {data.transfers.map((transfer) => (
          <li key={transfer.id}>
            {transfer.from} → {transfer.to}: {transfer.amount}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;
  • useQuery: Executes the GraphQL query and returns loading, error, and data.
  • variables: Pass dynamic values (e.g., first: 5) to control query results.

5. Best Practices

  • Environment Variables: Store the GraphQL URI in a .env file for security: bash
VITE_GRAPHQL_URI=https://api.thegraph.com/subgraphs/name/your-username/your-subgraph

Update client.js: javascript

const client = new ApolloClient({
  uri: import.meta.env.VITE_GRAPHQL_URI,
  cache: new InMemoryCache(),
});
  • Error Handling: Display user-friendly messages for network or query errors.
  • Cache Configuration: Customize InMemoryCache if your data requires specific merge strategies (e.g., for pagination): javascript
const client = new ApolloClient({
  uri: import.meta.env.VITE_GRAPHQL_URI,
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          transfers: {
            merge(existing = [], incoming) {
              return [...existing, ...incoming];
            },
          },
        },
      },
    },
  }),
});
  • Query Optimization: Use fetchPolicy (e.g., cache-and-network) to balance performance and freshness: javascript
useQuery(GET_TRANSFERS, {
  variables: { first: 5 },
  fetchPolicy: 'cache-and-network',
});

6. Testing the Setup

Run your app:

bash

npm run dev

Visit http://localhost:5173 to see the queried data rendered. If the endpoint is correct and the subgraph is active, you’ll see the list of transfers.

7. Troubleshooting

  • Invalid URI: Verify the GraphQL endpoint in The Graph’s playground.
  • CORS Issues: Ensure the subgraph allows queries from your app’s origin.
  • Query Errors: Check query syntax and schema alignment in the subgraph’s documentation.

This setup enables your React app to query GraphQL APIs efficiently with Apollo Client.

Fetching and rendering subgraph data

Fetching and rendering data from a subgraph in a React app using Apollo Client involves defining a GraphQL query, executing it with React hooks, and displaying the results in the UI. Below is a concise guide tailored to your context of integrating The Graph with a React frontend, assuming you’re querying a subgraph for token transfers.

1. Define the GraphQL Query

Create a file for your query, e.g., src/queries/transfers.js. Define a query to fetch data from the subgraph:

javascript

import { gql } from '@apollo/client';

export const GET_TRANSFERS = gql`
  query GetTransfers($first: Int, $orderBy: String, $orderDirection: String) {
    transfers(first: $first, orderBy: $orderBy, orderDirection: $orderDirection) {
      id
      from
      to
      amount
      timestamp
    }
  }
`;
  • Variables: $first, $orderBy, $orderDirection allow dynamic filtering and sorting.
  • Fields: id, from, to, amount, timestamp match the subgraph’s schema.

2. Set Up Apollo Client

Ensure Apollo Client is configured (as covered previously). Your src/apollo/client.js should look like:

javascript

import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://api.thegraph.com/subgraphs/name/your-username/your-subgraph', // Replace with your subgraph URL
  cache: new InMemoryCache(),
});

export default client;

Wrap your app with ApolloProvider in src/main.jsx:

javascript

import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import App from './App';
import client from './apollo/client';

ReactDOM.createRoot(document.getElementById('root')).render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

3. Fetch Data in a Component

Create a React component to fetch and render the subgraph data. Update src/App.jsx:

javascript

import { useQuery } from '@apollo/client';
import { GET_TRANSFERS } from './queries/transfers';

function App() {
  const { loading, error, data } = useQuery(GET_TRANSFERS, {
    variables: {
      first: 10,
      orderBy: 'timestamp',
      orderDirection: 'desc',
    },
  });

  return (
    <div style={{ padding: '20px' }}>
      <h1>Token Transfers</h1>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error.message}</p>}
      {data && (
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead>
            <tr>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>ID</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>From</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>To</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>Amount</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>Timestamp</th>
            </tr>
          </thead>
          <tbody>
            {data.transfers.map((transfer) => (
              <tr key={transfer.id}>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>{transfer.id}</td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  {transfer.from.slice(0, 6)}...{transfer.from.slice(-4)}
                </td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  {transfer.to.slice(0, 6)}...{transfer.to.slice(-4)}
                </td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>{transfer.amount}</td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  {new Date(Number(transfer.timestamp) * 1000).toLocaleString()}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

export default App;
  • useQuery: Fetches data with the GET_TRANSFERS query and specified variables.
  • Variables: Limits to 10 transfers, sorted by timestamp in descending order.
  • Rendering:
    • Displays a loading state while fetching.
    • Shows an error message if the query fails.
    • Renders a table with transfer data, formatting addresses (e.g., 0x123…abcd) and converting timestamp to a readable date.

4. Styling (Optional)

The inline styles above are basic. For better maintainability, create src/App.css:

css

table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 20px;
}
th,
td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}
th {
  background-color: #f2f2f2;
}

Update App.jsx to import the CSS:

javascript

import './App.css';

5. Run the App

Start the development server:

bash

npm run dev

Visit http://localhost:5173 to see the rendered table of token transfers. The app will:

  • Show “Loading…” while fetching.
  • Display an error if the subgraph URL or query fails.
  • Render the latest 10 transfers with formatted addresses and dates.

6. Enhancements

  • Pagination: Add skip to the query for pagination: javascript
const [page, setPage] = useState(0);
const { loading, error, data } = useQuery(GET_TRANSFERS, {
  variables: { first: 10, skip: page * 10, orderBy: 'timestamp', orderDirection: 'desc' },
});
  • Filtering: Allow users to filter by address: javascript
const [filter, setFilter] = useState('');
const { loading, error, data } = useQuery(GET_TRANSFERS, {
  variables: {
    first: 10,
    orderBy: 'timestamp',
    orderDirection: 'desc',
    where: filter ? { from: filter } : {},
  },
});

Add an input: javascript

<input
  type="text"
  placeholder="Filter by from address"
  value={filter}
  onChange={(e) => setFilter(e.target.value)}
/>
  • Refetching: Use refetch from useQuery to refresh data: javascript
const { refetch } = useQuery(...);
<button onClick={() => refetch()}>Refresh</button>

7. Best Practices

  • Error Handling: Provide user-friendly error messages, e.g., “Subgraph unavailable, try again later.”
  • Data Formatting: Convert raw values (e.g., amount in wei) to human-readable units using libraries like ethers: javascript
import { ethers } from 'ethers';
// In the table:
<td>{ethers.formatEther(transfer.amount)} ETH</td>
  • Environment Variables: Store the subgraph URL in .env: bash
VITE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/your-username/your-subgraph

8. Troubleshooting

  • No Data: Verify the subgraph URL and schema in The Graph’s playground.
  • Query Errors: Ensure query fields match the subgraph’s schema.
  • Network Issues: Check for CORS or rate-limiting on the subgraph endpoint.

This setup fetches and renders subgraph data efficiently.

Auto-updating UI with polling/subscriptions

Auto-updating a React UI to reflect real-time data from a subgraph can be achieved using polling or subscriptions with Apollo Client. Polling periodically refetches queries, while subscriptions provide live updates via WebSocket (if supported by the subgraph). Below is a concise guide to implement both approaches for your React app querying a subgraph (e.g., for token transfers), building on the previous setup.


1. Prerequisites

  • Apollo Client is set up as described earlier (src/apollo/client.js).
  • A query exists, e.g., src/queries/transfers.js: javascript
import { gql } from '@apollo/client';

export const GET_TRANSFERS = gql`
  query GetTransfers($first: Int, $orderBy: String, $orderDirection: String) {
    transfers(first: $first, orderBy: $orderBy, orderDirection: $orderDirection) {
      id
      from
      to
      amount
      timestamp
    }
  }
`;
  • The subgraph URL supports GraphQL queries (and subscriptions, if using them).

Option 1: Polling

Polling is simpler and works with any GraphQL endpoint, as it repeatedly executes the query at a set interval.

2. Implement Polling

Update your component (src/App.jsx) to enable polling with the pollInterval option in useQuery:

javascript

import { useQuery } from '@apollo/client';
import { GET_TRANSFERS } from './queries/transfers';

function App() {
  const { loading, error, data } = useQuery(GET_TRANSFERS, {
    variables: {
      first: 10,
      orderBy: 'timestamp',
      orderDirection: 'desc',
    },
    pollInterval: 5000, // Refetch every 5 seconds
  });

  return (
    <div style={{ padding: '20px' }}>
      <h1>Token Transfers (Polling)</h1>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error.message}</p>}
      {data && (
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead>
            <tr>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>ID</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>From</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>To</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>Amount</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>Timestamp</th>
            </tr>
          </thead>
          <tbody>
            {data.transfers.map((transfer) => (
              <tr key={transfer.id}>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>{transfer.id}</td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  {transfer.from.slice(0, 6)}...{transfer.from.slice(-4)}
                </td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  {transfer.to.slice(0, 6)}...{transfer.to.slice(-4)}
                </td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>{transfer.amount}</td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  {new Date(Number(transfer.timestamp) * 1000).toLocaleString()}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

export default App;
  • pollInterval: Specifies the interval (in milliseconds) to refetch the query (e.g., 5000 = 5 seconds).
  • Behavior: Every 5 seconds, Apollo refetches the GET_TRANSFERS query, updating the UI with new data.

3. Control Polling (Optional)

To toggle polling dynamically (e.g., enable/disable via a button):

javascript

import { useState } from 'react';
import { useQuery } from '@apollo/client';
import { GET_TRANSFERS } from './queries/transfers';

function App() {
  const [isPolling, setIsPolling] = useState(false);
  const { loading, error, data, startPolling, stopPolling } = useQuery(GET_TRANSFERS, {
    variables: {
      first: 10,
      orderBy: 'timestamp',
      orderDirection: 'desc',
    },
    pollInterval: isPolling ? 5000 : 0, // 0 disables polling
  });

  const togglePolling = () => {
    if (isPolling) {
      stopPolling();
    } else {
      startPolling(5000);
    }
    setIsPolling(!isPolling);
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>Token Transfers (Polling)</h1>
      <button onClick={togglePolling}>
        {isPolling ? 'Stop Polling' : 'Start Polling'}
      </button>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error.message}</p>}
      {data && (
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          {/* Table content same as above */}
        </table>
      )}
    </div>
  );
}

export default App;
  • startPolling/stopPolling: Methods from useQuery to control polling programmatically.
  • State: isPolling tracks whether polling is active.

4. Polling Pros and Cons

  • Pros: Simple, works with any GraphQL endpoint, no server-side changes needed.
  • Cons: Inefficient for frequent updates (repeated queries), higher network usage.

Option 2: Subscriptions

Subscriptions use WebSockets for real-time updates, pushing new data from the server when events occur. Note: The Graph’s hosted service doesn’t support subscriptions, but decentralized networks or custom deployments might.

5. Check Subgraph Support

Verify if your subgraph supports subscriptions by checking its schema in the GraphQL playground. A subscription might look like:

graphql

subscription OnTransfer {
  transfers(orderBy: timestamp, orderDirection: desc) {
    id
    from
    to
    amount
    timestamp
  }
}

If supported, the endpoint will use wss:// (WebSocket) instead of https://.

6. Update Apollo Client for Subscriptions

Install the WebSocket dependency:

bash

npm install @apollo/client subscriptions-transport-ws

Modify src/apollo/client.js to support both HTTP and WebSocket links:

javascript

import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';

// HTTP link for queries and mutations
const httpLink = new HttpLink({
  uri: 'https://api.thegraph.com/subgraphs/name/your-username/your-subgraph',
});

// WebSocket link for subscriptions
const wsLink = new WebSocketLink({
  uri: 'wss://api.thegraph.com/subgraphs/name/your-username/your-subgraph', // Replace with WebSocket URL
  options: {
    reconnect: true, // Auto-reconnect on disconnect
  },
});

// Split link: Use wsLink for subscriptions, httpLink for others
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink
);

const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});

export default client;
  • HttpLink: Handles queries and mutations.
  • WebSocketLink: Handles subscriptions via WebSocket.
  • splitLink: Routes requests based on operation type.

7. Define the Subscription

Create src/queries/subscriptions.js:

javascript

import { gql } from '@apollo/client';

export const ON_TRANSFER = gql`
  subscription OnTransfer {
    transfers(orderBy: timestamp, orderDirection: desc) {
      id
      from
      to
      amount
      timestamp
    }
  }
`;

8. Implement Subscription in Component

Update src/App.jsx to use useSubscription:

javascript

import { useState } from 'react';
import { useSubscription } from '@apollo/client';
import { ON_TRANSFER } from './queries/subscriptions';

function App() {
  const [transfers, setTransfers] = useState([]);
  const { loading, error } = useSubscription(ON_TRANSFER, {
    onData: ({ data }) => {
      if (data?.data?.transfers) {
        setTransfers((prev) => [...data.data.transfers, ...prev].slice(0, 10)); // Keep latest 10
      }
    },
  });

  return (
    <div style={{ padding: '20px' }}>
      <h1>Token Transfers (Subscription)</h1>
      {loading && <p>Connecting...</p>}
      {error && <p>Error: {error.message}</p>}
      {transfers.length > 0 && (
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead>
            <tr>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>ID</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>From</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>To</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>Amount</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>Timestamp</th>
            </tr>
          </thead>
          <tbody>
            {transfers.map((transfer) => (
              <tr key={transfer.id}>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>{transfer.id}</td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  {transfer.from.slice(0, 6)}...{transfer.from.slice(-4)}
                </td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  {transfer.to.slice(0, 6)}...{transfer.to.slice(-4)}
                </td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>{transfer.amount}</td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  {new Date(Number(transfer.timestamp) * 1000).toLocaleString()}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

export default App;
  • useSubscription: Listens for new transfers events.
  • onData: Updates the transfers state with new data, maintaining the latest 10 entries.
  • State: transfers stores the received data for rendering.

9. Subscriptions Pros and Cons

  • Pros: Efficient, real-time updates with minimal network overhead.
  • Cons: Requires WebSocket support, not available in The Graph’s hosted service, more complex setup.

10. Choosing Between Polling and Subscriptions

  • Use Polling if:
    • Your subgraph doesn’t support subscriptions (e.g., The Graph’s hosted service).
    • Simplicity is preferred over efficiency.
    • Updates are infrequent, and a 5–30 second delay is acceptable.
  • Use Subscriptions if:
    • Your subgraph runs on a decentralized network or custom server with WebSocket support.
    • You need instant updates (e.g., for live trading or event feeds).
    • Network efficiency is critical.

11. Best Practices

  • Polling Interval: Set pollInterval to balance freshness and performance (e.g., 5–30 seconds).
  • Error Handling: Display user-friendly messages for WebSocket disconnections or query failures. javascript
{error && <p>Failed to fetch updates. Retrying...</p>}
  • Data Deduplication: Avoid duplicate entries in subscriptions: javascript
setTransfers((prev) => {
  const newTransfers = data.data.transfers.filter(
    (t) => !prev.some((p) => p.id === t.id)
  );
  return [...newTransfers, ...prev].slice(0, 10);
});
  • Environment Variables: Store URLs in .env: bash
VITE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/your-username/your-subgraph
VITE_SUBGRAPH_WS_URL=wss://api.thegraph.com/subgraphs/name/your-username/your-subgraph

12. Testing

Run the app:

bash

npm run dev
  • Polling: The UI updates every 5 seconds with new transfers (simulate by adding data to the subgraph).
  • Subscriptions: New transfers appear instantly if the subgraph pushes updates (test with a compatible endpoint).

13. Troubleshooting

  • Polling Slow: Reduce pollInterval, but monitor server load.
  • Subscription Fails: Confirm the wss:// URL and subgraph schema support subscriptions.
  • Empty Data: Verify the subgraph has recent transfers in the GraphQL playground.
  • WebSocket Errors: Check for CORS or firewall issues blocking WebSocket connections.

This setup enables auto-updating UI with either polling or subscriptions.

Connecting with Ethers.js for on-chain interactions

Connecting a React app to Ethereum using Ethers.js enables on-chain interactions like reading blockchain data or sending transactions (e.g., interacting with a smart contract). Combined with your existing setup for querying a subgraph with Apollo Client, this allows your dApp to both fetch indexed data and perform live blockchain operations. Below is a concise guide to integrate Ethers.js for on-chain interactions in your React frontend, assuming you’re building on the token transfer example.


1. Prerequisites

  • A React app with Apollo Client querying a subgraph (e.g., for token transfers).
  • A deployed smart contract (e.g., an ERC20 token contract) to interact with. For this example, assume a simple ERC20 contract with a transfer(address to, uint256 amount) function.
  • MetaMask or another Web3 wallet installed in the browser.

2. Install Ethers.js

If not already installed, add Ethers.js to your project:

bash

npm install ethers

3. Set Up Ethers.js Utility

Create a utility file to handle wallet connections and contract interactions, e.g., src/utils/web3.js:

javascript

import { ethers } from 'ethers';

// Connect to wallet (MetaMask)
export const connectWallet = async () => {
  if (!window.ethereum) {
    throw new Error('Please install MetaMask');
  }
  const provider = new ethers.BrowserProvider(window.ethereum);
  await provider.send('eth_requestAccounts', []); // Request wallet connection
  const signer = await provider.getSigner();
  const address = await signer.getAddress();
  return { provider, signer, address };
};

// Get contract instance
export const getContract = (address, abi, signer) => {
  return new ethers.Contract(address, abi, signer);
};
  • connectWallet: Connects to MetaMask, returns the provider, signer, and user address.
  • getContract: Creates a contract instance for interactions, requiring the contract address, ABI, and signer.

4. Define Contract ABI

Obtain the ABI for your smart contract. For an ERC20 token, a minimal ABI might include the transfer function. Create src/abis/ERC20.js:

javascript

export const ERC20_ABI = [
  'function transfer(address to, uint256 amount) public returns (bool)',
  'function balanceOf(address account) public view returns (uint256)',
];

Replace with your contract’s full ABI if needed (e.g., from Remix or Hardhat).

5. Create a Component for On-Chain Interactions

Update src/App.jsx to connect the wallet, display the user’s balance, and allow token transfers. Combine this with the subgraph query for a cohesive UI.

javascript

import { useState, useEffect } from 'react';
import { useQuery } from '@apollo/client';
import { ethers } from 'ethers';
import { GET_TRANSFERS } from './queries/transfers';
import { connectWallet, getContract } from './utils/web3';
import { ERC20_ABI } from './abis/ERC20';

const CONTRACT_ADDRESS = '0xYourContractAddressHere'; // Replace with your contract address

function App() {
  const [wallet, setWallet] = useState(null);
  const [balance, setBalance] = useState('0');
  const [recipient, setRecipient] = useState('');
  const [amount, setAmount] = useState('');
  const [contract, setContract] = useState(null);
  const { loading, error, data } = useQuery(GET_TRANSFERS, {
    variables: { first: 10, orderBy: 'timestamp', orderDirection: 'desc' },
    pollInterval: 5000, // Auto-update subgraph data
  });

  // Connect wallet and initialize contract
  const handleConnect = async () => {
    try {
      const { signer, address } = await connectWallet();
      const contractInstance = getContract(CONTRACT_ADDRESS, ERC20_ABI, signer);
      setWallet(address);
      setContract(contractInstance);
    } catch (err) {
      console.error(err);
      alert(err.message);
    }
  };

  // Fetch balance
  useEffect(() => {
    const fetchBalance = async () => {
      if (contract && wallet) {
        try {
          const bal = await contract.balanceOf(wallet);
          setBalance(ethers.formatEther(bal)); // Assuming 18 decimals
        } catch (err) {
          console.error(err);
        }
      }
    };
    fetchBalance();
  }, [contract, wallet]);

  // Send transaction
  const handleTransfer = async () => {
    if (!contract || !recipient || !amount) {
      alert('Please fill all fields');
      return;
    }
    try {
      const tx = await contract.transfer(recipient, ethers.parseEther(amount));
      await tx.wait();
      alert('Transfer successful!');
      setRecipient('');
      setAmount('');
      // Balance will auto-update via useEffect
    } catch (err) {
      console.error(err);
      alert('Transfer failed: ' + err.message);
    }
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>Token dApp</h1>
      {/* Wallet Connection */}
      {wallet ? (
        <div>
          <p>Connected: {wallet.slice(0, 6)}...{wallet.slice(-4)}</p>
          <p>Balance: {balance} Tokens</p>
        </div>
      ) : (
        <button onClick={handleConnect}>Connect Wallet</button>
      )}

      {/* Transfer Form */}
      {wallet && (
        <div style={{ margin: '20px 0' }}>
          <h3>Transfer Tokens</h3>
          <input
            type="text"
            placeholder="Recipient Address"
            value={recipient}
            onChange={(e) => setRecipient(e.target.value)}
            style={{ marginRight: '10px', padding: '5px' }}
          />
          <input
            type="text"
            placeholder="Amount"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
            style={{ marginRight: '10px', padding: '5px' }}
          />
          <button onClick={handleTransfer}>Send</button>
        </div>
      )}

      {/* Subgraph Data */}
      <h3>Recent Transfers</h3>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error.message}</p>}
      {data && (
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead>
            <tr>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>ID</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>From</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>To</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>Amount</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>Timestamp</th>
            </tr>
          </thead>
          <tbody>
            {data.transfers.map((transfer) => (
              <tr key={transfer.id}>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>{transfer.id}</td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  {transfer.from.slice(0, 6)}...{transfer.from.slice(-4)}
                </td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  {transfer.to.slice(0, 6)}...{transfer.to.slice(-4)}
                </td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  {ethers.formatEther(transfer.amount)}
                </td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  {new Date(Number(transfer.timestamp) * 1000).toLocaleString()}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

export default App;

6. Explanation of Key Features

  • Wallet Connection:
    • connectWallet prompts MetaMask to connect, providing a signer for transactions.
    • Stores the user’s address in wallet state.
  • Contract Setup:
    • getContract creates an instance of the ERC20 contract using the ABI and signer.
    • Stored in contract state for reuse.
  • Balance Fetching:
    • useEffect calls balanceOf when contract and wallet are available.
    • Formats the balance (assuming 18 decimals) with ethers.formatEther.
  • Transfer Function:
    • handleTransfer calls the contract’s transfer function with the recipient and amount (parsed to wei with ethers.parseEther).
    • Waits for transaction confirmation and resets the form on success.
  • Subgraph Integration:
    • Fetches recent transfers using the GET_TRANSFERS query (polling every 5 seconds for auto-updates).
    • Formats subgraph amounts with ethers.formatEther for consistency.

7. Run the App

Start the development server:

bash

npm run dev

Visit http://localhost:5173. The app will:

  • Prompt wallet connection via MetaMask.
  • Display the user’s token balance.
  • Allow sending tokens to another address.
  • Show recent transfers from the subgraph, auto-updating every 5 seconds.

8. Enhancements

  • Input Validation:
    • Validate recipient as a valid Ethereum address: javascript
if (!ethers.isAddress(recipient)) {
  alert('Invalid recipient address');
  return;
}
  • Ensure amount is a positive number: javascript
if (isNaN(amount) || Number(amount) <= 0) {
  alert('Invalid amount');
  return;
}
  • Transaction Status:
    • Add a loading state for transactions: javascript
const [isSending, setIsSending] = useState(false);
// In handleTransfer:
setIsSending(true);
try {
  const tx = await contract.transfer(...);
  await tx.wait();
  alert('Transfer successful!');
} finally {
  setIsSending(false);
}

javascript

<button onClick={handleTransfer} disabled={isSending}>
  {isSending ? 'Sending...' : 'Send'}
</button>
  • Event Listening:
    • Listen for Transfer events to update the UI in real-time: javascript
useEffect(() => {
  if (contract) {
    const onTransfer = (from, to, value) => {
      console.log(`Transfer: ${from} -> ${to}, ${ethers.formatEther(value)}`);
      // Update UI or trigger refetch
    };
    contract.on('Transfer', onTransfer);
    return () => contract.off('Transfer', onTransfer);
  }
}, [contract]);
  • Network Switching:
    • Prompt MetaMask to switch to the correct network: javascript
await window.ethereum.request({
  method: 'wallet_switchEthereumChain',
  params: [{ chainId: '0x1' }], // e.g., Mainnet
});

9. Best Practices

  • Security:
    • Validate all inputs to prevent errors or malicious transactions.
    • Use try-catch blocks to handle transaction failures (e.g., insufficient balance).
  • Error Handling:
    • Display user-friendly messages: javascript
alert('Transfer failed: Insufficient balance or network issue');
  • Environment Variables:
    • Store the contract address in .env: bash
VITE_CONTRACT_ADDRESS=0xYourContractAddressHere

Update App.jsx: javascript

const CONTRACT_ADDRESS = import.meta.env.VITE_CONTRACT_ADDRESS;
  • Gas Optimization:
    • Allow users to set gas parameters: javascript
const tx = await contract.transfer(recipient, ethers.parseEther(amount), {
  gasLimit: 100000,
});

10. Troubleshooting

  • MetaMask Not Detected: Ensure window.ethereum exists; prompt users to install MetaMask.
  • Contract Errors: Verify the CONTRACT_ADDRESS and ERC20_ABI match the deployed contract.
  • Transaction Fails: Check for sufficient balance, correct network, or gas issues in MetaMask.
  • Subgraph Lag: New transactions may take seconds to appear in the subgraph due to indexing delays.

11. Testing

  • Connect Wallet: Click “Connect Wallet” and approve in MetaMask.
  • Check Balance: Ensure the balance updates after connection.
  • Send Tokens: Enter a valid address and amount, confirm the transaction, and verify the subgraph reflects it (may require a few seconds).
  • Network: Test on the correct network (e.g., Sepolia for testnets).

This setup integrates Ethers.js for on-chain interactions, complementing the subgraph queries for a full dApp experience. If you need help with specific contract functions, advanced transactions, or debugging, let me know!

Example: Dashboard showing real-time trades from Uniswap Subgraph

Since you’ve previously asked about integrating The Graph with a React frontend and connecting Ethers.js for on-chain interactions, I’ll assume you want a focused example of a dashboard displaying real-time Uniswap V3 trades using a subgraph, building on those concepts. This example will use Apollo Client to query the Uniswap V3 subgraph, display trades in a table, and include auto-updates via polling. I’ll also incorporate an Ethers.js wallet connection to show the user’s address and ETH balance, tying it to the dashboard context. If you have specific requirements (e.g., filters, additional contract interactions), please clarify!


1. Project Setup

Create a React project if you haven’t already:

bash

npm create vite@latest uniswap-dashboard -- --template react
cd uniswap-dashboard
npm install @apollo/client graphql ethers

2. Uniswap V3 Subgraph

We’ll use the Uniswap V3 subgraph hosted by The Graph. Example endpoint (Mainnet):

  • URL: https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3

Verify the schema in The Graph’s playground. We’ll query the swaps entity, which tracks trades.

3. Configure Apollo Client

Set up Apollo Client in src/apollo/client.js:

javascript

import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
  cache: new InMemoryCache(),
});

export default client;

Wrap the app with ApolloProvider in src/main.jsx:

javascript

import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import App from './App';
import client from './apollo/client';

ReactDOM.createRoot(document.getElementById('root')).render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

4. Define GraphQL Query

Create src/queries/swaps.js to fetch recent swaps:

javascript

import { gql } from '@apollo/client';

export const GET_SWAPS = gql`
  query GetSwaps($first: Int, $orderBy: String, $orderDirection: String) {
    swaps(first: $first, orderBy: $orderBy, orderDirection: $orderDirection) {
      id
      pool {
        token0 { symbol }
        token1 { symbol }
      }
      amount0
      amount1
      timestamp
    }
  }
`;
  • Fields: Fetches swap ID, token pair symbols, amounts, and timestamp.
  • Variables: Allows limiting results (first) and sorting (orderBy, orderDirection).

5. Ethers.js Utility

Add src/utils/web3.js for wallet connection and balance fetching:

javascript

import { ethers } from 'ethers';

export const connectWallet = async () => {
  if (!window.ethereum) {
    throw new Error('Please install MetaMask');
  }
  const provider = new ethers.BrowserProvider(window.ethereum);
  await provider.send('eth_requestAccounts', []);
  const signer = await provider.getSigner();
  const address = await signer.getAddress();
  return { provider, signer, address };
};

export const getBalance = async (provider, address) => {
  const balance = await provider.getBalance(address);
  return ethers.formatEther(balance);
};

6. Build the Dashboard Component

Update src/App.jsx to display real-time trades and wallet info:

javascript

import { useState, useEffect } from 'react';
import { useQuery } from '@apollo/client';
import { ethers } from 'ethers';
import { GET_SWAPS } from './queries/swaps';
import { connectWallet, getBalance } from './utils/web3';
import './App.css';

function App() {
  const [wallet, setWallet] = useState(null);
  const [balance, setBalance] = useState('0');
  const { loading, error, data } = useQuery(GET_SWAPS, {
    variables: {
      first: 10,
      orderBy: 'timestamp',
      orderDirection: 'desc',
    },
    pollInterval: 5000, // Update every 5 seconds
  });

  // Connect wallet
  const handleConnect = async () => {
    try {
      const { provider, address } = await connectWallet();
      setWallet(address);
      const ethBalance = await getBalance(provider, address);
      setBalance(ethBalance);
    } catch (err) {
      console.error(err);
      alert(err.message);
    }
  };

  // Update balance on wallet change
  useEffect(() => {
    if (wallet) {
      const provider = new ethers.BrowserProvider(window.ethereum);
      getBalance(provider, wallet).then(setBalance);
    }
  }, [wallet]);

  return (
    <div className="dashboard">
      <h1>Uniswap V3 Real-Time Trades</h1>

      {/* Wallet Info */}
      <div className="wallet-section">
        {wallet ? (
          <div>
            <p>Connected: {wallet.slice(0, 6)}...{wallet.slice(-4)}</p>
            <p>Balance: {parseFloat(balance).toFixed(4)} ETH</p>
          </div>
        ) : (
          <button onClick={handleConnect}>Connect Wallet</button>
        )}
      </div>

      {/* Trades Table */}
      <h3>Recent Trades</h3>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error.message}</p>}
      {data && (
        <table>
          <thead>
            <tr>
              <th>ID</th>
              <th>Pair</th>
              <th>Amount0</th>
              <th>Amount1</th>
              <th>Timestamp</th>
            </tr>
          </thead>
          <tbody>
            {data.swaps.map((swap) => (
              <tr key={swap.id}>
                <td>{swap.id.slice(0, 10)}...</td>
                <td>{swap.pool.token0.symbol}/{swap.pool.token1.symbol}</td>
                <td>{parseFloat(swap.amount0).toFixed(4)}</td>
                <td>{parseFloat(swap.amount1).toFixed(4)}</td>
                <td>{new Date(Number(swap.timestamp) * 1000).toLocaleString()}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

export default App;

7. Add Styling

Create src/App.css for a clean dashboard look:

css

.dashboard {
  max-width: 1000px;
  margin: 0 auto;
  padding: 20px;
}

.wallet-section {
  margin-bottom: 20px;
}

button {
  padding: 8px 16px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background-color: #0056b3;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 20px;
}

th, td {
  border: 1px solid #ddd;
  padding: 10px;
  text-align: left;
}

th {
  background-color: #f2f2f2;
}

tr:nth-child(even) {
  background-color: #f9f9f9;
}

8. Run the App

Start the development server:

bash

npm run dev

Visit http://localhost:5173. The dashboard will:

  • Allow wallet connection via MetaMask, showing the address and ETH balance.
  • Display the 10 most recent Uniswap V3 swaps (token pair, amounts, timestamp).
  • Auto-update every 5 seconds via polling to reflect new trades.

9. Enhancements

  • Filters:
    • Add a dropdown to filter by token pair: javascript
const [pairFilter, setPairFilter] = useState('');
const { loading, error, data } = useQuery(GET_SWAPS, {
  variables: {
    first: 10,
    orderBy: 'timestamp',
    orderDirection: 'desc',
    where: pairFilter ? { pool_: { token0_: { symbol: pairFilter } } } : {},
  },
  pollInterval: 5000,
});

// In JSX:
<input
  type="text"
  placeholder="Filter by token0 symbol (e.g., WETH)"
  value={pairFilter}
  onChange={(e) => setPairFilter(e.target.value)}
/>
  • Pagination:
    • Implement next/previous buttons: javascript
const [skip, setSkip] = useState(0);
const { loading, error, data } = useQuery(GET_SWAPS, {
  variables: { first: 10, skip, orderBy: 'timestamp', orderDirection: 'desc' },
  pollInterval: 5000,
});

// In JSX:
<div>
  <button onClick={() => setSkip((s) => Math.max(0, s - 10))}>Previous</button>
  <button onClick={() => setSkip((s) => s + 10)}>Next</button>
</div>
  • On-Chain Interaction:
    • Add a button to approve Uniswap V3 contracts (requires Uniswap contract ABI and address): javascript
import UNISWAP_ABI from './abis/UniswapV3.json';
const UNISWAP_ADDRESS = '0xYourUniswapContract';

const handleApprove = async () => {
  if (!wallet) return;
  const { signer } = await connectWallet();
  const contract = new ethers.Contract(UNISWAP_ADDRESS, UNISWAP_ABI, signer);
  try {
    const tx = await contract.approve(...); // Add params as needed
    await tx.wait();
    alert('Approved!');
  } catch (err) {
    alert('Approval failed');
  }
};

// In JSX:
<button onClick={handleApprove}>Approve Uniswap</button>
  • Formatting:
    • Use ethers.formatUnits for precise token amounts: javascript
<td>{ethers.formatUnits(swap.amount0, 18)}</td> // Adjust decimals as needed

10. Best Practices

  • Error Handling: Show user-friendly messages: javascript
{error && <p>Failed to load trades. Please try again later.</p>}
  • Environment Variables: Store the subgraph URL in .env: bash
VITE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3

Update client.js: javascript

uri: import.meta.env.VITE_SUBGRAPH_URL,
  • Performance: Limit polling interval (5–30 seconds) to avoid rate limits.
  • Security: Validate wallet addresses and sanitize inputs if adding filters.

11. Troubleshooting

  • No Trades: Verify the subgraph URL and ensure Uniswap V3 has recent activity.
  • Wallet Errors: Check MetaMask is installed and connected to Mainnet.
  • CORS Issues: Use a stable subgraph endpoint or proxy if needed.
  • Data Formatting: Adjust decimals for amount0/amount1 based on token metadata.

12. Testing

  • Wallet: Connect MetaMask, verify address and balance display.
  • Trades: Ensure the table shows recent swaps (e.g., WETH/USDC pairs) and updates every 5 seconds.
  • Network: Test on Mainnet or a testnet with Uniswap V3 deployed (e.g., Arbitrum).

This dashboard provides a real-time view of Uniswap V3 trades with wallet integration.

Comments

No comments yet. Why don’t you start the discussion?

    Leave a Reply

    Your email address will not be published. Required fields are marked *