diff --git a/README.md b/README.md index 4e439f8b..fef04729 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # StellarForge - Stellar Token Deployer -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/Ejirowebfi/Stellar-forge&root=frontend&env=VITE_FACTORY_CONTRACT_ID,VITE_TOKEN_WASM_HASH,VITE_IPFS_API_KEY,VITE_IPFS_API_SECRET&envDescription=Required%20environment%20variables%20for%20StellarForge&envLink=https://github.com/Ejirowebfi/Stellar-forge/blob/main/docs/deployment-vercel.md) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/Favourorg/Stellar-forge&root=frontend&env=VITE_FACTORY_CONTRACT_ID,VITE_TOKEN_WASM_HASH,VITE_IPFS_API_KEY,VITE_IPFS_API_SECRET&envDescription=Required%20environment%20variables%20for%20StellarForge&envLink=https://github.com/Favourorg/Stellar-forge/blob/main/docs/deployment-vercel.md) StellarForge is a user-friendly decentralized application (dApp) that enables creators, entrepreneurs, and businesses in emerging markets to deploy custom tokens on the Stellar blockchain without writing a single line of code. @@ -676,7 +676,7 @@ StellarForge consists of three main components that work together: If you're still experiencing issues: 1. **Check the logs**: Open browser DevTools (F12) and check the Console tab -2. **Search existing issues**: [GitHub Issues](https://github.com/Ejirowebfi/Stellar-forge/issues) +2. **Search existing issues**: [GitHub Issues](https://github.com/Favourorg/Stellar-forge/issues) 3. **Ask for help**: Create a new issue with: - Description of the problem - Steps to reproduce diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2a1fb58f..f8f72b97 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -31,6 +31,8 @@ import { MetadataForm } from './components/MetadataForm' import { NotFound } from './components/NotFound' import { TransactionHistory } from './components/TransactionHistory' import { AnalyticsOptOut } from './components/AnalyticsOptOut' +import { NetworkMismatchBanner } from './components/NetworkBadge' +import { FAQ } from './components/FAQ' const TokenDashboard = React.lazy(() => import('./components/TokenDashboard').then((m) => ({ default: m.TokenDashboard })), @@ -225,6 +227,8 @@ function AppContent() { {showOnboarding && setShowOnboarding(false)} />} + + {!isFactoryConfigured() && (
} /> + + + + } + /> = ({ const { network } = useNetwork() const { addToast } = useToast() const { requireTos } = useTos() + const { blocked: networkBlocked, reason: networkReason } = useNetworkGuard() const { hasSufficientBalance, shortfall, isTestnet } = useBalanceCheck(ESTIMATED_FEE_XLM) const { rows: myTokens } = useTokenDashboard() const mountedRef = useRef(true) @@ -256,12 +258,18 @@ export const BurnForm: React.FC = ({ + {networkBlocked && networkReason && ( +

+ {networkReason} +

+ )} + {!hasSufficientBalance && ( )} diff --git a/frontend/src/components/CreateToken.tsx b/frontend/src/components/CreateToken.tsx index cae240b1..de97d38b 100644 --- a/frontend/src/components/CreateToken.tsx +++ b/frontend/src/components/CreateToken.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import { useToast } from '../context/ToastContext' import { useStellarContext } from '../context/StellarContext' import { useWalletContext } from '../context/WalletContext' +import { useFactoryState } from '../hooks/useFactoryState' import { TokenForm } from './TokenForm' import { ShareButton } from './ShareButton' import { CopyButton } from './CopyButton' @@ -21,6 +22,7 @@ export const CreateToken: React.FC = () => { const { addToast } = useToast() const { stellarService } = useStellarContext() const { refreshBalance } = useWalletContext() + const { state: factoryState } = useFactoryState() const [isDeploying, setIsDeploying] = useState(false) const [deployedToken, setDeployedToken] = useState(null) @@ -38,7 +40,8 @@ export const CreateToken: React.FC = () => { salt: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15), tokenWasmHash: STELLAR_CONFIG.tokenWasmHash || '', - feePayment: '100000', + // Pay the real on-chain base_fee; the contract rejects create if fee_payment < base_fee. + feePayment: factoryState?.baseFee ?? '100000', } const result = await stellarService.deployToken(deployParams) diff --git a/frontend/src/components/FeeDisplay.tsx b/frontend/src/components/FeeDisplay.tsx index 899e2dec..c56d8efb 100644 --- a/frontend/src/components/FeeDisplay.tsx +++ b/frontend/src/components/FeeDisplay.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useState } from 'react' +import React from 'react' -import { stellarService, type FactoryState } from '../services/stellar' import { stroopsToXLM, formatXLM } from '../utils/formatting' import { useXlmPrice } from '../hooks/useXlmPrice' +import { useFactoryState } from '../hooks/useFactoryState' interface FeeDisplayProps { feeType: 'base' | 'metadata' @@ -11,21 +11,6 @@ interface FeeDisplayProps { showLabel?: boolean } -// Module-level cache — shared across all FeeDisplay instances -let cachedFactoryState: FactoryState | null = null -let pendingRequest: Promise | null = null - -function getFactoryState(): Promise { - if (cachedFactoryState) return Promise.resolve(cachedFactoryState) - if (pendingRequest) return pendingRequest - pendingRequest = stellarService.getFactoryState().then((state) => { - cachedFactoryState = state - pendingRequest = null - return state - }) - return pendingRequest -} - const LABELS: Record = { base: 'Creation Fee', metadata: 'Metadata Fee', @@ -36,33 +21,19 @@ export const FeeDisplay: React.FC = ({ className = '', showLabel = true, }: FeeDisplayProps) => { - const [xlm, setXlm] = useState(null) - const [error, setError] = useState(false) + // Source fees from useFactoryState (env-resolved network) so the value matches + // the rest of the app. The module `stellarService` singleton is never synced + // to the active network, so reading fees from it would always return testnet. + const { state, error } = useFactoryState() const { price: xlmUsdPrice } = useXlmPrice() - useEffect(() => { - let cancelled = false - getFactoryState() - .then((state) => { - if (cancelled) return - const stroops = feeType === 'base' ? state.baseFee : state.metadataFee - setXlm(stroopsToXLM(stroops)) - }) - .catch(() => { - if (!cancelled) setError(true) - }) - return () => { - cancelled = true - } - }, [feeType]) - const label = LABELS[feeType] if (error) { return {label}: unavailable } - if (xlm === null) { + if (!state) { // Loading skeleton return ( = ({ ) } + const stroops = feeType === 'base' ? state.baseFee : state.metadataFee + const xlm = stroopsToXLM(stroops) const usdAmount = xlmUsdPrice !== null ? (xlm * xlmUsdPrice).toFixed(2) : null return ( {showLabel && `${label}: `} - {formatXLM(xlm)} + {/* formatXLM expects stroops, not the converted XLM value */} + {formatXLM(stroops)} {usdAmount !== null && ≈ ${usdAmount} USD} ) diff --git a/frontend/src/components/MintForm.tsx b/frontend/src/components/MintForm.tsx index 9d7b2eb3..b46ba1e0 100644 --- a/frontend/src/components/MintForm.tsx +++ b/frontend/src/components/MintForm.tsx @@ -10,13 +10,15 @@ import { useNetwork } from '../context/NetworkContext' import { useBalanceCheck } from '../hooks/useBalanceCheck' import { useTokenDashboard } from '../hooks/useTokenDashboard' import { isValidStellarAddress, isValidContractAddress } from '../utils/validation' -import { stellarExplorerUrl } from '../utils/formatting' +import { stellarExplorerUrl, stroopsToXLM, formatXLM } from '../utils/formatting' import { useDebounce } from '../hooks/useDebounce' +import { useFactoryState } from '../hooks/useFactoryState' +import { useNetworkGuard } from '../hooks/useNetworkGuard' +import { FeeDisplay } from './FeeDisplay' import { useState } from 'react' +// Fallback fee used only until the on-chain factory state loads. const BASE_FEE_STROOPS = '100000' -const ESTIMATED_FEE_DISPLAY = '0.01 XLM' -const ESTIMATED_FEE_XLM = 0.01 const MANUAL_VALUE = '__manual__' interface MintFormData { @@ -40,7 +42,12 @@ export const MintForm: React.FC = ({ const { network } = useNetwork() const { addToast } = useToast() const { requireTos } = useTos() - const { hasSufficientBalance, shortfall, isTestnet } = useBalanceCheck(ESTIMATED_FEE_XLM) + const { state: factoryState } = useFactoryState() + const { blocked: networkBlocked, reason: networkReason } = useNetworkGuard() + // Pay the real on-chain base_fee; the contract rejects mint if fee_payment < base_fee. + const feePaymentStroops = factoryState?.baseFee ?? BASE_FEE_STROOPS + const feeXlm = stroopsToXLM(feePaymentStroops) + const { hasSufficientBalance, shortfall, isTestnet } = useBalanceCheck(feeXlm) const { rows: myTokens } = useTokenDashboard() const [pending, setPending] = useState(false) @@ -120,9 +127,9 @@ export const MintForm: React.FC = ({ tokenAddress: resolvedTokenAddress, to: recipient.trim(), amount, - feePayment: BASE_FEE_STROOPS, + feePayment: feePaymentStroops, }), - [stellarService, resolvedTokenAddress, recipient, amount], + [stellarService, resolvedTokenAddress, recipient, amount, feePaymentStroops], ) const { execute: executeMint, status: txStatus } = useTransaction(mintBuilder) @@ -260,19 +267,25 @@ export const MintForm: React.FC = ({ {/* Fee display */}

- Estimated fee: {ESTIMATED_FEE_DISPLAY} + Estimated fee:

+ {networkBlocked && networkReason && ( +

+ {networkReason} +

+ )} + {!hasSufficientBalance && ( )} @@ -309,7 +322,7 @@ export const MintForm: React.FC = ({ }, { label: 'Recipient', value: recipient }, { label: 'Amount', value: amount }, - { label: 'Estimated Fee', value: ESTIMATED_FEE_DISPLAY }, + { label: 'Estimated Fee', value: formatXLM(feePaymentStroops) }, ]} onConfirm={handleConfirm} onCancel={() => setPending(false)} diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx index 746115a2..7d59b28f 100644 --- a/frontend/src/components/NavBar.tsx +++ b/frontend/src/components/NavBar.tsx @@ -104,6 +104,9 @@ export const NavBar: React.FC = ({ onHelpClick, isAdmin = false }) {t('nav.activity', 'Activity')} + + {t('nav.faq', 'FAQ')} + {isAdmin && ( = ({ onSubmit, isLoading = fals const { addToast } = useToast() const { wallet } = useWalletContext() const { network } = useNetwork() + const { blocked: networkBlocked, reason: networkReason } = useNetworkGuard() const [formData, setFormData] = useState({ name: '', @@ -207,10 +209,20 @@ export const TokenForm: React.FC = ({ onSubmit, isLoading = fals
{/* Submit Button */} - + {networkBlocked && networkReason && ( +

+ {networkReason} +

+ )} + {!wallet.isConnected && (

{t('tokenForm.connectWalletFirst')} diff --git a/frontend/src/components/WalletConnectButton.tsx b/frontend/src/components/WalletConnectButton.tsx deleted file mode 100644 index d409346d..00000000 --- a/frontend/src/components/WalletConnectButton.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react' -import { useWalletContext } from '../context/WalletContext' -import { formatXLM, truncateAddress } from '../utils/formatting' -import { Button } from './UI/Button' -import { Spinner } from './UI/Spinner' -import { CopyButton } from './CopyButton' - -export const WalletConnectButton: React.FC = () => { - const { wallet, isConnecting, isInstalled, connect, disconnect } = useWalletContext() - - // Wallet not installed state - if (!isInstalled) { - return ( - - Install Freighter - - ) - } - - // Connecting state - if (isConnecting) { - return ( -

- - Connecting... -
- ) - } - - // Connected state - if (wallet.isConnected && wallet.address) { - return ( -
-
-
- - {truncateAddress(wallet.address)} - - -
- {wallet.balance !== undefined ? ( - - {formatXLM(wallet.balance)} - - ) : ( - Loading balance... - )} -
- -
- ) - } - - // Disconnected state (default) - return ( - - ) -}