unrip/src/operator-dashboard/static/pages/FundsPage.jsx
philipp f34f27065a
All checks were successful
deploy / deploy (push) Successful in 33s
Implement NEAR Intents request creation flow
Proof: Adds repo-owned EURe-to-BTC request preflight, signing, gated live submission, durable request/result/outcome persistence, dashboard request lifecycle rows, and tests proving submitted/relay accepted are not completed without inventory movement.

Assumptions: The NEAR Intents solver relay quote, publish_intent, and get_status JSON-RPC methods accept signed raw_ed25519 token_diff payloads with quote_hashes; live validation remains bounded to 5 EUR per attempt, at most five attempts, and 200 bps slippage.

Still fake: Venue-native terminal fill linkage and fee-complete realized PnL are still unavailable; request completion is attributed from durable inventory deltas unless the venue later exposes a linked settlement id.
2026-04-12 18:43:40 +02:00

717 lines
27 KiB
JavaScript

import { Fragment, useEffect, useState } from 'react';
import EmptyState from '../components/EmptyState.jsx';
import MetricCard from '../components/MetricCard.jsx';
import Pill from '../components/Pill.jsx';
import TableFrame from '../components/TableFrame.jsx';
import { formatEur, formatTimestamp, stringifyJson, truncateMiddle } from '../lib/format.js';
function buildInitialEstimateForm(balances, withdrawalDefaults) {
const firstAssetId = balances?.[0]?.asset_id || '';
return {
asset_id: firstAssetId,
amount: '',
destination_address:
withdrawalDefaults?.[firstAssetId]
|| Object.values(withdrawalDefaults || {})[0]
|| '',
chain: '',
};
}
function BalancesTable({ items }) {
if (!items?.length) return <EmptyState>No inventory snapshot is available yet.</EmptyState>;
return (
<TableFrame>
<table>
<thead>
<tr>
<th>Asset</th>
<th>Spendable</th>
<th>Pending inbound</th>
<th>Pending outbound</th>
<th>EUR value</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.asset_id}>
<td>
<strong>{item.symbol}</strong>
<div className="muted mono">{item.asset_id}</div>
</td>
<td className="mono">{item.spendable}</td>
<td className="mono">{item.pending_inbound}</td>
<td className="mono">{item.pending_outbound}</td>
<td className="mono">{formatEur(item.eur_value_eure)}</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function HandlesList({ handles }) {
if (!handles?.length) return <EmptyState>No funding handles are available.</EmptyState>;
return (
<div className="service-grid">
{handles.map((handle) => (
<div className="service-card" key={`${handle.chain}:${handle.address || handle.asset_id}`}>
<div className="service-head">
<strong>{handle.symbol}</strong>
<Pill label={handle.chain} stateLabel="healthy" />
</div>
<div className="service-detail">
<div className="mono">{handle.address || 'Unavailable'}</div>
{handle.memo ? <div className="mono">{`Memo ${handle.memo}`}</div> : null}
<div>{`Refreshed ${formatTimestamp(handle.refreshed_at)}`}</div>
</div>
</div>
))}
</div>
);
}
function FundingTable({ items, creditedOnly = false }) {
if (!items?.length) {
return (
<EmptyState>
{creditedOnly ? 'No credited deposits have been recorded yet.' : 'No funding observations are available.'}
</EmptyState>
);
}
return (
<TableFrame>
<table>
<thead>
<tr>
<th>Status</th>
<th>Asset</th>
<th>Amount</th>
<th>Handle</th>
<th>Observed</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.funding_observation_id || `${item.tx_hash}:${item.status}`}>
<td><Pill label={item.status} stateLabel={item.status} /></td>
<td>{item.asset_symbol || item.asset_id}</td>
<td className="mono">{item.amount_display || item.amount || 'Unavailable'}</td>
<td className="mono truncate-cell" title={item.funding_handle || ''}>{truncateMiddle(item.funding_handle || '', 36)}</td>
<td>{formatTimestamp(item.credited_at || item.last_seen_at || item.first_seen_at)}</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function PreCreditAssetTable({ items }) {
if (!items?.length) return <EmptyState>No pre-credit funding is currently tracked.</EmptyState>;
return (
<TableFrame>
<table>
<thead>
<tr>
<th>Asset</th>
<th>Pending amount</th>
<th>Observation count</th>
<th>Last seen</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.asset_id}>
<td>{item.asset_symbol || item.asset_id}</td>
<td className="mono">{item.amount_display || item.amount || 'Unavailable'}</td>
<td>{item.observation_count || 0}</td>
<td>{formatTimestamp(item.last_seen_at)}</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function WithdrawalsTable({ items }) {
if (!items?.length) return <EmptyState>No withdrawals are tracked yet.</EmptyState>;
return (
<TableFrame>
<table>
<thead>
<tr>
<th>Status</th>
<th>Asset</th>
<th>Amount</th>
<th>Destination</th>
<th>Observed</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.withdrawal_id || `${item.asset_id}:${item.requested_at}`}>
<td><Pill label={item.status} stateLabel={item.status} /></td>
<td>{item.asset_symbol || item.asset_id}</td>
<td className="mono">{item.amount_display || item.amount || 'Unavailable'}</td>
<td className="mono truncate-cell" title={item.destination_address || ''}>{truncateMiddle(item.destination_address || '', 36)}</td>
<td>{formatTimestamp(item.completed_at || item.requested_at)}</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function WithdrawalEstimateForm({ balances, withdrawalDefaults, onControl }) {
const [form, setForm] = useState(() => buildInitialEstimateForm(balances, withdrawalDefaults));
useEffect(() => {
setForm(buildInitialEstimateForm(balances, withdrawalDefaults));
}, [balances, withdrawalDefaults]);
async function handleSubmit(event) {
event.preventDefault();
const body = { ...form };
if (!body.chain) delete body.chain;
await onControl('liquidity-manager', 'withdrawal-estimate', body, { reload: false });
}
return (
<form onSubmit={handleSubmit}>
<div className="form-grid">
<div className="field">
<label htmlFor="estimate-asset">Asset</label>
<select
id="estimate-asset"
name="asset_id"
onChange={(event) => {
const assetId = event.target.value;
setForm((current) => ({
...current,
asset_id: assetId,
destination_address:
withdrawalDefaults?.[assetId]
|| current.destination_address
|| '',
}));
}}
value={form.asset_id}
>
{(balances || []).map((item) => (
<option key={item.asset_id} value={item.asset_id}>
{item.symbol}
</option>
))}
</select>
</div>
<div className="field">
<label htmlFor="estimate-amount">Amount units</label>
<input
id="estimate-amount"
name="amount"
onChange={(event) => setForm((current) => ({ ...current, amount: event.target.value }))}
placeholder="1000000"
required
value={form.amount}
/>
</div>
<div className="field">
<label htmlFor="estimate-destination">Destination</label>
<input
id="estimate-destination"
name="destination_address"
onChange={(event) => setForm((current) => ({ ...current, destination_address: event.target.value }))}
value={form.destination_address}
/>
</div>
<div className="field">
<label htmlFor="estimate-chain">Chain override</label>
<input
id="estimate-chain"
name="chain"
onChange={(event) => setForm((current) => ({ ...current, chain: event.target.value }))}
placeholder="Optional"
value={form.chain}
/>
</div>
</div>
<div className="button-row">
<button className="button" type="submit">Estimate Withdrawal</button>
</div>
</form>
);
}
async function copyIdentifier(value) {
if (!value || typeof navigator === 'undefined' || !navigator.clipboard?.writeText) return;
try {
await navigator.clipboard.writeText(value);
} catch {
// Full ids remain visible when clipboard access is unavailable.
}
}
function IdentifierLine({ label, value }) {
if (!value) return <div className="status-subtle">{`${label}: unavailable`}</div>;
return (
<div className="trace-row">
<span className="status-subtle">{`${label}:`}</span>
<span className="mono trace-id">{value}</span>
<button className="button secondary trace-copy-button" onClick={() => copyIdentifier(value)} type="button">
Copy
</button>
</div>
);
}
function IntentRequestForm({ summary, onControl }) {
const defaults = summary?.defaults || {};
const [form, setForm] = useState({
amount_eure: defaults.amount_eure || '5',
slippage_bps: String(defaults.slippage_bps ?? 200),
});
useEffect(() => {
setForm({
amount_eure: defaults.amount_eure || '5',
slippage_bps: String(defaults.slippage_bps ?? 200),
});
}, [defaults.amount_eure, defaults.slippage_bps]);
async function handlePreflight(event) {
event.preventDefault();
await onControl('trade-executor', 'intent-request-preflight', {
amount_eure: form.amount_eure,
slippage_bps: Number(form.slippage_bps),
});
}
return (
<form onSubmit={handlePreflight}>
<div className="form-grid">
<div className="field">
<label htmlFor="intent-request-amount">Spend EURe</label>
<input
id="intent-request-amount"
max={defaults.max_amount_eure || '5'}
min="0.01"
name="amount_eure"
onChange={(event) => setForm((current) => ({ ...current, amount_eure: event.target.value }))}
step="0.01"
type="number"
value={form.amount_eure}
/>
<div className="status-subtle">{`Max ${defaults.max_amount_eure || '5'} EURe per live test request`}</div>
</div>
<div className="field">
<label htmlFor="intent-request-slippage">Max slippage bps</label>
<input
id="intent-request-slippage"
max={defaults.max_slippage_bps ?? 200}
min="0"
name="slippage_bps"
onChange={(event) => setForm((current) => ({ ...current, slippage_bps: event.target.value }))}
step="1"
type="number"
value={form.slippage_bps}
/>
<div className="status-subtle">{`Max ${defaults.max_slippage_bps ?? 200} bps / 2%`}</div>
</div>
</div>
<div className="button-row">
<button className="button" type="submit">Preflight EURe to BTC</button>
<button
className="button secondary"
onClick={() => onControl('trade-executor', 'intent-request-refresh-outcomes')}
type="button"
>
Refresh Request Outcomes
</button>
</div>
<div className="panel-subtitle">Preflight asks solvers for a quote. It does not submit live funds.</div>
</form>
);
}
function IntentRequestLifecycle({ item }) {
return (
<div className="lifecycle-detail-panel">
<div className="lifecycle-stage-grid">
<div className="lifecycle-stage-card">
<div className="stage-title">1. Request preflight</div>
<div className="stage-meta">{formatTimestamp(item.created_at)}</div>
<div className="stage-status">{item.lifecycle?.preflight?.state || item.state}</div>
<div className="stage-body">
<div>{item.reason_text}</div>
<div className="status-subtle mono">{item.reason_code}</div>
</div>
</div>
<div className="lifecycle-stage-card">
<div className="stage-title">2. Solver quote</div>
<div className="stage-meta">{formatTimestamp(item.deadline_at)}</div>
<div className="stage-status">{`${item.solver_quote_count || 0} quote(s)`}</div>
<div className="stage-body">
<IdentifierLine label="Quote hash" value={item.quote_hash} />
<div className="status-subtle">{item.quoted_destination_amount ? `Quoted ${item.quoted_destination_amount} BTC` : 'No usable quote stored'}</div>
</div>
</div>
<div className="lifecycle-stage-card">
<div className="stage-title">3. Relay submission</div>
<div className="stage-meta">{formatTimestamp(item.submitted_at)}</div>
<div className="stage-status">{item.submission_status || 'Not submitted'}</div>
<div className="stage-body">
<IdentifierLine label="Submission" value={item.submission_id} />
<IdentifierLine label="Intent hash" value={item.intent_hash} />
<div className="status-subtle">Relay acceptance is not a completed trade.</div>
</div>
</div>
<div className="lifecycle-stage-card">
<div className="stage-title">4. Settlement truth</div>
<div className="stage-meta">{formatTimestamp(item.resolved_at)}</div>
<div className="stage-status">{item.outcome_status || item.state}</div>
<div className="stage-body">
<div>{item.settlement_summary?.text || 'No settled inventory delta is linked to this request.'}</div>
{item.settlement_summary?.caveat ? <div className="status-subtle">{item.settlement_summary.caveat}</div> : null}
</div>
</div>
</div>
<div className="trace-block">
<IdentifierLine label="Request" value={item.request_id} />
<IdentifierLine label="Idempotency" value={item.idempotency_key} />
<IdentifierLine label="Intent" value={item.intent_hash} />
<IdentifierLine label="Quote hash" value={item.quote_hash} />
</div>
<details className="raw-details">
<summary>Raw lifecycle evidence</summary>
<pre>{stringifyJson(item.lifecycle)}</pre>
</details>
</div>
);
}
function IntentRequestsTable({ items, executorArmed, onControl }) {
const [expanded, setExpanded] = useState(() => new Set());
if (!items?.length) return <EmptyState>No repo-created EURe-to-BTC requests are stored yet.</EmptyState>;
function toggle(rowKey) {
setExpanded((current) => {
const next = new Set(current);
if (next.has(rowKey)) next.delete(rowKey);
else next.add(rowKey);
return next;
});
}
return (
<TableFrame>
<table className="intent-requests-table">
<thead>
<tr>
<th>Time</th>
<th>Request id</th>
<th>Spend / min receive</th>
<th>Quote / intent</th>
<th>State</th>
<th>Reason</th>
<th>Settlement</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
const rowKey = item.request_id || item.idempotency_key || String(index);
const isExpanded = expanded.has(rowKey);
const canSubmit = item.live_submit_capable && executorArmed === true;
return (
<Fragment key={rowKey}>
<tr>
<td>{formatTimestamp(item.resolved_at || item.submitted_at || item.created_at)}</td>
<td><IdentifierLine label="Request" value={item.request_id} /></td>
<td>
<div className="mono">{`${item.source_amount} ${item.source_symbol || 'EURe'}`}</div>
<div className="status-subtle">{`Min ${item.min_destination_amount} ${item.destination_symbol || 'BTC'}`}</div>
<div className="status-subtle">{`${item.slippage_bps ?? 'n/a'} bps slippage`}</div>
</td>
<td>
<IdentifierLine label="Quote" value={item.quote_hash} />
<IdentifierLine label="Intent" value={item.intent_hash} />
</td>
<td><Pill label={item.state_label || item.state} stateLabel={item.state} /></td>
<td>
<div>{item.reason_text}</div>
<div className="status-subtle mono">{item.reason_code}</div>
</td>
<td>
<div>{item.settlement_summary?.text || 'No settled inventory delta linked'}</div>
<div className="status-subtle">{item.has_settlement_evidence ? 'Settlement evidence present' : 'Not completed'}</div>
</td>
<td>
<div className="button-row compact">
<button className="button secondary" onClick={() => toggle(rowKey)} type="button">
{isExpanded ? 'Hide lifecycle' : 'Show lifecycle'}
</button>
{item.live_submit_capable ? (
<button
className="button"
disabled={!canSubmit}
onClick={() => onControl('trade-executor', 'intent-request-submit', { request_id: item.request_id })}
type="button"
>
Submit live
</button>
) : null}
</div>
{item.live_submit_capable && executorArmed !== true ? (
<div className="status-subtle">Executor must be armed before live submit.</div>
) : null}
</td>
</tr>
{isExpanded ? (
<tr className="lifecycle-expanded-row">
<td colSpan={8}><IntentRequestLifecycle item={item} /></td>
</tr>
) : null}
</Fragment>
);
})}
</tbody>
</table>
</TableFrame>
);
}
function LastControlResult({ result }) {
if (!result) return null;
return (
<TableFrame style={{ marginTop: 14 }}>
<table>
<tbody>
<tr>
<td><strong>Last control result</strong></td>
<td className="mono"><pre>{stringifyJson(result)}</pre></td>
</tr>
</tbody>
</table>
</TableFrame>
);
}
export default function FundsPage({
funds,
onControl,
lastControlResult,
}) {
const profitability = funds.profitability;
const controlState = funds.funding.control_state || {};
const externalFlowAdjusted = profitability.external_flow_adjusted;
const externalFlowCount = profitability.external_flow_count || 0;
const baselineLabel = externalFlowAdjusted ? 'PnL vs net funded capital' : 'PnL vs deposit baseline';
const baselineMeta = externalFlowAdjusted
? `Adjusted for ${externalFlowCount} later deposit or withdrawal flows`
: 'Baseline anchored before first live trade';
const simpleHoldMeta = externalFlowAdjusted
? 'Simple hold includes later credited deposits and completed withdrawals'
: 'Comparison against a no-trade simple-hold baseline';
const marketMoveMeta = externalFlowAdjusted
? 'Simple-hold market move on baseline plus later external flows'
: 'Baseline mark move only';
const portfolioVsHoldMeta = externalFlowAdjusted
? 'Current minus cash-flow-adjusted simple hold; not realized trade PnL'
: 'Current minus simple hold; not realized trade PnL';
return (
<>
<section className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Default view</div>
<h2>Funds</h2>
<div className="panel-subtitle">
Profitability comes first. Spendable inventory remains verifier or bridge credit only,
while pre-credit funding stays separate.
</div>
</div>
<div className="pills">
<Pill
label={`Withdrawals ${controlState.withdrawals_frozen ? 'Frozen' : 'Unfrozen'}`}
stateLabel={controlState.withdrawals_frozen ? 'frozen' : 'healthy'}
/>
<Pill
label={`Funding observer ${controlState.funding_observer_paused ? 'Paused' : 'Running'}`}
stateLabel={controlState.funding_observer_paused ? 'paused' : 'healthy'}
/>
</div>
</div>
<div className="stack-grid">
<MetricCard
hero
label="Current EUR value"
meta={
profitability.computed_at
? `Latest durable snapshot ${formatTimestamp(profitability.computed_at)}`
: 'Latest durable profitability snapshot'
}
value={formatEur(profitability.current_total_portfolio_value_eure)}
/>
<MetricCard
label={baselineLabel}
meta={baselineMeta}
signedValue={profitability.pnl_vs_deposit_baseline_eure}
value={formatEur(profitability.pnl_vs_deposit_baseline_eure)}
/>
<MetricCard
label="PnL vs simple hold"
meta={simpleHoldMeta}
signedValue={profitability.pnl_vs_simple_hold_eure}
value={formatEur(profitability.pnl_vs_simple_hold_eure)}
/>
<MetricCard
label="Market move"
meta={marketMoveMeta}
signedValue={profitability.market_move_contribution_eure}
value={formatEur(profitability.market_move_contribution_eure)}
/>
<MetricCard
label="Portfolio vs simple hold"
meta={portfolioVsHoldMeta}
signedValue={profitability.portfolio_vs_simple_hold_eure}
value={formatEur(profitability.portfolio_vs_simple_hold_eure)}
/>
</div>
<div className="panel-subtitle">
{profitability.caveats.map((item) => (
<div key={item}>{item}</div>
))}
</div>
</section>
<section className="stack-grid">
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Current balances</div>
<h3>Spendable and pending</h3>
</div>
<div className="status-subtle">{`Inventory synced ${formatTimestamp(funds.balances.synced_at)}`}</div>
</div>
<BalancesTable items={funds.balances.items} />
</div>
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Operator controls</div>
<h3>Safe actions</h3>
</div>
</div>
<div className="button-row">
<button className="button secondary" onClick={() => onControl('market-reference-ingest', 'refresh')} type="button">Refresh Price</button>
<button className="button secondary" onClick={() => onControl('inventory-sync', 'refresh')} type="button">Refresh Inventory</button>
<button className="button secondary" onClick={() => onControl('liquidity-manager', 'refresh')} type="button">Refresh Liquidity</button>
<button
className="button secondary"
disabled={controlState.funding_observer_paused}
onClick={() => onControl('liquidity-manager', 'pause-funding-observer')}
type="button"
>
Pause Funding Observer
</button>
<button
className="button secondary"
disabled={!controlState.funding_observer_paused}
onClick={() => onControl('liquidity-manager', 'resume-funding-observer')}
type="button"
>
Resume Funding Observer
</button>
<button
className="button secondary"
disabled={controlState.withdrawals_frozen}
onClick={() => onControl('liquidity-manager', 'freeze-withdrawals', { frozen: true })}
type="button"
>
Freeze Withdrawals
</button>
<button
className="button secondary"
disabled={!controlState.withdrawals_frozen}
onClick={() => onControl('liquidity-manager', 'freeze-withdrawals', { frozen: false })}
type="button"
>
Unfreeze Withdrawals
</button>
</div>
<div className="panel-subtitle">
Risky treasury submit paths remain absent. Strategy and executor arm or disarm remain absent.
</div>
<WithdrawalEstimateForm
balances={funds.balances.items}
onControl={onControl}
withdrawalDefaults={controlState.withdrawal_defaults || {}}
/>
<LastControlResult result={lastControlResult} />
</div>
</section>
<section className="panel full-width-evidence-panel">
<div className="panel-head">
<div>
<div className="eyebrow">Own requests</div>
<h3>EURe to BTC request creation</h3>
<div className="panel-subtitle">Create a solver quote request first, then submit only a drafted request. Completed requires inventory movement, not relay acceptance.</div>
</div>
<div className="pills">
<Pill label={`Executor ${funds.intent_requests?.executor_armed ? 'Armed' : 'Disarmed'}`} stateLabel={funds.intent_requests?.executor_armed ? 'healthy' : 'warning'} />
</div>
</div>
<IntentRequestForm onControl={onControl} summary={funds.intent_requests} />
<div className="panel-subtitle">{funds.intent_requests?.caveat}</div>
<IntentRequestsTable
executorArmed={funds.intent_requests?.executor_armed}
items={funds.intent_requests?.items || []}
onControl={onControl}
/>
</section>
<section className="stack-grid">
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Funding handles</div>
<h3>Credited and pre-credit funding</h3>
</div>
</div>
<HandlesList handles={funds.funding.handles} />
<h3 style={{ marginTop: 18 }}>Credited deposits</h3>
<FundingTable creditedOnly items={funds.funding.credited_deposits} />
<h3 style={{ marginTop: 18 }}>Pre-credit by asset</h3>
<PreCreditAssetTable items={funds.funding.pre_credit_by_asset} />
</div>
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Transfer history</div>
<h3>Withdrawals and observations</h3>
</div>
</div>
<h3>Recent withdrawals</h3>
<WithdrawalsTable items={funds.recent_withdrawals} />
<h3 style={{ marginTop: 18 }}>Recent funding observations</h3>
<FundingTable items={funds.funding.recent_observations} />
</div>
</section>
</>
);
}