diff --git a/src/api/history.js b/src/api/history.js index ce1e5cb..2d1cb08 100644 --- a/src/api/history.js +++ b/src/api/history.js @@ -4,8 +4,7 @@ import { DEFAULT_HISTORY_COUNT } from '@lib/config' import { address, history, lastHistoryItemsCount, historySortKey, historyOrderStatusToShow } from '@lib/stores' import { getLabelForAsset, getChainData } from '@lib/utils' -export async function getUserHistory(params) { - +async function fetchUserHistoryPage(params) { const dataEndpoint = getChainData('dataEndpoint'); let _address = get(address); @@ -15,11 +14,7 @@ export async function getUserHistory(params) { if (!params) params = {}; - let { - first, - skip, - diff - } = params; + let { first, skip } = params; if (!first) first = DEFAULT_HISTORY_COUNT; if (!skip) skip = 0; @@ -31,9 +26,24 @@ export async function getUserHistory(params) { let sortBy = 'timestamp'; let sortDirection = 'desc'; + const response = await fetch(`${dataEndpoint}/history/${_address}?chain=arbitrum&limit=${first}&skip=${skip}&sortBy=${sortBy}&sortDirection=${sortDirection}&status=${statusesToShow.join(',')}`); + return await response.json() || []; +} + +export async function getUserHistory(params) { + + if (!params) params = {}; + + let { + skip, + diff + } = params; + + if (!skip) skip = 0; + try { - const response = await fetch(`${dataEndpoint}/history/${_address}?chain=arbitrum&limit=${first}&skip=${skip}&sortBy=${sortBy}&sortDirection=${sortDirection}&status=${statusesToShow.join(',')}`); - const orders = await response.json() || []; + const orders = await fetchUserHistoryPage(params); + if (!orders) return; lastHistoryItemsCount.set(orders.length); @@ -66,4 +76,35 @@ export async function getUserHistory(params) { } return true; -} \ No newline at end of file +} + +export async function exportUserHistory(params) { + if (!params) params = {}; + + const { + fromTimestamp, + pageSize = 500, + maxItems = 5000 + } = params; + + let skip = 0; + let exportedOrders = []; + + while (exportedOrders.length < maxItems) { + const orders = await fetchUserHistoryPage({first: pageSize, skip}); + if (!orders || orders.length == 0) break; + + const filteredOrders = fromTimestamp + ? orders.filter((order) => order.timestamp * 1 >= fromTimestamp) + : orders; + + exportedOrders = exportedOrders.concat(filteredOrders); + + if (orders.length < pageSize) break; + if (fromTimestamp && orders.every((order) => order.timestamp * 1 < fromTimestamp)) break; + + skip += pageSize; + } + + return exportedOrders.slice(0, maxItems); +} diff --git a/src/components/trade/account/History.svelte b/src/components/trade/account/History.svelte index 68ee72e..bc5857c 100644 --- a/src/components/trade/account/History.svelte +++ b/src/components/trade/account/History.svelte @@ -17,13 +17,14 @@ formatSide, formatDate, formatMarketName, - formatPriceForDisplay + formatPriceForDisplay, + getLabelForKey } from '@lib/formatters' import { address, historySortKey, historySorted, historyColumnsToShow, lastHistoryItemsCount, orders } from '@lib/stores' import { showModal } from '@lib/ui' import { saveUserSetting } from '@lib/utils' - import { getUserHistory } from '@api/history' + import { exportUserHistory, getUserHistory } from '@api/history' export let allColumns; @@ -106,6 +107,97 @@ $: formattedHistory = $historySorted.map((item) => formatHistoryItem(item)); let showDetails = {}; + let exportRange = '30d'; + let isExporting = false; + + function getExportFromTimestamp() { + const now = Math.floor(Date.now() / 1000); + if (exportRange == '7d') return now - 7 * 24 * 60 * 60; + if (exportRange == '30d') return now - 30 * 24 * 60 * 60; + if (exportRange == '90d') return now - 90 * 24 * 60 * 60; + return null; + } + + function cleanCsvValue(value) { + if (value == undefined || value == null) return ''; + return `${value}`.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); + } + + function csvEscape(value) { + value = cleanCsvValue(value); + if (/[",\r\n]/.test(value)) return `"${value.replace(/"/g, '""')}"`; + return value; + } + + function getCsvValue(item, key) { + switch (key) { + case 'id': + return item.status == 'liquidated' ? 'liq' : item.orderId; + case 'timestamp': + return formatDate(item.timestamp); + case 'isLong': + return formatSide(item.isLong, item.isReduceOnly); + case 'market': + return formatMarketName(item.market); + case 'price': + return item.price * 1 > 0 ? formatPriceForDisplay(item.price) : ''; + case 'size': + return `${formatForDisplay(item.size)} ${item.asset}`; + case 'margin': + return `${formatForDisplay(item.margin)} ${item.asset}`; + case 'leverage': + return item.leverage ? `${formatForDisplay(item.leverage)}x` : 'N/A'; + case 'orderType': + return formatOrderType(item.orderType); + case 'isReduceOnly': + return item.isReduceOnly ? 'Yes' : 'No'; + case 'status': + return item.status; + case 'reason': + return item.reason || ''; + case 'pnl': + return item.pnl ? `${formatPnl(item.pnl)} (${formatPnl(100*item.pnl/item.margin, true)})` : ''; + case 'fee': + return `${formatForDisplay(item.fee)} ${item.asset}`; + case 'expiry': + return formatDate(item.expiry) || ''; + case 'cancelOrderId': + return item.cancelOrderId * 1 > 0 ? item.cancelOrderId : ''; + default: + return item[key]; + } + } + + function downloadCsv(rows) { + const headers = allColumns.map((column) => getLabelForKey(column.key)); + const csvRows = [ + headers.map(csvEscape).join(','), + ...rows.map((item) => { + const formattedItem = formatHistoryItem(item); + return allColumns.map((column) => csvEscape(getCsvValue(formattedItem, column.key))).join(','); + }) + ]; + const blob = new Blob([csvRows.join('\n')], {type: 'text/csv;charset=utf-8'}); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `cap-trading-history-${new Date().toISOString().slice(0, 10)}.csv`; + link.click(); + URL.revokeObjectURL(url); + } + + async function exportHistory() { + if (isExporting) return; + isExporting = true; + try { + const rows = await exportUserHistory({fromTimestamp: getExportFromTimestamp()}); + if (rows && rows.length > 0) downloadCsv(rows); + } catch(e) { + console.error('History CSV export error', e); + } finally { + isExporting = false; + } + } function getItemStatus(item) { if (!item) return ''; @@ -234,6 +326,40 @@ .wrapper { max-height: calc(var(--account-height) - 50px - 39px); } + .history-panel { + height: 100%; + } + .export-tools { + height: 44px; + padding: 0 25px; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + border-bottom: 1px solid var(--layer100); + } + .export-tools select, .export-tools button { + height: 28px; + border: 1px solid var(--layer200); + border-radius: 5px; + background: var(--layer50); + color: var(--text0); + font-size: 85%; + } + .export-tools select { + padding: 0 8px; + } + .export-tools button { + padding: 0 12px; + cursor: pointer; + } + .export-tools button:disabled { + cursor: default; + color: var(--text400); + } + .history-table { + height: calc(100% - 45px); + } @media all and (max-width: 600px) { .wrapper { max-height: 100%; @@ -241,6 +367,17 @@ } +