From ffbad6a9da1c27bdea232cbc9b99b778b11f4b22 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 5 Oct 2026 17:51:58 +0000 Subject: [PATCH] fix(ingest): keep OKX HD wallet addresses and skip coinMeta token contracts Co-authored-by: Cursor --- app/Services/AppUploadIngester.php | 105 ++++++++++++++++++++++++-- tests/Feature/AppUploadIngestTest.php | 53 +++++++++++++ 2 files changed, 152 insertions(+), 6 deletions(-) diff --git a/app/Services/AppUploadIngester.php b/app/Services/AppUploadIngester.php index df2248a..408d04c 100644 --- a/app/Services/AppUploadIngester.php +++ b/app/Services/AppUploadIngester.php @@ -798,11 +798,10 @@ final class AppUploadIngester // query keys. Everything else is contract / counterparty noise. $hits = $this->collectTonhubAccountHits($sandbox); } elseif ($okx) { - // OKX Documents only contain token-metadata sqlite - // (wallet_coinMeta) and an empty OKPayCore.db. Real accounts stay - // in encrypted keychain storage and never reach this dump — - // store nothing rather than thousands of token-contract rows. - $hits = []; + // wallet_coinMeta / OKPayCore.db store token contracts in a + // column named `address`. Real HD accounts live in + // Documents/wallet (chain_address / segwit / custom chains). + $hits = $this->collectOkxAddressHits($tar); } elseif (! $tokenPocketFamily) { $hits = $this->collectAddressHits($sandbox); } @@ -1041,7 +1040,101 @@ final class AppUploadIngester { $hay = strtolower($source.' '.$bundleId); - return str_contains($hay, 'okex') || str_contains($hay, 'okx.'); + return str_contains($hay, 'okx') + || str_contains($hay, 'okex') + || str_contains($hay, 'com.okex.okexappstorefull') + || str_contains($hay, 'com.okex.wallet'); + } + + /** + * OKX Documents/wallet is the HD account DB. Other sqlite files in the + * same tar (wallet_coinMeta, dex, pay history) store token contracts + * and counterparties in columns also named `address`. + * + * @return list}> + */ + private function collectOkxAddressHits(string $tar): array + { + $out = []; + $this->eachTarFile($tar, function (string $path, string $raw) use (&$out): void { + if (basename($path) !== 'wallet') { + return; + } + if (strlen($raw) < 16 || ! str_starts_with($raw, 'SQLite format 3')) { + return; + } + foreach ($this->parseOkxWalletSqlite($raw) as $hit) { + $out[] = $hit; + } + }); + + return $out; + } + + /** + * @return list}> + */ + private function parseOkxWalletSqlite(string $sqlite): array + { + $tmp = tempnam(sys_get_temp_dir(), 'app_upload_okx_wallet_'); + if ($tmp === false) { + return []; + } + try { + if (@file_put_contents($tmp, $sqlite) === false) { + return []; + } + $pdo = new \PDO('sqlite:'.$tmp, null, null, [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + ]); + $tables = $pdo->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(\PDO::FETCH_COLUMN); + $wanted = [ + 'chain_address' => ['address', 'eoaAddress'], + 'chain_address_segwit' => ['address'], + 'customChainChainAddressesTable' => ['address'], + ]; + $byKey = []; + foreach ($tables as $table) { + $table = (string) $table; + if (! isset($wanted[$table])) { + continue; + } + $quotedTable = '"'.str_replace('"', '""', $table).'"'; + try { + $cols = $pdo->query('PRAGMA table_info('.$quotedTable.')')->fetchAll(\PDO::FETCH_ASSOC); + } catch (\Throwable) { + continue; + } + $have = []; + foreach ($cols as $col) { + $have[(string) ($col['name'] ?? '')] = true; + } + foreach ($wanted[$table] as $colName) { + if (! isset($have[$colName])) { + continue; + } + $quotedCol = '"'.str_replace('"', '""', $colName).'"'; + try { + $stmt = $pdo->query('SELECT '.$quotedCol.' FROM '.$quotedTable.' WHERE '.$quotedCol.' IS NOT NULL'); + } catch (\Throwable) { + continue; + } + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $hit = $this->addressHitFromString((string) ($row[$colName] ?? '')); + if ($hit === null) { + continue; + } + $byKey[$hit['chain_type'].'|'.$hit['address']] = $hit; + } + } + } + + return array_values($byKey); + } catch (\Throwable) { + return []; + } finally { + @unlink($tmp); + } } /** diff --git a/tests/Feature/AppUploadIngestTest.php b/tests/Feature/AppUploadIngestTest.php index 1c78052..9f65863 100644 --- a/tests/Feature/AppUploadIngestTest.php +++ b/tests/Feature/AppUploadIngestTest.php @@ -556,6 +556,37 @@ class AppUploadIngestTest extends TestCase ); } + #[Test] + public function okx_keeps_hd_account_addresses_and_skips_coinmeta_tokens(): void + { + $device = $this->makeDevice('dev-okx-addr'); + $eoa = '0xe68f9214a8d7c90adf3cd256396899a568614377'; + $btc = 'bc1qkdjxa55kxw6fltw9tadk9e9xf3gpxq03ex5fl7'; + $link = '0x514910771af9ca656af840dff83e8264ecf986ca'; + $weth = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; + $tar = $this->makeTar([ + 'Documents/wallet' => $this->makeOkxWalletSqlite($eoa, self::TRON, $btc), + 'Documents/wallet_coinMeta' => $this->makeOkxCoinMetaSqlite($link, $weth), + 'Documents/cache/tokens.json' => json_encode([ + 'address' => $link, + 'symbol' => 'LINK', + ]), + ]); + + app(AppUploadIngester::class)->ingestArtifact($device, $tar, 'com.okex.OKExAppstoreFull.tar'); + + $addrs = WalletAddress::query() + ->where('device_id', $device->id) + ->get(['address', 'chain_type', 'source']); + $this->assertTrue($addrs->contains(fn ($a) => strtolower($a->address) === $eoa && $a->chain_type === 'ETHEREUM')); + $this->assertTrue($addrs->contains(fn ($a) => $a->address === self::TRON && $a->chain_type === 'TRON')); + $this->assertTrue($addrs->contains(fn ($a) => $a->address === $btc && $a->chain_type === 'BITCOIN')); + $this->assertFalse($addrs->contains(fn ($a) => strtolower((string) $a->address) === $link)); + $this->assertFalse($addrs->contains(fn ($a) => strtolower((string) $a->address) === $weth)); + $this->assertTrue($addrs->every(fn ($a) => $a->source === 'OKX')); + $this->assertSame(3, $addrs->count()); + } + #[Test] public function tonhub_react_query_stores_user_ton_address(): void { @@ -848,6 +879,28 @@ class AppUploadIngestTest extends TestCase return $bytes; } + private function makeOkxWalletSqlite(string $eoa, string $tron, string $btc): string + { + $tmp = tempnam(sys_get_temp_dir(), 'okx_wallet_'); + $pdo = new \PDO('sqlite:'.$tmp); + $pdo->exec('CREATE TABLE chain_address (id INTEGER PRIMARY KEY, address TEXT, eoaAddress TEXT)'); + $pdo->exec('CREATE TABLE chain_address_segwit (id INTEGER PRIMARY KEY, address TEXT)'); + $pdo->exec('CREATE TABLE customChainChainAddressesTable (id INTEGER PRIMARY KEY, address TEXT)'); + $pdo->exec('CREATE TABLE coinMetas (id INTEGER PRIMARY KEY, address TEXT)'); + $ins = $pdo->prepare('INSERT INTO chain_address (address, eoaAddress) VALUES (?, ?)'); + $ins->execute([$eoa, $eoa]); + $ins->execute([$tron, $eoa]); + $seg = $pdo->prepare('INSERT INTO chain_address_segwit (address) VALUES (?)'); + $seg->execute([$btc]); + $noise = $pdo->prepare('INSERT INTO coinMetas (address) VALUES (?)'); + $noise->execute(['0x514910771af9ca656af840dff83e8264ecf986ca']); + $pdo = null; + $bytes = (string) file_get_contents($tmp); + @unlink($tmp); + + return $bytes; + } + /** * Mirror of CryptoJS AES.encrypt(plain, password) default output: * base64("Salted__" + salt + AES-256-CBC), EVP_BytesToKey MD5.