Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 48 additions & 6 deletions src/api/history.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import { DEFAULT_HISTORY_COUNT } from '@lib/config'
import { address, history, lastHistoryItemsCount, historySortKey, historyOrderStatusToShow } from '@lib/stores'
import { getLabelForAsset, getChainData } from '@lib/utils'

function getHistoryUrl(params) {
const dataEndpoint = getChainData('dataEndpoint');
const statusesToShow = get(historyOrderStatusToShow);

return `${dataEndpoint}/history/${params.address}?chain=arbitrum&limit=${params.first}&skip=${params.skip}&sortBy=${params.sortBy}&sortDirection=${params.sortDirection}&status=${statusesToShow.join(',')}`;
}

export async function getUserHistory(params) {

const dataEndpoint = getChainData('dataEndpoint');

let _address = get(address);
if (!_address) return;

Expand All @@ -24,15 +29,13 @@ export async function getUserHistory(params) {
if (!first) first = DEFAULT_HISTORY_COUNT;
if (!skip) skip = 0;

const statusesToShow = get(historyOrderStatusToShow);

const sortKey = get(historySortKey); // [columnName, isDesc]

let sortBy = 'timestamp';
let sortDirection = 'desc';

try {
const response = await fetch(`${dataEndpoint}/history/${_address}?chain=arbitrum&limit=${first}&skip=${skip}&sortBy=${sortBy}&sortDirection=${sortDirection}&status=${statusesToShow.join(',')}`);
const response = await fetch(getHistoryUrl({address: _address, first, skip, sortBy, sortDirection}));
const orders = await response.json() || [];

lastHistoryItemsCount.set(orders.length);
Expand Down Expand Up @@ -66,4 +69,43 @@ export async function getUserHistory(params) {
}

return true;
}
}

export async function getUserHistoryForExport(params) {
let _address = get(address);
if (!_address) return [];

if (!params) params = {};

const fromTimestamp = params.fromTimestamp || 0;
const toTimestamp = params.toTimestamp || Date.now() / 1000;
const pageSize = 500;
const maxPages = 200;
const sortBy = 'timestamp';
const sortDirection = 'desc';

_address = _address.toLowerCase();

let skip = 0;
let rows = [];

for (let page = 0; page < maxPages; page++) {
const response = await fetch(getHistoryUrl({address: _address, first: pageSize, skip, sortBy, sortDirection}));
const pageRows = await response.json() || [];

for (const row of pageRows) {
const timestamp = row.timestamp * 1;
if (!timestamp || timestamp > toTimestamp) continue;
if (timestamp >= fromTimestamp) rows.push(row);
}

if (pageRows.length < pageSize) break;

const oldestTimestamp = pageRows[pageRows.length - 1]?.timestamp * 1;
if (fromTimestamp && oldestTimestamp && oldestTimestamp < fromTimestamp) break;

skip += pageSize;
}

return rows;
}
7 changes: 6 additions & 1 deletion src/components/layout/Modals.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import MarketInfo from '../modals/MarketInfo.svelte'
import StakeCAP from '../modals/StakeCAP.svelte'
import UnstakeCAP from '../modals/UnstakeCAP.svelte'
import ExportHistory from '../modals/ExportHistory.svelte'
import HistoryOrderStatus from '../modals/HistoryOrderStatus.svelte'
import Settings from '../modals/Settings.svelte'

Expand Down Expand Up @@ -65,6 +66,10 @@
<HistoryOrderStatus />
{/if}

{#if $activeModal && $activeModal.name == 'ExportHistory'}
<ExportHistory />
{/if}

{#if $activeModal && $activeModal.name == 'MarketInfo'}
<MarketInfo data={$activeModal.data} />
{/if}
{/if}
238 changes: 238 additions & 0 deletions src/components/modals/ExportHistory.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
<script>
import Modal from './Modal.svelte'
import Button from '@components/layout/Button.svelte'

import {
formatHistoryItem,
formatOrderType,
formatSide
} from '@lib/formatters'
import { get } from 'svelte/store'
import { address } from '@lib/stores'
import { hideModal, showToast } from '@lib/ui'
import { getUserHistoryForExport } from '@api/history'

let timeframe = '30';
let customFrom;
let customTo;
let isExporting = false;
let exportedCount = 0;

const columns = [
['orderId', 'ID'],
['timestamp', 'Time'],
['isLong', 'Side'],
['market', 'Market'],
['price', 'Price'],
['size', 'Size'],
['asset', 'Asset'],
['margin', 'Margin'],
['leverage', 'Leverage'],
['orderType', 'Type'],
['isReduceOnly', 'Reduce Only'],
['status', 'Status'],
['reason', 'Reason'],
['pnl', 'PnL'],
['fee', 'Fee'],
['expiry', 'Expiry'],
['cancelOrderId', 'OCO']
];

function getDateStart(dateString) {
if (!dateString) return;
return new Date(`${dateString}T00:00:00`).getTime() / 1000;
}

function getDateEnd(dateString) {
if (!dateString) return;
return new Date(`${dateString}T23:59:59`).getTime() / 1000;
}

function getRange() {
const now = Date.now() / 1000;

if (timeframe == 'all') {
return {fromTimestamp: 0, toTimestamp: now};
}

if (timeframe == 'custom') {
return {
fromTimestamp: getDateStart(customFrom) || 0,
toTimestamp: getDateEnd(customTo) || now
};
}

return {
fromTimestamp: now - (timeframe * 1 * 24 * 60 * 60),
toTimestamp: now
};
}

function formatTimestamp(timestamp) {
if (!timestamp || timestamp * 1 == 0) return '';
return new Date(timestamp * 1000).toISOString();
}

function csvEscape(value) {
if (value == undefined || value == null) return '';
value = `${value}`;
if (value.includes('"') || value.includes(',') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}

function getCsvValue(item, key) {
switch(key) {
case 'timestamp':
case 'expiry':
return formatTimestamp(item[key]);
case 'isLong':
return formatSide(item.isLong, item.isReduceOnly, item.pnl);
case 'orderType':
return formatOrderType(item.orderType);
case 'isReduceOnly':
return item.isReduceOnly ? 'Yes' : 'No';
case 'leverage':
return item.leverage || '';
case 'reason':
return item.reason || '';
case 'cancelOrderId':
return item.cancelOrderId * 1 > 0 ? item.cancelOrderId : '';
default:
return item[key] == undefined || item[key] == null ? '' : item[key];
}
}

function buildCsv(rows) {
const header = columns.map(([, label]) => csvEscape(label)).join(',');
const csvRows = rows.map((row) => {
const item = formatHistoryItem(row);
return columns.map(([key]) => csvEscape(getCsvValue(item, key))).join(',');
});
return [header].concat(csvRows).join('\n');
}

function downloadCsv(csv) {
const blob = new Blob([csv], {type: 'text/csv;charset=utf-8'});
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = `cap-history-${new Date().toISOString().slice(0, 10)}.csv`;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(url);
}

async function exportHistory() {
if (!get(address)) {
showToast('Connect a wallet before exporting history.');
return;
}

isExporting = true;
exportedCount = 0;

try {
const rows = await getUserHistoryForExport(getRange());
exportedCount = rows.length;
downloadCsv(buildCsv(rows));
showToast(`Exported ${rows.length} history items.`, 1);
hideModal();
} catch(e) {
console.error('history export failed', e);
showToast('History export failed.');
}

isExporting = false;
}
</script>

<style>
.note {
color: var(--text300);
font-size: 90%;
line-height: 1.418;
margin-bottom: var(--base-padding);
}

.select {
width: 100%;
height: 48px;
padding: 0 14px;
border: 1px solid var(--layer200);
border-radius: var(--base-radius);
background-color: var(--layer50);
color: var(--text0);
font: inherit;
}

.date-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--semi-padding);
}

label {
display: block;
color: var(--text400);
font-size: 80%;
margin-bottom: 6px;
}

input {
width: 100%;
box-sizing: border-box;
height: 48px;
padding: 0 14px;
border: 1px solid var(--layer200);
border-radius: var(--base-radius);
background-color: var(--layer50);
color: var(--text0);
font: inherit;
}

.count {
color: var(--text300);
font-size: 85%;
margin-top: var(--semi-padding);
}
</style>

<Modal title='Export Trading History' width={360}>
<div class='container'>
<div class='note'>
Download a CSV report for the currently selected history status filters.
</div>

<div class='group'>
<select class='select' bind:value={timeframe}>
<option value='7'>Last 7 days</option>
<option value='30'>Last 30 days</option>
<option value='90'>Last 90 days</option>
<option value='all'>All history</option>
<option value='custom'>Custom range</option>
</select>
</div>

{#if timeframe == 'custom'}
<div class='group date-grid'>
<div>
<label for='history-export-from'>From</label>
<input id='history-export-from' type='date' bind:value={customFrom} />
</div>
<div>
<label for='history-export-to'>To</label>
<input id='history-export-to' type='date' bind:value={customTo} />
</div>
</div>
{/if}

<Button isLoading={isExporting} label='Download CSV' on:click={exportHistory} />

{#if exportedCount}
<div class='count'>{exportedCount} rows exported.</div>
{/if}
</div>
</Modal>
16 changes: 12 additions & 4 deletions src/components/trade/account/Account.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -139,17 +139,24 @@
margin-right: 10px;
}

.tools a {
.tools a,
.tools button {
display: flex;
align-items: center;
color: var(--text1);
margin-left: 16px;
padding: 4px 0;
}
.tools a:hover {
.tools button {
background: none;
font: inherit;
}
.tools a:hover,
.tools button:hover {
color: var(--text0);
}
.tools a:not(.leaderboard-link) :global(svg) {
.tools a:not(.leaderboard-link) :global(svg),
.tools button :global(svg) {
fill: currentColor;
height: 16px;
}
Expand Down Expand Up @@ -191,6 +198,7 @@
<div class='tools'>
{#if panel == 'history'}
<a on:click|stopPropagation={() => {showModal('HistoryOrderStatus')}} use:tooltip={{content: 'Filter history'}}>{@html FILTER_ICON}</a>
<button class='text' type='button' on:click|stopPropagation={() => {showModal('ExportHistory')}} use:tooltip={{content: 'Export history as CSV'}}>CSV</button>
{/if}
<a use:tooltip={{content: 'Customize columns'}} on:click|stopPropagation={() => {showModal('CustomizeColumns', {panel, allColumns: allColumns[panel]})}}>{@html TABLE_ICON}</a>
{#if panel == 'orders' && $ordersSorted.length}
Expand All @@ -205,4 +213,4 @@
{#if panel == 'history'}<History allColumns={allColumns['history']} />{/if}
</div>

</div>
</div>