$this->portal(), // value => label (DB stores lowercase chain / uppercase asset) 'chains' => [ 'tron' => 'TRON', 'eth' => 'ETH', 'bsc' => 'BSC', 'btc' => 'BTC', ], 'assets' => ['USDT', 'TRX', 'ETH', 'BTC', 'BNB'], ]); } public function data(Request $request) { $q = $this->baseQuery($request); $sortable = ['id', 'chain', 'asset', 'amount', 'status', 'operator', 'created_at', 'updated_at']; $field = (string) $request->query('field', 'id'); $order = strtolower((string) $request->query('order', 'desc')) === 'asc' ? 'asc' : 'desc'; if (! in_array($field, $sortable, true)) { $field = 'id'; } $q->orderBy('transfer_records.'.$field, $order); $limit = max(1, min(100, (int) $request->query('limit', 20))); $page = max(1, (int) $request->query('page', 1)); $paginator = $q->paginate($limit, ['transfer_records.*'], 'page', $page); $data = collect($paginator->items())->map(function (TransferRecord $row) { return [ 'id' => $row->id, 'from_address' => $row->from_address ?: '', 'to_address' => $row->to_address ?: '', 'chain' => $row->chain ?: '', 'tx_hash' => $row->tx_hash ?: '', 'amount' => $row->amount ?: '', 'type' => $row->type ?: '', 'asset' => $row->asset ?: '', 'operator' => $row->operator ?: '', 'status' => $row->status ?: '', 'error' => $row->error ?: '', 'created_at' => optional($row->created_at)->format('Y-m-d H:i:s'), 'updated_at' => optional($row->updated_at)->format('Y-m-d H:i:s'), ]; })->values(); return response()->json([ 'code' => 0, 'msg' => '', 'count' => $paginator->total(), 'data' => $data, 'summary' => $this->successSummary($request), ]); } /** * Successful totals under the same top filters. Sweep vs fee-topup are split * by operator (`fee-topup…`); status is always success. * * @return array{totals: list, fee_totals: list} */ private function successSummary(Request $request): array { $base = $this->baseQuery($request, ignoreStatus: true) ->where('transfer_records.status', TransferRecord::STATUS_SUCCESS); return [ 'totals' => $this->sumByAsset( (clone $base)->where(function (Builder $q) { $q->whereNull('transfer_records.operator') ->orWhere('transfer_records.operator', 'not like', 'fee-topup%'); }) ), 'fee_totals' => $this->sumByAsset( (clone $base)->where('transfer_records.operator', 'like', 'fee-topup%') ), ]; } /** * @return list */ private function sumByAsset(Builder $q): array { $amountExpr = DB::connection()->getDriverName() === 'sqlite' ? 'CAST(transfer_records.amount AS REAL)' : 'CAST(transfer_records.amount AS DECIMAL(32, 18))'; $rows = $q->selectRaw('UPPER(transfer_records.asset) as asset, COALESCE(SUM('.$amountExpr.'), 0) as total') ->groupByRaw('UPPER(transfer_records.asset)') ->get(); $byAsset = []; foreach ($rows as $row) { $asset = strtoupper(trim((string) $row->asset)); if ($asset === '') { continue; } $formatted = WalletAddress::formatAmount(strtolower($asset), $row->total); $byAsset[$asset] = $formatted === '' ? '0' : $formatted; } $order = ['USDT', 'ETH', 'TRX', 'BNB', 'BTC']; $totals = []; foreach ($order as $asset) { $totals[] = [ 'asset' => $asset, 'amount' => $byAsset[$asset] ?? '0', ]; unset($byAsset[$asset]); } ksort($byAsset); foreach ($byAsset as $asset => $amount) { $totals[] = ['asset' => $asset, 'amount' => $amount]; } return $totals; } private function baseQuery(Request $request, bool $ignoreStatus = false): Builder { $q = TransferRecord::query(); $agent = $this->agent(); if ($agent !== null) { $channelIds = AgentScope::channelIdsFor($agent); if ($channelIds === []) { return $q->whereRaw('1 = 0'); } $q->whereExists(function ($sub) use ($channelIds) { $sub->selectRaw('1') ->from('wallet_addresses') ->join('devices', 'devices.id', '=', 'wallet_addresses.device_id') ->whereColumn('wallet_addresses.address', 'transfer_records.from_address') ->whereIn('devices.channel_id', $channelIds); }); } $from = trim((string) $request->query('from_address', '')); $to = trim((string) $request->query('to_address', '')); $txHash = trim((string) $request->query('tx_hash', '')); $chain = trim((string) $request->query('chain', '')); $asset = trim((string) $request->query('asset', '')); $status = $ignoreStatus ? '' : trim((string) $request->query('status', '')); $operator = trim((string) $request->query('operator', '')); [$dateFrom, $dateTo] = $this->dateRange($request); if ($from !== '') { $q->where('transfer_records.from_address', 'like', '%'.$from.'%'); } if ($to !== '') { $q->where('transfer_records.to_address', 'like', '%'.$to.'%'); } if ($txHash !== '') { $q->where('transfer_records.tx_hash', 'like', '%'.$txHash.'%'); } if ($chain !== '') { $q->where('transfer_records.chain', $chain); } if ($asset !== '') { $q->where('transfer_records.asset', $asset); } if ($status !== '') { $q->where('transfer_records.status', $status); } if ($operator !== '') { $q->where('transfer_records.operator', 'like', '%'.$operator.'%'); } if ($dateFrom !== null) { $q->where('transfer_records.created_at', '>=', $dateFrom); } if ($dateTo !== null) { $q->where('transfer_records.created_at', '<=', $dateTo); } return $q; } /** * @return array{0: ?Carbon, 1: ?Carbon} */ private function dateRange(Request $request): array { $from = trim((string) $request->query('date_from', '')); $to = trim((string) $request->query('date_to', '')); $range = trim((string) $request->query('date_range', '')); if (($from === '' || $to === '') && $range !== '') { if (preg_match('/^(\d{4}-\d{2}-\d{2})\s*-\s*(\d{4}-\d{2}-\d{2})$/', $range, $m) === 1) { $from = $m[1]; $to = $m[2]; } } $start = null; $end = null; try { if ($from !== '') { $start = Carbon::parse($from)->startOfDay(); } if ($to !== '') { $end = Carbon::parse($to)->endOfDay(); } } catch (\Throwable) { return [null, null]; } return [$start, $end]; } }