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
3 changes: 2 additions & 1 deletion frontend/src/components/CreateToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ShareButton } from './ShareButton'
import { CopyButton } from './CopyButton'
import { STELLAR_CONFIG } from '../config/stellar'
import ErrorBoundary from './ErrorBoundary'
import { logger } from '../utils/logger'

interface DeployedToken {
address: string
Expand Down Expand Up @@ -54,7 +55,7 @@ export const CreateToken: React.FC = () => {
addToast(t('tokenForm.deployFailed'), 'error')
}
} catch (error) {
console.error('Deployment error:', error)
logger.error('Deployment error:', error)
addToast(error instanceof Error ? error.message : t('tokenForm.deployError'), 'error')
} finally {
setIsDeploying(false)
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/MetadataUploadForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ipfsService } from '../services/ipfs'
import { isIpfsConfigured } from '../config/env'
import { isValidImageFile } from '../utils/validation'
import { DropZone } from './DropZone'
import { logger } from '../utils/logger'

interface MetadataUploadFormProps {
onUploadComplete: (metadataUri: string) => void
Expand Down Expand Up @@ -92,7 +93,7 @@ export const MetadataUploadForm: React.FC<MetadataUploadFormProps> = ({
setImageFile(null)
setUploadProgress(0)
} catch (error) {
console.error('Upload error:', error)
logger.error('Upload error:', error)
addToast(error instanceof Error ? error.message : 'Failed to upload metadata', 'error')
} finally {
setIsUploading(false)
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/NetworkBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import type { Network } from '../context/NetworkContext'
const BADGE_COLORS: Record<Network, string> = {
testnet: 'bg-yellow-100 text-yellow-800 border-yellow-300',
mainnet: 'bg-green-100 text-green-800 border-green-300',
standalone: 'bg-purple-100 text-purple-800 border-purple-300',
}

const DOT_COLORS: Record<Network, string> = {
testnet: 'bg-yellow-500',
mainnet: 'bg-green-500',
standalone: 'bg-purple-500',
}

const LABELS: Record<Network, string> = {
testnet: 'Testnet',
mainnet: 'Mainnet',
standalone: 'Standalone',
}

/**
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/TokenDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const TokenDashboard: React.FC = () => {
if (!wallet.isConnected) return <NotConnected />

return (
<div className="space-y-6">
<div className="space-y-6 token-list-container">
{/* Header */}
<div className="flex items-center justify-between gap-4 flex-wrap">
<div>
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/TokenForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useNetwork } from '../context/NetworkContext'
import { validateTokenParams } from '../utils/validation'
import { formatXLM, stroopsToXLM } from '../utils/formatting'
import { useFactoryState } from '../hooks/useFactoryState'
import { logger } from '../utils/logger'

interface TokenFormProps {
onSubmit: (params: {
Expand Down Expand Up @@ -118,7 +119,7 @@ export const TokenForm: React.FC<TokenFormProps> = ({
try {
await onSubmit(formData)
} catch (error) {
console.error('Form submission error:', error)
logger.error('Form submission error:', error)
addToast(error instanceof Error ? error.message : t('tokenForm.submitError'), 'error')
}
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/UI/WalletButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const WalletButton: React.FC = () => {
className="hidden md:block font-mono text-sm text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-slate-700 px-2 py-1 rounded"
title={wallet.address}
>
{truncateAddress(wallet.address)}
{truncateAddress(wallet.address, 4, 6)}
</span>
<Button
onClick={disconnect}
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/config/stellar.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Stellar network configuration
import { ENV } from './env'

export type Network = 'testnet' | 'mainnet'
export type Network = 'testnet' | 'mainnet' | 'standalone'

export interface NetworkConfig {
networkPassphrase: string
Expand All @@ -20,6 +20,11 @@ export const NETWORK_CONFIGS: Record<Network, NetworkConfig> = {
horizonUrl: 'https://horizon.stellar.org',
sorobanRpcUrl: 'https://soroban-mainnet.stellar.org',
},
standalone: {
networkPassphrase: 'Standalone Network ; February 2017',
horizonUrl: 'http://localhost:8000',
sorobanRpcUrl: 'http://localhost:8000/soroban/rpc',
},
}

export const STELLAR_CONFIG = {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/context/NetworkContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { STELLAR_CONFIG } from '../config/stellar'
import { useLocalStorage } from '../hooks/useLocalStorage'
import { useNetworkMismatch, type NetworkMismatchState } from '../hooks/useNetworkMismatch'

export type Network = 'testnet' | 'mainnet'
export type Network = 'testnet' | 'mainnet' | 'standalone'

const STORAGE_KEY = 'stellarforge_network'

Expand Down
6 changes: 3 additions & 3 deletions frontend/src/services/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
getAddress,
signTransaction as freighterSignTransaction,
} from '@stellar/freighter-api'
import { NETWORK_CONFIGS } from '../config/stellar'
import { NETWORK_CONFIGS, type Network } from '../config/stellar'

interface HorizonBalance {
asset_type: string
Expand Down Expand Up @@ -87,7 +87,7 @@ export class WalletService {
this.clearAddress()
}

async signTransaction(xdr: string, network: 'testnet' | 'mainnet'): Promise<string> {
async signTransaction(xdr: string, network: Network): Promise<string> {
if (!(await this.isInstalled())) {
throw new Error('Freighter wallet is not installed')
}
Expand Down Expand Up @@ -120,7 +120,7 @@ export class WalletService {
}
}

async getBalance(address: string, network: 'testnet' | 'mainnet'): Promise<string> {
async getBalance(address: string, network: Network): Promise<string> {
try {
const horizonUrl = NETWORK_CONFIGS[network].horizonUrl

Expand Down
10 changes: 10 additions & 0 deletions frontend/src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const isProd = import.meta.env.PROD

export const logger = {
error(message: string, error?: unknown): void {
if (!isProd) {
console.error(message, error)
}
// Forward to Sentry or other monitoring when integrated
},
}
15 changes: 8 additions & 7 deletions frontend/tests/e2e/helpers/e2e-setup.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { Keypair } from 'stellar-sdk';
import { Keypair } from 'stellar-sdk'

/**
* Funds an account on the Stellar testnet using the public Friendbot.
* Funds an account using Friendbot. Defaults to the local standalone network
* friendbot (used in CI); override with FRIENDBOT_URL env var for testnet.
*/
export async function fundAccount(address: string) {
const friendbotUrl = 'https://friendbot.stellar.org';
const response = await fetch(`${friendbotUrl}?addr=${address}`);
const friendbotUrl = process.env.FRIENDBOT_URL ?? 'http://localhost:8000/friendbot'
const response = await fetch(`${friendbotUrl}?addr=${address}`)

if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Failed to fund account ${address}: ${response.statusText} - ${errorBody}`);
const errorBody = await response.text()
throw new Error(`Failed to fund account ${address}: ${response.statusText} - ${errorBody}`)
}
}

/**
* Generates a new random Keypair for testing.
*/
export function generateTestAccount() {
return Keypair.random();
return Keypair.random()
}
88 changes: 78 additions & 10 deletions frontend/tests/e2e/helpers/wallet-mock.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Page } from '@playwright/test';
import { Page } from '@playwright/test'

export interface FreighterMock {
isConnected: () => Promise<{ isConnected: boolean }>
Expand All @@ -15,20 +15,88 @@ declare global {
}

/**
* Mocks the Freighter wallet API on the window object.
* Mocks the Freighter wallet browser extension on the page.
*
* @stellar/freighter-api v6.0.1 uses two mechanisms:
* 1. window.freighter presence check (isConnected shortcircuits to truthy when set)
* 2. postMessage protocol (FREIGHTER_EXTERNAL_MSG_REQUEST / RESPONSE) for getAddress,
* signTransaction, etc.
*
* Key design: WatchWalletChanges.fetchInfo() calls REQUEST_PUBLIC_KEY then
* REQUEST_NETWORK_DETAILS in sequence. We intentionally do NOT respond to
* REQUEST_NETWORK_DETAILS — that makes the watcher hang at the second await and
* never fire its auto-connect callback, so "Connect Wallet" remains visible.
* Explicit connect() only sends REQUEST_PUBLIC_KEY, so it still works.
*
* ToS is pre-accepted via localStorage so the modal does not block connect().
*/
export async function mockFreighter(page: Page, address: string) {
await page.addInitScript((mockAddress: string) => {
// Mock the global freighter object if needed,
// but @stellar/freighter-api actually uses window.freighter or similar internally.
// We can also mock the individual functions if they are exported from a module.
// However, since we are testing the built app, we need to mock what the library expects.
// Pre-accept Terms of Service so the modal doesn't block wallet connection
localStorage.setItem('stellar_forge_tos_accepted', 'true')

// Set window.freighter so isConnected() returns {isConnected: <truthy>} immediately
window.freighter = {
isConnected: () => Promise.resolve({ isConnected: true }),
getAddress: () => Promise.resolve({ address: mockAddress }),
requestAccess: () => Promise.resolve({ address: mockAddress }),
signTransaction: (xdr: string) => Promise.resolve({ signedTxXdr: xdr }), // Return unsigned for simplicity in mock
getNetwork: () => Promise.resolve({ network: 'TESTNET' }),
};
}, address);
signTransaction: (xdr: string) => Promise.resolve({ signedTxXdr: xdr }),
getNetwork: () => Promise.resolve({ network: 'STANDALONE' }),
}

// Intercept the postMessage protocol used by freighter-api for getAddress(),
// signTransaction(), REQUEST_NETWORK_DETAILS, etc.
// Request format: { source: 'FREIGHTER_EXTERNAL_MSG_REQUEST', messageId: number, type: string, ... }
// Response format: { source: 'FREIGHTER_EXTERNAL_MSG_RESPONSE', messagedId: number, ... }
// Note: 'messagedId' (with trailing 'd') is the Freighter library's key name.
window.addEventListener('message', (event) => {
const data = event.data as Record<string, unknown>
if (!data || data['source'] !== 'FREIGHTER_EXTERNAL_MSG_REQUEST') return

const messageId = data['messageId']
const type = data['type'] as string

const base: Record<string, unknown> = {
source: 'FREIGHTER_EXTERNAL_MSG_RESPONSE',
messagedId: messageId,
}

switch (type) {
case 'REQUEST_CONNECTION_STATUS':
window.postMessage({ ...base, isConnected: true }, '*')
break
case 'REQUEST_PUBLIC_KEY':
window.postMessage({ ...base, publicKey: mockAddress }, '*')
break
case 'REQUEST_ACCESS':
window.postMessage({ ...base, publicKey: mockAddress }, '*')
break
case 'REQUEST_NETWORK_DETAILS':
// Intentionally no response: WatchWalletChanges.fetchInfo() awaits _() after A().
// Without a response _() hangs forever, so the watcher callback never fires and
// the wallet does not auto-connect on mount. Explicit connect() is unaffected
// because getAddress() only sends REQUEST_PUBLIC_KEY, not REQUEST_NETWORK_DETAILS.
break
case 'REQUEST_ALLOWED_STATUS':
window.postMessage({ ...base, isAllowed: true }, '*')
break
case 'SET_ALLOWED_STATUS':
window.postMessage({ ...base, isAllowed: true }, '*')
break
case 'SUBMIT_TRANSACTION':
window.postMessage(
{
...base,
signedTransaction: data['transactionXdr'],
signerAddress: mockAddress,
},
'*',
)
break
default:
// Don't respond to unknown message types
break
}
})
}, address)
}
Loading