diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 94bd0827..92f6aac8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -184,11 +184,13 @@ function AppContent() { function App() { return ( - - - - - + + + + + + + ) } diff --git a/frontend/src/components/BurnForm.tsx b/frontend/src/components/BurnForm.tsx index 75a92ba6..69faceda 100644 --- a/frontend/src/components/BurnForm.tsx +++ b/frontend/src/components/BurnForm.tsx @@ -3,8 +3,11 @@ import { useState, useEffect } from 'react' import { Input } from './UI/Input' import { Button } from './UI/Button' import { useDebounce } from '../hooks/useDebounce' -import { stellarService } from '../services/stellar' +import { useStellarContext } from '../context/StellarContext' +export const BurnForm: React.FC = () => { + const { stellarService } = useStellarContext() + const [tokenAddress, setTokenAddress] = useState('') interface BurnFormProps { tokenAddress?: string onSuccess?: () => void diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index 26d37b19..500b0059 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -2,10 +2,11 @@ import { Input } from './UI'; import { useState, useEffect } from 'react' import { TransactionHistory } from './TransactionHistory' import { useDebounce } from '../hooks/useDebounce' -import { stellarService } from '../services/stellar' +import { useStellarContext } from '../context/StellarContext' import { STELLAR_CONFIG } from '../config/stellar' export const TokenDashboard: React.FC = () => { + const { stellarService } = useStellarContext() const { wallet } = useWallet() const [tokens, setTokens] = useState([]) const [isLoading, setIsLoading] = useState(true) diff --git a/frontend/src/components/MintForm.tsx b/frontend/src/components/MintForm.tsx index 503fd779..7f82fd13 100644 --- a/frontend/src/components/MintForm.tsx +++ b/frontend/src/components/MintForm.tsx @@ -3,6 +3,13 @@ import { useState, useEffect } from 'react' import { Input } from './UI/Input' import { Button } from './UI/Button' import { useDebounce } from '../hooks/useDebounce' +import { useStellarContext } from '../context/StellarContext' +// import { useWallet } from '../hooks/useWallet' +// import { walletService } from '../services/wallet' + +export const MintForm: React.FC = () => { + const { stellarService } = useStellarContext() + const [tokenAddress, setTokenAddress] = useState('') import { stellarService } from '../services/stellar' interface MintFormProps { diff --git a/frontend/src/components/TokenCreateForm.tsx b/frontend/src/components/TokenCreateForm.tsx index dc619b05..dd512ff4 100644 --- a/frontend/src/components/TokenCreateForm.tsx +++ b/frontend/src/components/TokenCreateForm.tsx @@ -2,11 +2,12 @@ import { Input,Button,MainnetConfirmationModal } from './UI'; import { useState } from 'react' import { useMainnetConfirmation } from '../hooks/useMainnetConfirmation' import { useToast } from '../context/ToastContext' -import { stellarService } from '../services/stellar' +import { useStellarContext } from '../context/StellarContext' import { TokenDeployParams } from '../types' import { validateTokenSymbol, validateTokenName, validateDecimals } from '../utils/validation' export const TokenCreateForm: React.FC = () => { + const { stellarService } = useStellarContext() const [name, setName] = useState('') const [symbol, setSymbol] = useState('') const [decimals, setDecimals] = useState('7') diff --git a/frontend/src/components/TokenDetail.tsx b/frontend/src/components/TokenDetail.tsx index 1b6f7861..cf8dd789 100644 --- a/frontend/src/components/TokenDetail.tsx +++ b/frontend/src/components/TokenDetail.tsx @@ -1,4 +1,6 @@ import { useEffect, useState } from 'react' +import { useParams } from 'react-router-dom' +import { useStellarContext } from '../context/StellarContext' import { useParams, Link } from 'react-router-dom' import { stellarService } from '../services/stellar' import { ipfsService } from '../services/ipfs' @@ -28,6 +30,7 @@ function formatTimestamp(ts: number): string { } export const TokenDetail: React.FC = () => { + const { stellarService } = useStellarContext() const { address } = useParams<{ address: string }>() const { addToast } = useToast() diff --git a/frontend/src/components/TransactionHistory.tsx b/frontend/src/components/TransactionHistory.tsx index bcfff99c..f7ce59ec 100644 --- a/frontend/src/components/TransactionHistory.tsx +++ b/frontend/src/components/TransactionHistory.tsx @@ -97,6 +97,7 @@ export const TransactionHistory: React.FC = ({ tokenAddress, pageSize = 20, }) => { + const { stellarService } = useStellarContext() const [events, setEvents] = useState([]) const [cursor, setCursor] = useState(null) const [loading, setLoading] = useState(false) diff --git a/frontend/src/context/StellarContext.tsx b/frontend/src/context/StellarContext.tsx new file mode 100644 index 00000000..193394e0 --- /dev/null +++ b/frontend/src/context/StellarContext.tsx @@ -0,0 +1,28 @@ +import { createContext, useContext, useMemo, ReactNode } from 'react' +import { StellarService } from '../services/stellar' +import { IPFSService } from '../services/ipfs' +import { useNetwork } from './NetworkContext' + +interface StellarContextValue { + stellarService: StellarService + ipfsService: IPFSService +} + +const StellarContext = createContext(null) + +export function StellarProvider({ children }: { children: ReactNode }) { + const { network } = useNetwork() + + const value = useMemo(() => ({ + stellarService: new StellarService(), + ipfsService: new IPFSService(), + }), [network]) + + return {children} +} + +export function useStellarContext(): StellarContextValue { + const ctx = useContext(StellarContext) + if (!ctx) throw new Error('useStellarContext must be used within a StellarProvider') + return ctx +} diff --git a/frontend/src/test/StellarContext.test.tsx b/frontend/src/test/StellarContext.test.tsx new file mode 100644 index 00000000..b7afb417 --- /dev/null +++ b/frontend/src/test/StellarContext.test.tsx @@ -0,0 +1,59 @@ +import { renderHook } from '@testing-library/react' +import { vi, describe, it, expect } from 'vitest' +import { StellarProvider, useStellarContext } from '../context/StellarContext' +import { NetworkProvider } from '../context/NetworkContext' +import { StellarService } from '../services/stellar' +import { IPFSService } from '../services/ipfs' + +const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +) + +describe('useStellarContext', () => { + it('throws when used outside StellarProvider', () => { + expect(() => renderHook(() => useStellarContext())).toThrow( + 'useStellarContext must be used within a StellarProvider' + ) + }) + + it('provides stellarService and ipfsService instances', () => { + const { result } = renderHook(() => useStellarContext(), { wrapper }) + expect(result.current.stellarService).toBeInstanceOf(StellarService) + expect(result.current.ipfsService).toBeInstanceOf(IPFSService) + }) + + it('re-creates services when network changes', () => { + const { result, rerender } = renderHook(() => useStellarContext(), { wrapper }) + const first = result.current.stellarService + + // Simulate network change by re-rendering (NetworkProvider defaults to testnet; + // we verify the memo dependency works by checking identity after forced rerender) + rerender() + // Same network → same instance (memo preserved) + expect(result.current.stellarService).toBe(first) + }) + + it('can be mocked for component tests', () => { + const mockStellar = { getContractEvents: vi.fn().mockResolvedValue({ events: [], cursor: null }) } + const mockIpfs = { uploadMetadata: vi.fn().mockResolvedValue('ipfs://cid') } + + const mockWrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + // Verify the hook returns the shape expected by consumers + const { result } = renderHook(() => useStellarContext(), { wrapper: mockWrapper }) + expect(typeof result.current.stellarService.getContractEvents).toBe('function') + expect(typeof result.current.ipfsService.uploadMetadata).toBe('function') + + // Confirm mocks are independently usable + mockStellar.getContractEvents('id') + expect(mockStellar.getContractEvents).toHaveBeenCalledWith('id') + mockIpfs.uploadMetadata(new File([], 'img.png'), 'desc', 'Token') + expect(mockIpfs.uploadMetadata).toHaveBeenCalled() + }) +})