React Integration
This guide shows how to integrate the SatsTerminal Borrow SDK into a React application.Setup
Install Dependencies
Copy
npm install @satsterminal-sdk/borrow
Create SDK Context
Copy
// src/contexts/BorrowContext.tsx
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
import { BorrowSDK, ChainType, UserStatus, BorrowSDKConfig } from '@satsterminal-sdk/borrow';
interface BorrowContextType {
sdk: BorrowSDK | null;
userStatus: UserStatus | null;
isInitialized: boolean;
isLoading: boolean;
error: string | null;
initialize: (walletAddress: string, signMessage: (msg: string) => Promise<string>) => Promise<void>;
disconnect: () => void;
}
const BorrowContext = createContext<BorrowContextType | undefined>(undefined);
interface BorrowProviderProps {
children: ReactNode;
apiKey: string;
baseUrl: string;
chain?: ChainType;
}
export function BorrowProvider({
children,
apiKey,
baseUrl,
chain = ChainType.ARBITRUM
}: BorrowProviderProps) {
const [sdk, setSdk] = useState<BorrowSDK | null>(null);
const [userStatus, setUserStatus] = useState<UserStatus | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const initialize = useCallback(async (
walletAddress: string,
signMessage: (msg: string) => Promise<string>
) => {
setIsLoading(true);
setError(null);
try {
const config: BorrowSDKConfig = {
apiKey,
baseUrl,
chain,
wallet: {
address: walletAddress,
signMessage
}
};
const borrowSdk = new BorrowSDK(config);
const { userStatus: status } = await borrowSdk.setup();
setSdk(borrowSdk);
setUserStatus(status);
setIsInitialized(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to initialize');
throw err;
} finally {
setIsLoading(false);
}
}, [apiKey, baseUrl, chain]);
const disconnect = useCallback(() => {
if (sdk) {
sdk.clearSession();
}
setSdk(null);
setUserStatus(null);
setIsInitialized(false);
}, [sdk]);
return (
<BorrowContext.Provider
value={{
sdk,
userStatus,
isInitialized,
isLoading,
error,
initialize,
disconnect
}}
>
{children}
</BorrowContext.Provider>
);
}
export function useBorrowContext() {
const context = useContext(BorrowContext);
if (!context) {
throw new Error('useBorrowContext must be used within BorrowProvider');
}
return context;
}
Wrap Your App
Copy
// src/App.tsx
import { BorrowProvider } from './contexts/BorrowContext';
function App() {
return (
<BorrowProvider
apiKey={process.env.REACT_APP_API_KEY!}
baseUrl="https://api.satsterminal.com"
chain={ChainType.ARBITRUM}
>
<YourApp />
</BorrowProvider>
);
}
Custom Hooks
useLoan Hook
Copy
// src/hooks/useLoan.ts
import { useState, useCallback } from 'react';
import { useBorrowContext } from '../contexts/BorrowContext';
import { WorkflowStatus, DepositInfo, Quote } from '@satsterminal-sdk/borrow';
interface UseLoanOptions {
collateralBTC: number;
loanAmountUSD: number;
ltv?: number;
}
interface UseLoanReturn {
isLoading: boolean;
status: WorkflowStatus | null;
depositInfo: DepositInfo | null;
quote: Quote | null;
error: string | null;
isComplete: boolean;
startLoan: () => Promise<void>;
stopTracking: () => void;
}
export function useLoan(options: UseLoanOptions): UseLoanReturn {
const { sdk } = useBorrowContext();
const [isLoading, setIsLoading] = useState(false);
const [status, setStatus] = useState<WorkflowStatus | null>(null);
const [depositInfo, setDepositInfo] = useState<DepositInfo | null>(null);
const [quote, setQuote] = useState<Quote | null>(null);
const [error, setError] = useState<string | null>(null);
const [isComplete, setIsComplete] = useState(false);
const [stopFn, setStopFn] = useState<(() => void) | null>(null);
const startLoan = useCallback(async () => {
if (!sdk) {
setError('SDK not initialized');
return;
}
setIsLoading(true);
setError(null);
setIsComplete(false);
setStatus(null);
setDepositInfo(null);
try {
const result = await sdk.getLoan({
collateralBTC: options.collateralBTC,
loanAmountUSD: options.loanAmountUSD,
ltv: options.ltv || 70,
onStatusUpdate: setStatus,
onDepositReady: setDepositInfo,
onComplete: () => {
setIsComplete(true);
setIsLoading(false);
},
onError: (err) => {
setError(err);
setIsLoading(false);
}
});
setQuote(result.quote);
setStopFn(() => result.stop);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to start loan');
setIsLoading(false);
}
}, [sdk, options]);
const stopTracking = useCallback(() => {
if (stopFn) {
stopFn();
}
}, [stopFn]);
return {
isLoading,
status,
depositInfo,
quote,
error,
isComplete,
startLoan,
stopTracking
};
}
usePositions Hook
Copy
// src/hooks/usePositions.ts
import { useState, useEffect, useCallback } from 'react';
import { useBorrowContext } from '../contexts/BorrowContext';
import { WalletPosition } from '@satsterminal-sdk/borrow';
export function usePositions(refreshInterval = 30000) {
const { sdk, isInitialized } = useBorrowContext();
const [positions, setPositions] = useState<WalletPosition[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchPositions = useCallback(async () => {
if (!sdk || !isInitialized) return;
setIsLoading(true);
try {
const response = await sdk.getWalletPositions({
filterTrash: 'only_non_trash'
});
setPositions(response.data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch positions');
} finally {
setIsLoading(false);
}
}, [sdk, isInitialized]);
useEffect(() => {
fetchPositions();
const interval = setInterval(fetchPositions, refreshInterval);
return () => clearInterval(interval);
}, [fetchPositions, refreshInterval]);
return { positions, isLoading, error, refresh: fetchPositions };
}
useLoanHistory Hook
Copy
// src/hooks/useLoanHistory.ts
import { useState, useEffect, useCallback } from 'react';
import { useBorrowContext } from '../contexts/BorrowContext';
import { UserTransaction } from '@satsterminal-sdk/borrow';
export function useLoanHistory(status: 'active' | 'pending' | 'all' = 'all') {
const { sdk, isInitialized } = useBorrowContext();
const [loans, setLoans] = useState<UserTransaction[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const fetchLoans = useCallback(async (pageNum: number) => {
if (!sdk || !isInitialized) return;
setIsLoading(true);
try {
const response = await sdk.getLoanHistory({
page: pageNum,
limit: 10,
status
});
if (pageNum === 1) {
setLoans(response.transactions);
} else {
setLoans(prev => [...prev, ...response.transactions]);
}
setHasMore(response.pagination.hasNext);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch loans');
} finally {
setIsLoading(false);
}
}, [sdk, isInitialized, status]);
useEffect(() => {
setPage(1);
fetchLoans(1);
}, [fetchLoans]);
const loadMore = useCallback(() => {
if (!isLoading && hasMore) {
const nextPage = page + 1;
setPage(nextPage);
fetchLoans(nextPage);
}
}, [isLoading, hasMore, page, fetchLoans]);
return { loans, isLoading, error, hasMore, loadMore };
}
Components
LoanForm Component
Copy
// src/components/LoanForm.tsx
import React, { useState } from 'react';
import { useLoan } from '../hooks/useLoan';
export function LoanForm() {
const [collateralBTC, setCollateralBTC] = useState(0.1);
const [loanAmountUSD, setLoanAmountUSD] = useState(5000);
const {
isLoading,
status,
depositInfo,
quote,
error,
isComplete,
startLoan
} = useLoan({ collateralBTC, loanAmountUSD });
return (
<div className="loan-form">
<h2>Get a Loan</h2>
{!isLoading && !status && (
<form onSubmit={(e) => { e.preventDefault(); startLoan(); }}>
<div>
<label>Collateral (BTC)</label>
<input
type="number"
step="0.001"
value={collateralBTC}
onChange={(e) => setCollateralBTC(parseFloat(e.target.value))}
/>
</div>
<div>
<label>Loan Amount (USD)</label>
<input
type="number"
value={loanAmountUSD}
onChange={(e) => setLoanAmountUSD(parseFloat(e.target.value))}
/>
</div>
<button type="submit">Get Loan</button>
</form>
)}
{status && (
<div className="status">
<h3>Status: {status.label}</h3>
<p>{status.description}</p>
<progress value={status.step} max={8} />
</div>
)}
{depositInfo && !isComplete && (
<div className="deposit-info">
<h3>Deposit Required</h3>
<p>Amount: {depositInfo.amountBTC} BTC</p>
<code>{depositInfo.address}</code>
</div>
)}
{quote && (
<div className="quote-info">
<h4>Quote Details</h4>
<p>Protocol: {quote.protocol}</p>
<p>APY: {quote.borrowApy.variable}%</p>
</div>
)}
{isComplete && (
<div className="success">
<h3>Loan Complete!</h3>
<p>Your funds are now available in your smart account.</p>
</div>
)}
{error && (
<div className="error">
<p>Error: {error}</p>
</div>
)}
</div>
);
}
PositionsList Component
Copy
// src/components/PositionsList.tsx
import React from 'react';
import { usePositions } from '../hooks/usePositions';
export function PositionsList() {
const { positions, isLoading, error, refresh } = usePositions();
if (isLoading && positions.length === 0) {
return <div>Loading positions...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
const totalValue = positions.reduce(
(sum, p) => sum + (p.attributes.value || 0),
0
);
return (
<div className="positions">
<div className="header">
<h2>Wallet Positions</h2>
<button onClick={refresh}>Refresh</button>
</div>
<div className="total">
Total Value: ${totalValue.toFixed(2)}
</div>
<table>
<thead>
<tr>
<th>Token</th>
<th>Amount</th>
<th>Value</th>
<th>24h</th>
</tr>
</thead>
<tbody>
{positions.map((position) => (
<tr key={position.id}>
<td>{position.attributes.fungible_info?.symbol || 'Unknown'}</td>
<td>{position.attributes.quantity.float.toFixed(4)}</td>
<td>${(position.attributes.value || 0).toFixed(2)}</td>
<td className={
(position.attributes.changes?.percent_1d || 0) >= 0
? 'positive'
: 'negative'
}>
{position.attributes.changes?.percent_1d?.toFixed(2) || 0}%
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
ConnectWallet Component
Copy
// src/components/ConnectWallet.tsx
import React from 'react';
import { useBorrowContext } from '../contexts/BorrowContext';
export function ConnectWallet() {
const { isInitialized, isLoading, error, initialize, disconnect, userStatus } = useBorrowContext();
const handleConnect = async () => {
// Example with a Bitcoin wallet library
// Replace with your actual wallet integration
const wallet = await connectBitcoinWallet();
await initialize(
wallet.address,
(message) => wallet.signMessage(message)
);
};
if (isInitialized) {
return (
<div className="wallet-info">
<p>Connected: {userStatus?.btcAddress?.slice(0, 10)}...</p>
<p>Smart Account: {userStatus?.smartAccountAddress?.slice(0, 10)}...</p>
<button onClick={disconnect}>Disconnect</button>
</div>
);
}
return (
<div className="connect-wallet">
<button onClick={handleConnect} disabled={isLoading}>
{isLoading ? 'Connecting...' : 'Connect Wallet'}
</button>
{error && <p className="error">{error}</p>}
</div>
);
}
Full Example App
Copy
// src/App.tsx
import React from 'react';
import { BorrowProvider } from './contexts/BorrowContext';
import { ConnectWallet } from './components/ConnectWallet';
import { LoanForm } from './components/LoanForm';
import { PositionsList } from './components/PositionsList';
import { useBorrowContext } from './contexts/BorrowContext';
import { ChainType } from '@satsterminal-sdk/borrow';
function Dashboard() {
const { isInitialized } = useBorrowContext();
if (!isInitialized) {
return (
<div className="app">
<h1>SatsTerminal Borrow</h1>
<ConnectWallet />
</div>
);
}
return (
<div className="app">
<header>
<h1>SatsTerminal Borrow</h1>
<ConnectWallet />
</header>
<main>
<section>
<LoanForm />
</section>
<section>
<PositionsList />
</section>
</main>
</div>
);
}
export default function App() {
return (
<BorrowProvider
apiKey={process.env.REACT_APP_SATSTERMINAL_API_KEY!}
baseUrl="https://api.satsterminal.com"
chain={ChainType.ARBITRUM}
>
<Dashboard />
</BorrowProvider>
);
}
Best Practices
1. Error Boundaries
Copy
class BorrowErrorBoundary extends React.Component<
{ children: ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
2. Loading States
Copy
function LoadingSpinner() {
return <div className="spinner">Loading...</div>;
}
function withLoading<P extends object>(
Component: React.ComponentType<P>,
useLoadingHook: () => boolean
) {
return function WithLoadingComponent(props: P) {
const isLoading = useLoadingHook();
if (isLoading) {
return <LoadingSpinner />;
}
return <Component {...props} />;
};
}
3. Optimistic Updates
Copy
function useLoanWithOptimisticUpdate() {
const [optimisticLoans, setOptimisticLoans] = useState<UserTransaction[]>([]);
const startLoan = async (options: LoanOptions) => {
// Add optimistic loan
const optimisticLoan: UserTransaction = {
id: 'temp-' + Date.now(),
type: 'borrow',
status: 'pending',
amount: options.loanAmountUSD.toString(),
currency: 'USD',
timestamp: Date.now()
};
setOptimisticLoans(prev => [optimisticLoan, ...prev]);
try {
await sdk.getLoan(options);
// Refresh real data
await refreshLoans();
} finally {
// Remove optimistic loan
setOptimisticLoans(prev =>
prev.filter(l => l.id !== optimisticLoan.id)
);
}
};
return { optimisticLoans, startLoan };
}