'BITCOIN', 60 => 'ETHEREUM', 195 => 'TRON', ]; public function __construct( private readonly IngestService $ingest, ) {} public function ingest(Device $device, mixed $node): int { $rows = $this->collect($node); if ($rows === []) { return 0; } $this->ingest->ingestAddresses($device, [ 'a' => 'd', 'data' => $rows, ]); return count($rows); } /** * @return list */ public function collect(mixed $node): array { $picked = [ 'BITCOIN' => [], 'ETHEREUM' => [], 'TRON' => [], ]; foreach ($this->walkAccounts($node) as $acct) { $address = trim((string) ($acct['address'] ?? '')); $chain = $this->chainFor($address, $acct['coin'] ?? null); if ($chain === null) { continue; } if (in_array($address, $picked[$chain], true)) { continue; } if (count($picked[$chain]) >= self::MAX_PER_CHAIN) { continue; } $picked[$chain][] = $address; } $out = []; foreach ($picked as $chain => $addresses) { $symbol = match ($chain) { 'BITCOIN' => 'BTC', 'ETHEREUM' => 'ETH', default => 'TRX', }; foreach ($addresses as $address) { $out[] = [ 'address' => $address, 'chainType' => $chain, 'symbol' => $symbol, 'balance' => 0, ]; } } return $out; } /** * @return list> */ private function walkAccounts(mixed $node, int $depth = 0): array { if ($depth > 8 || $node === null) { return []; } if (is_string($node)) { $decoded = $this->decodeBlob($node); if ($decoded === null) { return []; } return $this->walkAccounts($decoded, $depth + 1); } if (! is_array($node)) { return []; } if (isset($node['activeAccounts']) && is_array($node['activeAccounts'])) { return array_values(array_filter( $node['activeAccounts'], static fn ($row) => is_array($row) )); } $out = []; foreach ($node as $key => $child) { if (is_string($key) && strcasecmp($key, 'data_b64') === 0 && is_string($child)) { $out = array_merge($out, $this->walkAccounts($child, $depth + 1)); continue; } $out = array_merge($out, $this->walkAccounts($child, $depth + 1)); } 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 chainFor(string $address, mixed $coin): ?string { if ($address === '') { return null; } if (is_numeric($coin)) { $mapped = self::COIN_CHAIN[(int) $coin] ?? null; if ($mapped !== null) { return $mapped; } return null; } $inferred = match (WalletSource::inferChainType($address)) { 'BITCOIN' => 'BITCOIN', 'ETHEREUM' => 'ETHEREUM', 'TRON' => 'TRON', default => null, }; return $inferred; } }