@thebarmaeffect/barter-react

React hooks and context provider for the BARTER Protocol -- a Proof of Trade system. Build trust-aware dApps with idiomatic React patterns: automatic data fetching, loading states, error handling, and refetch support.
Table of Contents
Features
- Declarative data fetching -- Pass an address or trade ID, get back
{ data, loading, error, refetch }.
- Automatic cleanup -- All hooks cancel in-flight requests on unmount or dependency changes. No stale state.
- Refetch on demand -- Every hook returns a
refetch() function for manual re-queries.
- Context-based configuration -- Configure RPC and network once at the root; all hooks inherit automatically.
- TypeScript-first -- All hooks, return types, and config interfaces are fully typed.
- Lightweight -- No runtime dependencies beyond React and ethers. Tree-shakeable ESM build.
- SSR-safe -- All hooks handle server rendering gracefully with no
window access during SSR.
Installation
npm install @thebarmaeffect/barter-react react ethers
yarn add @thebarmaeffect/barter-react react ethers
pnpm add @thebarmaeffect/barter-react react ethers
Peer Dependencies
react | ^18.0.0 | React framework |
ethers | ^6.0.0 | Ethereum provider and address validation |
Quick Start
Wrap your application in BarterProvider, then use any hook in descendant components.
import { BarterProvider, useBarterScore } from '@thebarmaeffect/barter-react';
function App() {
return (
<BarterProvider config={{ rpcUrl: 'https://rpc.sepolia.org', network: 'sepolia' }}>
<Dashboard />
</BarterProvider>
);
}
function Dashboard() {
const { data: score, loading, error } = useBarterScore('0xAbC123...def456');
if (loading) return <div>Loading trust score...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Trust Score: {score}</div>;
}
API Reference
BarterProvider
Context provider that makes BARTER configuration available to all descendant hooks.
import { BarterProvider } from '@thebarmaeffect/barter-react';
<BarterProvider config={config}>
{children}
</BarterProvider>
Props
config | BarterConfig | No | RPC and network configuration |
children | React.ReactNode | Yes | Child components that can use BARTER hooks |
BarterConfig
interface BarterConfig {
rpcUrl?: string;
network?: string;
}
Example
<BarterProvider>
<App />
</BarterProvider>
<BarterProvider config={{
rpcUrl: 'https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY',
network: 'sepolia',
}}>
<App />
</BarterProvider>
function Root() {
const [network, setNetwork] = useState('sepolia');
return (
<BarterProvider config={{ rpcUrl: RPC_URLS[network], network }}>
<NetworkSelector value={network} onChange={setNetwork} />
<App />
</BarterProvider>
);
}
useBarterConfig
Access the current BarterConfig from the nearest BarterProvider.
import { useBarterConfig } from '@thebarmaeffect/barter-react';
function NetworkInfo() {
const config = useBarterConfig();
return <span>Network: {config.network || 'default'}</span>;
}
Signature
function useBarterConfig(): BarterConfig
Returns: BarterConfig -- The configuration object from the nearest provider, or {} if no provider is found.
useBarterClient
Get the low-level BARTER client instance for direct API access. Returns null if the provider has no configuration.
import { useBarterClient } from '@thebarmaeffect/barter-react';
Signature
function useBarterClient(): BarterClientLike | null
Return Type
interface BarterClientLike {
trust: unknown;
trade: unknown;
credit: unknown;
compliance: unknown;
webln: unknown;
}
Returns: BarterClientLike | null -- The client instance, or null if the provider configuration is empty (no rpcUrl and no network).
The client is memoized and only re-created when the provider configuration changes.
Example
function AdvancedQuery() {
const client = useBarterClient();
async function handleCustomQuery() {
if (!client) return;
}
return <button onClick={handleCustomQuery}>Run Query</button>;
}
useBarterScore
Fetch the trust score for an Ethereum address. Automatically fetches on mount and when the address changes.
import { useBarterScore } from '@thebarmaeffect/barter-react';
Signature
function useBarterScore(address: string | undefined): UseBarterScoreResult
Parameters
address | string | undefined | Ethereum address to query. Pass undefined to skip fetching. |
Return Type
interface UseBarterScoreResult {
data: number | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
Behavior
- On mount: If
address is defined, begins fetching immediately.
- On address change: Cancels any in-flight request, resets state, and fetches for the new address.
- On
undefined: Resets data to null, loading to false, error to null.
- On unmount: Cancels any in-flight request. No state updates after unmount.
Example
function TrustScore({ address }: { address: string }) {
const { data: score, loading, error, refetch } = useBarterScore(address);
if (loading) return <div className="skeleton" />;
if (error) return <div className="error">{error.message}</div>;
return (
<div>
<span>Score: {score}</span>
<button onClick={refetch}>Refresh</button>
</div>
);
}
Conditional Fetching
function ConditionalScore() {
const [address, setAddress] = useState<string | undefined>(undefined);
const { data: score, loading } = useBarterScore(address);
return (
<div>
<input
placeholder="Enter address..."
onChange={(e) => setAddress(e.target.value || undefined)}
/>
{loading && <span>Loading...</span>}
{score !== null && <span>Score: {score}</span>}
</div>
);
}
useBarterProfile
Fetch the full trust profile for an Ethereum address, including score, trade count, completion rate, counterparty count, and timestamps.
import { useBarterProfile } from '@thebarmaeffect/barter-react';
Signature
function useBarterProfile(address: string | undefined): UseBarterProfileResult
Parameters
address | string | undefined | Ethereum address to query. Pass undefined to skip. |
Return Type
interface UseBarterProfileResult {
data: TrustProfile | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
TrustProfile
interface TrustProfile {
address: string;
score: number;
uniqueCounterparties: number;
memberSince: Date | null;
tradeCount: number;
completionRate: number;
lastTradeAt: Date | null;
}
Example
function ProfileCard({ address }: { address: string }) {
const { data: profile, loading, error, refetch } = useBarterProfile(address);
if (loading) return <ProfileSkeleton />;
if (error) return <ErrorBanner message={error.message} />;
if (!profile) return null;
return (
<div className="profile-card">
<h2>Trust Profile</h2>
<dl>
<dt>Score</dt>
<dd>{profile.score}</dd>
<dt>Trades</dt>
<dd>{profile.tradeCount}</dd>
<dt>Completion Rate</dt>
<dd>{(profile.completionRate * 100).toFixed(1)}%</dd>
<dt>Counterparties</dt>
<dd>{profile.uniqueCounterparties}</dd>
<dt>Member Since</dt>
<dd>{profile.memberSince?.toLocaleDateString() ?? 'N/A'}</dd>
<dt>Last Trade</dt>
<dd>{profile.lastTradeAt?.toLocaleDateString() ?? 'N/A'}</dd>
</dl>
<button onClick={refetch}>Refresh</button>
</div>
);
}
useBarterTrade
Fetch the details of a single trade by its ID.
import { useBarterTrade } from '@thebarmaeffect/barter-react';
Signature
function useBarterTrade(tradeId: string | undefined): UseBarterTradeResult
Parameters
tradeId | string | undefined | Trade ID to query. Pass undefined to skip. |
Return Type
interface UseBarterTradeResult {
data: Trade | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
Trade
interface Trade {
tradeId: string;
partyA: string;
partyB: string;
paymentHash: string;
amountSats: number;
status: number;
createdAt: Date;
acceptedAt: Date | null;
settledAt: Date | null;
category: string;
description: string;
}
Example
function TradeDetail({ tradeId }: { tradeId: string }) {
const { data: trade, loading, error, refetch } = useBarterTrade(tradeId);
if (loading) return <Spinner />;
if (error) return <p>Error loading trade: {error.message}</p>;
if (!trade) return <p>Trade not found</p>;
const STATUS_LABELS = ['Proposed', 'Accepted', 'Settled', 'Disputed', 'Expired'];
return (
<div>
<h3>Trade {trade.tradeId}</h3>
<p>From: {trade.partyA}</p>
<p>To: {trade.partyB}</p>
<p>Amount: {trade.amountSats.toLocaleString()} sats</p>
<p>Status: {STATUS_LABELS[trade.status]}</p>
<p>Category: {trade.category}</p>
<p>Created: {trade.createdAt.toLocaleString()}</p>
{trade.settledAt && <p>Settled: {trade.settledAt.toLocaleString()}</p>}
<button onClick={refetch}>Refresh</button>
</div>
);
}
useBarterTrades
Fetch a list of trades for an address, optionally filtered by status.
import { useBarterTrades } from '@thebarmaeffect/barter-react';
Signature
function useBarterTrades(
address: string | undefined,
status?: number,
): UseBarterTradesResult
Parameters
address | string | undefined | Filter trades by this address. undefined to skip. |
status | number | Optional status filter (0-4). Omit for all trades. |
Return Type
interface UseBarterTradesResult {
data: Trade[];
loading: boolean;
error: Error | null;
refetch: () => void;
}
Example
function TradeHistory({ address }: { address: string }) {
const { data: trades, loading, error, refetch } = useBarterTrades(address);
if (loading) return <TableSkeleton rows={5} />;
if (error) return <p>Error: {error.message}</p>;
const STATUS_LABELS = ['Proposed', 'Accepted', 'Settled', 'Disputed', 'Expired'];
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<h2>Trade History ({trades.length})</h2>
<button onClick={refetch}>Refresh</button>
</div>
{trades.length === 0 ? (
<p>No trades found.</p>
) : (
<table>
<thead>
<tr>
<th>ID</th>
<th>Counterparty</th>
<th>Amount</th>
<th>Status</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{trades.map((trade) => (
<tr key={trade.tradeId}>
<td>{trade.tradeId.slice(0, 10)}...</td>
<td>{trade.partyB.slice(0, 6)}...{trade.partyB.slice(-4)}</td>
<td>{trade.amountSats.toLocaleString()} sats</td>
<td>{STATUS_LABELS[trade.status]}</td>
<td>{trade.createdAt.toLocaleDateString()}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}
Filtering by Status
function ActiveTrades({ address }: { address: string }) {
const { data: proposed } = useBarterTrades(address, 0);
const { data: accepted } = useBarterTrades(address, 1);
return (
<div>
<h3>Proposed ({proposed.length})</h3>
<h3>Accepted ({accepted.length})</h3>
</div>
);
}
TypeScript Types
All types are exported from the package root:
import type {
BarterConfig,
BarterClientLike,
UseBarterScoreResult,
UseBarterProfileResult,
TrustProfile,
UseBarterTradeResult,
Trade,
UseBarterTradesResult,
} from '@thebarmaeffect/barter-react';
Error Handling
All hooks follow the same error pattern: errors are captured in the error field and never thrown to the component. This prevents unhandled promise rejections and makes error handling declarative.
Per-Hook Error Handling
function SafeScore({ address }: { address: string }) {
const { data, loading, error } = useBarterScore(address);
if (error) {
return <div className="error-banner">{error.message}</div>;
}
}
Error Boundary Integration
For unexpected errors (e.g., from event handlers that use the client directly), use React Error Boundaries:
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<BarterProvider config={{ rpcUrl: 'https://rpc.sepolia.org' }}>
<Dashboard />
</BarterProvider>
</ErrorBoundary>
);
}
Common Error Scenarios
Invalid address: 0x... | Address failed ethers.isAddress() validation | Validate address before passing to hook |
Contract not initialized | Provider config is missing or invalid | Ensure BarterProvider has valid config |
| Network timeout | RPC endpoint is unreachable | Check RPC URL and network connectivity |
Retry Pattern
function ScoreWithRetry({ address }: { address: string }) {
const { data, loading, error, refetch } = useBarterScore(address);
if (error) {
return (
<div>
<p>Failed to load score: {error.message}</p>
<button onClick={refetch}>Retry</button>
</div>
);
}
}
SSR Considerations
The React SDK is designed to work safely in server-side rendering environments (Next.js, Remix, etc.).
Key Behaviors During SSR
- No
window access -- Hooks use useState and useEffect, so no data is fetched during server rendering. The initial render always returns the loading/empty state.
- No hydration mismatch -- Because hooks start in
loading: true (or data: null) during both SSR and the initial client render, there is no mismatch.
- Data fetches on the client -- All blockchain queries run exclusively in
useEffect, which only fires on the client after hydration.
Recommended Patterns
'use client';
import { BarterProvider, useBarterScore } from '@thebarmaeffect/barter-react';
function TrustDisplay({ address }: { address: string }) {
const { data: score, loading } = useBarterScore(address);
return (
<div>
{loading ? (
<div className="skeleton" style={{ width: 60, height: 24 }} />
) : (
<span>{score}</span>
)}
</div>
);
}
Server-Side Data Fetching
For server-rendered trust data, use the core SDK (@thebarmaeffect/barter-sdk) directly in server components or getServerSideProps:
import { BarterClient } from '@thebarmaeffect/barter-sdk';
const client = new BarterClient({ rpcUrl: process.env.RPC_URL });
export default async function TrustPage({ params }: { params: { address: string } }) {
const profile = await client.trust.getProfile(params.address);
return (
<div>
<h1>Trust Profile</h1>
<p>Score: {profile.score}</p>
<p>Trades: {profile.tradeCount}</p>
</div>
);
}
Framework Integration
Next.js App Router
'use client';
import { BarterProvider } from '@thebarmaeffect/barter-react';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<BarterProvider config={{
rpcUrl: process.env.NEXT_PUBLIC_RPC_URL,
network: 'sepolia',
}}>
{children}
</BarterProvider>
);
}
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
'use client';
import { useBarterProfile } from '@thebarmaeffect/barter-react';
export default function TrustPage({ params }: { params: { address: string } }) {
const { data: profile, loading } = useBarterProfile(params.address);
}
Next.js Pages Router
import type { AppProps } from 'next/app';
import { BarterProvider } from '@thebarmaeffect/barter-react';
export default function App({ Component, pageProps }: AppProps) {
return (
<BarterProvider config={{
rpcUrl: process.env.NEXT_PUBLIC_RPC_URL,
network: 'sepolia',
}}>
<Component {...pageProps} />
</BarterProvider>
);
}
Vite + React
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BarterProvider } from '@thebarmaeffect/barter-react';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BarterProvider config={{
rpcUrl: import.meta.env.VITE_RPC_URL,
network: 'sepolia',
}}>
<App />
</BarterProvider>
</React.StrictMode>,
);
Remix
import { BarterProvider } from '@thebarmaeffect/barter-react';
export default function App() {
return (
<html>
<body>
<BarterProvider config={{ rpcUrl: 'https://rpc.sepolia.org', network: 'sepolia' }}>
<Outlet />
</BarterProvider>
</body>
</html>
);
}
Recipes
Trust Badge Component
A reusable badge that displays a color-coded trust score.
import { useBarterScore } from '@thebarmaeffect/barter-react';
function TrustBadge({ address }: { address: string }) {
const { data: score, loading, error } = useBarterScore(address);
if (loading) return <span className="badge badge-loading">...</span>;
if (error) return <span className="badge badge-error">ERR</span>;
if (score === null) return null;
const level =
score >= 80 ? 'high' :
score >= 50 ? 'medium' :
score >= 20 ? 'low' : 'minimal';
return (
<span className={`badge badge-trust-${level}`} title={`Trust score: ${score}`}>
{score}
</span>
);
}
Trade History Table
import { useBarterTrades } from '@thebarmaeffect/barter-react';
function TradeTable({ address }: { address: string }) {
const { data: trades, loading, error, refetch } = useBarterTrades(address);
if (loading) return <p>Loading trades...</p>;
if (error) return <p>Error: {error.message}</p>;
if (trades.length === 0) return <p>No trades found for this address.</p>;
return (
<table>
<thead>
<tr>
<th>Trade ID</th>
<th>Amount (sats)</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{trades.map((t) => (
<tr key={t.tradeId}>
<td><code>{t.tradeId.slice(0, 12)}...</code></td>
<td>{t.amountSats.toLocaleString()}</td>
<td>{['Proposed', 'Accepted', 'Settled', 'Disputed', 'Expired'][t.status]}</td>
</tr>
))}
</tbody>
</table>
);
}
Profile Card
import { useBarterProfile } from '@thebarmaeffect/barter-react';
function ProfileCard({ address }: { address: string }) {
const { data: profile, loading } = useBarterProfile(address);
if (loading || !profile) return <div className="card-skeleton" />;
return (
<div className="card">
<div className="card-header">
<h3>{address.slice(0, 6)}...{address.slice(-4)}</h3>
<span className="score">{profile.score}</span>
</div>
<div className="card-body">
<div className="stat">
<span className="stat-label">Trades</span>
<span className="stat-value">{profile.tradeCount}</span>
</div>
<div className="stat">
<span className="stat-label">Completion</span>
<span className="stat-value">{(profile.completionRate * 100).toFixed(0)}%</span>
</div>
<div className="stat">
<span className="stat-label">Partners</span>
<span className="stat-value">{profile.uniqueCounterparties}</span>
</div>
</div>
</div>
);
}
Polling for Updates
Use refetch with setInterval to poll for live updates:
import { useEffect } from 'react';
import { useBarterTrade } from '@thebarmaeffect/barter-react';
function LiveTradeStatus({ tradeId }: { tradeId: string }) {
const { data: trade, refetch } = useBarterTrade(tradeId);
useEffect(() => {
if (!trade || trade.status < 2) {
const interval = setInterval(refetch, 10_000);
return () => clearInterval(interval);
}
}, [trade, refetch]);
if (!trade) return <span>Loading...</span>;
const STATUS_LABELS = ['Proposed', 'Accepted', 'Settled', 'Disputed', 'Expired'];
return <span>{STATUS_LABELS[trade.status]}</span>;
}
Development
Prerequisites
Setup
git clone https://github.com/TheBarmaEffect/barter-protocol.git
cd barter-protocol/sdk-react
npm install
Commands
npm run build
npm test
npm run lint
Testing
Tests use Jest with React Testing Library:
npm test
Contributing
- Fork the repository.
- Create a feature branch:
git checkout -b feat/my-hook
- Make your changes and add tests.
- Ensure tests pass:
npm test
- Ensure type-checking passes:
npm run lint
- Submit a pull request.
License
MIT -- see LICENSE for details.