All checks were successful
deploy / deploy (push) Successful in 26s
Proof: Runtime health sentinel, alert routing, and anomaly detection for stale/disconnected quote truth, truthful dashboard severity, webhook notifications, and safe executor containment. Assumptions: Existing control APIs remain the service-local truth surface; external notification stays as a generic webhook sink; executor disarm is an allowed non-fund-moving containment action; current dashboard/operator files in the worktree belong to this turn and are intended to ship together. Still fake: No live external receiver is configured; webhook delivery is implemented but unverified end-to-end in production; cluster rollout still depends on deploying the new image; no automatic deployment restart path was added.
1200 lines
42 KiB
JavaScript
1200 lines
42 KiB
JavaScript
import { unitsToNumber } from './assets.mjs';
|
|
import { summarizeFundingObservations } from './funding-observations.mjs';
|
|
import { resolveDashboardRequestAuth } from './operator-dashboard-auth.mjs';
|
|
import { deriveServiceHealth, inferServiceFreshnessTimestamp as inferRuntimeFreshnessTimestamp } from './runtime-health.mjs';
|
|
|
|
export const DASHBOARD_LIVE_QUOTE_LIMIT = 10;
|
|
|
|
const DECIMAL_SCALE = 18;
|
|
const DECIMAL_FACTOR = 10n ** BigInt(DECIMAL_SCALE);
|
|
const ALERT_SEVERITY_ORDER = {
|
|
critical: 3,
|
|
warning: 2,
|
|
info: 1,
|
|
};
|
|
const CREDITED_FUNDING_STATUSES = new Set(['CREDITED', 'COMPLETED', 'FINALIZED', 'SETTLED']);
|
|
|
|
const CONTROL_DEFINITIONS = [
|
|
{
|
|
service: 'near-intents-ingest',
|
|
action: 'reconnect',
|
|
method: 'POST',
|
|
path: '/reconnect',
|
|
label: 'Reconnect Ingest',
|
|
description: 'Reconnect the ingest websocket without restarting the deployment.',
|
|
page: 'system',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'market-reference-ingest',
|
|
action: 'refresh',
|
|
method: 'POST',
|
|
path: '/refresh',
|
|
label: 'Refresh Price',
|
|
description: 'Fetch the latest BTC/EUR reference price.',
|
|
page: 'funds',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'inventory-sync',
|
|
action: 'refresh',
|
|
method: 'POST',
|
|
path: '/refresh',
|
|
label: 'Refresh Inventory',
|
|
description: 'Sync verifier balances into the latest inventory snapshot.',
|
|
page: 'funds',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'liquidity-manager',
|
|
action: 'refresh',
|
|
method: 'POST',
|
|
path: '/refresh',
|
|
label: 'Refresh Liquidity',
|
|
description: 'Refresh bridge deposit handles, deposits, and tracked withdrawals.',
|
|
page: 'funds',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'liquidity-manager',
|
|
action: 'pause',
|
|
method: 'POST',
|
|
path: '/pause',
|
|
label: 'Pause Liquidity',
|
|
description: 'Pause non-fund-moving liquidity state refreshes.',
|
|
page: 'system',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'liquidity-manager',
|
|
action: 'resume',
|
|
method: 'POST',
|
|
path: '/resume',
|
|
label: 'Resume Liquidity',
|
|
description: 'Resume liquidity state refreshes.',
|
|
page: 'system',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'liquidity-manager',
|
|
action: 'pause-funding-observer',
|
|
method: 'POST',
|
|
path: '/pause-funding-observer',
|
|
label: 'Pause Funding Observer',
|
|
description: 'Pause pre-credit funding observations without touching spendable truth.',
|
|
page: 'funds',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'liquidity-manager',
|
|
action: 'resume-funding-observer',
|
|
method: 'POST',
|
|
path: '/resume-funding-observer',
|
|
label: 'Resume Funding Observer',
|
|
description: 'Resume pre-credit funding observations.',
|
|
page: 'funds',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'liquidity-manager',
|
|
action: 'freeze-withdrawals',
|
|
method: 'POST',
|
|
path: '/freeze-withdrawals',
|
|
label: 'Update Withdrawal Freeze',
|
|
description: 'Toggle withdrawal freeze without submitting live withdrawals.',
|
|
page: 'funds',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'liquidity-manager',
|
|
action: 'withdrawal-estimate',
|
|
method: 'POST',
|
|
path: '/withdrawal-estimate',
|
|
label: 'Estimate Withdrawal',
|
|
description: 'Estimate a bridge withdrawal without moving funds.',
|
|
page: 'funds',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'trade-executor',
|
|
action: 'reconnect',
|
|
method: 'POST',
|
|
path: '/reconnect',
|
|
label: 'Reconnect Relay',
|
|
description: 'Reconnect the trade-executor solver relay websocket.',
|
|
page: 'system',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'trade-executor',
|
|
action: 'pause',
|
|
method: 'POST',
|
|
path: '/pause',
|
|
label: 'Pause Executor',
|
|
description: 'Pause trade-executor command consumption without moving funds.',
|
|
page: 'system',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'trade-executor',
|
|
action: 'resume',
|
|
method: 'POST',
|
|
path: '/resume',
|
|
label: 'Resume Executor',
|
|
description: 'Resume trade-executor command consumption.',
|
|
page: 'system',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'trade-executor',
|
|
action: 'disarm',
|
|
method: 'POST',
|
|
path: '/disarm',
|
|
label: 'Disarm Executor',
|
|
description: 'Force the executor into a non-fund-moving safe state.',
|
|
page: 'system',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'ops-sentinel',
|
|
action: 'pause',
|
|
method: 'POST',
|
|
path: '/pause',
|
|
label: 'Pause Alerts',
|
|
description: 'Pause alert evaluation without changing trade arming state.',
|
|
page: 'system',
|
|
risk_class: 'safe',
|
|
},
|
|
{
|
|
service: 'ops-sentinel',
|
|
action: 'resume',
|
|
method: 'POST',
|
|
path: '/resume',
|
|
label: 'Resume Alerts',
|
|
description: 'Resume alert evaluation.',
|
|
page: 'system',
|
|
risk_class: 'safe',
|
|
},
|
|
];
|
|
|
|
const SERVICE_DEFINITIONS = [
|
|
['near-intents-ingest', 'Intents Ingest', 'nearIntentsControlBaseUrl'],
|
|
['market-reference-ingest', 'Reference Price', 'marketReferenceControlBaseUrl'],
|
|
['inventory-sync', 'Inventory Sync', 'inventorySyncControlBaseUrl'],
|
|
['liquidity-manager', 'Liquidity Manager', 'liquidityManagerControlBaseUrl'],
|
|
['history-writer', 'History Writer', 'historyWriterControlBaseUrl'],
|
|
['ops-sentinel', 'Ops Sentinel', 'opsSentinelControlBaseUrl'],
|
|
['strategy-engine', 'Strategy Engine', 'strategyEngineControlBaseUrl'],
|
|
['trade-executor', 'Trade Executor', 'tradeExecutorControlBaseUrl'],
|
|
];
|
|
|
|
export function resolveDashboardAuth({ mode = 'stub' } = {}) {
|
|
return resolveDashboardRequestAuth({ mode });
|
|
}
|
|
|
|
export function listDashboardControls({ page = null } = {}) {
|
|
const controls = CONTROL_DEFINITIONS.map((definition) => ({ ...definition }));
|
|
if (!page) return controls;
|
|
return controls.filter((definition) => definition.page === page);
|
|
}
|
|
|
|
export function resolveDashboardControl({ service, action }) {
|
|
return CONTROL_DEFINITIONS.find((definition) => (
|
|
definition.service === service && definition.action === action
|
|
)) || null;
|
|
}
|
|
|
|
export function listDashboardServices(config) {
|
|
return SERVICE_DEFINITIONS.map(([service, label, configKey]) => ({
|
|
service,
|
|
label,
|
|
base_url: config[configKey],
|
|
}));
|
|
}
|
|
|
|
export function createDashboardLiveState({
|
|
config,
|
|
recentQuotes = [],
|
|
latestMarketPrice = null,
|
|
latestInventory = null,
|
|
successfulTradeCount = 0,
|
|
lastSuccessfulTradeAt = null,
|
|
activeAlerts = [],
|
|
} = {}) {
|
|
const state = {
|
|
active_pair: config.activePair,
|
|
btc_asset: config.tradingBtc,
|
|
eure_asset: config.tradingEure,
|
|
quote_limit: config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT,
|
|
recent_quotes: recentQuotes.slice(0, config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT),
|
|
latest_market_price: latestMarketPrice?.payload || latestMarketPrice || null,
|
|
latest_inventory: latestInventory?.payload || latestInventory || null,
|
|
successful_trade_count: Number(successfulTradeCount || 0),
|
|
last_successful_trade_at: lastSuccessfulTradeAt || null,
|
|
active_alerts: new Map(),
|
|
};
|
|
|
|
for (const alert of activeAlerts) {
|
|
if (!alert) continue;
|
|
state.active_alerts.set(buildAlertKey(alert), alert);
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
export function applyDashboardLiveEvent(state, { topic, event }) {
|
|
if (!event?.payload) return [];
|
|
|
|
switch (topic) {
|
|
case 'norm.swap_demand': {
|
|
const quote = normalizeLiveQuote(event.payload, event);
|
|
if (!quote) return [];
|
|
state.recent_quotes = appendUniqueRecentQuote(state.recent_quotes, quote, state.quote_limit);
|
|
return [{
|
|
type: 'quotes.recent',
|
|
recent_quotes: state.recent_quotes,
|
|
}];
|
|
}
|
|
case 'ref.market_price':
|
|
state.latest_market_price = {
|
|
...event.payload,
|
|
observed_at: event.observed_at || event.payload.observed_at || null,
|
|
ingested_at: event.ingested_at || null,
|
|
};
|
|
return [{
|
|
type: 'status_bar.updated',
|
|
status_bar: buildLiveStatusBar(state),
|
|
}];
|
|
case 'state.intent_inventory':
|
|
state.latest_inventory = {
|
|
...event.payload,
|
|
observed_at: event.observed_at || event.payload.synced_at || null,
|
|
ingested_at: event.ingested_at || null,
|
|
};
|
|
return [{
|
|
type: 'status_bar.updated',
|
|
status_bar: buildLiveStatusBar(state),
|
|
}];
|
|
case 'ops.alert': {
|
|
const alert = normalizeAlert(event.payload);
|
|
const key = buildAlertKey(alert);
|
|
if (alert.status === 'raised') {
|
|
state.active_alerts.set(key, alert);
|
|
} else if (alert.status === 'cleared') {
|
|
state.active_alerts.delete(key);
|
|
}
|
|
return [{
|
|
type: 'alerts.updated',
|
|
alerts: {
|
|
active_alert_count: state.active_alerts.size,
|
|
highest_alert_severity: highestAlertSeverity([...state.active_alerts.values()]),
|
|
},
|
|
}, {
|
|
type: 'status_bar.updated',
|
|
status_bar: buildLiveStatusBar(state),
|
|
}];
|
|
}
|
|
case 'exec.trade_result':
|
|
if (event.payload.status !== 'submitted') return [];
|
|
state.successful_trade_count += 1;
|
|
state.last_successful_trade_at = event.observed_at || event.ingested_at || new Date().toISOString();
|
|
return [{
|
|
type: 'status_bar.updated',
|
|
status_bar: buildLiveStatusBar(state),
|
|
}];
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function buildDashboardBootstrap({
|
|
config,
|
|
auth,
|
|
portfolioMetric,
|
|
inventorySnapshot,
|
|
marketPrice,
|
|
recentQuotes,
|
|
successfulTrades,
|
|
successfulTradeSummary,
|
|
fundingObservations,
|
|
recentDepositStatuses,
|
|
recentTradeDecisions,
|
|
recentAlertTransitions,
|
|
serviceSnapshots,
|
|
sourceErrors = [],
|
|
} = {}) {
|
|
const servicesByName = Object.fromEntries(
|
|
(serviceSnapshots || []).map((snapshot) => [snapshot.service, snapshot]),
|
|
);
|
|
const activeAlerts = normalizeAlertList(
|
|
servicesByName['ops-sentinel']?.state?.active_alerts || [],
|
|
);
|
|
const recentAlerts = normalizeAlertList(
|
|
servicesByName['ops-sentinel']?.state?.recent_transitions
|
|
|| recentAlertTransitions?.map((entry) => entry.payload)
|
|
|| [],
|
|
);
|
|
const profitability = buildProfitabilitySummary({
|
|
metric: portfolioMetric,
|
|
successfulTradeSummary,
|
|
});
|
|
const balances = buildBalanceSummary({
|
|
inventorySnapshot,
|
|
marketPrice,
|
|
config,
|
|
});
|
|
const funding = buildFundingSummary({
|
|
config,
|
|
fundingObservations,
|
|
recentDepositStatuses,
|
|
liquidityState: servicesByName['liquidity-manager']?.state || {},
|
|
});
|
|
const tradesPage = normalizeSuccessfulTradesPage({
|
|
config,
|
|
successfulTrades,
|
|
});
|
|
|
|
return {
|
|
session: auth,
|
|
source_errors: sourceErrors,
|
|
default_page: 'funds',
|
|
status_bar: buildStatusBar({
|
|
config,
|
|
profitability,
|
|
inventorySnapshot,
|
|
marketPrice,
|
|
activeAlerts,
|
|
servicesByName,
|
|
}),
|
|
funds: {
|
|
profitability,
|
|
balances,
|
|
funding,
|
|
recent_deposits: funding.credited_deposits,
|
|
recent_withdrawals: buildRecentWithdrawals({
|
|
config,
|
|
liquidityState: servicesByName['liquidity-manager']?.state || {},
|
|
}),
|
|
trade_asset_changes: buildTradeAssetChanges({
|
|
config,
|
|
trades: tradesPage.items,
|
|
}),
|
|
recent_quotes: (recentQuotes || []).slice(0, config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT),
|
|
successful_trades: tradesPage,
|
|
controls: listDashboardControls({ page: 'funds' }),
|
|
caveats: profitability.caveats,
|
|
},
|
|
strategy: buildStrategySummary({
|
|
servicesByName,
|
|
activeAlerts,
|
|
recentTradeDecisions,
|
|
}),
|
|
system: buildSystemSummary({
|
|
servicesByName,
|
|
activeAlerts,
|
|
recentAlerts,
|
|
}),
|
|
};
|
|
}
|
|
|
|
export function buildProfitabilitySummary({ metric, successfulTradeSummary } = {}) {
|
|
const externalCashFlows = metric?.payload?.external_cash_flows || {};
|
|
const externalFlowCount = Number(externalCashFlows.flow_count || 0);
|
|
const externalFlowAdjusted = externalFlowCount > 0;
|
|
const summary = {
|
|
computed_at: metric?.computed_at || null,
|
|
current_total_portfolio_value_eure: metric?.payload?.current_portfolio_value_eure || null,
|
|
deposit_baseline_value_eure: metric?.payload?.baseline_portfolio_value_eure_at_baseline_price || null,
|
|
simple_hold_value_eure: metric?.payload?.baseline_portfolio_value_eure_at_current_price || null,
|
|
pnl_vs_deposit_baseline_eure: null,
|
|
pnl_vs_simple_hold_eure: null,
|
|
market_move_contribution_eure: null,
|
|
trading_contribution_eure: null,
|
|
baseline_anchor_at: metric?.baseline_anchor_at || null,
|
|
baseline_status: metric?.baseline_status || metric?.payload?.baseline_status || 'unavailable',
|
|
external_flow_adjusted: externalFlowAdjusted,
|
|
external_flow_count: externalFlowCount,
|
|
external_deposit_count: Number(externalCashFlows.deposit_count || 0),
|
|
external_withdrawal_count: Number(externalCashFlows.withdrawal_count || 0),
|
|
latest_external_flow_at: externalCashFlows.latest_effective_at || null,
|
|
net_external_flow_value_eure: externalCashFlows.net_value_eure_at_flow_time || '0',
|
|
recent_trade_count: successfulTradeSummary?.total ?? metric?.payload?.result_count ?? 0,
|
|
last_successful_trade_at: successfulTradeSummary?.last_successful_trade_at || null,
|
|
caveats: [
|
|
'Portfolio PnL is truthful to the current durable inventory and reference price path.',
|
|
'Fees and per-trade realized net settlement deltas are not fully tracked yet.',
|
|
],
|
|
};
|
|
|
|
if (summary.current_total_portfolio_value_eure == null) {
|
|
summary.caveats.unshift('Profitability is unavailable until durable portfolio metrics exist.');
|
|
return summary;
|
|
}
|
|
|
|
if (summary.deposit_baseline_value_eure) {
|
|
summary.pnl_vs_deposit_baseline_eure = formatDecimalDifference(
|
|
summary.current_total_portfolio_value_eure,
|
|
summary.deposit_baseline_value_eure,
|
|
);
|
|
}
|
|
|
|
if (summary.simple_hold_value_eure) {
|
|
summary.pnl_vs_simple_hold_eure = formatDecimalDifference(
|
|
summary.current_total_portfolio_value_eure,
|
|
summary.simple_hold_value_eure,
|
|
);
|
|
}
|
|
|
|
if (summary.deposit_baseline_value_eure && summary.simple_hold_value_eure) {
|
|
summary.market_move_contribution_eure = formatDecimalDifference(
|
|
summary.simple_hold_value_eure,
|
|
summary.deposit_baseline_value_eure,
|
|
);
|
|
}
|
|
|
|
if (summary.simple_hold_value_eure) {
|
|
summary.trading_contribution_eure = formatDecimalDifference(
|
|
summary.current_total_portfolio_value_eure,
|
|
summary.simple_hold_value_eure,
|
|
);
|
|
}
|
|
|
|
if (summary.external_flow_adjusted) {
|
|
summary.caveats.unshift(
|
|
`Later credited deposits and completed withdrawals (${summary.external_flow_count}) are treated as external cash flows, not trading PnL.`,
|
|
);
|
|
}
|
|
|
|
return summary;
|
|
}
|
|
|
|
export function buildLiveStatusBar(state) {
|
|
return {
|
|
latest_reference_price_eure_per_btc: state.latest_market_price?.eure_per_btc || null,
|
|
market_observed_at:
|
|
state.latest_market_price?.observed_at
|
|
|| state.latest_market_price?.ingested_at
|
|
|| null,
|
|
market_freshness_ms: ageMs(
|
|
state.latest_market_price?.observed_at || state.latest_market_price?.ingested_at,
|
|
),
|
|
inventory_observed_at:
|
|
state.latest_inventory?.synced_at
|
|
|| state.latest_inventory?.observed_at
|
|
|| state.latest_inventory?.ingested_at
|
|
|| null,
|
|
inventory_freshness_ms: ageMs(
|
|
state.latest_inventory?.synced_at
|
|
|| state.latest_inventory?.observed_at
|
|
|| state.latest_inventory?.ingested_at,
|
|
),
|
|
current_total_portfolio_value_eure: computeCurrentPortfolioValue({
|
|
inventory: state.latest_inventory,
|
|
marketPrice: state.latest_market_price,
|
|
btcAsset: state.btc_asset,
|
|
eureAsset: state.eure_asset,
|
|
}),
|
|
active_alert_count: state.active_alerts.size,
|
|
highest_alert_severity: highestAlertSeverity([...state.active_alerts.values()]),
|
|
recent_trade_count: state.successful_trade_count,
|
|
last_successful_trade_at: state.last_successful_trade_at,
|
|
};
|
|
}
|
|
|
|
function buildStatusBar({
|
|
config,
|
|
profitability,
|
|
inventorySnapshot,
|
|
marketPrice,
|
|
activeAlerts,
|
|
servicesByName,
|
|
}) {
|
|
return {
|
|
active_pair: config.activePair,
|
|
latest_reference_price_eure_per_btc: marketPrice?.payload?.eure_per_btc || null,
|
|
market_observed_at: marketPrice?.payload?.observed_at || marketPrice?.ingested_at || null,
|
|
market_freshness_ms: ageMs(marketPrice?.payload?.observed_at || marketPrice?.ingested_at),
|
|
inventory_observed_at:
|
|
inventorySnapshot?.payload?.synced_at || inventorySnapshot?.ingested_at || null,
|
|
inventory_freshness_ms: ageMs(
|
|
inventorySnapshot?.payload?.synced_at || inventorySnapshot?.ingested_at,
|
|
),
|
|
active_alert_count: activeAlerts.length,
|
|
highest_alert_severity: highestAlertSeverity(activeAlerts),
|
|
strategy_armed: servicesByName['strategy-engine']?.state?.armed ?? null,
|
|
executor_armed: servicesByName['trade-executor']?.state?.armed ?? null,
|
|
current_total_portfolio_value_eure: profitability.current_total_portfolio_value_eure,
|
|
recent_trade_count: profitability.recent_trade_count,
|
|
last_successful_trade_at: profitability.last_successful_trade_at,
|
|
};
|
|
}
|
|
|
|
function buildBalanceSummary({ inventorySnapshot, marketPrice, config }) {
|
|
const inventory = inventorySnapshot?.payload || {};
|
|
const spendable = inventory.spendable || {};
|
|
const pendingInbound = inventory.pending_inbound || {};
|
|
const pendingOutbound = inventory.pending_outbound || {};
|
|
|
|
return {
|
|
synced_at: inventory.synced_at || inventorySnapshot?.ingested_at || null,
|
|
reconciliation_status: inventory.reconciliation_status || null,
|
|
items: [...config.assetRegistry.values()].map((asset) => {
|
|
const spendableUnits = String(spendable[asset.assetId] || '0');
|
|
const pendingInboundUnits = String(pendingInbound[asset.assetId] || '0');
|
|
const pendingOutboundUnits = String(pendingOutbound[asset.assetId] || '0');
|
|
return {
|
|
asset_id: asset.assetId,
|
|
symbol: asset.symbol,
|
|
chain: asset.chain,
|
|
spendable_units: spendableUnits,
|
|
spendable: formatUnits(spendableUnits, asset.decimals),
|
|
pending_inbound_units: pendingInboundUnits,
|
|
pending_inbound: formatUnits(pendingInboundUnits, asset.decimals),
|
|
pending_outbound_units: pendingOutboundUnits,
|
|
pending_outbound: formatUnits(pendingOutboundUnits, asset.decimals),
|
|
eur_value_eure: valueAssetInEur({
|
|
asset,
|
|
units: spendableUnits,
|
|
marketPrice: marketPrice?.payload || marketPrice || null,
|
|
}),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
|
|
function buildFundingSummary({ config, fundingObservations, recentDepositStatuses, liquidityState }) {
|
|
const observations = (fundingObservations || []).map((entry) => entry.payload || entry);
|
|
const summary = summarizeFundingObservations(observations, {
|
|
now: new Date().toISOString(),
|
|
});
|
|
const observerItems = observations.map((observation) => normalizeFundingObservationForUi({
|
|
config,
|
|
observation,
|
|
}));
|
|
const depositItems = buildRecentDepositItems({
|
|
config,
|
|
recentDepositStatuses,
|
|
liquidityState,
|
|
});
|
|
const recentFundingActivity = mergeFundingActivityItems({
|
|
observerItems,
|
|
depositItems,
|
|
});
|
|
|
|
return {
|
|
latest_observed_at: latestFundingActivityAt(recentFundingActivity, summary.latest_funding_observation_at),
|
|
control_state: {
|
|
paused: liquidityState?.paused ?? null,
|
|
funding_observer_paused: liquidityState?.funding_observer_paused ?? null,
|
|
withdrawals_frozen: liquidityState?.withdrawals_frozen ?? null,
|
|
withdrawal_defaults: liquidityState?.withdrawal_defaults || {},
|
|
},
|
|
handles: Object.entries(liquidityState?.deposit_addresses || {}).map(([chain, details]) => ({
|
|
chain,
|
|
asset_id: config.tradingBtc.chain === chain ? config.tradingBtc.assetId : config.tradingEure.assetId,
|
|
symbol: config.tradingBtc.chain === chain ? config.tradingBtc.symbol : config.tradingEure.symbol,
|
|
address: details?.address || null,
|
|
memo: details?.memo || null,
|
|
refreshed_at: details?.refreshed_at || null,
|
|
})),
|
|
credited_deposits: recentFundingActivity
|
|
.filter((observation) => CREDITED_FUNDING_STATUSES.has(String(observation?.status || '').toUpperCase()))
|
|
.sort((left, right) => sortTimestamps(
|
|
fundingActivityTimestamp(right),
|
|
fundingActivityTimestamp(left),
|
|
))
|
|
.slice(0, 10)
|
|
.map((observation) => ({ ...observation })),
|
|
pre_credit_by_asset: Object.values(summary.funding_visibility_by_asset || {}).map((entry) => {
|
|
const asset = config.assetRegistry.get(entry.asset_id);
|
|
return {
|
|
asset_id: entry.asset_id,
|
|
symbol: asset?.symbol || entry.asset_id,
|
|
pre_credit_total_units: entry.pre_credit_total || '0',
|
|
pre_credit_total: formatUnits(entry.pre_credit_total || '0', asset?.decimals || 0),
|
|
latest_status: entry.latest_status,
|
|
latest_observation_at: entry.latest_observation_at,
|
|
};
|
|
}),
|
|
pre_credit_by_handle: Object.values(summary.funding_observations_by_handle || {}).map((entry) => {
|
|
const asset = config.assetRegistry.get(entry.asset_id);
|
|
return {
|
|
funding_handle: entry.funding_handle,
|
|
chain: entry.chain,
|
|
asset_id: entry.asset_id,
|
|
symbol: asset?.symbol || entry.asset_id,
|
|
pre_credit_total_units: entry.pre_credit_total || '0',
|
|
pre_credit_total: formatUnits(entry.pre_credit_total || '0', asset?.decimals || 0),
|
|
latest_status: entry.latest_status,
|
|
latest_observation_at: entry.latest_observation_at,
|
|
observation_count: entry.observations?.length || 0,
|
|
};
|
|
}),
|
|
recent_observations: recentFundingActivity
|
|
.sort((left, right) => sortTimestamps(
|
|
fundingActivityTimestamp(right),
|
|
fundingActivityTimestamp(left),
|
|
))
|
|
.slice(0, 10)
|
|
.map((observation) => ({ ...observation })),
|
|
};
|
|
}
|
|
|
|
function buildRecentWithdrawals({ config, liquidityState }) {
|
|
return Object.values(liquidityState?.tracked_withdrawals || {})
|
|
.sort((left, right) => sortTimestamps(
|
|
right.last_checked_at || right.submitted_at || right.noted_at,
|
|
left.last_checked_at || left.submitted_at || left.noted_at,
|
|
))
|
|
.slice(0, 10)
|
|
.map((withdrawal) => {
|
|
const asset = config.assetRegistry.get(withdrawal.asset_id);
|
|
return {
|
|
withdrawal_hash: withdrawal.withdrawal_hash,
|
|
asset_id: withdrawal.asset_id,
|
|
symbol: asset?.symbol || withdrawal.asset_id,
|
|
chain: withdrawal.chain || null,
|
|
amount_units: String(withdrawal.amount || '0'),
|
|
amount: formatUnits(withdrawal.amount || '0', asset?.decimals || 0),
|
|
status: withdrawal.status || null,
|
|
address: withdrawal.address || null,
|
|
submitted_at: withdrawal.submitted_at || null,
|
|
last_checked_at: withdrawal.last_checked_at || null,
|
|
noted_at: withdrawal.noted_at || null,
|
|
};
|
|
});
|
|
}
|
|
|
|
function buildTradeAssetChanges({ config, trades }) {
|
|
return (trades || []).slice(0, 10).map((trade) => {
|
|
const assetIn = config.assetRegistry.get(trade.asset_in);
|
|
const assetOut = config.assetRegistry.get(trade.asset_out);
|
|
return {
|
|
observed_at: trade.observed_at,
|
|
quote_id: trade.quote_id,
|
|
request_kind: trade.request_kind,
|
|
asset_in: trade.asset_in,
|
|
asset_in_symbol: assetIn?.symbol || trade.asset_in,
|
|
amount_in_units: trade.amount_in,
|
|
amount_in: formatUnits(trade.amount_in || '0', assetIn?.decimals || 0),
|
|
asset_out: trade.asset_out,
|
|
asset_out_symbol: assetOut?.symbol || trade.asset_out,
|
|
amount_out_units: trade.amount_out,
|
|
amount_out: formatUnits(trade.amount_out || '0', assetOut?.decimals || 0),
|
|
};
|
|
});
|
|
}
|
|
|
|
function normalizeSuccessfulTradesPage({ config, successfulTrades }) {
|
|
return {
|
|
...successfulTrades,
|
|
items: (successfulTrades?.items || []).map((trade) => normalizeTradeForUi({
|
|
config,
|
|
trade,
|
|
})),
|
|
};
|
|
}
|
|
|
|
function buildStrategySummary({ servicesByName, activeAlerts, recentTradeDecisions = [] }) {
|
|
const strategyState = servicesByName['strategy-engine']?.state || {};
|
|
const executorState = servicesByName['trade-executor']?.state || {};
|
|
const durableDecisionsById = new Map(
|
|
(recentTradeDecisions || [])
|
|
.map((entry) => normalizeDecision({
|
|
...(entry.payload || {}),
|
|
decision_at:
|
|
entry?.payload?.decision_at
|
|
|| entry?.observed_at
|
|
|| entry?.ingested_at
|
|
|| null,
|
|
}))
|
|
.filter((entry) => entry?.decision_id)
|
|
.map((entry) => [entry.decision_id, entry]),
|
|
);
|
|
const recentDecisions = (strategyState.recent_decisions || []).slice(0, 20).map((decision) => {
|
|
const durableDecision = durableDecisionsById.get(decision?.decision_id) || null;
|
|
return normalizeDecision({
|
|
...(durableDecision || {}),
|
|
...(decision || {}),
|
|
decision_at:
|
|
decision?.decision_at
|
|
|| durableDecision?.decision_at
|
|
|| null,
|
|
});
|
|
});
|
|
const latestDecision = normalizeDecision({
|
|
...(durableDecisionsById.get(strategyState.latest_decision?.decision_id) || {}),
|
|
...(strategyState.latest_decision || {}),
|
|
decision_at:
|
|
strategyState.latest_decision?.decision_at
|
|
|| durableDecisionsById.get(strategyState.latest_decision?.decision_id)?.decision_at
|
|
|| null,
|
|
});
|
|
|
|
return {
|
|
strategy_state: {
|
|
armed: strategyState.armed ?? null,
|
|
paused: strategyState.paused ?? null,
|
|
threshold_pct: strategyState.threshold_pct ?? null,
|
|
max_notional_eure: strategyState.max_notional_eure ?? null,
|
|
latest_decision: latestDecision?.decision_id ? latestDecision : null,
|
|
recent_decisions: recentDecisions.length
|
|
? recentDecisions
|
|
: [...durableDecisionsById.values()].slice(0, 20),
|
|
skipped_counts: strategyState.skipped_counts || {},
|
|
durable_control_state: strategyState.durable_control_state || null,
|
|
},
|
|
executor_state: {
|
|
armed: executorState.armed ?? null,
|
|
paused: executorState.paused ?? null,
|
|
draining: executorState.draining ?? null,
|
|
in_flight_count: executorState.in_flight_count ?? 0,
|
|
completed_count: executorState.completed_count ?? 0,
|
|
last_command: executorState.last_command || null,
|
|
last_venue_response: executorState.last_venue_response || null,
|
|
last_error: executorState.last_error || null,
|
|
signer_registered: executorState.signer_registered ?? null,
|
|
account_id: executorState.account_id || null,
|
|
signer_public_key: executorState.signer_public_key || null,
|
|
},
|
|
relevant_alerts: activeAlerts.filter((alert) => (
|
|
['strategy-engine', 'trade-executor', 'liquidity-manager'].includes(alert.service_scope)
|
|
)),
|
|
omitted_controls: [
|
|
'Strategy arm and disarm are intentionally absent in this turn.',
|
|
'Executor arm and drain are intentionally absent in this turn.',
|
|
'Live withdrawal submission is intentionally absent in this turn.',
|
|
],
|
|
};
|
|
}
|
|
|
|
function buildSystemSummary({ servicesByName, activeAlerts, recentAlerts }) {
|
|
const historyWriterState = servicesByName['history-writer']?.state || {};
|
|
const sentinelServiceHealth = new Map(
|
|
(servicesByName['ops-sentinel']?.state?.service_health || []).map((entry) => [entry.service, entry]),
|
|
);
|
|
|
|
return {
|
|
service_health: Object.values(servicesByName).map((snapshot) => (
|
|
summarizeServiceSnapshot(snapshot, {
|
|
authoritativeHealth: sentinelServiceHealth.get(snapshot.service) || null,
|
|
activeAlerts,
|
|
})
|
|
)),
|
|
alerts: {
|
|
active: activeAlerts,
|
|
recent: recentAlerts,
|
|
},
|
|
persistence: {
|
|
database_connectivity: historyWriterState.database_connectivity ?? null,
|
|
last_write_at: historyWriterState.last_write_at || null,
|
|
last_alert_write_at: historyWriterState.last_alert_write_at || null,
|
|
last_funding_observation_write_at: historyWriterState.last_funding_observation_write_at || null,
|
|
last_metrics_at: historyWriterState.last_metrics_at || null,
|
|
latest_portfolio_metrics: historyWriterState.latest_portfolio_metrics || null,
|
|
offsets: historyWriterState.offsets || {},
|
|
metrics_error: historyWriterState.metrics_error || null,
|
|
},
|
|
controls: listDashboardControls({ page: 'system' }),
|
|
};
|
|
}
|
|
|
|
function summarizeServiceSnapshot(snapshot, { authoritativeHealth = null, activeAlerts = [] } = {}) {
|
|
const state = snapshot.state || {};
|
|
const health = snapshot.health || {};
|
|
const derived = authoritativeHealth || deriveServiceHealth({
|
|
service: snapshot.service,
|
|
snapshot,
|
|
activeAlerts: activeAlerts.filter((alert) => alert.service_scope === snapshot.service),
|
|
});
|
|
const freshnessAt = derived.freshness_at || inferServiceFreshnessTimestamp(snapshot.service, state, health);
|
|
|
|
return {
|
|
service: snapshot.service,
|
|
label: snapshot.label,
|
|
base_url: snapshot.base_url,
|
|
reachable: snapshot.reachable,
|
|
health_ok: derived.health_ok,
|
|
health_status: derived.status,
|
|
health_reasons: derived.reasons || [],
|
|
highest_alert_severity: derived.highest_alert_severity || null,
|
|
paused: derived.paused ?? state.paused ?? health.paused ?? null,
|
|
armed: derived.armed ?? state.armed ?? null,
|
|
draining: state.draining ?? null,
|
|
freshness_at: freshnessAt,
|
|
freshness_age_ms: derived.freshness_age_ms ?? ageMs(freshnessAt),
|
|
last_error: state.last_error || health.last_error || null,
|
|
summary: buildServiceSummary(snapshot.service, state),
|
|
};
|
|
}
|
|
|
|
function buildServiceSummary(service, state) {
|
|
switch (service) {
|
|
case 'near-intents-ingest':
|
|
return {
|
|
connected: state.ingest?.connected ?? null,
|
|
reconnect_count: state.ingest?.reconnect_count ?? null,
|
|
pair_filter: state.pair_filter?.pair_filter || null,
|
|
last_message_at: state.ingest?.last_message_at || null,
|
|
last_matching_quote_at: state.ingest?.last_matching_quote_at || null,
|
|
last_published_at: state.ingest?.last_published_at || null,
|
|
};
|
|
case 'market-reference-ingest':
|
|
return {
|
|
last_published_at: state.last_published_at || null,
|
|
source_used: state.kraken?.healthy ? 'kraken' : state.coingecko?.healthy ? 'coingecko' : null,
|
|
};
|
|
case 'inventory-sync':
|
|
return {
|
|
last_sync_at: state.last_sync_at || null,
|
|
reconciliation_status: state.last_snapshot?.reconciliation_status || null,
|
|
};
|
|
case 'liquidity-manager':
|
|
return {
|
|
last_refresh_at: state.last_refresh_at || null,
|
|
funding_observer_paused: state.funding_observer_paused ?? null,
|
|
withdrawals_frozen: state.withdrawals_frozen ?? null,
|
|
};
|
|
case 'history-writer':
|
|
return {
|
|
last_write_at: state.last_write_at || null,
|
|
last_alert_write_at: state.last_alert_write_at || null,
|
|
database_connectivity: state.database_connectivity ?? null,
|
|
};
|
|
case 'ops-sentinel':
|
|
return {
|
|
last_event_at: state.last_event_at || null,
|
|
last_runtime_eval_at: state.last_runtime_eval_at || null,
|
|
active_alert_count: state.active_alerts?.length || 0,
|
|
stale: state.stale ?? null,
|
|
};
|
|
case 'strategy-engine':
|
|
return {
|
|
latest_decision_id: state.latest_decision?.decision_id || null,
|
|
threshold_pct: state.threshold_pct ?? null,
|
|
max_notional_eure: state.max_notional_eure ?? null,
|
|
};
|
|
case 'trade-executor':
|
|
return {
|
|
in_flight_count: state.in_flight_count ?? 0,
|
|
completed_count: state.completed_count ?? 0,
|
|
signer_registered: state.signer_registered ?? null,
|
|
relay_connected: state.relay?.connected ?? null,
|
|
relay_last_message_at: state.relay?.last_message_at || null,
|
|
};
|
|
case 'operator-dashboard':
|
|
return {
|
|
source_error_count: state.source_error_count ?? 0,
|
|
last_source_error_at: state.last_source_error_at || null,
|
|
last_bootstrap_at: state.last_bootstrap_at || null,
|
|
};
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function inferServiceFreshnessTimestamp(service, state, health) {
|
|
return inferRuntimeFreshnessTimestamp(service, state, health);
|
|
}
|
|
|
|
function normalizeTradeForUi({ config, trade }) {
|
|
const assetIn = config.assetRegistry.get(trade.asset_in);
|
|
const assetOut = config.assetRegistry.get(trade.asset_out);
|
|
|
|
return {
|
|
...trade,
|
|
asset_in_symbol: assetIn?.symbol || trade.asset_in,
|
|
asset_out_symbol: assetOut?.symbol || trade.asset_out,
|
|
amount_in_display: formatUnits(trade.amount_in || '0', assetIn?.decimals || 0),
|
|
amount_out_display: formatUnits(trade.amount_out || '0', assetOut?.decimals || 0),
|
|
};
|
|
}
|
|
|
|
function buildRecentDepositItems({ config, recentDepositStatuses, liquidityState }) {
|
|
const recentItems = (recentDepositStatuses || []).map((entry) => normalizeDepositStatusForUi({
|
|
config,
|
|
depositStatus: entry,
|
|
}));
|
|
const itemsByKey = new Map(
|
|
recentItems.map((item) => [buildFundingActivityKey(item), item]),
|
|
);
|
|
|
|
for (const deposit of Object.values(liquidityState?.deposits || {})) {
|
|
const fallbackItem = normalizeLiquidityDepositForUi({
|
|
config,
|
|
deposit,
|
|
observedAt: liquidityState?.last_refresh_at || null,
|
|
});
|
|
const key = buildFundingActivityKey(fallbackItem);
|
|
if (!itemsByKey.has(key)) {
|
|
itemsByKey.set(key, fallbackItem);
|
|
}
|
|
}
|
|
|
|
return [...itemsByKey.values()];
|
|
}
|
|
|
|
function mergeFundingActivityItems({ observerItems, depositItems }) {
|
|
const merged = new Map();
|
|
|
|
for (const item of depositItems || []) {
|
|
merged.set(buildFundingActivityKey(item), item);
|
|
}
|
|
for (const item of observerItems || []) {
|
|
merged.set(buildFundingActivityKey(item), item);
|
|
}
|
|
|
|
return [...merged.values()];
|
|
}
|
|
|
|
function latestFundingActivityAt(items, fallback = null) {
|
|
let latest = fallback;
|
|
for (const item of items || []) {
|
|
if (sortTimestamps(fundingActivityTimestamp(item), latest) > 0) {
|
|
latest = fundingActivityTimestamp(item);
|
|
}
|
|
}
|
|
return latest;
|
|
}
|
|
|
|
function fundingActivityTimestamp(item) {
|
|
return item?.last_seen_at || item?.credited_at || item?.first_seen_at || null;
|
|
}
|
|
|
|
function buildFundingActivityKey(item) {
|
|
return [
|
|
item?.tx_hash || 'no-tx',
|
|
item?.chain || 'no-chain',
|
|
item?.asset_id || 'no-asset',
|
|
item?.funding_handle || 'no-handle',
|
|
item?.amount_units || 'no-amount',
|
|
].join('|');
|
|
}
|
|
|
|
function normalizeFundingObservationForUi({ config, observation }) {
|
|
const asset = config.assetRegistry.get(observation.asset_id);
|
|
return {
|
|
funding_observation_id: observation.funding_observation_id,
|
|
asset_id: observation.asset_id,
|
|
symbol: asset?.symbol || observation.asset_id,
|
|
chain: observation.chain,
|
|
funding_handle: observation.funding_handle,
|
|
tx_hash: observation.tx_hash,
|
|
status: observation.status,
|
|
amount_units: observation.amount,
|
|
amount: formatUnits(observation.amount || '0', asset?.decimals || 0),
|
|
confirmations: observation.confirmations,
|
|
first_seen_at: observation.first_seen_at,
|
|
last_seen_at: observation.last_seen_at,
|
|
credited_at: observation.credited_at || null,
|
|
};
|
|
}
|
|
|
|
function normalizeDepositStatusForUi({ config, depositStatus }) {
|
|
const payload = depositStatus?.payload || {};
|
|
const details = payload.details || {};
|
|
return normalizeLiquidityDepositForUi({
|
|
config,
|
|
deposit: {
|
|
tx_hash: details.tx_hash || null,
|
|
chain: payload.chain || details.chain || null,
|
|
asset_id: payload.asset_id || details.asset_id || null,
|
|
amount: details.amount || '0',
|
|
address: details.address || details.deposit_address || null,
|
|
status: payload.status || details.status || null,
|
|
},
|
|
observedAt: depositStatus?.observed_at || depositStatus?.ingested_at || null,
|
|
});
|
|
}
|
|
|
|
function normalizeLiquidityDepositForUi({ config, deposit, observedAt }) {
|
|
const asset = config.assetRegistry.get(deposit?.asset_id);
|
|
const status = deposit?.status || null;
|
|
const timestamp = observedAt || null;
|
|
|
|
return {
|
|
funding_observation_id: null,
|
|
asset_id: deposit?.asset_id || null,
|
|
symbol: asset?.symbol || deposit?.asset_id || null,
|
|
chain: deposit?.chain || null,
|
|
funding_handle: deposit?.address || null,
|
|
tx_hash: deposit?.tx_hash || null,
|
|
status,
|
|
amount_units: String(deposit?.amount || '0'),
|
|
amount: formatUnits(deposit?.amount || '0', asset?.decimals || 0),
|
|
confirmations: null,
|
|
first_seen_at: timestamp,
|
|
last_seen_at: timestamp,
|
|
credited_at: CREDITED_FUNDING_STATUSES.has(String(status || '').toUpperCase()) ? timestamp : null,
|
|
};
|
|
}
|
|
|
|
function normalizeDecision(decision) {
|
|
if (!decision) return null;
|
|
return {
|
|
decision_id: decision.decision_id || null,
|
|
decision_at: decision.decision_at || null,
|
|
quote_id: decision.quote_id || null,
|
|
pair: decision.pair || null,
|
|
direction: decision.direction || null,
|
|
request_kind: decision.request_kind || null,
|
|
decision: decision.decision || null,
|
|
decision_reason: decision.decision_reason || null,
|
|
gross_edge_pct: decision.gross_edge_pct || null,
|
|
threshold_pct: decision.threshold_pct || null,
|
|
max_notional_eure: decision.max_notional_eure || null,
|
|
strategy_armed: decision.strategy_armed ?? null,
|
|
inventory_asset: decision.inventory_asset || null,
|
|
eure_notional: decision.eure_notional || null,
|
|
};
|
|
}
|
|
|
|
function normalizeAlertList(alerts) {
|
|
return (alerts || []).map(normalizeAlert).sort((left, right) => sortTimestamps(
|
|
right.raised_at || right.first_raised_at || right.cleared_at,
|
|
left.raised_at || left.first_raised_at || left.cleared_at,
|
|
));
|
|
}
|
|
|
|
function normalizeAlert(alert) {
|
|
return {
|
|
alert_code: alert.alert_code,
|
|
status: alert.status,
|
|
severity: alert.severity,
|
|
reason: alert.reason,
|
|
service_scope: alert.service_scope,
|
|
pair: alert.pair || null,
|
|
asset_id: alert.asset_id || null,
|
|
tx_hash: alert.tx_hash || null,
|
|
raised_at: alert.raised_at || null,
|
|
first_raised_at: alert.first_raised_at || null,
|
|
cleared_at: alert.cleared_at || null,
|
|
last_evaluated_at: alert.last_evaluated_at || null,
|
|
details: alert.details || {},
|
|
};
|
|
}
|
|
|
|
function appendUniqueRecentQuote(quotes, nextQuote, limit) {
|
|
const deduped = [nextQuote, ...quotes.filter((quote) => quote.quote_id !== nextQuote.quote_id)];
|
|
return deduped.slice(0, limit);
|
|
}
|
|
|
|
function normalizeLiveQuote(payload, event) {
|
|
if (!payload?.quote_id) return null;
|
|
return {
|
|
quote_id: payload.quote_id,
|
|
pair: payload.pair || `${payload.asset_in}->${payload.asset_out}`,
|
|
asset_in: payload.asset_in || null,
|
|
asset_out: payload.asset_out || null,
|
|
request_kind: payload.request_kind || null,
|
|
amount_in: payload.amount_in ?? null,
|
|
amount_out: payload.amount_out ?? null,
|
|
observed_at: event.observed_at || null,
|
|
ingested_at: event.ingested_at || null,
|
|
};
|
|
}
|
|
|
|
function buildAlertKey(alert) {
|
|
return [
|
|
alert.alert_code,
|
|
alert.service_scope,
|
|
alert.pair || '',
|
|
alert.asset_id || '',
|
|
alert.tx_hash || '',
|
|
].join('|');
|
|
}
|
|
|
|
function highestAlertSeverity(alerts) {
|
|
return (alerts || []).reduce((highest, alert) => {
|
|
const currentRank = ALERT_SEVERITY_ORDER[highest] || 0;
|
|
const nextRank = ALERT_SEVERITY_ORDER[alert.severity] || 0;
|
|
return nextRank > currentRank ? alert.severity : highest;
|
|
}, null);
|
|
}
|
|
|
|
function computeCurrentPortfolioValue({ inventory, marketPrice, btcAsset, eureAsset }) {
|
|
if (!inventory || !marketPrice || !btcAsset || !eureAsset) return null;
|
|
|
|
const btcUnits = String(inventory.spendable?.[btcAsset.assetId] || '0');
|
|
const eureUnits = String(inventory.spendable?.[eureAsset.assetId] || '0');
|
|
const btcScaled = unitsToScaledDecimal(btcUnits, btcAsset.decimals);
|
|
const eureScaled = unitsToScaledDecimal(eureUnits, eureAsset.decimals);
|
|
const priceScaled = parseScaledDecimal(marketPrice.eure_per_btc || marketPrice.eur_per_btc || '0');
|
|
const total = eureScaled + multiplyScaled(btcScaled, priceScaled);
|
|
|
|
return formatScaledDecimal(total);
|
|
}
|
|
|
|
function valueAssetInEur({ asset, units, marketPrice }) {
|
|
if (!asset) return null;
|
|
if (asset.symbol === 'EURe') {
|
|
return formatUnits(units || '0', asset.decimals);
|
|
}
|
|
if (!marketPrice || asset.symbol !== 'BTC') return null;
|
|
|
|
const scaledUnits = unitsToScaledDecimal(units || '0', asset.decimals);
|
|
const priceScaled = parseScaledDecimal(marketPrice.eure_per_btc || marketPrice.eur_per_btc || '0');
|
|
return formatScaledDecimal(multiplyScaled(scaledUnits, priceScaled));
|
|
}
|
|
|
|
function formatDecimalDifference(left, right) {
|
|
return formatScaledDecimal(parseScaledDecimal(left) - parseScaledDecimal(right));
|
|
}
|
|
|
|
function unitsToScaledDecimal(units, decimals) {
|
|
return BigInt(String(units || '0')) * 10n ** BigInt(DECIMAL_SCALE - decimals);
|
|
}
|
|
|
|
function parseScaledDecimal(value) {
|
|
const normalized = String(value ?? '0').trim();
|
|
const negative = normalized.startsWith('-');
|
|
const unsigned = normalized.replace(/^[+-]/, '');
|
|
const [wholePart, fractionalPart = ''] = unsigned.split('.');
|
|
const whole = BigInt(wholePart || '0');
|
|
const fractional = BigInt(
|
|
(fractionalPart.padEnd(DECIMAL_SCALE, '0')).slice(0, DECIMAL_SCALE) || '0',
|
|
);
|
|
const scaled = (whole * DECIMAL_FACTOR) + fractional;
|
|
return negative ? -scaled : scaled;
|
|
}
|
|
|
|
function multiplyScaled(left, right) {
|
|
return (left * right) / DECIMAL_FACTOR;
|
|
}
|
|
|
|
function formatScaledDecimal(value) {
|
|
const negative = value < 0n;
|
|
const absolute = negative ? -value : value;
|
|
const whole = absolute / DECIMAL_FACTOR;
|
|
const fractional = absolute % DECIMAL_FACTOR;
|
|
if (fractional === 0n) {
|
|
return `${negative ? '-' : ''}${whole}`;
|
|
}
|
|
const fractionalText = fractional
|
|
.toString()
|
|
.padStart(DECIMAL_SCALE, '0')
|
|
.replace(/0+$/, '');
|
|
return `${negative ? '-' : ''}${whole}.${fractionalText}`;
|
|
}
|
|
|
|
function formatUnits(units, decimals) {
|
|
const numeric = unitsToNumber(units, decimals);
|
|
if (!Number.isFinite(numeric)) return '0';
|
|
if (Math.abs(numeric) >= 1000) return numeric.toLocaleString('en-US', { maximumFractionDigits: 8 });
|
|
return numeric.toLocaleString('en-US', { maximumFractionDigits: 8 });
|
|
}
|
|
|
|
function ageMs(value) {
|
|
const parsed = Date.parse(value || '');
|
|
if (!Number.isFinite(parsed)) return null;
|
|
return Math.max(0, Date.now() - parsed);
|
|
}
|
|
|
|
function sortTimestamps(left, right) {
|
|
return timestampValue(left) - timestampValue(right);
|
|
}
|
|
|
|
function timestampValue(value) {
|
|
const parsed = Date.parse(value || '');
|
|
return Number.isFinite(parsed) ? parsed : -Infinity;
|
|
}
|