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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 })),
Expand Down Expand Up @@ -225,6 +227,8 @@ function AppContent() {
</header>
{showOnboarding && <OnboardingModal forceOpen onClose={() => setShowOnboarding(false)} />}

<NetworkMismatchBanner />

{!isFactoryConfigured() && (
<div
className="bg-yellow-50 dark:bg-yellow-900/30 border-b border-yellow-300 dark:border-yellow-700 p-4"
Expand Down Expand Up @@ -328,6 +332,14 @@ function AppContent() {
</RouteBoundary>
}
/>
<Route
path="/faq"
element={
<RouteBoundary routeName="FAQ">
<FAQ />
</RouteBoundary>
}
/>
<Route
path="/metadata"
element={
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/components/BurnForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -38,6 +39,7 @@ export const BurnForm: React.FC<BurnFormProps> = ({
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)
Expand Down Expand Up @@ -256,12 +258,18 @@ export const BurnForm: React.FC<BurnFormProps> = ({
<Button
type="submit"
loading={isSubmitting}
disabled={isSubmitting || amountExceedsBalance || !hasSufficientBalance}
disabled={isSubmitting || amountExceedsBalance || !hasSufficientBalance || networkBlocked}
className="w-full sm:w-auto bg-red-600 hover:bg-red-700 focus:ring-red-500 text-white disabled:opacity-50"
>
{isSubmitting ? 'Processing…' : '🔥 Burn Tokens'}
</Button>

{networkBlocked && networkReason && (
<p className="text-sm text-red-600 dark:text-red-400" role="alert">
{networkReason}
</p>
)}

{!hasSufficientBalance && (
<InsufficientBalanceWarning shortfall={shortfall} isTestnet={isTestnet} />
)}
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/CreateToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<DeployedToken | null>(null)
Expand All @@ -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)
Expand Down
48 changes: 11 additions & 37 deletions frontend/src/components/FeeDisplay.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -11,21 +11,6 @@ interface FeeDisplayProps {
showLabel?: boolean
}

// Module-level cache — shared across all FeeDisplay instances
let cachedFactoryState: FactoryState | null = null
let pendingRequest: Promise<FactoryState> | null = null

function getFactoryState(): Promise<FactoryState> {
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<FeeDisplayProps['feeType'], string> = {
base: 'Creation Fee',
metadata: 'Metadata Fee',
Expand All @@ -36,33 +21,19 @@ export const FeeDisplay: React.FC<FeeDisplayProps> = ({
className = '',
showLabel = true,
}: FeeDisplayProps) => {
const [xlm, setXlm] = useState<number | null>(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 <span className={`text-sm text-red-500 ${className}`}>{label}: unavailable</span>
}

if (xlm === null) {
if (!state) {
// Loading skeleton
return (
<span
Expand All @@ -73,12 +44,15 @@ export const FeeDisplay: React.FC<FeeDisplayProps> = ({
)
}

const stroops = feeType === 'base' ? state.baseFee : state.metadataFee
const xlm = stroopsToXLM(stroops)
const usdAmount = xlmUsdPrice !== null ? (xlm * xlmUsdPrice).toFixed(2) : null

return (
<span className={`text-sm text-gray-700 ${className}`}>
{showLabel && `${label}: `}
{formatXLM(xlm)}
{/* formatXLM expects stroops, not the converted XLM value */}
{formatXLM(stroops)}
{usdAmount !== null && <span className="text-gray-400 ml-1">≈ ${usdAmount} USD</span>}
</span>
)
Expand Down
31 changes: 22 additions & 9 deletions frontend/src/components/MintForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -40,7 +42,12 @@ export const MintForm: React.FC<MintFormProps> = ({
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)
Expand Down Expand Up @@ -120,9 +127,9 @@ export const MintForm: React.FC<MintFormProps> = ({
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)
Expand Down Expand Up @@ -260,19 +267,25 @@ export const MintForm: React.FC<MintFormProps> = ({

{/* Fee display */}
<p className="text-xs text-gray-500 dark:text-gray-400">
Estimated fee: <span className="font-medium">{ESTIMATED_FEE_DISPLAY}</span>
Estimated fee: <FeeDisplay feeType="base" showLabel={false} className="text-xs" />
</p>

<Button
type="submit"
variant="primary"
loading={isSubmitting}
disabled={isSubmitting || !hasSufficientBalance}
disabled={isSubmitting || !hasSufficientBalance || networkBlocked}
className="w-full sm:w-auto"
>
{isSubmitting ? 'Processing Transaction…' : 'Mint Tokens'}
</Button>

{networkBlocked && networkReason && (
<p className="text-sm text-red-600 dark:text-red-400" role="alert">
{networkReason}
</p>
)}

{!hasSufficientBalance && (
<InsufficientBalanceWarning shortfall={shortfall} isTestnet={isTestnet} />
)}
Expand Down Expand Up @@ -309,7 +322,7 @@ export const MintForm: React.FC<MintFormProps> = ({
},
{ 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)}
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export const NavBar: React.FC<NavBarProps> = ({ onHelpClick, isAdmin = false })
<NavLink to="/activity" className={getLinkClass} onClick={closeMenu}>
{t('nav.activity', 'Activity')}
</NavLink>
<NavLink to="/faq" className={getLinkClass} onClick={closeMenu}>
{t('nav.faq', 'FAQ')}
</NavLink>
{isAdmin && (
<NavLink
to="/admin"
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/components/TokenForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -31,6 +32,7 @@ export const TokenForm: React.FC<TokenFormProps> = ({ onSubmit, isLoading = fals
const { addToast } = useToast()
const { wallet } = useWalletContext()
const { network } = useNetwork()
const { blocked: networkBlocked, reason: networkReason } = useNetworkGuard()

const [formData, setFormData] = useState({
name: '',
Expand Down Expand Up @@ -207,10 +209,20 @@ export const TokenForm: React.FC<TokenFormProps> = ({ onSubmit, isLoading = fals
</div>

{/* Submit Button */}
<Button type="submit" disabled={!isFormValid() || isLoading} className="w-full">
<Button
type="submit"
disabled={!isFormValid() || isLoading || networkBlocked}
className="w-full"
>
{isLoading ? t('tokenForm.deploying') : t('tokenForm.deploy')}
</Button>

{networkBlocked && networkReason && (
<p className="text-sm text-red-600 dark:text-red-400 text-center" role="alert">
{networkReason}
</p>
)}

{!wallet.isConnected && (
<p className="text-sm text-amber-600 dark:text-amber-400 text-center">
{t('tokenForm.connectWalletFirst')}
Expand Down
Loading