Skip to content

React & Wagmi Integration

The Grapevine SDK provides first-class support for React applications with wagmi wallet integration. Build decentralized applications where users connect their own wallets.

Installation

npm
npm install @pinata/grapevine-sdk wagmi viem @tanstack/react-query

Quick Start

1. Configure Wagmi

Set up wagmi with your preferred chains and connectors:

wagmi.config.ts
import { createConfig, http } from 'wagmi';
import { base, baseSepolia } from 'wagmi/chains';
import { injected, walletConnect } from 'wagmi/connectors';
 
const projectId = 'YOUR_WALLET_CONNECT_PROJECT_ID';
 
export const config = createConfig({
  chains: [base, baseSepolia],
  connectors: [
    injected(),
    walletConnect({ projectId }),
  ],
  transports: {
    [base.id]: http(),
    [baseSepolia.id]: http(),
  },
});

2. Wrap Your App

App.tsx
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from './wagmi.config';
 
const queryClient = new QueryClient();
 
function App() {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <YourApp />
      </QueryClientProvider>
    </WagmiProvider>
  );
}

3. Use Grapevine with Wagmi

YourComponent.tsx
import { useGrapevine, useGrapevineReady } from '@pinata/grapevine-sdk/react';
import { useWalletClient, useAccount, useConnect, useDisconnect } from 'wagmi';
import { injected } from 'wagmi/connectors';
 
function GrapevineFeeds() {
  const { address, isConnected } = useAccount();
  const { data: walletClient } = useWalletClient();
  const { connect } = useConnect();
  const { disconnect } = useDisconnect();
  
  // Initialize Grapevine with wagmi
  const grapevine = useGrapevine({
    walletClient,
    network: 'testnet', // or 'mainnet'
    debug: true
  });
  
  // Check if Grapevine is ready
  const isReady = useGrapevineReady(grapevine);
  
  const [feeds, setFeeds] = useState([]);
  
  const loadFeeds = async () => {
    if (!grapevine || !isReady) return;
    
    const result = await grapevine.feeds.list({ page_size: 10 });
    setFeeds(result.data);
  };
  
  const createFeed = async () => {
    if (!grapevine || !isReady) return;
    
    const feed = await grapevine.feeds.create({
      name: `Feed ${Date.now()}`,
      description: 'Created with wagmi',
      tags: ['demo', 'wagmi']
    });
    
    console.log('Created feed:', feed);
    await loadFeeds(); // Refresh list
  };
  
  if (!isConnected) {
    return (
      <button onClick={() => connect({ connector: injected() })}>
        Connect Wallet
      </button>
    );
  }
  
  return (
    <div>
      <p>Connected: {address}</p>
      <button onClick={() => disconnect()}>Disconnect</button>
      
      {isReady ? (
        <>
          <button onClick={createFeed}>Create Feed</button>
          <button onClick={loadFeeds}>Load Feeds</button>
          
          <ul>
            {feeds.map(feed => (
              <li key={feed.id}>{feed.name}</li>
            ))}
          </ul>
        </>
      ) : (
        <p>Initializing Grapevine...</p>
      )}
    </div>
  );
}

React Hooks

useGrapevine

Main hook for creating a Grapevine client with wagmi integration.

const grapevine = useGrapevine({
  walletClient,  // From useWalletClient()
  network,       // 'testnet' | 'mainnet'
  debug?         // Enable debug logging
});

Note: The wallet address is automatically extracted from walletClient.account.address.

Returns: GrapevineClient | null

useGrapevineReady

Check if the Grapevine client is initialized and ready to use.

const isReady = useGrapevineReady(grapevine);

Returns: boolean - true if client is not null

useGrapevineWalletReady

Check if the Grapevine client has a wallet configured and is ready for authenticated requests.

const hasWallet = useGrapevineWalletReady(grapevine);

Returns: boolean - true if client exists AND has a wallet configured

Usage:
import { useGrapevine, useGrapevineReady, useGrapevineWalletReady } from '@pinata/grapevine-sdk/react';
import { useWalletClient } from 'wagmi';
 
function MyComponent() {
  const { data: walletClient } = useWalletClient();
  const grapevine = useGrapevine({ walletClient, network: 'testnet' });
  
  const isReady = useGrapevineReady(grapevine);      // Client initialized
  const hasWallet = useGrapevineWalletReady(grapevine); // Wallet connected
  
  // Reading data - only need isReady
  const loadFeeds = async () => {
    if (!isReady) return;
    const feeds = await grapevine.feeds.list();
  };
  
  // Writing data - need hasWallet
  const createFeed = async () => {
    if (!hasWallet) {
      alert('Please connect your wallet');
      return;
    }
    const feed = await grapevine.feeds.create({ name: 'My Feed' });
  };
  
  return (
    <div>
      <button onClick={loadFeeds} disabled={!isReady}>
        Load Feeds (Public)
      </button>
      <button onClick={createFeed} disabled={!hasWallet}>
        Create Feed (Auth Required)
      </button>
    </div>
  );
}

Complete Example

Here's a full-featured component with wallet connection, feed management, and entry creation:

import React, { useState, useEffect } from 'react';
import { useGrapevine, useGrapevineReady } from '@pinata/grapevine-sdk/react';
import { 
  useAccount, 
  useWalletClient, 
  useConnect, 
  useDisconnect,
  useChainId,
  useSwitchChain 
} from 'wagmi';
import { injected } from 'wagmi/connectors';
import { baseSepolia } from 'wagmi/chains';
 
export function GrapevineManager() {
  const { address, isConnected } = useAccount();
  const { data: walletClient } = useWalletClient();
  const chainId = useChainId();
  const { switchChain } = useSwitchChain();
  const { connect } = useConnect();
  const { disconnect } = useDisconnect();
  
  // Initialize Grapevine
  const grapevine = useGrapevine({
    walletClient,
    network: 'testnet'
  });
  
  const isReady = useGrapevineReady(grapevine);
  
  // State
  const [feeds, setFeeds] = useState([]);
  const [selectedFeed, setSelectedFeed] = useState(null);
  const [entries, setEntries] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  // Check if on correct chain
  const isCorrectChain = chainId === baseSepolia.id;
  
  // Load user's feeds
  const loadMyFeeds = async () => {
    if (!grapevine || !isReady) return;
    
    setLoading(true);
    setError(null);
    
    try {
      const result = await grapevine.feeds.myFeeds();
      setFeeds(result.data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
  
  // Create a new feed
  const createFeed = async () => {
    if (!grapevine || !isReady) return;
    
    const name = prompt('Feed name:');
    if (!name) return;
    
    setLoading(true);
    setError(null);
    
    try {
      const feed = await grapevine.feeds.create({
        name,
        description: prompt('Feed description:') || '',
        tags: (prompt('Tags (comma-separated):') || '').split(',').filter(Boolean)
      });
      
      console.log('Created feed:', feed);
      await loadMyFeeds();
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
  
  // Load entries for a feed
  const loadEntries = async (feedId) => {
    if (!grapevine || !isReady) return;
    
    setLoading(true);
    setError(null);
    
    try {
      const result = await grapevine.entries.list(feedId, { page_size: 20 });
      setEntries(result.data);
      setSelectedFeed(feedId);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
  
  // Add an entry to the selected feed
  const addEntry = async () => {
    if (!grapevine || !isReady || !selectedFeed) return;
    
    const content = prompt('Entry content:');
    if (!content) return;
    
    setLoading(true);
    setError(null);
    
    try {
      const entry = await grapevine.entries.create(selectedFeed, {
        content,
        title: prompt('Entry title:') || undefined,
        description: prompt('Entry description:') || undefined,
        is_free: confirm('Make this entry free?'),
        tags: (prompt('Tags (comma-separated):') || '').split(',').filter(Boolean)
      });
      
      console.log('Created entry:', entry);
      await loadEntries(selectedFeed);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
  
  // Auto-load feeds when ready
  useEffect(() => {
    if (isReady) {
      loadMyFeeds();
    }
  }, [isReady]);
  
  // Render wallet connection
  if (!isConnected) {
    return (
      <div className="p-4">
        <h2 className="text-xl mb-4">Connect Your Wallet</h2>
        <button 
          onClick={() => connect({ connector: injected() })}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          Connect Wallet
        </button>
      </div>
    );
  }
  
  // Check chain
  if (!isCorrectChain) {
    return (
      <div className="p-4">
        <h2 className="text-xl mb-4">Wrong Network</h2>
        <p>Please switch to Base Sepolia</p>
        <button 
          onClick={() => switchChain({ chainId: baseSepolia.id })}
          className="px-4 py-2 bg-orange-500 text-white rounded"
        >
          Switch Network
        </button>
      </div>
    );
  }
  
  // Render main UI
  return (
    <div className="p-4 max-w-6xl mx-auto">
      {/* Header */}
      <div className="mb-6 p-4 bg-gray-100 rounded">
        <h1 className="text-2xl font-bold mb-2">Grapevine Manager</h1>
        <div className="flex justify-between items-center">
          <div>
            <p className="text-sm text-gray-600">Wallet: {address?.slice(0, 8)}...</p>
            <p className="text-sm text-gray-600">Network: Base Sepolia</p>
            <p className="text-sm text-gray-600">
              Status: {isReady ? '✅ Ready' : '⏳ Initializing...'}
            </p>
          </div>
          <button 
            onClick={() => disconnect()}
            className="px-3 py-1 bg-red-500 text-white rounded text-sm"
          >
            Disconnect
          </button>
        </div>
      </div>
      
      {/* Error display */}
      {error && (
        <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
          Error: {error}
        </div>
      )}
      
      {/* Loading indicator */}
      {loading && (
        <div className="mb-4 p-3 bg-blue-100 text-blue-700 rounded">
          Loading...
        </div>
      )}
      
      {/* Main content */}
      <div className="grid grid-cols-2 gap-6">
        {/* Feeds panel */}
        <div className="border rounded p-4">
          <div className="flex justify-between items-center mb-4">
            <h2 className="text-lg font-semibold">My Feeds</h2>
            <button
              onClick={createFeed}
              disabled={!isReady || loading}
              className="px-3 py-1 bg-green-500 text-white rounded text-sm disabled:opacity-50"
            >
              + Create Feed
            </button>
          </div>
          
          <div className="space-y-2">
            {feeds.length === 0 ? (
              <p className="text-gray-500">No feeds yet</p>
            ) : (
              feeds.map(feed => (
                <div
                  key={feed.id}
                  onClick={() => loadEntries(feed.id)}
                  className={`p-3 border rounded cursor-pointer hover:bg-gray-50 ${
                    selectedFeed === feed.id ? 'bg-blue-50 border-blue-500' : ''
                  }`}
                >
                  <h3 className="font-medium">{feed.name}</h3>
                  <p className="text-sm text-gray-600">{feed.description}</p>
                  <p className="text-xs text-gray-500">
                    {feed.total_entries} entries • {feed.is_active ? 'Active' : 'Inactive'}
                  </p>
                </div>
              ))
            )}
          </div>
        </div>
        
        {/* Entries panel */}
        <div className="border rounded p-4">
          <div className="flex justify-between items-center mb-4">
            <h2 className="text-lg font-semibold">
              {selectedFeed ? 'Feed Entries' : 'Select a Feed'}
            </h2>
            {selectedFeed && (
              <button
                onClick={addEntry}
                disabled={!isReady || loading}
                className="px-3 py-1 bg-green-500 text-white rounded text-sm disabled:opacity-50"
              >
                + Add Entry
              </button>
            )}
          </div>
          
          <div className="space-y-2">
            {!selectedFeed ? (
              <p className="text-gray-500">Select a feed to view entries</p>
            ) : entries.length === 0 ? (
              <p className="text-gray-500">No entries in this feed</p>
            ) : (
              entries.map(entry => (
                <div key={entry.id} className="p-3 border rounded">
                  <h3 className="font-medium">{entry.title || 'Untitled'}</h3>
                  <p className="text-sm text-gray-600 line-clamp-2">
                    {entry.description || 'No description'}
                  </p>
                  <p className="text-xs text-gray-500">
                    {entry.is_free ? '🆓 Free' : '💰 Paid'}
                    {entry.mime_type}
                    {new Date(entry.created_at * 1000).toLocaleDateString()}
                  </p>
                </div>
              ))
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

Advanced Patterns

Error Handling

const [error, setError] = useState<Error | null>(null);
 
const handleGrapevineAction = async () => {
  try {
    setError(null);
    const result = await grapevine.feeds.create({ name: 'Test' });
    // Handle success
  } catch (err) {
    setError(err);
    
    // Check specific error types
    if (err.message.includes('402')) {
      console.log('Payment required');
    } else if (err.message.includes('401')) {
      console.log('Authentication failed');
    }
  }
};

Pagination with React Query

import { useInfiniteQuery } from '@tanstack/react-query';
 
function useFeedEntries(grapevine, feedId) {
  return useInfiniteQuery({
    queryKey: ['entries', feedId],
    queryFn: async ({ pageParam }) => {
      if (!grapevine) throw new Error('Grapevine not initialized');
      
      return grapevine.entries.list(feedId, {
        page_token: pageParam,
        page_size: 20
      });
    },
    initialPageParam: undefined,
    getNextPageParam: (lastPage) => {
      if (lastPage.has_more) {
        return lastPage.next_page_token;
      }
      return undefined;
    },
    enabled: !!grapevine && !!feedId,
  });
}

Auto-refresh with SWR

import useSWR from 'swr';
 
function useMyFeeds(grapevine) {
  const { data, error, mutate } = useSWR(
    grapevine ? 'my-feeds' : null,
    () => grapevine.feeds.myFeeds(),
    {
      refreshInterval: 30000, // Refresh every 30 seconds
      revalidateOnFocus: true,
    }
  );
  
  return {
    feeds: data || [],
    isLoading: !error && !data,
    isError: error,
    refresh: mutate
  };
}

TypeScript Support

The SDK provides full TypeScript support:

import type { Feed, Entry, GrapevineClient } from '@pinata/grapevine-sdk';
 
interface GrapevineContextValue {
  client: GrapevineClient | null;
  isReady: boolean;
  feeds: Feed[];
  entries: Map<string, Entry[]>;
}

Testing

Mock wagmi hooks for testing:

// __tests__/GrapevineComponent.test.tsx
import { renderHook } from '@testing-library/react';
import { useGrapevine } from '@pinata/grapevine-sdk/react';
 
jest.mock('wagmi', () => ({
  useWalletClient: () => ({ data: mockWalletClient }),
  useAccount: () => ({ address: '0x123...', isConnected: true }),
}));
 
test('initializes grapevine client', () => {
  const { result } = renderHook(() => 
    useGrapevine({
      walletClient: mockWalletClient,
      network: 'testnet'
    })
  );
  
  expect(result.current).toBeDefined();
});

Common Issues

Wallet Not Connecting

Ensure wagmi is properly configured with supported chains:

// Must include the chain you're using
const config = createConfig({
  chains: [base, baseSepolia], // Include both for flexibility
  // ...
});

Client Not Initializing

Check that walletClient is available (address is automatically extracted from walletClient.account.address):

const grapevine = useGrapevine({
  walletClient, // Must be defined
  network: 'testnet'
});
 
// Debug
console.log('Wallet client:', walletClient);
console.log('Grapevine ready:', isReady);

Wrong Network

Handle network switching:

const { switchChain } = useSwitchChain();
const chainId = useChainId();
 
// Base Sepolia = 84532, Base = 8453
const correctChainId = network === 'testnet' ? 84532 : 8453;
 
if (chainId !== correctChainId) {
  switchChain({ chainId: correctChainId });
}

Next Steps