diff --git a/wallets/rn_cli_wallet/src/components/SegmentedControl.tsx b/wallets/rn_cli_wallet/src/components/SegmentedControl.tsx new file mode 100644 index 00000000..7168ff4c --- /dev/null +++ b/wallets/rn_cli_wallet/src/components/SegmentedControl.tsx @@ -0,0 +1,157 @@ +import { useEffect } from 'react'; +import { StyleSheet, View, LayoutChangeEvent } from 'react-native'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withSpring, + interpolateColor, + useDerivedValue, +} from 'react-native-reanimated'; + +import { useTheme } from '@/hooks/useTheme'; +import { Spacing, BorderRadius } from '@/utils/ThemeUtil'; +import { Button } from '@/components/Button'; + +interface SegmentedControlProps { + options: readonly T[]; + selectedOption: T; + onSelect: (option: T) => void; +} + +const SPRING_CONFIG = { + damping: 15, + stiffness: 150, + mass: 0.5, +}; + +export function SegmentedControl({ + options, + selectedOption, + onSelect, +}: SegmentedControlProps) { + const Theme = useTheme(); + + const selectedIndex = options.indexOf(selectedOption); + const segmentWidth = useSharedValue(0); + const containerWidth = useSharedValue(0); + const translateX = useSharedValue(0); + + // Update translateX when selection changes + useEffect(() => { + if (segmentWidth.value > 0) { + translateX.value = withSpring( + selectedIndex * segmentWidth.value, + SPRING_CONFIG, + ); + } + }, [selectedIndex, segmentWidth, translateX]); + + const handleContainerLayout = (event: LayoutChangeEvent) => { + const { width } = event.nativeEvent.layout; + containerWidth.value = width; + const padding = Spacing['05'] * 2; + segmentWidth.value = (width - padding) / options.length; + translateX.value = selectedIndex * segmentWidth.value; + }; + + const indicatorStyle = useAnimatedStyle(() => { + return { + transform: [{ translateX: translateX.value }], + width: segmentWidth.value, + }; + }); + + return ( + + + {options.map(option => { + const isActive = option === selectedOption; + return ( + + ); + })} + + ); +} + +interface AnimatedTextProps { + text: string; + isActive: boolean; + activeColor: string; + inactiveColor: string; +} + +function AnimatedText({ + text, + isActive, + activeColor, + inactiveColor, +}: AnimatedTextProps) { + const progress = useDerivedValue(() => { + return withSpring(isActive ? 1 : 0, SPRING_CONFIG); + }, [isActive]); + + const animatedStyle = useAnimatedStyle(() => { + const color = interpolateColor( + progress.value, + [0, 1], + [inactiveColor, activeColor], + ); + return { color }; + }); + + return ( + {text} + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + borderRadius: BorderRadius[3], + padding: Spacing['05'], + width: '100%', + position: 'relative', + }, + indicator: { + position: 'absolute', + top: Spacing['05'], + left: Spacing['05'], + bottom: Spacing['05'], + borderRadius: BorderRadius[2], + }, + segment: { + flex: 1, + paddingVertical: Spacing[2], + borderRadius: BorderRadius[2], + alignItems: 'center', + zIndex: 1, + }, + text: { + fontSize: 13, + fontWeight: '500', + }, +}); diff --git a/wallets/rn_cli_wallet/src/modals/ImportWalletModal.tsx b/wallets/rn_cli_wallet/src/modals/ImportWalletModal.tsx index fc179ce1..0a0e937c 100644 --- a/wallets/rn_cli_wallet/src/modals/ImportWalletModal.tsx +++ b/wallets/rn_cli_wallet/src/modals/ImportWalletModal.tsx @@ -8,51 +8,138 @@ import { import Toast from 'react-native-toast-message'; import { useTheme } from '@/hooks/useTheme'; +import LogStore from '@/store/LogStore'; import ModalStore from '@/store/ModalStore'; import SettingsStore from '@/store/SettingsStore'; import WalletStore from '@/store/WalletStore'; import { loadEIP155Wallet } from '@/utils/EIP155WalletUtil'; +import { loadTonWallet } from '@/utils/TonWalletUtil'; +import { loadTronWallet } from '@/utils/TronWalletUtil'; +import { loadSuiWallet } from '@/utils/SuiWalletUtil'; import { Text } from '@/components/Text'; import { ModalCloseButton } from '@/components/ModalCloseButton'; +import { SegmentedControl } from '@/components/SegmentedControl'; import { Spacing, BorderRadius, FontFamily } from '@/utils/ThemeUtil'; import { ActionButton } from '@/components/ActionButton'; +const CHAIN_OPTIONS = ['EVM', 'TON', 'TRON', 'SUI'] as const; +type ChainOption = (typeof CHAIN_OPTIONS)[number]; + +const PLACEHOLDER_TEXT: Record = { + EVM: 'Enter mnemonic or private key (0x...)', + TON: 'Enter secret key (128 hex) or seed (64 hex)', + TRON: 'Enter private key (64 hex)', + SUI: 'Enter mnemonic phrase (12-24 words)', +}; + +const EMPTY_INPUT_ERROR: Record = { + EVM: 'Please enter a mnemonic or private key', + TON: 'Please enter a secret key or seed', + TRON: 'Please enter a private key', + SUI: 'Please enter a mnemonic phrase', +}; + export default function ImportWalletModal() { const Theme = useTheme(); + const [selectedChain, setSelectedChain] = useState('EVM'); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); + const handleChainChange = (chain: ChainOption) => { + setSelectedChain(chain); + setInput(''); // Clear input when switching chains + }; + const handleImport = async () => { - if (!input.trim()) { + // Normalize input: trim and collapse multiple whitespace to single space + const sanitizedInput = input.trim().replace(/\s+/g, ' '); + + if (!sanitizedInput) { Toast.show({ type: 'error', - text1: 'Please enter a mnemonic or private key', + text1: EMPTY_INPUT_ERROR[selectedChain], }); return; } setIsLoading(true); try { - const { address } = loadEIP155Wallet(input); + let address: string; - // Refetch balances with the new address - WalletStore.fetchBalances({ - eip155Address: address, - tonAddress: SettingsStore.state.tonAddress, - tronAddress: SettingsStore.state.tronAddress, - }); + switch (selectedChain) { + case 'EVM': { + const result = loadEIP155Wallet(sanitizedInput); + address = result.address; + // Refetch balances with the new EVM address + WalletStore.fetchBalances({ + eip155Address: address, + tonAddress: SettingsStore.state.tonAddress, + tronAddress: SettingsStore.state.tronAddress, + suiAddress: SettingsStore.state.suiAddress, + }); + break; + } + case 'TON': { + const result = await loadTonWallet(sanitizedInput); + address = result.address; + // Refetch balances with the new TON address + WalletStore.fetchBalances({ + eip155Address: SettingsStore.state.eip155Address, + tonAddress: address, + tronAddress: SettingsStore.state.tronAddress, + suiAddress: SettingsStore.state.suiAddress, + }); + break; + } + case 'TRON': { + const result = await loadTronWallet(sanitizedInput); + address = result.address; + // Refetch balances with the new TRON address + WalletStore.fetchBalances({ + eip155Address: SettingsStore.state.eip155Address, + tonAddress: SettingsStore.state.tonAddress, + tronAddress: address, + suiAddress: SettingsStore.state.suiAddress, + }); + break; + } + case 'SUI': { + const result = await loadSuiWallet(sanitizedInput); + address = result.address; + // Refetch balances with the new SUI address + WalletStore.fetchBalances({ + eip155Address: SettingsStore.state.eip155Address, + tonAddress: SettingsStore.state.tonAddress, + tronAddress: SettingsStore.state.tronAddress, + suiAddress: address, + }); + break; + } + default: { + const unsupportedChain = selectedChain satisfies never; + LogStore.error( + `Unsupported chain: ${unsupportedChain}`, + 'ImportWalletModal', + 'handleImport', + ); + Toast.show({ + type: 'error', + text1: 'Error', + text2: `Unsupported chain: ${unsupportedChain}`, + }); + return; + } + } Toast.show({ type: 'success', - text1: 'Wallet imported!', + text1: `${selectedChain} wallet imported!`, text2: `New address: ${address}`, }); ModalStore.close(); } catch (error: unknown) { const message = - error instanceof Error - ? error.message - : 'Invalid mnemonic or private key'; + error instanceof Error ? error.message : 'Invalid input'; Toast.show({ type: 'error', text1: 'Error', @@ -73,9 +160,17 @@ export default function ImportWalletModal() { - Import EVM Wallet + Import Wallet + + + + { - if (mnemonic) { - Clipboard.setString(mnemonic); + const copySecret = () => { + if (secret) { + Clipboard.setString(secret); Toast.show({ type: 'info', - text1: 'Secret phrase copied to clipboard', + text1: `${title} copied to clipboard`, }); } }; - if (!mnemonic) { - return ( - - - No Secret Phrase Available - - + + {title} + + + {!secret ? ( + - This wallet was imported using a private key, so there is no recovery - phrase to display. - - - ); - } + + {notAvailableMessage} + + + ) : ( + <> + {type === 'mnemonic' ? ( + + {words.map((word, index) => ( + + + {index + 1}. + + + {word} + + + ))} + + ) : ( + + + {secret} + + + )} + + + + )} + + ); +} + +export default function SecretPhrase() { + const { eip155Address, suiWallet, tonWallet, tronWallet } = useSnapshot( + SettingsStore.state, + ); + const Theme = useTheme(); + + // Get EVM mnemonic + const evmMnemonic = eip155Wallets[eip155Address]?.getMnemonic?.() ?? null; + + // Get SUI mnemonic + const suiMnemonic = suiWallet?.getMnemonic?.() ?? null; + + // Get TON secret key + const tonSecretKey = tonWallet?.getSecretKey?.() ?? null; + + // Get TRON private key + const tronPrivateKey = tronWallet?.privateKey ?? null; return ( - Never share this phrase with anyone. Anyone with this phrase can take - your funds. + Mnemonics and secret keys are provided for development purposes only and should not be used elsewhere + - - {words.map((word, index) => ( - - - {index + 1}. - - - {word} - - - ))} - + - + + + ); } @@ -109,31 +181,26 @@ const styles = StyleSheet.create({ container: { flex: 1, }, - centerContent: { - justifyContent: 'center', - alignItems: 'center', - padding: Spacing[5], - }, content: { padding: Spacing[5], + paddingBottom: Spacing[10], }, - title: { - marginBottom: Spacing[4], - textAlign: 'center', + section: { + marginBottom: Spacing[10], }, - description: { - textAlign: 'center', + sectionTitle: { + marginBottom: Spacing[3], }, - warningContainer: { + notAvailableContainer: { padding: Spacing[4], borderRadius: BorderRadius[3], - marginBottom: Spacing[5], + alignItems: 'center', }, wordsContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing[2], - marginBottom: Spacing[5], + marginBottom: Spacing[3], }, wordCard: { flexDirection: 'row', @@ -145,12 +212,27 @@ const styles = StyleSheet.create({ width: '30%', minWidth: 90, }, + hexContainer: { + padding: Spacing[3], + borderRadius: BorderRadius[3], + marginBottom: Spacing[3], + }, + hexText: { + fontFamily: 'monospace', + fontSize: 12, + lineHeight: 18, + }, copyButton: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: Spacing[2], + padding: Spacing[3], + borderRadius: BorderRadius[3], + }, + warningContainer: { padding: Spacing[4], borderRadius: BorderRadius[3], + marginBottom: Spacing[5], }, }); diff --git a/wallets/rn_cli_wallet/src/screens/Settings/index.tsx b/wallets/rn_cli_wallet/src/screens/Settings/index.tsx index c9158eb9..358549fe 100644 --- a/wallets/rn_cli_wallet/src/screens/Settings/index.tsx +++ b/wallets/rn_cli_wallet/src/screens/Settings/index.tsx @@ -81,12 +81,12 @@ export default function Settings() { /> navigation.navigate('SecretPhrase')} icon="chevronRight" /> ModalStore.open('ImportWalletModal', {})} icon="chevronRight" /> diff --git a/wallets/rn_cli_wallet/src/screens/Wallets/components/TokenBalanceCard.tsx b/wallets/rn_cli_wallet/src/screens/Wallets/components/TokenBalanceCard.tsx index a2b241b7..96a52b5f 100644 --- a/wallets/rn_cli_wallet/src/screens/Wallets/components/TokenBalanceCard.tsx +++ b/wallets/rn_cli_wallet/src/screens/Wallets/components/TokenBalanceCard.tsx @@ -21,7 +21,7 @@ interface TokenBalanceCardProps { function truncateAddress(address: string): string { if (!address || address.length < 10) return address; - return `${address.slice(0, 6)}...${address.slice(-4)}`; + return `${address.slice(0, 6)}...${address.slice(-6)}`; } function formatBalance(numeric: string, symbol: string): string { diff --git a/wallets/rn_cli_wallet/src/screens/Wallets/index.tsx b/wallets/rn_cli_wallet/src/screens/Wallets/index.tsx index a1e7cd14..b4746dba 100644 --- a/wallets/rn_cli_wallet/src/screens/Wallets/index.tsx +++ b/wallets/rn_cli_wallet/src/screens/Wallets/index.tsx @@ -22,12 +22,15 @@ function getAddressForChain( if (chainId.startsWith('tron:')) { return addresses.tronAddress || ''; } + if (chainId.startsWith('sui:')) { + return addresses.suiAddress || ''; + } // Default to EIP155 address for all EVM chains - return addresses.eip155Address; + return addresses.eip155Address || ''; } export default function Wallets() { - const { eip155Address, tonAddress, tronAddress } = useSnapshot( + const { eip155Address, tonAddress, tronAddress, suiAddress } = useSnapshot( SettingsStore.state, ); const { balances, isLoading } = useSnapshot(WalletStore.state); @@ -38,12 +41,18 @@ export default function Wallets() { eip155Address, tonAddress, tronAddress, + suiAddress, }), - [eip155Address, tonAddress, tronAddress], + [eip155Address, tonAddress, tronAddress, suiAddress], ); const fetchBalances = useCallback(() => { - if (addresses.eip155Address) { + if ( + addresses.eip155Address || + addresses.tonAddress || + addresses.tronAddress || + addresses.suiAddress + ) { WalletStore.fetchBalances(addresses); } }, [addresses]); diff --git a/wallets/rn_cli_wallet/src/store/SettingsStore.ts b/wallets/rn_cli_wallet/src/store/SettingsStore.ts index 49589d8d..2ebb66a5 100644 --- a/wallets/rn_cli_wallet/src/store/SettingsStore.ts +++ b/wallets/rn_cli_wallet/src/store/SettingsStore.ts @@ -5,6 +5,8 @@ import { Verify, SessionTypes } from '@walletconnect/types'; import { storage } from '@/utils/storage'; import EIP155Lib from '../lib/EIP155Lib'; import SuiLib from '../lib/SuiLib'; +import TonLib from '../lib/TonLib'; +import TronLib from '../lib/TronLib'; import { MMKV } from 'react-native-mmkv'; function getInitialThemeMode(): 'light' | 'dark' { @@ -27,7 +29,9 @@ interface State { suiAddress: string; suiWallet: SuiLib | null; tonAddress: string; + tonWallet: TonLib | null; tronAddress: string; + tronWallet: TronLib | null; relayerRegionURL: string; activeChainId: string; currentRequestVerifyContext?: Verify.Context; @@ -55,7 +59,9 @@ const state = proxy({ suiAddress: '', suiWallet: null, tonAddress: '', + tonWallet: null, tronAddress: '', + tronWallet: null, relayerRegionURL: '', sessions: [], wallet: null, @@ -134,10 +140,18 @@ const SettingsStore = { state.tonAddress = tonAddress; }, + setTonWallet(tonWallet: TonLib) { + state.tonWallet = tonWallet; + }, + setTronAddress(tronAddress: string) { state.tronAddress = tronAddress; }, + setTronWallet(tronWallet: TronLib) { + state.tronWallet = tronWallet; + }, + setThemeMode(value: 'light' | 'dark') { state.themeMode = value; storage.setItem('THEME_MODE', value); diff --git a/wallets/rn_cli_wallet/src/store/WalletStore.ts b/wallets/rn_cli_wallet/src/store/WalletStore.ts index a9ddbed9..e44ecdc4 100644 --- a/wallets/rn_cli_wallet/src/store/WalletStore.ts +++ b/wallets/rn_cli_wallet/src/store/WalletStore.ts @@ -11,11 +11,13 @@ const STORAGE_KEY = 'WALLET_BALANCES'; // Supported chains by the blockchain API for non-EIP155 const TON_SUPPORTED_CHAINS = ['ton:-239']; const TRON_SUPPORTED_CHAINS = ['tron:0x2b6653dc']; +const SUI_SUPPORTED_CHAINS = ['sui:mainnet']; export interface WalletAddresses { - eip155Address: string; + eip155Address?: string; tonAddress?: string; tronAddress?: string; + suiAddress?: string; } interface WalletState { @@ -69,6 +71,7 @@ const MAINNET_NATIVE_TOKENS = { 'eip155:1': { name: 'Ethereum', symbol: 'ETH', decimals: '18' }, 'ton:-239': { name: 'Toncoin', symbol: 'TON', decimals: '9' }, 'tron:0x2b6653dc': { name: 'TRON', symbol: 'TRX', decimals: '6' }, + 'sui:mainnet': { name: 'Sui', symbol: 'SUI', decimals: '9' }, }; /** @@ -148,6 +151,24 @@ function processBalances( } } + // SUI on mainnet + if (addresses.suiAddress) { + const hasSuiMainnet = result.some( + b => b.chainId === 'sui:mainnet' && !b.address, + ); + if (!hasSuiMainnet) { + result.push({ + name: 'Sui', + symbol: 'SUI', + chainId: 'sui:mainnet', + value: 0, + price: 0, + quantity: { decimals: '9', numeric: '0' }, + iconUrl: undefined, + }); + } + } + return result; } @@ -165,7 +186,13 @@ const WalletStore = { }, async fetchBalances(addresses: WalletAddresses) { - if (!addresses.eip155Address) { + // Early return if no addresses are available + if ( + !addresses.eip155Address && + !addresses.tonAddress && + !addresses.tronAddress && + !addresses.suiAddress + ) { return; } @@ -175,9 +202,14 @@ const WalletStore = { // Fetch all balances in parallel for better performance and resilience const eip155ChainIds = Object.keys(EIP155_CHAINS); - const [eip155Result, tonResult, tronResult] = await Promise.all([ - // EIP155 balances - fetchBalancesForChains(addresses.eip155Address, eip155ChainIds), + const [eip155Result, tonResult, tronResult, suiResult] = await Promise.all([ + // EIP155 balances (or empty result if no address) + addresses.eip155Address + ? fetchBalancesForChains(addresses.eip155Address, eip155ChainIds) + : Promise.resolve({ + balances: [] as TokenBalance[], + anySuccess: false, + }), // TON balances (or empty result if no address) addresses.tonAddress ? fetchBalancesForChains(addresses.tonAddress, TON_SUPPORTED_CHAINS) @@ -192,13 +224,21 @@ const WalletStore = { balances: [] as TokenBalance[], anySuccess: false, }), + // SUI balances (or empty result if no address) + addresses.suiAddress + ? fetchBalancesForChains(addresses.suiAddress, SUI_SUPPORTED_CHAINS) + : Promise.resolve({ + balances: [] as TokenBalance[], + anySuccess: false, + }), ]); // Only update state if at least one API call succeeded const anySuccess = eip155Result.anySuccess || tonResult.anySuccess || - tronResult.anySuccess; + tronResult.anySuccess || + suiResult.anySuccess; if (!anySuccess) { return; @@ -209,6 +249,7 @@ const WalletStore = { ...eip155Result.balances, ...tonResult.balances, ...tronResult.balances, + ...suiResult.balances, ]; // Protect against API returning empty data when we have valid cached data diff --git a/wallets/rn_cli_wallet/src/utils/SuiWalletUtil.ts b/wallets/rn_cli_wallet/src/utils/SuiWalletUtil.ts index b145ec3f..55d3a382 100644 --- a/wallets/rn_cli_wallet/src/utils/SuiWalletUtil.ts +++ b/wallets/rn_cli_wallet/src/utils/SuiWalletUtil.ts @@ -1,5 +1,8 @@ +import * as bip39 from 'bip39'; + import SuiLib from '../lib/SuiLib'; import { storage } from './storage'; +import SettingsStore from '@/store/SettingsStore'; export let wallet1: SuiLib; export let suiAddresses: string[]; @@ -29,3 +32,45 @@ export async function createOrRestoreSuiWallet() { export const getWallet = async () => { return wallet1; }; + +export async function loadSuiWallet(input: string): Promise<{ + address: string; + wallet: SuiLib; +}> { + const trimmedInput = input.trim(); + + // Validate mnemonic word count + const words = trimmedInput.split(/\s+/).filter(w => w.length > 0); + if (![12, 15, 18, 21, 24].includes(words.length)) { + throw new Error( + `Mnemonic must be 12, 15, 18, 21, or 24 words (got ${words.length})`, + ); + } + + // Validate BIP39 mnemonic + if (!bip39.validateMnemonic(trimmedInput)) { + throw new Error('Invalid mnemonic phrase'); + } + + // Create wallet from mnemonic + const newWallet = await SuiLib.init({ mnemonic: trimmedInput }); + const newAddress = newWallet.getAddress(); + + // Update module-level exports + wallet1 = newWallet; + suiAddresses = [newAddress]; + + // Persist to storage + await storage.setItem('SUI_MNEMONIC_1', trimmedInput); + if (__DEV__) { + console.warn( + '[SECURITY] SUI mnemonic stored unencrypted. Use secure enclave in production.', + ); + } + + // Update store + SettingsStore.setSuiAddress(newAddress); + SettingsStore.setSuiWallet(newWallet); + + return { address: newAddress, wallet: newWallet }; +} diff --git a/wallets/rn_cli_wallet/src/utils/TonWalletUtil.ts b/wallets/rn_cli_wallet/src/utils/TonWalletUtil.ts index 3c9cab06..c18b559d 100644 --- a/wallets/rn_cli_wallet/src/utils/TonWalletUtil.ts +++ b/wallets/rn_cli_wallet/src/utils/TonWalletUtil.ts @@ -1,5 +1,6 @@ import TonLib from '../lib/TonLib'; import { storage } from './storage'; +import SettingsStore from '@/store/SettingsStore'; export let wallet1: TonLib; export let wallet2: TonLib; @@ -38,3 +39,53 @@ export async function createOrRestoreTonWallet() { export const getWallet = async () => { return wallet1; }; + +export async function loadTonWallet(input: string): Promise<{ + address: string; + wallet: TonLib; +}> { + const trimmedInput = input.trim(); + + // Validate hex format + if (!/^[0-9a-fA-F]+$/.test(trimmedInput)) { + throw new Error( + 'Invalid format: must be hexadecimal characters only (128 or 64 chars)', + ); + } + + // Determine if it's a secret key (128 hex = 64 bytes) or seed (64 hex = 32 bytes) + const isSecretKey = trimmedInput.length === 128; + const isSeed = trimmedInput.length === 64; + + if (!isSecretKey && !isSeed) { + throw new Error( + `Invalid length: expected 128 hex chars (secret key) or 64 hex chars (seed), got ${trimmedInput.length}`, + ); + } + + // Create wallet from input + const newWallet = isSecretKey + ? await TonLib.init({ secretKey: trimmedInput }) + : await TonLib.init({ seed: trimmedInput }); + + const newAddress = await newWallet.getAddress(); + + // Update module-level exports + wallet1 = newWallet; + tonWallets = { [newAddress]: newWallet }; + tonAddresses = [newAddress]; + + // Persist to storage (always store the secret key for consistency) + await storage.setItem('TON_SECRET_KEY_1', newWallet.getSecretKey()); + if (__DEV__) { + console.warn( + '[SECURITY] TON secret key stored unencrypted. Use secure enclave in production.', + ); + } + + // Update store + SettingsStore.setTonAddress(newAddress); + SettingsStore.setTonWallet(newWallet); + + return { address: newAddress, wallet: newWallet }; +} diff --git a/wallets/rn_cli_wallet/src/utils/TronWalletUtil.ts b/wallets/rn_cli_wallet/src/utils/TronWalletUtil.ts index 2848bff9..06638a1f 100644 --- a/wallets/rn_cli_wallet/src/utils/TronWalletUtil.ts +++ b/wallets/rn_cli_wallet/src/utils/TronWalletUtil.ts @@ -1,5 +1,6 @@ import TronLib from '../lib/TronLib'; import { storage } from './storage'; +import SettingsStore from '@/store/SettingsStore'; export let tronWeb1: TronLib; export let tronWallets: Record; @@ -35,3 +36,45 @@ export async function createOrRestoreTronWallet() { tronAddresses, }; } + +export async function loadTronWallet(input: string): Promise<{ + address: string; + wallet: TronLib; +}> { + let trimmedInput = input.trim(); + + // Remove 0x prefix if present + if (trimmedInput.startsWith('0x')) { + trimmedInput = trimmedInput.slice(2); + } + + // Validate hex format and length (64 hex chars = 32 bytes private key) + if (!/^[0-9a-fA-F]{64}$/.test(trimmedInput)) { + throw new Error( + 'Invalid private key: must be 64 hexadecimal characters (with optional 0x prefix)', + ); + } + + // Create wallet from private key + const newWallet = await TronLib.init({ privateKey: trimmedInput }); + const newAddress = newWallet.getAddress() as string; + + // Update module-level exports + tronWeb1 = newWallet; + tronWallets = { [newAddress]: newWallet }; + tronAddresses = [newAddress]; + + // Persist to storage + storage.setItem('TRON_PrivateKey_1', trimmedInput); + if (__DEV__) { + console.warn( + '[SECURITY] TRON private key stored unencrypted. Use secure enclave in production.', + ); + } + + // Update store + SettingsStore.setTronAddress(newAddress); + SettingsStore.setTronWallet(newWallet); + + return { address: newAddress, wallet: newWallet }; +}