Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,13 @@ function AppContent() {
function App() {
return (
<NetworkProvider>
<WalletProvider>
<ToastProvider>
<AppContent />
</ToastProvider>
</WalletProvider>
<StellarProvider>
<WalletProvider>
<ToastProvider>
<AppContent />
</ToastProvider>
</WalletProvider>
</StellarProvider>
</NetworkProvider>
)
}
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/BurnForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<FactoryTokenInfo[]>([])
const [isLoading, setIsLoading] = useState(true)
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/components/MintForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/TokenCreateForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/TokenDetail.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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()

Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/TransactionHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export const TransactionHistory: React.FC<Props> = ({
tokenAddress,
pageSize = 20,
}) => {
const { stellarService } = useStellarContext()
const [events, setEvents] = useState<ContractEvent[]>([])
const [cursor, setCursor] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/context/StellarContext.tsx
Original file line number Diff line number Diff line change
@@ -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<StellarContextValue | null>(null)

export function StellarProvider({ children }: { children: ReactNode }) {
const { network } = useNetwork()

const value = useMemo(() => ({
stellarService: new StellarService(),
ipfsService: new IPFSService(),
}), [network])

return <StellarContext.Provider value={value}>{children}</StellarContext.Provider>
}

export function useStellarContext(): StellarContextValue {
const ctx = useContext(StellarContext)
if (!ctx) throw new Error('useStellarContext must be used within a StellarProvider')
return ctx
}
59 changes: 59 additions & 0 deletions frontend/src/test/StellarContext.test.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<NetworkProvider>
<StellarProvider>{children}</StellarProvider>
</NetworkProvider>
)

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 }) => (
<NetworkProvider>
<StellarProvider>{children}</StellarProvider>
</NetworkProvider>
)

// 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()
})
})
Loading