Skip to content

Commit f35c723

Browse files
authored
Merge pull request #293 from eischideraa-unn/feature/228-multi-network-support
feat: Implement subscription multi-network deployment support
2 parents 3e5a313 + 374796e commit f35c723

10 files changed

Lines changed: 493 additions & 13 deletions

File tree

App.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import '@walletconnect/react-native-compat';
1111
import { createAppKit, defaultConfig, AppKit } from '@reown/appkit-ethers-react-native';
1212

1313
import { EVM_RPC_URLS } from './src/config/evm';
14+
import { useNetworkStore } from './src/store';
1415

1516
// Get projectId from environment variable
1617
const projectId = process.env.WALLET_CONNECT_PROJECT_ID || 'YOUR_PROJECT_ID';
@@ -67,6 +68,12 @@ createAppKit({
6768
function NotificationBootstrap() {
6869
useNotifications();
6970
useTransactionQueue();
71+
72+
const { initialize } = useNetworkStore();
73+
React.useEffect(() => {
74+
initialize();
75+
}, [initialize]);
76+
7077
return null;
7178
}
7279

contracts/networks/mainnet.env

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Stellar Mainnet Network Configuration
2+
# Contract IDs will be populated after deployment
3+
4+
# Soroban RPC URL
5+
RPC_URL="https://soroban.stellar.org"
6+
7+
# Horizon URL
8+
HORIZON_URL="https://horizon.stellar.org"
9+
10+
# Network Passphrase
11+
NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015"
12+
13+
# Contract IDs (to be set after deployment)
14+
PROXY_CONTRACT_ID=""
15+
STORAGE_CONTRACT_ID=""
16+
SUBSCRIPTION_CONTRACT_ID=""
17+
18+
# Admin Address (set during deployment)
19+
ADMIN_ADDRESS=""

contracts/networks/testnet.env

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Stellar Testnet Network Configuration
2+
# Contract IDs will be populated after deployment
3+
4+
# Soroban RPC URL
5+
RPC_URL="https://soroban-testnet.stellar.org"
6+
7+
# Horizon URL
8+
HORIZON_URL="https://horizon-testnet.stellar.org"
9+
10+
# Network Passphrase
11+
NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
12+
13+
# Contract IDs (to be set after deployment)
14+
PROXY_CONTRACT_ID=""
15+
STORAGE_CONTRACT_ID=""
16+
SUBSCRIPTION_CONTRACT_ID=""
17+
18+
# Admin Address (set during deployment)
19+
ADMIN_ADDRESS=""

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@walletconnect/react-native-compat": "^2.21.8",
4848
"@walletconnect/utils": "^2.21.8",
4949
"@walletconnect/web3wallet": "^1.16.1",
50+
"@stellar/stellar-sdk": "^12.0.0",
5051
"ethers": "^5.8.0",
5152
"expo": "~53.0.20",
5253
"expo-application": "~6.1.5",

src/config/networks.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* Network configuration for multi-network support.
3+
* Supports both EVM and Stellar networks.
4+
*/
5+
6+
export interface Network {
7+
id: string;
8+
name: string;
9+
type: 'evm' | 'stellar';
10+
rpcUrl?: string;
11+
chainId?: number;
12+
networkPassphrase?: string;
13+
horizonUrl?: string;
14+
isTestnet?: boolean;
15+
isCustom?: boolean;
16+
}
17+
18+
// EVM Networks (existing)
19+
export const EVM_NETWORKS: Network[] = [
20+
{
21+
id: 'ethereum',
22+
name: 'Ethereum',
23+
type: 'evm',
24+
rpcUrl: 'https://cloudflare-eth.com',
25+
chainId: 1,
26+
isTestnet: false,
27+
},
28+
{
29+
id: 'polygon',
30+
name: 'Polygon',
31+
type: 'evm',
32+
rpcUrl: 'https://polygon-rpc.com',
33+
chainId: 137,
34+
isTestnet: false,
35+
},
36+
{
37+
id: 'arbitrum',
38+
name: 'Arbitrum',
39+
type: 'evm',
40+
rpcUrl: 'https://arb1.arbitrum.io/rpc',
41+
chainId: 42161,
42+
isTestnet: false,
43+
},
44+
];
45+
46+
// Stellar Networks
47+
export const STELLAR_NETWORKS: Network[] = [
48+
{
49+
id: 'stellar-testnet',
50+
name: 'Stellar Testnet',
51+
type: 'stellar',
52+
rpcUrl: 'https://soroban-testnet.stellar.org',
53+
horizonUrl: 'https://horizon-testnet.stellar.org',
54+
networkPassphrase: 'Test SDF Network ; September 2015',
55+
isTestnet: true,
56+
},
57+
{
58+
id: 'stellar-mainnet',
59+
name: 'Stellar Mainnet',
60+
type: 'stellar',
61+
rpcUrl: 'https://soroban.stellar.org',
62+
horizonUrl: 'https://horizon.stellar.org',
63+
networkPassphrase: 'Public Global Stellar Network ; September 2015',
64+
isTestnet: false,
65+
},
66+
];
67+
68+
// All networks
69+
export const ALL_NETWORKS = [...EVM_NETWORKS, ...STELLAR_NETWORKS];
70+
71+
// Network-specific contract addresses
72+
export interface ContractAddresses {
73+
proxy?: string;
74+
storage?: string;
75+
subscription?: string;
76+
usdc?: string;
77+
}
78+
79+
export const NETWORK_CONTRACT_ADDRESSES: Record<string, ContractAddresses> = {
80+
'stellar-testnet': {
81+
// These would be populated after deployment
82+
proxy: process.env.STELLAR_TESTNET_PROXY_ID,
83+
storage: process.env.STELLAR_TESTNET_STORAGE_ID,
84+
subscription: process.env.STELLAR_TESTNET_SUBSCRIPTION_ID,
85+
},
86+
'stellar-mainnet': {
87+
proxy: process.env.STELLAR_MAINNET_PROXY_ID,
88+
storage: process.env.STELLAR_MAINNET_STORAGE_ID,
89+
subscription: process.env.STELLAR_MAINNET_SUBSCRIPTION_ID,
90+
},
91+
// EVM addresses (existing)
92+
ethereum: {
93+
usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
94+
},
95+
polygon: {
96+
usdc: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
97+
},
98+
arbitrum: {
99+
usdc: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
100+
},
101+
};
102+
103+
export function getNetworkById(id: string): Network | undefined {
104+
return ALL_NETWORKS.find(network => network.id === id);
105+
}
106+
107+
export function getContractAddresses(networkId: string): ContractAddresses | undefined {
108+
return NETWORK_CONTRACT_ADDRESSES[networkId];
109+
}
110+
111+
export function isStellarNetwork(network: Network): boolean {
112+
return network.type === 'stellar';
113+
}
114+
115+
export function isEvmNetwork(network: Network): boolean {
116+
return network.type === 'evm';
117+
}

src/contracts/addresses.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/**
2-
* Canonical EVM contract addresses by chain. Prefer these over literals in services.
3-
* Add new networks or contracts here as the app gains support.
2+
* Canonical contract addresses by network. Prefer these over literals in services.
3+
* Supports both EVM and Stellar networks.
44
*/
5+
import { Network, getContractAddresses } from '../config/networks';
6+
57
export const CHAIN_IDS = {
68
ETHEREUM: 1,
79
POLYGON: 137,
@@ -10,11 +12,13 @@ export const CHAIN_IDS = {
1012

1113
export type KnownChainId = (typeof CHAIN_IDS)[keyof typeof CHAIN_IDS];
1214

13-
export type ContractKey = 'usdc';
15+
export type EVMContractKey = 'usdc';
16+
export type StellarContractKey = 'proxy' | 'storage' | 'subscription';
1417

15-
type ChainContracts = Record<ContractKey, `0x${string}`>;
18+
type EVMChainContracts = Record<EVMContractKey, `0x${string}`>;
19+
type StellarNetworkContracts = Record<StellarContractKey, string>;
1620

17-
export const CONTRACT_ADDRESSES: Record<KnownChainId, ChainContracts> = {
21+
export const EVM_CONTRACT_ADDRESSES: Record<KnownChainId, EVMChainContracts> = {
1822
[CHAIN_IDS.ETHEREUM]: {
1923
usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
2024
},
@@ -27,13 +31,25 @@ export const CONTRACT_ADDRESSES: Record<KnownChainId, ChainContracts> = {
2731
},
2832
};
2933

30-
const SUPPORTED = new Set<number>(Object.values(CHAIN_IDS));
34+
const SUPPORTED_EVM = new Set<number>(Object.values(CHAIN_IDS));
3135

3236
export function isKnownEvmChainId(chainId: number): chainId is KnownChainId {
33-
return SUPPORTED.has(chainId);
37+
return SUPPORTED_EVM.has(chainId);
3438
}
3539

36-
export function getContractAddress(chainId: number, key: ContractKey): string | undefined {
40+
export function getEvmContractAddress(chainId: number, key: EVMContractKey): string | undefined {
3741
if (!isKnownEvmChainId(chainId)) return undefined;
38-
return CONTRACT_ADDRESSES[chainId][key];
42+
return EVM_CONTRACT_ADDRESSES[chainId][key];
43+
}
44+
45+
export function getStellarContractAddress(networkId: string, key: StellarContractKey): string | undefined {
46+
const addresses = getContractAddresses(networkId);
47+
return addresses?.[key];
48+
}
49+
50+
// Legacy compatibility
51+
export type ContractKey = EVMContractKey;
52+
export const CONTRACT_ADDRESSES = EVM_CONTRACT_ADDRESSES;
53+
export function getContractAddress(chainId: number, key: ContractKey): string | undefined {
54+
return getEvmContractAddress(chainId, key);
3955
}

src/screens/SettingsScreen.tsx

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ import {
99
Switch,
1010
Alert,
1111
Linking,
12+
Modal,
13+
FlatList,
1214
} from 'react-native';
1315
import AsyncStorage from '@react-native-async-storage/async-storage';
1416
import { colors, spacing, typography, borderRadius } from '../utils/constants';
15-
import { useWalletStore } from '../store';
17+
import { useWalletStore, useNetworkStore } from '../store';
1618
import { Card } from '../components/common/Card';
1719
import { useNavigation } from '@react-navigation/native';
1820
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
1921
import { RootStackParamList } from '../navigation/types';
22+
import { Network } from '../config/networks';
2023

2124
const APP_VERSION = '1.0.0';
2225
interface Settings {
@@ -28,13 +31,16 @@ const SETTINGS_KEY = '@subtrackr_settings';
2831
const SettingsScreen: React.FC = () => {
2932
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
3033
const { address, network, disconnect } = useWalletStore();
34+
const { currentNetwork, availableNetworks, setNetwork, initialize } = useNetworkStore();
3135
const [settings, setSettings] = useState<Settings>({
3236
notificationsEnabled: true,
3337
defaultCurrency: 'USD',
3438
});
39+
const [networkModalVisible, setNetworkModalVisible] = useState(false);
3540

3641
useEffect(() => {
3742
loadSettings();
43+
initialize();
3844
}, []);
3945

4046
const loadSettings = async () => {
@@ -101,12 +107,20 @@ const SettingsScreen: React.FC = () => {
101107
<Text style={styles.settingValue}>{shortenAddress(address || '')}</Text>
102108
</View>
103109
</View>
104-
<View style={styles.settingRow}>
110+
<TouchableOpacity
111+
style={styles.settingRow}
112+
onPress={() => setNetworkModalVisible(true)}
113+
accessibilityRole="button"
114+
accessibilityLabel="Select network"
115+
accessibilityHint="Opens network selection modal">
105116
<View style={styles.settingInfo}>
106117
<Text style={styles.settingLabel}>Network</Text>
107-
<Text style={styles.settingValue}>{network || 'Not connected'}</Text>
118+
<Text style={styles.settingValue}>
119+
{currentNetwork ? currentNetwork.name : 'Select Network'}
120+
</Text>
108121
</View>
109-
</View>
122+
<Text style={styles.linkArrow} accessibilityElementsHidden={true}></Text>
123+
</TouchableOpacity>
110124
{address && (
111125
<TouchableOpacity
112126
style={styles.dangerButton}
@@ -218,6 +232,54 @@ const SettingsScreen: React.FC = () => {
218232
<Text style={styles.linkArrow} accessibilityElementsHidden={true}></Text>
219233
</TouchableOpacity>
220234
</Card>
235+
236+
{/* Network Selection Modal */}
237+
<Modal
238+
visible={networkModalVisible}
239+
animationType="slide"
240+
presentationStyle="pageSheet"
241+
onRequestClose={() => setNetworkModalVisible(false)}>
242+
<SafeAreaView style={styles.modalContainer}>
243+
<View style={styles.modalHeader}>
244+
<TouchableOpacity
245+
onPress={() => setNetworkModalVisible(false)}
246+
accessibilityRole="button"
247+
accessibilityLabel="Close network selection">
248+
<Text style={styles.closeButton}>Cancel</Text>
249+
</TouchableOpacity>
250+
<Text style={styles.modalTitle}>Select Network</Text>
251+
<View style={{ width: 50 }} />
252+
</View>
253+
<FlatList
254+
data={availableNetworks}
255+
keyExtractor={(item) => item.id}
256+
renderItem={({ item }) => (
257+
<TouchableOpacity
258+
style={[
259+
styles.networkItem,
260+
currentNetwork?.id === item.id && styles.networkItemSelected,
261+
]}
262+
onPress={async () => {
263+
await setNetwork(item.id);
264+
setNetworkModalVisible(false);
265+
}}
266+
accessibilityRole="radio"
267+
accessibilityLabel={`Select ${item.name}`}
268+
accessibilityState={{ checked: currentNetwork?.id === item.id }}>
269+
<View style={styles.networkInfo}>
270+
<Text style={styles.networkName}>{item.name}</Text>
271+
<Text style={styles.networkType}>
272+
{item.type.toUpperCase()} {item.isTestnet ? '(Testnet)' : '(Mainnet)'}
273+
</Text>
274+
</View>
275+
{currentNetwork?.id === item.id && (
276+
<Text style={styles.checkmark}></Text>
277+
)}
278+
</TouchableOpacity>
279+
)}
280+
/>
281+
</SafeAreaView>
282+
</Modal>
221283
</ScrollView>
222284
</SafeAreaView>
223285
);
@@ -274,6 +336,30 @@ const styles = StyleSheet.create({
274336
linkRowLast: { borderBottomWidth: 0 },
275337
linkText: { ...typography.body, color: colors.text },
276338
linkArrow: { ...typography.body, color: colors.textSecondary },
339+
modalContainer: { flex: 1, backgroundColor: colors.background },
340+
modalHeader: {
341+
flexDirection: 'row',
342+
justifyContent: 'space-between',
343+
alignItems: 'center',
344+
padding: spacing.lg,
345+
borderBottomWidth: 1,
346+
borderBottomColor: colors.border,
347+
},
348+
modalTitle: { ...typography.h2, color: colors.text },
349+
closeButton: { ...typography.body, color: colors.primary },
350+
networkItem: {
351+
flexDirection: 'row',
352+
justifyContent: 'space-between',
353+
alignItems: 'center',
354+
padding: spacing.lg,
355+
borderBottomWidth: 1,
356+
borderBottomColor: colors.border,
357+
},
358+
networkItemSelected: { backgroundColor: colors.primary + '10' },
359+
networkInfo: { flex: 1 },
360+
networkName: { ...typography.body, color: colors.text, fontWeight: '600' },
361+
networkType: { ...typography.caption, color: colors.textSecondary, marginTop: spacing.xs },
362+
checkmark: { ...typography.h3, color: colors.primary },
277363
});
278364

279365
export default SettingsScreen;

0 commit comments

Comments
 (0)