From e388e7aacaf66708e01aba263121bd9b932895b2 Mon Sep 17 00:00:00 2001 From: Ejirowebfi Date: Wed, 1 Jul 2026 14:03:21 +0000 Subject: [PATCH 1/3] fix(frontend): source FeeDisplay fees from useFactoryState, not the singleton The module stellarService singleton is never setNetwork-synced (nothing calls it), so it stays on the default testnet. Now that FeeDisplay is rendered in TokenForm, reading fees from that singleton would show testnet fees on a mainnet deployment. Switch to useFactoryState (env-resolved network, same source as the rest of the app) and drop FeeDisplay's stale module cache. Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/FeeDisplay.tsx | 44 +++++--------------------- 1 file changed, 8 insertions(+), 36 deletions(-) diff --git a/frontend/src/components/FeeDisplay.tsx b/frontend/src/components/FeeDisplay.tsx index 899e2dec..37c805c0 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 xlm = stroopsToXLM(feeType === 'base' ? state.baseFee : state.metadataFee) const usdAmount = xlmUsdPrice !== null ? (xlm * xlmUsdPrice).toFixed(2) : null return ( From 901021f9ce8eac98d3d50bf5970006818bd835f8 Mon Sep 17 00:00:00 2001 From: Ejirowebfi Date: Wed, 1 Jul 2026 14:31:50 +0000 Subject: [PATCH 2/3] feat(frontend): wire network-mismatch banner + FAQ page, drop dead duplicate, fix repo URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finish merged-but-unwired campaign features: - Render NetworkMismatchBanner (from NetworkBadge.tsx) in App — it warns when Freighter's network differs from the app's; it was built but never mounted. - Add a /faq route + NavBar link for the FAQ page, which existed but was never routed. - Remove WalletConnectButton.tsx, a dead duplicate of the actively-used UI/WalletButton (never imported anywhere). - README: point deploy/docs/issues links at Favourorg/Stellar-forge (the actual origin) instead of the stale Ejirowebfi URLs. --- README.md | 4 +- frontend/src/App.tsx | 12 ++++ frontend/src/components/NavBar.tsx | 3 + .../src/components/WalletConnectButton.tsx | 71 ------------------- 4 files changed, 17 insertions(+), 73 deletions(-) delete mode 100644 frontend/src/components/WalletConnectButton.tsx 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() && (
} /> + + + + } + /> = ({ onHelpClick, isAdmin = false }) {t('nav.activity', 'Activity')} + + {t('nav.faq', 'FAQ')} + {isAdmin && ( { - 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 ( - - ) -} From 6d5697d3f661be16c4fefccdb1a71f5b5d35734b Mon Sep 17 00:00:00 2001 From: Ejirowebfi Date: Wed, 1 Jul 2026 14:55:13 +0000 Subject: [PATCH 3/3] fix(frontend): pay real base_fee, surface FeeDisplay in mint, block writes on network mismatch - Create + mint now pay the on-chain base_fee (from useFactoryState) instead of a hardcoded 0.01 XLM. The contract rejects create/mint when fee_payment < base_fee, so the hardcode would fail InsufficientFee on any factory whose fee is above 0.01 XLM. - Render FeeDisplay (real fee + USD) in the mint flow; drop the hardcoded ESTIMATED_FEE display constants. - Fix a latent unit bug in FeeDisplay/MintForm: formatXLM expects stroops but was passed already-converted XLM, which throws 'Cannot convert 0.01 to a BigInt' for fractional fees (dormant only because FeeDisplay was orphaned). - Wire useNetworkGuard into create/mint/burn: submit is disabled with a reason when Freighter's network differs from the app's, completing the mismatch guard alongside the NetworkMismatchBanner. --- frontend/src/components/BurnForm.tsx | 10 +++++++- frontend/src/components/CreateToken.tsx | 5 +++- frontend/src/components/FeeDisplay.tsx | 6 +++-- frontend/src/components/MintForm.tsx | 31 ++++++++++++++++++------- frontend/src/components/TokenForm.tsx | 14 ++++++++++- 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/BurnForm.tsx b/frontend/src/components/BurnForm.tsx index 0e19b165..01be2cba 100644 --- a/frontend/src/components/BurnForm.tsx +++ b/frontend/src/components/BurnForm.tsx @@ -9,6 +9,7 @@ import { useTos } from '../context/TosContext' import { useStellarContext } from '../context/StellarContext' import { useToast } from '../context/ToastContext' import { useNetwork } from '../context/NetworkContext' +import { useNetworkGuard } from '../hooks/useNetworkGuard' import { useBalanceCheck } from '../hooks/useBalanceCheck' import { useTokenDashboard } from '../hooks/useTokenDashboard' import { isValidContractAddress } from '../utils/validation' @@ -38,6 +39,7 @@ export const BurnForm: React.FC = ({ 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 37c805c0..c56d8efb 100644 --- a/frontend/src/components/FeeDisplay.tsx +++ b/frontend/src/components/FeeDisplay.tsx @@ -44,13 +44,15 @@ export const FeeDisplay: React.FC = ({ ) } - const xlm = stroopsToXLM(feeType === 'base' ? state.baseFee : state.metadataFee) + 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/TokenForm.tsx b/frontend/src/components/TokenForm.tsx index 1f51d258..a79a2f58 100644 --- a/frontend/src/components/TokenForm.tsx +++ b/frontend/src/components/TokenForm.tsx @@ -5,6 +5,7 @@ import { Button } from './UI/Button' import { useToast } from '../context/ToastContext' import { useWalletContext } from '../context/WalletContext' import { useNetwork } from '../context/NetworkContext' +import { useNetworkGuard } from '../hooks/useNetworkGuard' import { validateTokenParams } from '../utils/validation' import { logger } from '../utils/logger' import { FeeDisplay } from './FeeDisplay' @@ -31,6 +32,7 @@ export const TokenForm: React.FC = ({ 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')}