All checks were successful
deploy / deploy (push) Successful in 33s
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.
717 lines
27 KiB
JavaScript
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>
|
|
</>
|
|
);
|
|
}
|