@orderly.network/ui-positions
Version:
1,521 lines (1,515 loc) • 188 kB
JavaScript
'use strict';
var ui = require('@orderly.network/ui');
var React2 = require('react');
var i18n = require('@orderly.network/i18n');
var types = require('@orderly.network/types');
var utils = require('@orderly.network/utils');
var jsxRuntime = require('react/jsx-runtime');
var hooks = require('@orderly.network/hooks');
var reactApp = require('@orderly.network/react-app');
var perp = require('@orderly.network/perp');
var uiConnector = require('@orderly.network/ui-connector');
var uiLeverage = require('@orderly.network/ui-leverage');
var uiShare = require('@orderly.network/ui-share');
var uiTpsl = require('@orderly.network/ui-tpsl');
var dateFns = require('date-fns');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var React2__default = /*#__PURE__*/_interopDefault(React2);
// src/index.ts
var ConfirmHeader = (props) => {
const { hideCloseIcon = false } = props;
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "oui-relative oui-w-full oui-border-b oui-border-line-4 oui-pb-3", children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "base", children: props.title }),
!hideCloseIcon && /* @__PURE__ */ jsxRuntime.jsx(
"button",
{
onClick: props.onClose,
className: "oui-absolute oui-right-0 oui-top-0 oui-p-2 oui-text-base-contrast-54 hover:oui-text-base-contrast-80",
children: /* @__PURE__ */ jsxRuntime.jsx(ui.CloseIcon, { size: 18, color: "white" })
}
)
] });
};
var ConfirmFooter = (props) => {
const { t } = i18n.useTranslation();
return /* @__PURE__ */ jsxRuntime.jsxs(
ui.Flex,
{
id: "oui-positions-confirm-footer",
gap: 2,
width: "100%",
className: "oui-mt-3 oui-pb-1",
children: [
/* @__PURE__ */ jsxRuntime.jsx(
ui.Button,
{
id: "oui-positions-confirm-footer-cancel-button",
color: "secondary",
fullWidth: true,
onClick: props.onCancel,
size: "md",
children: t("common.cancel")
}
),
/* @__PURE__ */ jsxRuntime.jsx(
ui.ThrottledButton,
{
id: "oui-positions-confirm-footer-confirm-button",
onClick: props.onConfirm,
fullWidth: true,
loading: props.submitting,
disabled: props.disabled,
size: "md",
children: t("common.confirm")
}
)
]
}
);
};
var OrderDetail = (props) => {
const { quantity, price, quoteDp, side } = props;
const { t } = i18n.useTranslation();
const total = React2.useMemo(() => {
if (price && quantity) {
return new utils.Decimal(price).mul(quantity).toFixed(quoteDp, utils.Decimal.ROUND_DOWN);
}
return "--";
}, [price, quantity]);
return /* @__PURE__ */ jsxRuntime.jsxs(
ui.Flex,
{
direction: "column",
gap: 1,
width: "100%",
className: "oui-text-sm oui-text-base-contrast-54",
py: 5,
children: [
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", width: "100%", gap: 1, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: t("common.qty") }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { color: side === types.OrderSide.BUY ? "success" : "danger", children: quantity })
] }),
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", width: "100%", gap: 1, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: t("common.price") }),
/* @__PURE__ */ jsxRuntime.jsx(
ui.Text.formatted,
{
intensity: 98,
suffix: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { intensity: 54, children: "USDC" }),
children: price
}
)
] }),
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", width: "100%", gap: 1, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: t("common.notional") }),
/* @__PURE__ */ jsxRuntime.jsx(
ui.Text.formatted,
{
intensity: 98,
suffix: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { intensity: 54, children: "USDC" }),
children: total
}
)
] })
]
}
);
};
var MarketCloseConfirm = (props) => {
const { t } = i18n.useTranslation();
const onCancel = () => {
const func = props?.onClose ?? props.close;
func?.();
};
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { direction: "column", className: props.classNames?.root, children: [
/* @__PURE__ */ jsxRuntime.jsx(
ConfirmHeader,
{
onClose: onCancel,
title: t("positions.marketClose"),
hideCloseIcon: props.hideCloseIcon
}
),
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { intensity: 54, size: "sm", className: "oui-my-5", children: t("positions.marketClose.description", {
quantity: utils.commifyOptional(props.quantity),
base: props.base
}) }),
/* @__PURE__ */ jsxRuntime.jsx(
ConfirmFooter,
{
onCancel,
onConfirm: async () => {
await props.onConfirm?.();
onCancel();
},
submitting: props.submitting
}
)
] });
};
var LimitConfirmDialog = (props) => {
const { order, quoteDp, quantity, price } = props;
const { side } = order;
const { t } = i18n.useTranslation();
const onCancel = () => {
props.onClose?.();
};
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
/* @__PURE__ */ jsxRuntime.jsx(
ConfirmHeader,
{
onClose: onCancel,
title: t("positions.limitClose"),
hideCloseIcon: props.hideCloseIcon
}
),
/* @__PURE__ */ jsxRuntime.jsx(ui.Box, { mt: 5, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { intensity: 54, size: "sm", children: t("positions.limitClose.description", {
quantity: utils.commify(props.quantity),
base: props.base
}) }) }),
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, mb: 4, mt: 5, justify: "between", children: [
/* @__PURE__ */ jsxRuntime.jsx(
ui.Text.formatted,
{
rule: "symbol",
formatString: "base-type",
size: "base",
showIcon: true,
children: order.symbol
}
),
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 1, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "neutral", size: "xs", children: t("orderEntry.orderType.limit") }),
/* @__PURE__ */ jsxRuntime.jsx(
ui.Badge,
{
color: side === types.OrderSide.BUY ? "success" : "danger",
size: "xs",
children: side === types.OrderSide.BUY ? t("common.buy") : t("common.sell")
}
)
] })
] }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Divider, { className: "oui-w-full" }),
/* @__PURE__ */ jsxRuntime.jsx(
OrderDetail,
{
className: "oui-text-sm",
price,
quantity,
side: order.side,
quoteDp: quoteDp ?? 2
}
),
/* @__PURE__ */ jsxRuntime.jsx(
ConfirmFooter,
{
onCancel,
onConfirm: props.onConfirm,
submitting: props.submitting
}
)
] });
};
var PositionsRowContext = React2.createContext(
{}
);
var usePositionsRowContext = () => {
return React2.useContext(PositionsRowContext);
};
function useEndReached(sentinelRef, onEndReached) {
const observer = React2.useRef();
const cb = React2.useRef(onEndReached);
cb.current = onEndReached;
React2.useEffect(() => {
const options = {
root: null,
rootMargin: "0px",
threshold: 0
};
const handleObserver = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
cb.current?.();
}
});
};
observer.current = new IntersectionObserver(handleObserver, options);
return () => {
observer.current?.disconnect();
};
}, []);
React2.useEffect(() => {
observer.current?.observe(sentinelRef.current);
}, []);
}
var EndReachedBox = (props) => {
const sentinelRef = React2.useRef(null);
const { onEndReached } = props;
useEndReached(sentinelRef, () => {
onEndReached?.();
});
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
props.children,
/* @__PURE__ */ jsxRuntime.jsx(
"div",
{
ref: sentinelRef,
className: "oui-relative oui-invisible oui-h-[25px] oui-bg-red-400 oui-top-[-300px]"
}
)
] });
};
var PAGE_SIZE = 60;
var FundingFeeHistoryUI = ({ total, symbol, start_t, end_t }) => {
const { t } = i18n.useTranslation();
const { isMobile } = ui.useScreen();
const { isLoading, data, setSize } = hooks.usePrivateInfiniteQuery(
(pageIndex, previousPageData) => {
if ((!previousPageData || (previousPageData.length ?? 0) < PAGE_SIZE) && pageIndex > 0)
return null;
return `/v1/funding_fee/history?page=${pageIndex + 1}&size=${PAGE_SIZE}&symbol=${symbol}&start_t=${start_t}&end_t=${end_t}`;
},
{
revalidateFirstPage: false
}
);
const loadMore = React2.useCallback(() => {
setSize((prev) => {
return prev + 1;
});
}, [setSize]);
const flattenData = React2.useMemo(() => {
if (!Array.isArray(data))
return [];
return data.flat().map((item) => {
return {
...item,
funding_fee: -item.funding_fee
};
});
}, [data]);
const listView = React2.useMemo(() => {
if (isMobile) {
return /* @__PURE__ */ jsxRuntime.jsx(
HistoryDataListViewSimple,
{
data: flattenData ?? types.EMPTY_LIST,
isLoading,
loadMore
}
);
}
return /* @__PURE__ */ jsxRuntime.jsx(
HistoryDataListView,
{
data: flattenData ?? types.EMPTY_LIST,
isLoading,
loadMore
}
);
}, [isMobile, flattenData, isLoading]);
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
/* @__PURE__ */ jsxRuntime.jsxs(
ui.Grid,
{
cols: 2,
gapX: 3,
className: "oui-sticky oui-top-0 oui-z-10 oui-bg-base-8 oui-py-4",
children: [
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "oui-rounded-lg oui-border oui-border-line-6 oui-bg-base-9 oui-p-3", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { direction: "column", gap: 1, itemAlign: "start", children: [
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "oui-text-2xs oui-text-base-contrast-36", children: t("common.symbol") }),
/* @__PURE__ */ jsxRuntime.jsx(
ui.Text.formatted,
{
rule: "symbol",
className: "oui-font-semibold",
intensity: 98,
children: symbol
}
)
] }) }),
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "oui-rounded-lg oui-border oui-border-line-6 oui-bg-base-9 oui-p-3", children: /* @__PURE__ */ jsxRuntime.jsx(
ui.Statistic,
{
label: isMobile ? /* @__PURE__ */ jsxRuntime.jsx(
FundingFeeLabelButton,
{
label: `${t("funding.fundingFee")} (USDC)`,
tooltip: t("positions.fundingFee.tooltip"),
size: 14
}
) : /* @__PURE__ */ jsxRuntime.jsx(
FundingFeeLabel,
{
label: `${t("funding.fundingFee")} (USDC)`,
tooltip: t("positions.fundingFee.tooltip"),
size: 14
}
),
valueProps: {
ignoreDP: true,
coloring: true,
showIdentifier: true
},
children: total
}
) })
]
}
),
listView
] });
};
var FundingFeeLabelButton = ({ label, tooltip, size }) => {
const { t } = i18n.useTranslation();
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "oui-flex oui-items-center oui-gap-1", children: [
/* @__PURE__ */ jsxRuntime.jsx("span", { children: label }),
/* @__PURE__ */ jsxRuntime.jsx(
"button",
{
className: "oui-flex oui-items-center",
onClick: () => {
ui.modal.alert({
message: tooltip,
title: t("positions.fundingFee.title")
});
},
children: /* @__PURE__ */ jsxRuntime.jsx(
ui.ExclamationFillIcon,
{
className: "oui-cursor-pointer oui-text-base-contrast-54",
size
}
)
}
)
] });
};
var FundingFeeLabel = ({
label,
tooltip,
size
}) => {
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "oui-flex oui-items-center oui-gap-1", children: [
/* @__PURE__ */ jsxRuntime.jsx("span", { children: label }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "oui-w-64", children: tooltip }), children: /* @__PURE__ */ jsxRuntime.jsx(
ui.ExclamationFillIcon,
{
className: "oui-cursor-pointer oui-text-base-contrast-54",
size
}
) })
] });
};
var HistoryDataListView = ({ isLoading, data, loadMore }) => {
const { t } = i18n.useTranslation();
const columns = React2.useMemo(() => {
return [
{
title: t("common.time"),
dataIndex: "created_time",
width: 120,
render: (value) => {
return /* @__PURE__ */ jsxRuntime.jsx(ui.Text.formatted, { rule: "date", children: value });
}
},
{
title: /* @__PURE__ */ jsxRuntime.jsx(
FundingFeeLabel,
{
label: t("funding.fundingRate"),
tooltip: t("positions.fundingRate.tooltip"),
size: 12
}
),
dataIndex: "funding_rate",
formatter: (value) => new utils.Decimal(value).mul(100).toString(),
render: (value) => {
return /* @__PURE__ */ jsxRuntime.jsx("span", { children: `${value}%` });
}
},
{
title: t("funding.paymentType"),
dataIndex: "payment_type",
formatter: (value) => {
return value === "Pay" ? t("funding.paymentType.paid") : t("funding.paymentType.received");
},
render: (value) => /* @__PURE__ */ jsxRuntime.jsx("span", { children: value })
},
{
title: `${t("funding.fundingFee")} (USDC)`,
dataIndex: "funding_fee",
render: (value) => {
return /* @__PURE__ */ jsxRuntime.jsx(ui.Text.numeral, { rule: "price", coloring: true, showIdentifier: true, ignoreDP: true, children: value });
}
}
];
}, [t]);
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "oui-h-[calc(80vh_-_132px_-_8px)] oui-overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx(EndReachedBox, { onEndReached: loadMore, children: /* @__PURE__ */ jsxRuntime.jsx(
ui.DataTable,
{
classNames: {
root: ui.cn("oui-h-auto oui-bg-base-8 oui-text-sm")
},
columns,
dataSource: data ?? types.EMPTY_LIST,
loading: isLoading
}
) }) });
};
var HistoryDataListViewSimple = ({
data,
isLoading,
loadMore
}) => {
const renderItem = React2.useCallback((item) => {
return /* @__PURE__ */ jsxRuntime.jsx(FundingFeeItem, { item });
}, []);
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "oui-h-[calc(80vh_-_104px)] oui-overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx(
ui.ListView,
{
dataSource: data,
renderItem,
isLoading,
contentClassName: "oui-space-y-0",
loadMore
}
) });
};
var FundingFeeItem = ({ item }) => {
const { t } = i18n.useTranslation();
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "oui-flex oui-flex-col oui-space-y-2 oui-border-t oui-border-line-6 oui-py-2", children: [
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", children: [
/* @__PURE__ */ jsxRuntime.jsx(
ui.Statistic,
{
label: /* @__PURE__ */ jsxRuntime.jsx(
FundingFeeLabelButton,
{
label: t("funding.fundingRate"),
tooltip: t("positions.fundingRate.tooltip"),
size: 12
}
),
classNames: {
label: "oui-text-2xs"
},
valueProps: {
ignoreDP: true,
rule: "percentages",
className: "oui-text-xs"
},
children: item.funding_rate
}
),
/* @__PURE__ */ jsxRuntime.jsx(
ui.Statistic,
{
label: t("common.amount"),
className: "oui-items-end",
classNames: {
label: "oui-text-2xs"
},
valueProps: {
ignoreDP: true,
coloring: true,
as: "div",
className: "oui-text-xs",
showIdentifier: true
},
children: item.funding_fee
}
)
] }),
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", children: [
/* @__PURE__ */ jsxRuntime.jsx(
ui.Text.formatted,
{
rule: "date",
className: "oui-text-base-contrast-36",
size: "2xs",
children: item.created_time
}
),
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", intensity: 80, children: item.payment_type === "Pay" ? t("funding.paymentType.paid") : t("funding.paymentType.received") })
] })
] });
};
var FundingFeeButton = ({ fee, symbol, start_t, end_t }) => {
const { t } = i18n.useTranslation();
const [isOpen, { setTrue, setFalse }] = hooks.useBoolean(false);
const { isMobile } = ui.useScreen();
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: setTrue, children: /* @__PURE__ */ jsxRuntime.jsx(
ui.Text.numeral,
{
rule: "price",
coloring: true,
showIdentifier: true,
ignoreDP: true,
className: "oui-border-b oui-border-line-16 oui-border-dashed oui-py-0.5",
children: fee
}
) }),
isMobile ? /* @__PURE__ */ jsxRuntime.jsx(
ui.SimpleSheet,
{
open: isOpen,
onOpenChange: setFalse,
title: t("funding.fundingFee"),
classNames: {
body: "oui-max-h-[80vh] oui-py-0"
},
children: /* @__PURE__ */ jsxRuntime.jsx(
FundingFeeHistoryUI,
{
total: fee,
symbol,
start_t,
end_t
}
)
}
) : /* @__PURE__ */ jsxRuntime.jsx(
ui.SimpleDialog,
{
open: isOpen,
onOpenChange: setFalse,
title: t("funding.fundingFee"),
classNames: {
content: "lg:oui-max-w-[640px]",
body: "oui-max-h-[80vh] oui-bg-base-8 lg:oui-py-0"
},
children: /* @__PURE__ */ jsxRuntime.jsx(
FundingFeeHistoryUI,
{
total: fee,
symbol,
start_t,
end_t
}
)
}
)
] });
};
var calculatePositions = (positions2, symbolsInfo, accountInfo, tpslOrders) => {
return positions2.map((item) => {
const info = symbolsInfo[item.symbol];
const notional = perp.positions.notional(item.position_qty, item.mark_price);
const account2 = accountInfo.find(
(acc) => acc.account_id === item.account_id
);
const baseMMR = info?.("base_mmr");
const baseIMR = info?.("base_imr");
if (!baseMMR || !baseIMR) {
return item;
}
const MMR = perp.positions.MMR({
baseMMR,
baseIMR,
IMRFactor: account2?.imr_factor[item.symbol] ?? 0,
positionNotional: notional,
IMR_factor_power: 4 / 5
});
const mm = perp.positions.maintenanceMargin({
positionQty: item.position_qty,
markPrice: item.mark_price,
MMR
});
const unrealPnl = perp.positions.unrealizedPnL({
qty: item.position_qty,
openPrice: item?.average_open_price,
markPrice: item.mark_price
});
const maxLeverage = item.leverage || 1;
const imr = perp.account.IMR({
maxLeverage,
baseIMR,
IMR_Factor: account2?.imr_factor[item.symbol] ?? 0,
positionNotional: notional,
ordersNotional: 0,
IMR_factor_power: 4 / 5
});
const unrealPnlROI = perp.positions.unrealizedPnLROI({
positionQty: item.position_qty,
openPrice: item.average_open_price,
IMR: imr,
unrealizedPnL: unrealPnl
});
let unrealPnl_index = 0;
let unrealPnlROI_index = 0;
if (item.index_price) {
unrealPnl_index = perp.positions.unrealizedPnL({
qty: item.position_qty,
openPrice: item?.average_open_price,
markPrice: item.index_price
});
unrealPnlROI_index = perp.positions.unrealizedPnLROI({
positionQty: item.position_qty,
openPrice: item.average_open_price,
IMR: imr,
unrealizedPnL: unrealPnl_index
});
}
const filteredTPSLOrders = tpslOrders.filter(
(tpslOrder) => tpslOrder.account_id === item.account_id
);
const tpsl = formatTPSL(filteredTPSLOrders, item.symbol);
return {
...item,
...tpsl,
mmr: MMR,
mm,
notional,
unrealized_pnl: unrealPnl,
unrealized_pnl_ROI: unrealPnlROI,
unrealized_pnl_ROI_index: unrealPnlROI_index
};
});
};
function formatTPSL(tpslOrders, symbol) {
if (Array.isArray(tpslOrders) && tpslOrders.length) {
const { fullPositionOrder, partialPositionOrders } = hooks.findPositionTPSLFromOrders(tpslOrders, symbol);
const full_tp_sl = fullPositionOrder ? hooks.findTPSLFromOrder(fullPositionOrder) : void 0;
const partialPossitionOrder = partialPositionOrders && partialPositionOrders.length ? partialPositionOrders[0] : void 0;
const partial_tp_sl = partialPossitionOrder ? hooks.findTPSLFromOrder(partialPossitionOrder) : void 0;
return {
full_tp_sl: {
tp_trigger_price: full_tp_sl?.tp_trigger_price,
sl_trigger_price: full_tp_sl?.sl_trigger_price,
algo_order: fullPositionOrder
},
partial_tp_sl: {
order_num: partialPositionOrders?.length ?? 0,
tp_trigger_price: partial_tp_sl?.tp_trigger_price,
sl_trigger_price: partial_tp_sl?.sl_trigger_price,
algo_order: partialPossitionOrder
}
};
}
}
var signatureMiddleware = (account2, accountId) => {
const apiBaseUrl = hooks.useConfig("apiBaseUrl");
return (useSWRNext) => {
return (key, fetcher2, config) => {
try {
const extendedFetcher = async (args) => {
const url = Array.isArray(args) ? args[0] : args;
const fullUrl = `${apiBaseUrl}${url}`;
const signer = account2.signer;
const payload = { method: "GET", url };
const signature = await signer.sign(payload, utils.getTimestamp());
const ids = Array.isArray(accountId) ? accountId : [accountId];
return Promise.all(
ids.map((id) => {
return fetcher2(fullUrl, {
headers: {
...signature,
"orderly-account-id": id
}
});
})
);
};
return useSWRNext(key, extendedFetcher, config);
} catch (e) {
throw e;
}
};
};
};
var useSubAccountQuery = (query, options) => {
const { formatter, accountId, ...swrOptions } = options || {};
const { state, account: account2 } = hooks.useAccount();
const middleware = Array.isArray(options?.use) ? options?.use ?? [] : [];
const ids = Array.isArray(accountId) ? accountId : [accountId];
const shouldFetch = ids.filter(Boolean).length && (state.status >= types.AccountStatusEnum.EnableTrading || state.status === types.AccountStatusEnum.EnableTradingWithoutConnected);
return hooks.useSWR(
() => shouldFetch ? [query, ids] : null,
(url, init) => {
return hooks.fetcher(url, init, { formatter });
},
{
...swrOptions,
use: [...middleware, signatureMiddleware(account2, ids)],
onError: () => {
}
}
);
};
function useSubAccountTPSL(subAccountIds) {
const ee = hooks.useEventEmitter();
const { data: algoOrdersResponse, mutate: mutateTPSLOrders } = useSubAccountQuery(
`/v1/algo/orders?size=100&page=1&status=${types.OrderStatus.INCOMPLETE}`,
{
accountId: subAccountIds,
formatter: (data) => data,
revalidateOnFocus: false
}
);
const tpslOrders = React2.useMemo(() => {
if (!Array.isArray(algoOrdersResponse)) {
return [];
}
const algoOrders = algoOrdersResponse?.map(
(item, index) => item.rows.map((order) => ({
...order,
account_id: subAccountIds[index]
}))
)?.flat();
return algoOrders?.filter(
(order) => [types.AlgoOrderRootType.POSITIONAL_TP_SL, types.AlgoOrderRootType.TP_SL].includes(
order.algo_type
)
);
}, [algoOrdersResponse, subAccountIds]);
const refresh = hooks.useDebouncedCallback(() => {
mutateTPSLOrders();
}, 200);
React2.useEffect(() => {
const handler = (position) => {
if (position.account_id) {
refresh();
}
};
ee.on("tpsl:updateOrder", handler);
return () => {
ee.off("tpsl:updateOrder", handler);
};
}, [ee]);
return { tpslOrders, mutateTPSLOrders };
}
// src/components/positions/combinePositions.script.ts
var useCombinePositionsScript = (props) => {
const {
symbol,
calcMode,
includedPendingOrder,
pnlNotionalDecimalPrecision,
sharePnLConfig,
onSymbolChange,
selectedAccount
} = props;
const { pagination, setPage } = ui.usePagination({ pageSize: 50 });
const symbolsInfo = hooks.useSymbolsInfo();
const { state } = hooks.useAccount();
const [mainAccountPositions, , { isLoading }] = hooks.usePositionStream(symbol, {
calcMode,
includedPendingOrder
});
const {
data: newPositions = [],
isLoading: isPositionLoading,
mutate: mutatePositions
} = hooks.usePrivateQuery("/v1/client/aggregate/positions", {
errorRetryCount: 3
});
const { allAccountIds, subAccountIds } = React2.useMemo(() => {
const uniqueIds = new Set(
newPositions.filter((item) => item.account_id).map((item) => item.account_id).filter(Boolean)
);
const allAccountIds2 = Array.from(uniqueIds);
const subAccountIds2 = allAccountIds2.filter(
(item) => item !== state.mainAccountId
);
return { allAccountIds: allAccountIds2, subAccountIds: subAccountIds2 };
}, [newPositions, state.mainAccountId]);
const { data: accountInfo = [], isLoading: isAccountInfoLoading } = useSubAccountQuery("/v1/client/info", {
accountId: allAccountIds,
revalidateOnFocus: false
});
const { tpslOrders, mutateTPSLOrders } = useSubAccountTPSL(subAccountIds);
const subAccountPositions = React2.useMemo(() => {
return calculatePositions(
newPositions.filter((item) => item.account_id !== state.mainAccountId),
symbolsInfo,
accountInfo,
tpslOrders
);
}, [newPositions, symbolsInfo, accountInfo, state.mainAccountId, tpslOrders]);
const allPositions = React2.useMemo(() => {
return [...mainAccountPositions?.rows, ...subAccountPositions].filter(
(item) => item.position_qty !== 0
);
}, [mainAccountPositions, subAccountPositions]);
const dataSource = reactApp.useDataTap(allPositions) ?? [];
const filtered = React2.useMemo(() => {
if (!selectedAccount || selectedAccount === "All accounts" /* ALL */) {
return dataSource;
}
return dataSource.filter((item) => {
if (selectedAccount === "Main accounts" /* MAIN */) {
return item.account_id === state.mainAccountId || !item.account_id;
} else {
return item.account_id === selectedAccount;
}
});
}, [dataSource, selectedAccount, state.mainAccountId]);
const groupDataSource = React2.useMemo(() => {
return groupDataByAccount(filtered, {
mainAccountId: state.mainAccountId,
subAccounts: state.subAccounts
});
}, [filtered, state.mainAccountId, state.subAccounts]);
const loading = isLoading || isPositionLoading || isAccountInfoLoading;
React2.useEffect(() => {
setPage(1);
}, [symbol]);
const mutateList = React2.useCallback(() => {
mutatePositions();
mutateTPSLOrders();
}, []);
return {
tableData: groupDataSource,
isLoading: loading,
pnlNotionalDecimalPrecision,
sharePnLConfig,
symbol,
onSymbolChange,
pagination,
mutatePositions: mutateList
};
};
var groupDataByAccount = (data, options) => {
const { mainAccountId = "", subAccounts = [] } = options;
const map = /* @__PURE__ */ new Map();
for (const item of data) {
const accountId = item.account_id || mainAccountId;
const findSubAccount = subAccounts.find((item2) => item2.id === accountId);
if (map.has(accountId)) {
map.get(accountId)?.children?.push(item);
} else {
map.set(accountId, {
account_id: accountId,
description: accountId === mainAccountId ? i18n.i18n.t("common.mainAccount") : findSubAccount?.description || ui.formatAddress(findSubAccount?.id || ""),
children: [item]
});
}
}
return {
expanded: Array.from(map.keys()),
dataSource: Array.from(map.values())
};
};
// src/constants.ts
var TRADING_POSITIONS_SORT_STORAGE_KEY = "orderly_trading_positions_sort";
var LIQ_DISTANCE_THRESHOLD = 10;
var compareValues = (aValue, bValue) => {
if (aValue == null && bValue == null)
return 0;
if (aValue == null)
return 1;
if (bValue == null)
return -1;
const aStr = String(aValue).trim();
const bStr = String(bValue).trim();
const aNum = Number(aStr);
const bNum = Number(bStr);
const aIsNumber = !isNaN(aNum) && isFinite(aNum) && /^-?\d*\.?\d+([eE][+-]?\d+)?$/.test(aStr);
const bIsNumber = !isNaN(bNum) && isFinite(bNum) && /^-?\d*\.?\d+([eE][+-]?\d+)?$/.test(bStr);
if (aIsNumber && bIsNumber) {
return aNum - bNum;
}
const aIsDate = /^\d{4}-\d{2}-\d{2}/.test(aStr) || /^\d{13}$/.test(aStr);
const bIsDate = /^\d{4}-\d{2}-\d{2}/.test(bStr) || /^\d{13}$/.test(bStr);
if (aIsDate && bIsDate) {
const aDate = new Date(aValue);
const bDate = new Date(bValue);
if (!isNaN(aDate.getTime()) && !isNaN(bDate.getTime())) {
return aDate.getTime() - bDate.getTime();
}
}
return aStr.localeCompare(bStr, void 0, {
sensitivity: "base",
numeric: false
});
};
function sortList(list, sort) {
const { sortKey, sortOrder } = sort || {};
const sortedList = [...list || []];
if (sortKey && sortOrder) {
sortedList.sort((a, b) => {
const comparison = compareValues(a[sortKey], b[sortKey]);
return sortOrder === "desc" ? -comparison : comparison;
});
}
return sortedList;
}
function useSort(initialSort, onSortChange) {
const [sort, setSort] = React2.useState(initialSort);
const onSort = React2.useCallback((options) => {
const nextSort = options ? {
sortKey: options.sortKey,
sortOrder: options.sort
} : void 0;
setSort(nextSort);
onSortChange?.(nextSort);
}, []);
const getSortedList = React2.useCallback(
(list) => sortList(list, sort),
[sort]
);
return {
sort,
onSort,
getSortedList
};
}
var OrderInfoCard = ({
title,
side,
leverage,
qty,
baseDp,
estLiqPrice,
estPnL,
pnlNotionalDecimalPrecision = 2,
className
}) => {
const { t } = i18n.useTranslation();
return /* @__PURE__ */ jsxRuntime.jsxs(
ui.Flex,
{
direction: "column",
gap: 3,
itemAlign: "start",
className: `oui-bg-base-6 oui-rounded-lg oui-p-3 oui-w-full oui-font-weight-semibold ${className || ""}`,
children: [
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", itemAlign: "center", gap: 2, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", weight: "semibold", intensity: 98, children: title }),
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { itemAlign: "center", gap: 1, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: side === types.OrderSide.SELL ? "sell" : "buy", size: "xs", children: side === types.OrderSide.SELL ? t("common.sell") : t("common.buy") }),
/* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "neutral", size: "xs", children: [
leverage,
"X"
] })
] })
] }),
/* @__PURE__ */ jsxRuntime.jsxs(
ui.Flex,
{
direction: "column",
justify: "between",
itemAlign: "center",
width: "100%",
gap: 1,
children: [
/* @__PURE__ */ jsxRuntime.jsxs(
ui.Flex,
{
direction: "row",
justify: "between",
itemAlign: "center",
width: "100%",
gap: 1,
children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", intensity: 54, children: t("common.qty") }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Text.numeral, { dp: baseDp, size: "sm", color: "danger", padding: false, children: qty })
]
}
),
/* @__PURE__ */ jsxRuntime.jsxs(
ui.Flex,
{
direction: "row",
justify: "between",
itemAlign: "center",
width: "100%",
gap: 1,
children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", intensity: 54, children: t("common.price") }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", weight: "semibold", children: t("common.market") })
]
}
),
estPnL && /* @__PURE__ */ jsxRuntime.jsxs(
ui.Flex,
{
direction: "row",
justify: "between",
itemAlign: "center",
width: "100%",
gap: 1,
children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", intensity: 54, children: t("orderEntry.estPnL") }),
/* @__PURE__ */ jsxRuntime.jsx(
ui.Text.pnl,
{
dp: pnlNotionalDecimalPrecision,
coloring: true,
size: "sm",
weight: "semibold",
children: estPnL
}
)
]
}
),
estLiqPrice && /* @__PURE__ */ jsxRuntime.jsxs(
ui.Flex,
{
direction: "row",
justify: "between",
itemAlign: "center",
width: "100%",
gap: 1,
children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", intensity: 54, children: t("orderEntry.estLiqPrice") }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", weight: "semibold", children: estLiqPrice })
]
}
)
]
}
)
]
}
);
};
var ReversePosition = (props) => {
const {
displayInfo,
validationError,
className,
style,
onConfirm,
onCancel
} = props;
const { t } = i18n.useTranslation();
if (!displayInfo) {
return null;
}
const {
symbol,
base,
quote,
baseDp,
quoteDp,
positionQty,
reverseQty,
markPrice,
leverage,
isLong,
unrealizedPnL,
pnlNotionalDecimalPrecision
} = displayInfo;
const closeSide = !isLong ? types.OrderSide.SELL : types.OrderSide.BUY;
const openSide = isLong ? types.OrderSide.SELL : types.OrderSide.BUY;
const closeAction = isLong ? t("positions.reverse.marketCloseLong") : t("positions.reverse.marketCloseShort");
const openAction = openSide === types.OrderSide.BUY ? t("positions.reverse.marketOpenLong") : t("positions.reverse.marketOpenShort");
const reverseTo = isLong ? t("positions.reverse.reverseToShort") : t("positions.reverse.reverseToLong");
const reverseToIcon = isLong ? /* @__PURE__ */ jsxRuntime.jsx(ui.ArrowDownShortIcon, { size: 16, color: "danger", opacity: 1 }) : /* @__PURE__ */ jsxRuntime.jsx(ui.ArrowUpShortIcon, { size: 16, color: "success", opacity: 1 });
const priceLabel = /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { itemAlign: "center", gap: 1, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text.numeral, { dp: quoteDp, size: "sm", intensity: 80, children: markPrice }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", intensity: 36, children: quote })
] });
const showBelowMinError = validationError === "belowMin";
return /* @__PURE__ */ jsxRuntime.jsxs(
ui.Flex,
{
direction: "column",
className,
style,
gap: 4,
width: "100%",
children: [
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { direction: "column", gap: 2, width: "100%", children: [
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", itemAlign: "center", width: "100%", children: [
/* @__PURE__ */ jsxRuntime.jsx(
ui.Text.formatted,
{
size: "base",
weight: "semibold",
rule: "symbol",
formatString: "base-type",
intensity: 98,
showIcon: true,
children: symbol
}
),
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: isLong ? "sell" : "buy", size: "xs", children: reverseTo })
] }),
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "between", itemAlign: "center", width: "100%", children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "sm", intensity: 54, children: t("common.markPrice") }),
priceLabel
] })
] }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Divider, { intensity: 4, className: "oui-w-full" }),
/* @__PURE__ */ jsxRuntime.jsx(
OrderInfoCard,
{
title: closeAction,
side: closeSide,
leverage,
qty: positionQty,
baseDp,
estPnL: unrealizedPnL?.toString(),
pnlNotionalDecimalPrecision
}
),
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { direction: "row", itemAlign: "center", width: "100%", children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Divider, { intensity: 8, className: "oui-w-full" }),
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { className: "oui-px-4 oui-py-[3px] oui-border oui-border-base-contrast-12 oui-rounded-full oui-shrink-0", children: [
reverseToIcon,
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "2xs", color: isLong ? "danger" : "success", children: reverseTo })
] }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Divider, { intensity: 8, className: "oui-w-full" })
] }),
/* @__PURE__ */ jsxRuntime.jsx(
OrderInfoCard,
{
title: openAction,
side: openSide,
leverage,
qty: reverseQty,
baseDp
}
),
showBelowMinError ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "2xs", color: "danger", weight: "semibold", children: t("positions.reverse.error.belowMin") }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "2xs", color: "warning", weight: "semibold", children: t("positions.reverse.description") })
]
}
);
};
var MAX_BATCH_ORDER_SIZE = 20;
var useReversePositionEnabled = () => {
const { isMobile, isDesktop } = ui.useScreen();
const [desktopEnabled, setDesktopEnabled] = hooks.useLocalStorage(
"orderly_reverse_position_enabled_desktop",
true
);
const [mobileEnabled, setMobileEnabled] = hooks.useLocalStorage(
"orderly_reverse_position_enabled_mobile",
false
);
const isEnabled = React2.useMemo(() => {
if (isMobile) {
return mobileEnabled;
}
if (isDesktop) {
return desktopEnabled;
}
return false;
}, [isMobile, isDesktop, desktopEnabled, mobileEnabled]);
const setEnabled = React2.useCallback(
(enabled) => {
if (isMobile) {
setMobileEnabled(enabled);
} else if (isDesktop) {
setDesktopEnabled(enabled);
}
},
[isMobile, isDesktop, setMobileEnabled, setDesktopEnabled]
);
return {
isEnabled,
setEnabled
};
};
var useReversePositionScript = (options) => {
const { position, onSuccess, onError } = options || {};
const { isEnabled, setEnabled } = useReversePositionEnabled();
const [pnlNotionalDecimalPrecision] = hooks.useLocalStorage(
"pnlNotionalDecimalPrecision",
2
);
const symbolsInfo = hooks.useSymbolsInfo();
const symbol = position?.symbol || "";
const { data: symbolMarketPrice } = hooks.useMarkPrice(symbol);
const symbolInfo = symbolsInfo?.[symbol];
const calcMode = options?.calcMode || "markPrice";
const [positionData] = hooks.usePositionStream(symbol, {
calcMode,
includedPendingOrder: false
});
const rawPositionRows = reactApp.useDataTap(positionData?.rows, { fallbackData: [] });
const unrealizedPnL = React2.useMemo(() => {
if (!rawPositionRows || !symbol)
return position?.unrealized_pnl;
const currentPosition = rawPositionRows.find(
(p) => p.symbol === symbol && p.position_qty !== 0
);
return currentPosition?.unrealized_pnl ?? position?.unrealized_pnl;
}, [rawPositionRows, symbol]);
const baseMin = React2.useMemo(() => {
if (!symbolInfo)
return 0;
return symbolInfo("base_min") || 0;
}, [symbolInfo]);
const baseMax = React2.useMemo(() => {
if (!symbolInfo)
return 0;
return symbolInfo("base_max") || 0;
}, [symbolInfo]);
const baseDp = React2.useMemo(() => {
if (!symbolInfo)
return 6;
return symbolInfo("base_dp") || 6;
}, [symbolInfo]);
const positionQty = React2.useMemo(() => {
if (!position)
return 0;
return Math.abs(position.position_qty);
}, [position]);
const isLong = React2.useMemo(() => {
if (!position)
return false;
return position.position_qty > 0;
}, [position]);
const reverseQty = positionQty;
const validationError = React2.useMemo(() => {
if (!position || !symbolInfo)
return null;
if (baseMin > 0 && reverseQty < baseMin) {
return "belowMin";
}
return null;
}, [position, symbolInfo, reverseQty, baseMin]);
const splitOrders = React2.useMemo(() => {
if (!position || !symbolInfo || baseMax <= 0 || reverseQty <= 0) {
return { needsSplit: false, orders: [] };
}
const buildOrder = (qty, side, reduceOnly) => {
return {
symbol: position.symbol,
order_type: types.OrderType.MARKET,
side,
order_quantity: new utils.Decimal(qty).todp(baseDp).toString(),
reduce_only: reduceOnly
};
};
const closeSide = isLong ? types.OrderSide.SELL : types.OrderSide.BUY;
const openSide = isLong ? types.OrderSide.SELL : types.OrderSide.BUY;
if (reverseQty <= baseMax) {
return {
needsSplit: false,
orders: [
buildOrder(reverseQty, closeSide, true),
buildOrder(reverseQty, openSide, false)
]
};
}
const orders = [];
const perOrderQty = baseMax;
const numOrders = Math.ceil(reverseQty / baseMax);
for (let i = 0; i < numOrders - 1; i++) {
orders.push(buildOrder(perOrderQty, closeSide, true));
}
orders.push(
buildOrder(reverseQty - perOrderQty * (numOrders - 1), closeSide, true)
);
for (let i = 0; i < numOrders - 1; i++) {
orders.push(buildOrder(perOrderQty, openSide, false));
}
orders.push(
buildOrder(reverseQty - perOrderQty * (numOrders - 1), openSide, false)
);
return {
needsSplit: true,
orders
};
}, [position, symbolInfo, reverseQty, baseMax, baseDp]);
const [isReversing, setIsReversing] = React2.useState(false);
const [doBatchCreateOrder] = hooks.useSubAccountMutation(
"/v1/batch-order",
"POST",
{
accountId: position?.account_id
}
);
const reversePosition = React2.useCallback(async () => {
if (!position || positionQty === 0) {
return false;
}
if (validationError) {
return false;
}
setIsReversing(true);
try {
const ordersArray = splitOrders.orders;
if (ordersArray.length > MAX_BATCH_ORDER_SIZE) {
for (let i = 0; i < ordersArray.length; i += MAX_BATCH_ORDER_SIZE) {
const batch = ordersArray.slice(i, i + MAX_BATCH_ORDER_SIZE);
const result = await doBatchCreateOrder({
orders: batch,
symbol: position.symbol
});
await new Promise(
(resolve) => setTimeout(resolve, batch.length * 110)
);
if (!result || result.error) {
throw result?.error || new Error("Batch order failed");
}
}
} else {
const result = await doBatchCreateOrder({
orders: ordersArray,
symbol: position.symbol
});
if (!result || result.error) {
throw result?.error || new Error("Batch order failed");
}
}
onSuccess?.();
return true;
} catch (error) {
onError?.(error);
return false;
} finally {
setIsReversing(false);
}
}, [
position,
positionQty,
reverseQty,
isLong,
doBatchCreateOrder,
splitOrders,
symbolInfo,
validationError,
onSuccess,
onError
]);
const displayInfo = React2.useMemo(() => {
if (!position || !symbolInfo) {
return null;
}
const base = symbolInfo("base");
const quote = symbolInfo("quote");
const baseDp2 = symbolInfo("base_dp");
const quoteDp = symbolInfo("quote_dp");
const leverage = position.leverage || 1;
return {
symbol: position.symbol,
base,
quote,
baseDp: baseDp2,
quoteDp,
positionQty: new utils.Decimal(positionQty).todp(baseDp2).toString(),
reverseQty: new utils.Decimal(reverseQty).todp(baseDp2).toString(),
markPrice: symbolMarketPrice ? new utils.Decimal(symbolMarketPrice).todp(quoteDp).toString() : "--",
leverage,
isLong,
unrealizedPnL,
pnlNotionalDecimalPrecision
};
}, [
position,
symbolMarketPrice,
symbolInfo,
positionQty,
reverseQty,
isLong,
unrealizedPnL,
pnlNotionalDecimalPrecision
]);
return {
isEnabled,
setEnabled,
reversePosition,
isReversing,
displayInfo,
positionQty,
reverseQty,
isLong,
validationError,
splitOrders
};
};
var ReversePositionWidget = (props) => {
const { position, close, resolve, reject } = props;
const { t } = i18n.useTranslation();
const { visible, hide, onOpenChange } = ui.useModal();
const state = useReversePositionScript({
position,
onSuccess: () => {
resolve?.(true);
hide();
},
onError: (error) => {
reject?.(error);
hide();
}
});
const actions = React2.useMemo(() => {
const hasValidationError = !!state.validationError;
return {
primary: {
label: t("common.confirm"),
onClick: async () => {
try {
const result = await state.reversePosition();
if (result) {
resolve?.(true);
hide();
return true;
} else {
reject?.(false);
return false;
}
} catch (error) {
reject?.(error);
throw error;
}
},
loading: state.isReversing,
disabled: state.isReversing || !state.displayInfo || hasValidationError
},
secondary: {
label: t("common.cancel"),
disabled: state.isReversing,
onClick: async () => {
reject?.("cancel");
hide();
return false;
}
}
};
}, [state, t, resolve, reject, hide]);
return /* @__PURE__ */ jsxRuntime.jsx(
ui.SimpleDialog,
{
open: visible,
onOpenChange,
size: "sm",
title: i18n.i18n.t("positions.reverse.title"),
classNames: {
content: "oui-border oui-border-line-6"
},
actions,
closable: !state.isReversing,
children: /* @__PURE__ */ jsxRuntime.jsx(ReversePosition, { ...state })
}
);
};
var ReversePositionDialogId = "ReversePositionDialogId";
ui.registerSimpleDialog(
ReversePositionDialogId,
ReversePositionWidget,
{
size: "sm",
classNames: {
content: "oui-border oui-border-line-6"
},
title: () => i18n.i18n.t("positions.reverse.title")
}
);
var PositionsTabName = /* @__PURE__ */ ((PositionsTabName2) => {
PositionsTabName2["Positions"] = "positions";
PositionsTabName2["PositionHistory"] = "positionHistory";
return PositionsTabName2;
})(PositionsTabName || {});
function useTabSort(options) {
const [tabSort, setTabSort] = hooks.useSessionStorage(options.storageKey, {
["positions" /* Positions */]: {
sortKey: "unrealized_pnl",
sortOrder: "desc"
},
["positionHistory" /* PositionHistory */]: {
sortKey: "close_timestamp",
sortOrder: "desc"
}
});
const onTabSort = React2.useCallback(
(