Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6b275f1
fix(frontend): remove unused useLocalStorage import from App.tsx
sanmipaul May 29, 2026
d3cdac2
fix(frontend): remove unused FocusTrapWrapper import from App.tsx
sanmipaul May 29, 2026
611b6f4
fix(frontend): remove unused detectNetworkFromAddress function
sanmipaul May 29, 2026
eb3e513
fix(frontend): remove unused debouncedWithdrawAmount variable
sanmipaul May 29, 2026
7dd9162
fix(frontend): remove unused handleRefreshStats function
sanmipaul May 29, 2026
86a1baf
fix(frontend): add missing connectionError state declaration
sanmipaul May 29, 2026
2ff526b
fix(frontend): add missing retryCount, showHelp, currentTransaction, …
sanmipaul May 29, 2026
249ea9e
fix(frontend): define fetchUserStats function that was referenced but…
sanmipaul May 29, 2026
06e69f6
fix(frontend): remove misplaced ErrorBoundary closing tag from AppCon…
sanmipaul May 29, 2026
ee1fe9f
fix(frontend): add error logging to handle2FAVerify for debugging
sanmipaul May 29, 2026
4d5c669
fix(frontend): add address validation to WalletConnect session handler
sanmipaul May 29, 2026
c5bc72b
fix(frontend): add missing validateNetwork function referenced in dep…
sanmipaul May 29, 2026
b01e218
fix(frontend): add missing getAnalyticsUrl import, remove unused call…
sanmipaul May 29, 2026
32c8345
fix(frontend): remove unused ContractError and StacksContractCallOpti…
sanmipaul May 29, 2026
9344cb7
fix(wallet): implement LeatherWalletProvider signTransaction using op…
sanmipaul May 29, 2026
0c95676
fix(wallet): implement HiroWalletProvider signTransaction via openCon…
sanmipaul May 29, 2026
dab0df0
fix(wallet): implement XverseWalletProvider signTransaction via provi…
sanmipaul May 29, 2026
9acbf27
fix(wallet): fix LedgerWalletProvider signTransaction type misuse
sanmipaul May 29, 2026
7293780
fix(frontend): implement WalletConnect transaction handler and add re…
sanmipaul May 29, 2026
c499442
fix(wallet): implement WalletConnectProvider with real session handli…
sanmipaul May 29, 2026
ce8591f
fix(frontend): remove invalid AppKit config options for Stacks
sanmipaul May 29, 2026
b514322
fix(wallet): fix TrezorWalletProvider signTransaction type crash
sanmipaul May 29, 2026
bd478bb
fix(frontend): fix BalanceService memory leak and remove duplicate re…
sanmipaul May 29, 2026
b6d4926
fix(wallet): fix MultiSigWalletProvider return types to match SignedT…
sanmipaul May 29, 2026
a9698b0
fix(frontend): replace Promise.resolve().then() anti-pattern with use…
sanmipaul May 29, 2026
6f578aa
fix(frontend): add VAPID key validation before push subscription
sanmipaul May 29, 2026
94c1b70
fix(frontend): add timeout and AbortController to analytics fetch req…
sanmipaul May 29, 2026
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
154 changes: 118 additions & 36 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { logger } from './utils/logger';
import React, { useState, useEffect, useMemo } from 'react';
import { useDebounce } from './hooks/useDebounce';
import { useLocalStorage } from './hooks/useLocalStorage';
import { AppConfig, UserSession, showConnect, UserData, openContractCall } from '@stacks/connect';
import { StacksMainnet, StacksTestnet } from '@stacks/network';
import { StacksMainnet } from '@stacks/network';
import {
callReadOnlyFunction,
makeContractCall,
broadcastTransaction,
AnchorMode,
uintCV,
standardPrincipalCV
uintCV
} from '@stacks/transactions';
import { WalletConnect } from './components/WalletConnect';
import { WithdrawTxDetails, WalletConnectSession, WalletConnectTransactionParams, SignedTransactionResult, StacksContractCallOptions } from './types/wallet';
Expand All @@ -21,15 +18,16 @@ import { AutoReconnect } from './components/AutoReconnect';
import NotificationService from './services/notificationService';
import TransactionHistory from './components/TransactionHistory';
import NotificationCenter from './components/NotificationCenter';
import { FocusTrapWrapper } from './components/FocusTrapWrapper';
import AmountInput from './components/AmountInput';
import { useAmountValidation } from './hooks/useAmountValidation';
import { validateDepositAmount, validateWithdrawAmount, parseSTXInput } from './utils/amountValidator';
import { TwoFactorSecureStorage } from './services/security/TwoFactorSecureStorage';
import { TwoFactorMigration } from './services/security/TwoFactorMigration';
import { ContractErrorMapper, ContractError } from './utils/contractErrorMapper';
import { ContractErrorMapper } from './utils/contractErrorMapper';
import { ErrorBoundary } from './components/ErrorBoundary';
import { ErrorFallback } from './components/ErrorFallback';
import { WalletManager } from './services/wallet/WalletManager';
import { getAnalyticsUrl } from './config/api';

const appConfig = new AppConfig(['store_write', 'publish_data']);
const userSession = new UserSession({ appConfig });
Expand All @@ -48,11 +46,6 @@ const APP_CONFIG = {
tfaBackupCodesKey: 'tfa-backup-codes',
} as const;

const detectNetworkFromAddress = (address: string): 'mainnet' | 'testnet' => {
// Stacks mainnet addresses start with 'SP', testnet with 'ST'
return address.startsWith('SP') ? 'mainnet' : 'testnet';
};

const getCurrentNetwork = () => {
// Always return mainnet for RenVault operations
return new StacksMainnet();
Expand Down Expand Up @@ -83,7 +76,6 @@ function AppContent() {
const [depositAmount, setDepositAmount] = useState<string>('');
const [withdrawAmount, setWithdrawAmount] = useState<string>('');
const debouncedDepositAmount = useDebounce(depositAmount, 300);
const debouncedWithdrawAmount = useDebounce(withdrawAmount, 300);
const [status, setStatus] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [detectedNetwork, setDetectedNetwork] = useState<'mainnet' | 'testnet' | null>(null);
Expand All @@ -94,6 +86,11 @@ function AppContent() {
const [showConnectionOptions, setShowConnectionOptions] = useState<boolean>(false);
const [walletConnectSession, setWalletConnectSession] = useState<WalletConnectSession | null>(null);
const [toastMessage, setToastMessage] = useState<string | null>(null);
const [connectionError, setConnectionError] = useState<string | null>(null);
const [retryCount, setRetryCount] = useState<number>(0);
const [showHelp, setShowHelp] = useState<boolean>(false);
const [currentTransaction, setCurrentTransaction] = useState<WalletConnectTransactionParams | null>(null);
const [walletManager] = useState<WalletManager>(() => new WalletManager());

// Modal visibility state
const [show2FASetup, setShow2FASetup] = useState<boolean>(false);
Expand Down Expand Up @@ -137,6 +134,60 @@ const [walletConnectSession, setWalletConnectSession] = useState<WalletConnectSe
userData?.profile?.stxAddress?.testnet ??
'';

const fetchUserStats = async (): Promise<void> => {
if (!userAddress) return;
try {
const response = await fetch(`/api/stats/${userAddress}`);
if (response.ok) {
const stats = await response.json();
if (stats.balance !== undefined) setBalance(stats.balance);
if (stats.points !== undefined) setPoints(stats.points);
}
} catch (error) {
logger.warn('Failed to fetch user stats:', error);
}
};

const fetchStats = async (address: string, _mismatch: boolean): Promise<void> => {
try {
const response = await fetch(`/api/stats/${address}`);
if (response.ok) {
const stats = await response.json();
if (stats.balance !== undefined) setBalance(stats.balance);
if (stats.points !== undefined) setPoints(stats.points);
}
} catch (error) {
logger.warn('Failed to fetch stats:', error);
}
};

const validateNetwork = (): boolean => {
if (networkMismatch) {
setStatus('Network mismatch detected. Please switch your wallet to mainnet.');
return false;
}
return true;
};

const promptSwitch = (): string => {
const msg = detectedNetwork === 'testnet'
? 'You are connected to testnet. Mainnet is recommended for real transactions.'
: 'Network mismatch detected. Please switch your wallet network.';
return msg;
};

const handleWalletBackupComplete = (_data: string): void => {
setShowWalletBackup(false);
setStatus('Wallet backup completed successfully.');
setTimeout(() => setStatus(''), 5000);
};

const handleWalletRecoveryComplete = (): void => {
setShowWalletRecovery(false);
setStatus('Wallet recovery completed successfully.');
setTimeout(() => setStatus(''), 5000);
};

const handle2FASetupComplete = async (secret: string, backupCodes: string[]) => {
setTfaSecret(secret);
localStorage.setItem(APP_CONFIG.tfaEnabledKey, 'true');
Expand All @@ -163,8 +214,12 @@ const [walletConnectSession, setWalletConnectSession] = useState<WalletConnectSe
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'current-user', code })
});
if (!response.ok) {
logger.warn('2FA verification failed with status:', response.status);
}
return response.ok;
} catch (error) {
logger.error('2FA verification network error:', error);
return false;
}
};
Expand Down Expand Up @@ -446,57 +501,89 @@ const [walletConnectSession, setWalletConnectSession] = useState<WalletConnectSe
if (!walletConnectSession) return;

try {
// Create transaction payload for WalletConnect
const txPayload = {
setLoading(true);
const amount = params.amount;
const network = getCurrentNetwork();

// Build transaction using the WalletConnect provider
const walletConnectProvider = new (await import('./services/wallet/WalletConnectProvider')).WalletConnectProvider();
const txOptions: StacksContractCallOptions = {
contractAddress: CONTRACT_ADDRESS,
contractName: CONTRACT_NAME,
functionName: action,
functionArgs: action === 'deposit' ? [uintCV(params.amount)] : [uintCV(params.amount)],
network: 'stacks:1', // Stacks mainnet
functionArgs: [uintCV(amount)],
network,
anchorMode: AnchorMode.Any,
};

// Use WalletConnect to sign and send the transaction
// This would typically involve calling walletKit.request() with the appropriate method
// For now, show a placeholder message
setStatus(`WalletConnect ${action} transaction initiated. Please check your wallet app.`);


const signedResult = await walletConnectProvider.signTransaction(txOptions);

// Broadcast the signed transaction
if (signedResult.transaction) {
const broadcastResponse = await broadcastTransaction(signedResult.transaction, network);
setStatus(`${action} transaction submitted: ${broadcastResponse.txid}`);
} else {
setStatus(`${action} transaction signed. Broadcasting via wallet...`);
}

// Clear form
if (action === 'deposit') {
setDepositAmount('');
depositValidation.reset();
} else {
setWithdrawAmount('');
withdrawValidation.reset();
}

setTimeout(fetchUserStats, 5000); // Longer delay for WalletConnect

// Send notification
if (notificationService) {
if (action === 'deposit') {
notificationService.testDepositNotification(amount, parseFloat(balance) + amount);
} else {
notificationService.testWithdrawalNotification(amount, parseFloat(balance) - amount);
}
}

trackAnalytics(action, { user: userAddress ?? 'anonymous', amount });
setTimeout(fetchUserStats, 3000);
} catch (error: unknown) {
const friendlyMsg = ContractErrorMapper.isContractError(error)
? ContractErrorMapper.toStatusMessage(error, CONTRACT_NAME)
: error instanceof Error ? error.message : 'Unknown error';
setStatus(`❌ WalletConnect error: ${friendlyMsg}`);
} finally {
setLoading(false);
}
};

const handleWalletConnectSession = (session: WalletConnectSession) => {
// Extract Stacks account from WalletConnect session
const stacksAccount = session.namespaces.stacks?.accounts?.[0];
if (stacksAccount) {
// Create a mock userData object compatible with @stacks/connect
const mockUserData = {
const addressParts = stacksAccount.split(':');
const walletAddress = addressParts.length >= 3 ? addressParts[2] : null;
if (!walletAddress) {
setStatus('❌ Invalid WalletConnect session: missing address');
return;
}
// Create a userData object compatible with @stacks/connect
const mockUserData: Partial<UserData> = {
profile: {
stxAddress: {
mainnet: stacksAccount.split(':')[2], // Extract address from stacks:1:address
testnet: stacksAccount.split(':')[2],
mainnet: walletAddress,
testnet: walletAddress,
},
name: 'WalletConnect User',
},
appPrivateKey: '', // WalletConnect handles signing
};

setUserData(mockUserData as any);
setUserData(mockUserData as UserData);
setWalletConnectSession(session);
setStatus('✅ Connected via WalletConnect');
trackAnalytics('wallet-connect', { user: stacksAccount.split(':')[2], method: 'walletconnect', success: true });
trackAnalytics('wallet-connect', { user: walletAddress, method: 'walletconnect', success: true });
} else {
setStatus('❌ No Stacks accounts found in WalletConnect session');
trackAnalytics('wallet-connect', { user: 'anonymous', method: 'walletconnect', success: false });
}
};
Expand Down Expand Up @@ -551,10 +638,6 @@ const [walletConnectSession, setWalletConnectSession] = useState<WalletConnectSe
trackAnalytics('withdrawal', { user: userAddress ?? 'anonymous', amount });
};

const handleRefreshStats = () => {
if (userAddress) fetchStats(userAddress, networkMismatch);
};

if (!userData) {
return (
<>
Expand Down Expand Up @@ -761,7 +844,6 @@ const [walletConnectSession, setWalletConnectSession] = useState<WalletConnectSe
</button>
</div>
</div>
</ErrorBoundary>

{status && (
<div className={`status ${status.toLowerCase().includes('error') ? 'error' : 'success'}`}>
Expand Down
28 changes: 8 additions & 20 deletions frontend/src/hooks/useDebouncedValidation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useCallback, useRef } from 'react';
import { useState, useCallback, useEffect, useRef } from 'react';
import { useDebounce } from './useDebounce';

export interface ValidationResult {
Expand All @@ -9,14 +9,6 @@ export interface ValidationResult {

const VALID_RESULT: ValidationResult = { valid: true, error: '' };

/**
* Wraps a synchronous validator function with debounced execution.
* The `result` reflects the last completed validation; `isPending` is true
* while the debounce timer is still running.
*
* @param validator Pure function mapping a raw string to a ValidationResult.
* @param delay Debounce delay in milliseconds (default 300).
*/
export function useDebouncedValidation(
validator: (raw: string) => ValidationResult,
delay = 300
Expand All @@ -29,10 +21,8 @@ export function useDebouncedValidation(
const [raw, setRaw] = useState('');
const [isPending, setIsPending] = useState(false);

// useDebouncedValue of raw drives the actual validation
const debouncedRaw = useDebounce(raw, delay);

// Compute result synchronously from the debounced value
const result: ValidationResult = debouncedRaw === '' ? VALID_RESULT : validator(debouncedRaw);

const validate = useCallback(
Expand All @@ -43,17 +33,15 @@ export function useDebouncedValidation(
[]
);

// Once the debounced value catches up, clear isPending
const prevDebouncedRef = useRef(debouncedRaw);
if (prevDebouncedRef.current !== debouncedRaw) {
prevDebouncedRef.current = debouncedRaw;
// Side-effect during render — safe here because it's synchronous state sync
// React will immediately re-render with isPending=false
if (isPending) {
// schedule setIsPending(false) after this render commit
Promise.resolve().then(() => setIsPending(false));
useEffect(() => {
if (prevDebouncedRef.current !== debouncedRaw) {
prevDebouncedRef.current = debouncedRaw;
if (isPending) {
setIsPending(false);
}
}
}
}, [debouncedRaw, isPending]);

const reset = useCallback(() => {
setRaw('');
Expand Down
9 changes: 1 addition & 8 deletions frontend/src/services/appkit-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export class AppKitService {
blockExplorers: {
default: { name: 'Stacks Explorer', url: 'https://explorer.stacks.co' },
},
testnet: false,
} as any,
],
metadata: walletConnectConfig.metadata,
Expand All @@ -119,21 +120,13 @@ export class AppKitService {
themeVariables: walletConnectConfig.appKit.themeVariables,
termsConditionsUrl: walletConnectConfig.termsConditionsUrl,
privacyPolicyUrl: walletConnectConfig.privacyPolicyUrl,
featuredWalletIds: ['hiro', 'leather', 'xverse'],
features: {
analytics: true,
// Enable email login for Web2 users
email: authConfig.email.enabled,
// Enable social login with specified providers
socials: authConfig.social.enabled ? authConfig.social.providers : false,
history: true,
onramp: true,
swaps: true,
},
enableWalletConnect: true,
enableInjected: true,
enableEIP6963: true,
enableCoinbase: true,
});

AppKitService.instance = new AppKitService(appKit, authConfig);
Expand Down
31 changes: 1 addition & 30 deletions frontend/src/services/balance/BalanceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,36 +136,7 @@ export class BalanceService {
this.websockets.delete(address);
}
this.balanceCallbacks.delete(address);
}

setRefreshInterval(address: string, intervalMs: number): void {
// Clear existing interval
const existingInterval = this.refreshIntervals.get(address);
if (existingInterval) {
clearInterval(existingInterval);
}

// Set new interval
const interval = setInterval(async () => {
try {
const provider = this.getProviderForAddress(address);
if (provider) {
await this.getBalance(address, provider);
}
} catch (error) {
logger.error('Error refreshing balance:', error);
}
}, intervalMs);

this.refreshIntervals.set(address, interval);
}

getRefreshInterval(address: string): number {
return this.refreshIntervals.has(address) ? this.DEFAULT_REFRESH_INTERVAL : 0;
}

getDefaultRefreshInterval(): number {
return this.DEFAULT_REFRESH_INTERVAL;
this.stopRefresh(address);
}

stopRefresh(address: string): void {
Expand Down
Loading
Loading