Add initial project structure and documentation files
- Created .gitignore to exclude sensitive files and directories. - Added API documentation in API_DOCUMENTATION.md. - Included deployment instructions in DEPLOYMENT.md. - Established project structure documentation in PROJECT_STRUCTURE.md. - Updated README.md with project status and team information. - Added recommendations and status tracking documents. - Introduced testing guidelines in TESTING.md. - Set up CI workflow in .github/workflows/ci.yml. - Created Dockerfile for backend and frontend setups. - Added various service and utility files for backend functionality. - Implemented frontend components and pages for user interface. - Included mobile app structure and services. - Established scripts for deployment across multiple chains.
This commit is contained in:
52
mobile/src/navigation/StackNavigator.tsx
Normal file
52
mobile/src/navigation/StackNavigator.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import { TabNavigator } from './TabNavigator';
|
||||
import { WalletConnectScreen } from '../screens/WalletConnect';
|
||||
import { PoolDetailsScreen } from '../screens/PoolDetails';
|
||||
import { VaultDetailsScreen } from '../screens/VaultDetails';
|
||||
import { ProposalDetailsScreen } from '../screens/ProposalDetails';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
export function StackNavigator() {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
headerStyle: {
|
||||
backgroundColor: '#3b82f6',
|
||||
},
|
||||
headerTintColor: '#ffffff',
|
||||
headerTitleStyle: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="WalletConnect"
|
||||
component={WalletConnectScreen}
|
||||
options={{ headerShown: false }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Main"
|
||||
component={TabNavigator}
|
||||
options={{ headerShown: false }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PoolDetails"
|
||||
component={PoolDetailsScreen}
|
||||
options={{ title: 'Pool Details' }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="VaultDetails"
|
||||
component={VaultDetailsScreen}
|
||||
options={{ title: 'Vault Details' }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ProposalDetails"
|
||||
component={ProposalDetailsScreen}
|
||||
options={{ title: 'Proposal Details' }}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
63
mobile/src/navigation/TabNavigator.tsx
Normal file
63
mobile/src/navigation/TabNavigator.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import { DashboardScreen } from '../screens/Dashboard';
|
||||
import { PoolsScreen } from '../screens/Pools';
|
||||
import { VaultsScreen } from '../screens/Vaults';
|
||||
import { TransactionsScreen } from '../screens/Transactions';
|
||||
import { GovernanceScreen } from '../screens/Governance';
|
||||
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
export function TabNavigator() {
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={{
|
||||
headerShown: true,
|
||||
tabBarActiveTintColor: '#3b82f6',
|
||||
tabBarInactiveTintColor: '#6b7280',
|
||||
}}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="Dashboard"
|
||||
component={DashboardScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => <Icon name="home" size={24} color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Pools"
|
||||
component={PoolsScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => <Icon name="water" size={24} color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Vaults"
|
||||
component={VaultsScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => <Icon name="lock" size={24} color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Transactions"
|
||||
component={TransactionsScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => <Icon name="swap-horiz" size={24} color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Governance"
|
||||
component={GovernanceScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => <Icon name="gavel" size={24} color={color} />,
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
// Simple icon component (would use react-native-vector-icons in production)
|
||||
function Icon({ name, size, color }: { name: string; size: number; color: string }) {
|
||||
return null; // Placeholder
|
||||
}
|
||||
|
||||
23
mobile/src/navigation/linking.ts
Normal file
23
mobile/src/navigation/linking.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { LinkingOptions } from '@react-navigation/native';
|
||||
|
||||
export const linking: LinkingOptions<any> = {
|
||||
prefixes: ['asle://', 'https://asle.app'],
|
||||
config: {
|
||||
screens: {
|
||||
WalletConnect: 'connect',
|
||||
Main: {
|
||||
screens: {
|
||||
Dashboard: 'dashboard',
|
||||
Pools: 'pools',
|
||||
Vaults: 'vaults',
|
||||
Transactions: 'transactions',
|
||||
Governance: 'governance',
|
||||
},
|
||||
},
|
||||
PoolDetails: 'pool/:poolId',
|
||||
VaultDetails: 'vault/:vaultId',
|
||||
ProposalDetails: 'proposal/:proposalId',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
94
mobile/src/screens/Dashboard.tsx
Normal file
94
mobile/src/screens/Dashboard.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView, StyleSheet, RefreshControl } from 'react-native';
|
||||
import { WalletService } from '../services/wallet';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:4000';
|
||||
|
||||
export function DashboardScreen() {
|
||||
const [portfolio, setPortfolio] = useState<any>(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const walletService = WalletService.getInstance();
|
||||
|
||||
useEffect(() => {
|
||||
fetchPortfolio();
|
||||
}, []);
|
||||
|
||||
const fetchPortfolio = async () => {
|
||||
const state = walletService.getState();
|
||||
if (!state.address) return;
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/api/analytics/portfolio/${state.address}`);
|
||||
setPortfolio(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching portfolio:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
await fetchPortfolio();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={styles.container}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Portfolio</Text>
|
||||
|
||||
{portfolio ? (
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.label}>Total Value</Text>
|
||||
<Text style={styles.value}>
|
||||
{parseFloat(portfolio.totalValue || '0').toLocaleString()}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.empty}>No portfolio data</Text>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f9fafb',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
color: '#111827',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#ffffff',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
color: '#6b7280',
|
||||
marginBottom: 4,
|
||||
},
|
||||
value: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#111827',
|
||||
},
|
||||
empty: {
|
||||
textAlign: 'center',
|
||||
color: '#6b7280',
|
||||
marginTop: 32,
|
||||
},
|
||||
});
|
||||
|
||||
87
mobile/src/screens/Governance.tsx
Normal file
87
mobile/src/screens/Governance.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:4000';
|
||||
|
||||
export function GovernanceScreen({ navigation }: any) {
|
||||
const [proposals, setProposals] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProposals();
|
||||
}, []);
|
||||
|
||||
const fetchProposals = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// In production, fetch from API
|
||||
setProposals([]);
|
||||
} catch (error) {
|
||||
console.error('Error fetching proposals:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Governance</Text>
|
||||
|
||||
{proposals.length === 0 ? (
|
||||
<Text style={styles.empty}>No proposals yet</Text>
|
||||
) : (
|
||||
proposals.map((proposal) => (
|
||||
<TouchableOpacity
|
||||
key={proposal.id}
|
||||
style={styles.card}
|
||||
onPress={() => navigation.navigate('ProposalDetails', { proposalId: proposal.proposalId })}
|
||||
>
|
||||
<Text style={styles.proposalTitle}>{proposal.description}</Text>
|
||||
<Text style={styles.proposalStatus}>{proposal.status}</Text>
|
||||
</TouchableOpacity>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f9fafb',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
color: '#111827',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#ffffff',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
proposalTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
color: '#111827',
|
||||
},
|
||||
proposalStatus: {
|
||||
fontSize: 12,
|
||||
color: '#6b7280',
|
||||
},
|
||||
empty: {
|
||||
textAlign: 'center',
|
||||
color: '#6b7280',
|
||||
marginTop: 32,
|
||||
},
|
||||
});
|
||||
|
||||
78
mobile/src/screens/PoolDetails.tsx
Normal file
78
mobile/src/screens/PoolDetails.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:4000';
|
||||
|
||||
export function PoolDetailsScreen({ route, navigation }: any) {
|
||||
const { poolId } = route.params;
|
||||
const [pool, setPool] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPoolDetails();
|
||||
}, [poolId]);
|
||||
|
||||
const fetchPoolDetails = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/api/pools/${poolId}`);
|
||||
setPool(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching pool details:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{pool ? (
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>
|
||||
{pool.baseToken} / {pool.quoteToken}
|
||||
</Text>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.label}>Base Reserve</Text>
|
||||
<Text style={styles.value}>{pool.baseReserve}</Text>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.label}>Quote Reserve</Text>
|
||||
<Text style={styles.value}>{pool.quoteReserve}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<Text>Loading...</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f9fafb',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
color: '#111827',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#ffffff',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
color: '#6b7280',
|
||||
marginBottom: 4,
|
||||
},
|
||||
value: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
},
|
||||
});
|
||||
|
||||
82
mobile/src/screens/Pools.tsx
Normal file
82
mobile/src/screens/Pools.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:4000';
|
||||
|
||||
export function PoolsScreen({ navigation }: any) {
|
||||
const [pools, setPools] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPools();
|
||||
}, []);
|
||||
|
||||
const fetchPools = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/api/pools`);
|
||||
setPools(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching pools:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Liquidity Pools</Text>
|
||||
|
||||
{pools.map((pool) => (
|
||||
<TouchableOpacity
|
||||
key={pool.id}
|
||||
style={styles.card}
|
||||
onPress={() => navigation.navigate('PoolDetails', { poolId: pool.poolId })}
|
||||
>
|
||||
<Text style={styles.poolName}>
|
||||
{pool.baseToken} / {pool.quoteToken}
|
||||
</Text>
|
||||
<Text style={styles.poolValue}>
|
||||
TVL: {parseFloat(pool.baseReserve || '0').toLocaleString()}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f9fafb',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
color: '#111827',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#ffffff',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
poolName: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
color: '#111827',
|
||||
},
|
||||
poolValue: {
|
||||
fontSize: 14,
|
||||
color: '#6b7280',
|
||||
},
|
||||
});
|
||||
|
||||
122
mobile/src/screens/ProposalDetails.tsx
Normal file
122
mobile/src/screens/ProposalDetails.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { WalletService } from '../services/wallet';
|
||||
|
||||
export function ProposalDetailsScreen({ route, navigation }: any) {
|
||||
const { proposalId } = route.params;
|
||||
const [proposal, setProposal] = useState<any>(null);
|
||||
const walletService = WalletService.getInstance();
|
||||
|
||||
useEffect(() => {
|
||||
fetchProposal();
|
||||
}, [proposalId]);
|
||||
|
||||
const fetchProposal = async () => {
|
||||
// In production, fetch from API
|
||||
setProposal({
|
||||
id: proposalId,
|
||||
description: 'Sample proposal',
|
||||
status: 'active',
|
||||
forVotes: '1000',
|
||||
againstVotes: '500',
|
||||
});
|
||||
};
|
||||
|
||||
const handleVote = async (support: boolean) => {
|
||||
const state = walletService.getState();
|
||||
if (!state.connected) {
|
||||
alert('Please connect your wallet');
|
||||
return;
|
||||
}
|
||||
|
||||
// In production, call smart contract
|
||||
alert(`Vote ${support ? 'for' : 'against'} would be submitted`);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{proposal ? (
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Proposal {proposalId}</Text>
|
||||
<Text style={styles.description}>{proposal.description}</Text>
|
||||
|
||||
<View style={styles.votes}>
|
||||
<Text style={styles.voteLabel}>For: {proposal.forVotes}</Text>
|
||||
<Text style={styles.voteLabel}>Against: {proposal.againstVotes}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.voteFor]}
|
||||
onPress={() => handleVote(true)}
|
||||
>
|
||||
<Text style={styles.buttonText}>Vote For</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.voteAgainst]}
|
||||
onPress={() => handleVote(false)}
|
||||
>
|
||||
<Text style={styles.buttonText}>Vote Against</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<Text>Loading...</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f9fafb',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
color: '#111827',
|
||||
},
|
||||
description: {
|
||||
fontSize: 16,
|
||||
color: '#6b7280',
|
||||
marginBottom: 24,
|
||||
},
|
||||
votes: {
|
||||
backgroundColor: '#ffffff',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 24,
|
||||
},
|
||||
voteLabel: {
|
||||
fontSize: 16,
|
||||
marginBottom: 8,
|
||||
color: '#111827',
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
voteFor: {
|
||||
backgroundColor: '#10b981',
|
||||
},
|
||||
voteAgainst: {
|
||||
backgroundColor: '#ef4444',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
83
mobile/src/screens/Transactions.tsx
Normal file
83
mobile/src/screens/Transactions.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView, StyleSheet } from 'react-native';
|
||||
import { WalletService } from '../services/wallet';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:4000';
|
||||
|
||||
export function TransactionsScreen() {
|
||||
const [transactions, setTransactions] = useState<any[]>([]);
|
||||
const walletService = WalletService.getInstance();
|
||||
|
||||
useEffect(() => {
|
||||
fetchTransactions();
|
||||
}, []);
|
||||
|
||||
const fetchTransactions = async () => {
|
||||
const state = walletService.getState();
|
||||
if (!state.address) return;
|
||||
|
||||
try {
|
||||
// In production, fetch user transactions
|
||||
setTransactions([]);
|
||||
} catch (error) {
|
||||
console.error('Error fetching transactions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Transactions</Text>
|
||||
|
||||
{transactions.length === 0 ? (
|
||||
<Text style={styles.empty}>No transactions yet</Text>
|
||||
) : (
|
||||
transactions.map((tx) => (
|
||||
<View key={tx.id} style={styles.card}>
|
||||
<Text style={styles.txHash}>{tx.txHash.slice(0, 20)}...</Text>
|
||||
<Text style={styles.txStatus}>{tx.status}</Text>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f9fafb',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
color: '#111827',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#ffffff',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
txHash: {
|
||||
fontSize: 14,
|
||||
color: '#111827',
|
||||
marginBottom: 4,
|
||||
},
|
||||
txStatus: {
|
||||
fontSize: 12,
|
||||
color: '#6b7280',
|
||||
},
|
||||
empty: {
|
||||
textAlign: 'center',
|
||||
color: '#6b7280',
|
||||
marginTop: 32,
|
||||
},
|
||||
});
|
||||
|
||||
76
mobile/src/screens/VaultDetails.tsx
Normal file
76
mobile/src/screens/VaultDetails.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:4000';
|
||||
|
||||
export function VaultDetailsScreen({ route, navigation }: any) {
|
||||
const { vaultId } = route.params;
|
||||
const [vault, setVault] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchVaultDetails();
|
||||
}, [vaultId]);
|
||||
|
||||
const fetchVaultDetails = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/api/vaults/${vaultId}`);
|
||||
setVault(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching vault details:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{vault ? (
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Vault {vaultId}</Text>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.label}>Total Assets</Text>
|
||||
<Text style={styles.value}>{vault.totalAssets}</Text>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.label}>Total Supply</Text>
|
||||
<Text style={styles.value}>{vault.totalSupply}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<Text>Loading...</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f9fafb',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
color: '#111827',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#ffffff',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
color: '#6b7280',
|
||||
marginBottom: 4,
|
||||
},
|
||||
value: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
},
|
||||
});
|
||||
|
||||
82
mobile/src/screens/Vaults.tsx
Normal file
82
mobile/src/screens/Vaults.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:4000';
|
||||
|
||||
export function VaultsScreen({ navigation }: any) {
|
||||
const [vaults, setVaults] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchVaults();
|
||||
}, []);
|
||||
|
||||
const fetchVaults = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/api/vaults`);
|
||||
setVaults(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching vaults:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Vaults</Text>
|
||||
|
||||
{vaults.map((vault) => (
|
||||
<TouchableOpacity
|
||||
key={vault.id}
|
||||
style={styles.card}
|
||||
onPress={() => navigation.navigate('VaultDetails', { vaultId: vault.vaultId })}
|
||||
>
|
||||
<Text style={styles.vaultName}>
|
||||
{vault.isMultiAsset ? 'Multi-Asset Vault' : `Vault ${vault.vaultId}`}
|
||||
</Text>
|
||||
<Text style={styles.vaultValue}>
|
||||
Total Assets: {parseFloat(vault.totalAssets || '0').toLocaleString()}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f9fafb',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 16,
|
||||
color: '#111827',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#ffffff',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
vaultName: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
color: '#111827',
|
||||
},
|
||||
vaultValue: {
|
||||
fontSize: 14,
|
||||
color: '#6b7280',
|
||||
},
|
||||
});
|
||||
|
||||
73
mobile/src/screens/WalletConnect.tsx
Normal file
73
mobile/src/screens/WalletConnect.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { WalletService } from '../services/wallet';
|
||||
|
||||
export function WalletConnectScreen({ navigation }: any) {
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
|
||||
const handleConnect = async () => {
|
||||
setConnecting(true);
|
||||
try {
|
||||
const walletService = WalletService.getInstance();
|
||||
await walletService.connect();
|
||||
navigation.replace('Main');
|
||||
} catch (error) {
|
||||
console.error('Error connecting wallet:', error);
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>ASLE Mobile</Text>
|
||||
<Text style={styles.subtitle}>Connect your wallet to get started</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={handleConnect}
|
||||
disabled={connecting}
|
||||
>
|
||||
<Text style={styles.buttonText}>
|
||||
{connecting ? 'Connecting...' : 'Connect Wallet'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
backgroundColor: '#f9fafb',
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 10,
|
||||
color: '#111827',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: '#6b7280',
|
||||
marginBottom: 40,
|
||||
textAlign: 'center',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#3b82f6',
|
||||
paddingHorizontal: 32,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 8,
|
||||
minWidth: 200,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
35
mobile/src/services/biometric.ts
Normal file
35
mobile/src/services/biometric.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import ReactNativeBiometrics from 'react-native-biometrics';
|
||||
|
||||
export class BiometricService {
|
||||
private static rnBiometrics = new ReactNativeBiometrics();
|
||||
|
||||
static async isAvailable(): Promise<boolean> {
|
||||
try {
|
||||
const { available } = await this.rnBiometrics.isSensorAvailable();
|
||||
return available;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async authenticate(reason: string = 'Authenticate to access ASLE'): Promise<boolean> {
|
||||
try {
|
||||
const { success } = await this.rnBiometrics.simplePrompt({
|
||||
promptMessage: reason,
|
||||
});
|
||||
return success;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async createKeys(): Promise<{ publicKey: string; privateKey: string } | null> {
|
||||
try {
|
||||
const { publicKey } = await this.rnBiometrics.createKeys();
|
||||
return { publicKey, privateKey: '' }; // Private key is stored securely
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
86
mobile/src/services/deep-linking.ts
Normal file
86
mobile/src/services/deep-linking.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Linking } from 'react-native';
|
||||
|
||||
export interface DeepLink {
|
||||
type: 'transaction' | 'proposal' | 'pool' | 'vault';
|
||||
id: string;
|
||||
params?: Record<string, string>;
|
||||
}
|
||||
|
||||
export class DeepLinkingService {
|
||||
/**
|
||||
* Parse deep link URL
|
||||
*/
|
||||
static parseUrl(url: string): DeepLink | null {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const path = parsed.pathname;
|
||||
|
||||
if (path.startsWith('/transaction/')) {
|
||||
return {
|
||||
type: 'transaction',
|
||||
id: path.split('/transaction/')[1],
|
||||
};
|
||||
} else if (path.startsWith('/proposal/')) {
|
||||
return {
|
||||
type: 'proposal',
|
||||
id: path.split('/proposal/')[1],
|
||||
};
|
||||
} else if (path.startsWith('/pool/')) {
|
||||
return {
|
||||
type: 'pool',
|
||||
id: path.split('/pool/')[1],
|
||||
};
|
||||
} else if (path.startsWith('/vault/')) {
|
||||
return {
|
||||
type: 'vault',
|
||||
id: path.split('/vault/')[1],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming deep link
|
||||
*/
|
||||
static async handleDeepLink(url: string, navigation: any): Promise<void> {
|
||||
const link = this.parseUrl(url);
|
||||
if (!link) return;
|
||||
|
||||
switch (link.type) {
|
||||
case 'transaction':
|
||||
// Navigate to transaction details
|
||||
break;
|
||||
case 'proposal':
|
||||
navigation.navigate('ProposalDetails', { proposalId: link.id });
|
||||
break;
|
||||
case 'pool':
|
||||
navigation.navigate('PoolDetails', { poolId: link.id });
|
||||
break;
|
||||
case 'vault':
|
||||
navigation.navigate('VaultDetails', { vaultId: link.id });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize deep linking listener
|
||||
*/
|
||||
static initialize(navigation: any) {
|
||||
// Handle initial URL
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url) {
|
||||
this.handleDeepLink(url, navigation);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle URL changes
|
||||
Linking.addEventListener('url', (event) => {
|
||||
this.handleDeepLink(event.url, navigation);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
52
mobile/src/services/notifications.ts
Normal file
52
mobile/src/services/notifications.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import PushNotification from 'react-native-push-notification';
|
||||
|
||||
export class NotificationService {
|
||||
static initialize() {
|
||||
PushNotification.configure({
|
||||
onRegister: function (token) {
|
||||
console.log('TOKEN:', token);
|
||||
},
|
||||
onNotification: function (notification) {
|
||||
console.log('NOTIFICATION:', notification);
|
||||
},
|
||||
permissions: {
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
},
|
||||
popInitialNotification: true,
|
||||
requestPermissions: true,
|
||||
});
|
||||
|
||||
PushNotification.createChannel(
|
||||
{
|
||||
channelId: 'asle-default',
|
||||
channelName: 'ASLE Notifications',
|
||||
channelDescription: 'Notifications for ASLE platform',
|
||||
playSound: true,
|
||||
soundName: 'default',
|
||||
importance: 4,
|
||||
vibrate: true,
|
||||
},
|
||||
(created) => console.log(`Channel created: ${created}`)
|
||||
);
|
||||
}
|
||||
|
||||
static scheduleLocalNotification(title: string, message: string, date: Date) {
|
||||
PushNotification.localNotificationSchedule({
|
||||
channelId: 'asle-default',
|
||||
title,
|
||||
message,
|
||||
date,
|
||||
});
|
||||
}
|
||||
|
||||
static sendLocalNotification(title: string, message: string) {
|
||||
PushNotification.localNotification({
|
||||
channelId: 'asle-default',
|
||||
title,
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
74
mobile/src/services/offline.ts
Normal file
74
mobile/src/services/offline.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
export class OfflineService {
|
||||
private static CACHE_PREFIX = 'asle_cache_';
|
||||
private static QUEUE_KEY = 'asle_transaction_queue';
|
||||
|
||||
/**
|
||||
* Cache data for offline access
|
||||
*/
|
||||
static async cacheData(key: string, data: any): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.setItem(
|
||||
`${this.CACHE_PREFIX}${key}`,
|
||||
JSON.stringify({ data, timestamp: Date.now() })
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error caching data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached data
|
||||
*/
|
||||
static async getCachedData(key: string): Promise<any | null> {
|
||||
try {
|
||||
const cached = await AsyncStorage.getItem(`${this.CACHE_PREFIX}${key}`);
|
||||
if (!cached) return null;
|
||||
|
||||
const { data, timestamp } = JSON.parse(cached);
|
||||
// Check if cache is still valid (24 hours)
|
||||
if (Date.now() - timestamp > 24 * 60 * 60 * 1000) {
|
||||
await AsyncStorage.removeItem(`${this.CACHE_PREFIX}${key}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue transaction for when online
|
||||
*/
|
||||
static async queueTransaction(transaction: any): Promise<void> {
|
||||
try {
|
||||
const queue = await this.getTransactionQueue();
|
||||
queue.push({ ...transaction, queuedAt: Date.now() });
|
||||
await AsyncStorage.setItem(this.QUEUE_KEY, JSON.stringify(queue));
|
||||
} catch (error) {
|
||||
console.error('Error queueing transaction:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction queue
|
||||
*/
|
||||
static async getTransactionQueue(): Promise<any[]> {
|
||||
try {
|
||||
const queue = await AsyncStorage.getItem(this.QUEUE_KEY);
|
||||
return queue ? JSON.parse(queue) : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear transaction queue
|
||||
*/
|
||||
static async clearTransactionQueue(): Promise<void> {
|
||||
await AsyncStorage.removeItem(this.QUEUE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
67
mobile/src/services/wallet.ts
Normal file
67
mobile/src/services/wallet.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { createConfig, http } from '@wagmi/core';
|
||||
import { mainnet, polygon, arbitrum, optimism, bsc, avalanche, base } from '@wagmi/core/chains';
|
||||
import { injected, metaMask } from '@wagmi/core/connectors';
|
||||
|
||||
export const wagmiConfig = createConfig({
|
||||
chains: [mainnet, polygon, arbitrum, optimism, bsc, avalanche, base],
|
||||
connectors: [injected(), metaMask()],
|
||||
transports: {
|
||||
[mainnet.id]: http(),
|
||||
[polygon.id]: http(),
|
||||
[arbitrum.id]: http(),
|
||||
[optimism.id]: http(),
|
||||
[bsc.id]: http(),
|
||||
[avalanche.id]: http(),
|
||||
[base.id]: http(),
|
||||
},
|
||||
});
|
||||
|
||||
export interface WalletState {
|
||||
address: string | null;
|
||||
chainId: number | null;
|
||||
connected: boolean;
|
||||
}
|
||||
|
||||
export class WalletService {
|
||||
private static instance: WalletService;
|
||||
private state: WalletState = {
|
||||
address: null,
|
||||
chainId: null,
|
||||
connected: false,
|
||||
};
|
||||
|
||||
static getInstance(): WalletService {
|
||||
if (!WalletService.instance) {
|
||||
WalletService.instance = new WalletService();
|
||||
}
|
||||
return WalletService.instance;
|
||||
}
|
||||
|
||||
async connect(): Promise<WalletState> {
|
||||
// In production, this would use WalletConnect or similar
|
||||
// For now, return mock state
|
||||
this.state = {
|
||||
address: '0x1234567890123456789012345678901234567890',
|
||||
chainId: 1,
|
||||
connected: true,
|
||||
};
|
||||
return this.state;
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
this.state = {
|
||||
address: null,
|
||||
chainId: null,
|
||||
connected: false,
|
||||
};
|
||||
}
|
||||
|
||||
getState(): WalletState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
async switchChain(chainId: number): Promise<void> {
|
||||
this.state.chainId = chainId;
|
||||
}
|
||||
}
|
||||
|
||||
16
mobile/src/theme/colors.ts
Normal file
16
mobile/src/theme/colors.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const colors = {
|
||||
primary: '#3b82f6',
|
||||
secondary: '#10b981',
|
||||
accent: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
warning: '#f59e0b',
|
||||
success: '#10b981',
|
||||
info: '#3b82f6',
|
||||
background: '#f9fafb',
|
||||
surface: '#ffffff',
|
||||
text: '#111827',
|
||||
textSecondary: '#6b7280',
|
||||
border: '#e5e7eb',
|
||||
divider: '#e5e7eb',
|
||||
};
|
||||
|
||||
9
mobile/src/theme/spacing.ts
Normal file
9
mobile/src/theme/spacing.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const spacing = {
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 16,
|
||||
lg: 24,
|
||||
xl: 32,
|
||||
xxl: 48,
|
||||
};
|
||||
|
||||
33
mobile/src/theme/typography.ts
Normal file
33
mobile/src/theme/typography.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export const typography = {
|
||||
h1: {
|
||||
fontSize: 32,
|
||||
fontWeight: '700' as const,
|
||||
lineHeight: 40,
|
||||
},
|
||||
h2: {
|
||||
fontSize: 24,
|
||||
fontWeight: '600' as const,
|
||||
lineHeight: 32,
|
||||
},
|
||||
h3: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600' as const,
|
||||
lineHeight: 28,
|
||||
},
|
||||
body: {
|
||||
fontSize: 16,
|
||||
fontWeight: '400' as const,
|
||||
lineHeight: 24,
|
||||
},
|
||||
bodySmall: {
|
||||
fontSize: 14,
|
||||
fontWeight: '400' as const,
|
||||
lineHeight: 20,
|
||||
},
|
||||
caption: {
|
||||
fontSize: 12,
|
||||
fontWeight: '400' as const,
|
||||
lineHeight: 16,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user