Prerequisites
@keystoneos/reactinstalledKeystoneProviderconfigured withsettlements:readscope- A React project with TypeScript
What you will build
Four components that work together or independently:- Progress stepper - visual indicator of where a settlement is in its lifecycle
- Event timeline - chronological list of every state transition
- Deposit tracker - per-leg deposit status for settlements awaiting deposits
- Notification badge - compact count of settlements needing action
Progress stepper
The settlement state machine follows a linear path (with branches for failure). UsegetCurrentStepIndex() and SETTLEMENT_STEPS from @keystoneos/react to build a visual stepper.
// progress-stepper.tsx
import { useSettlement, getCurrentStepIndex, isTerminalState, SETTLEMENT_STEPS } from '@keystoneos/react';
interface ProgressStepperProps {
settlementId: string;
}
export function ProgressStepper({ settlementId }: ProgressStepperProps) {
const { settlement, isLoading } = useSettlement(settlementId);
if (isLoading || !settlement) return <div>Loading...</div>;
const currentIndex = getCurrentStepIndex(settlement.state);
const isTerminal = isTerminalState(settlement.state);
const isFailed = settlement.state === 'ROLLED_BACK' || settlement.state === 'TIMED_OUT';
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0' }}>
{SETTLEMENT_STEPS.map((step, index) => {
const isCompleted = index < currentIndex;
const isCurrent = index === currentIndex;
const isPending = index > currentIndex;
let dotColor = '#d1d5db'; // gray - pending
if (isCompleted) dotColor = '#16a34a'; // green - done
if (isCurrent && isFailed) dotColor = '#dc2626'; // red - failed
if (isCurrent && !isFailed && !isTerminal) dotColor = '#2563eb'; // blue - active
if (isCurrent && isTerminal && !isFailed) dotColor = '#16a34a'; // green - finalized
let lineColor = '#e5e7eb'; // gray
if (isCompleted) lineColor = '#16a34a'; // green
return (
<div key={step.state} style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
{/* Dot and connector line */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '24px' }}>
<div style={{
width: isCurrent ? '14px' : '10px',
height: isCurrent ? '14px' : '10px',
borderRadius: '50%',
backgroundColor: dotColor,
border: isCurrent ? `3px solid ${dotColor}33` : 'none',
flexShrink: 0,
}} />
{index < SETTLEMENT_STEPS.length - 1 && (
<div style={{ width: '2px', height: '32px', backgroundColor: lineColor }} />
)}
</div>
{/* Label and description */}
<div style={{ paddingBottom: '16px' }}>
<p style={{
fontSize: '14px',
fontWeight: isCurrent ? 700 : 500,
color: isPending ? '#9ca3af' : '#111827',
}}>
{step.label}
</p>
{isCurrent && !isTerminal && (
<p style={{ fontSize: '12px', color: '#6b7280', marginTop: '2px' }}>
{step.description}
</p>
)}
</div>
</div>
);
})}
{/* Terminal state banner */}
{isTerminal && (
<div style={{
marginTop: '8px',
padding: '12px 16px',
borderRadius: '8px',
backgroundColor: isFailed ? '#fee2e2' : '#dcfce7',
color: isFailed ? '#991b1b' : '#166534',
fontSize: '14px',
fontWeight: 600,
}}>
{settlement.state === 'FINALIZED' && 'Settlement complete. All assets have been transferred.'}
{settlement.state === 'ROLLED_BACK' && 'Settlement rolled back. All deposits have been returned.'}
{settlement.state === 'TIMED_OUT' && 'Settlement timed out. Deposits have been returned to original owners.'}
</div>
)}
</div>
);
}
SETTLEMENT_STEPS array contains the ordered steps:
| Index | State | Label | Description |
|---|---|---|---|
| 0 | INSTRUCTED | Instructed | Settlement created from matched instructions |
| 1 | COMPLIANCE_CHECKING | Compliance Check | Screening parties and wallets |
| 2 | COMPLIANCE_CLEARED | Compliance Cleared | All checks passed |
| 3 | AWAITING_DEPOSITS | Awaiting Deposits | Waiting for escrow deposits |
| 4 | SETTLED | Settled | Atomic swap executed on-chain |
| 5 | FINALIZED | Finalized | Settlement complete |
Event timeline
TheuseSettlementEvents hook returns every state transition that has occurred, in chronological order. This gives a full audit trail.
// event-timeline.tsx
import { useSettlementEvents, getStateInfo } from '@keystoneos/react';
interface EventTimelineProps {
settlementId: string;
}
const TRIGGER_LABELS: Record<string, string> = {
'system:engine': 'Settlement Engine',
'system:contract': 'Smart Contract',
'system:compliance': 'Compliance Check',
'm2m:platform': 'Platform API',
'session:user': 'User Action',
};
export function EventTimeline({ settlementId }: EventTimelineProps) {
const { events, isLoading, error } = useSettlementEvents(settlementId);
if (isLoading) return <div>Loading events...</div>;
if (error) return <div style={{ color: '#dc2626' }}>Error: {error}</div>;
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0' }}>
<h3 style={{ fontSize: '16px', fontWeight: 700, marginBottom: '16px' }}>Event Timeline</h3>
{events.length === 0 ? (
<p style={{ color: '#6b7280', fontSize: '14px' }}>No events yet.</p>
) : (
events.map((event, index) => {
const stateInfo = getStateInfo(event.to_state);
const isLast = index === events.length - 1;
return (
<div key={event.id} style={{ display: 'flex', gap: '12px' }}>
{/* Timeline connector */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '12px' }}>
<div style={{
width: '8px', height: '8px', borderRadius: '50%',
backgroundColor: isLast ? '#2DD4A8' : '#9ca3af',
flexShrink: 0, marginTop: '6px',
}} />
{!isLast && (
<div style={{ width: '1px', height: '100%', minHeight: '40px', backgroundColor: '#e5e7eb' }} />
)}
</div>
{/* Event content */}
<div style={{ paddingBottom: '20px', flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '14px', fontWeight: 600 }}>{stateInfo.label}</span>
{event.from_state && (
<span style={{ fontSize: '12px', color: '#9ca3af' }}>
from {getStateInfo(event.from_state).label}
</span>
)}
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '4px', fontSize: '12px', color: '#6b7280' }}>
<span>{new Date(event.created_at).toLocaleString()}</span>
<span>{TRIGGER_LABELS[event.triggered_by] ?? event.triggered_by}</span>
</div>
{event.evidence_hash && (
<p style={{ marginTop: '4px', fontSize: '11px', fontFamily: 'monospace', color: '#9ca3af' }}>
Evidence: {event.evidence_hash.slice(0, 16)}...
</p>
)}
</div>
</div>
);
})
)}
</div>
);
}
Deposit tracker
When a settlement is inAWAITING_DEPOSITS, each leg needs a deposit to escrow. This component shows which legs are deposited and which are still pending.
// deposit-tracker.tsx
import { useSettlement, useDeposit } from '@keystoneos/react';
interface DepositTrackerProps {
settlementId: string;
}
export function DepositTracker({ settlementId }: DepositTrackerProps) {
const { settlement, isLoading } = useSettlement(settlementId);
if (isLoading || !settlement) return <div>Loading...</div>;
if (settlement.state !== 'AWAITING_DEPOSITS') {
return null; // Only show during deposit phase
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<h3 style={{ fontSize: '16px', fontWeight: 700 }}>Deposit Status</h3>
{settlement.legs.map((leg, index) => (
<LegDepositRow
key={index}
settlementId={settlementId}
legIndex={index}
leg={leg}
/>
))}
</div>
);
}
interface LegDepositRowProps {
settlementId: string;
legIndex: number;
leg: {
instrument_id: string;
quantity: string;
leg_type: string;
deposited?: boolean;
deposit_tx_hash?: string;
};
}
function LegDepositRow({ settlementId, legIndex, leg }: LegDepositRowProps) {
const { status, txHash, error, deposit } = useDeposit(settlementId, legIndex);
const isDeposited = leg.deposited || status === 'deposit_confirmed';
const isProcessing = ['fetching_info', 'approving', 'approval_confirmed', 'depositing'].includes(status);
const displayTxHash = txHash ?? leg.deposit_tx_hash;
return (
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '12px 16px', borderRadius: '8px',
border: `1px solid ${isDeposited ? '#bbf7d0' : '#e5e7eb'}`,
backgroundColor: isDeposited ? '#f0fdf4' : '#fff',
}}>
<div>
<p style={{ fontSize: '14px', fontWeight: 600 }}>
{leg.leg_type === 'asset_delivery' ? 'Asset Leg' : 'Payment Leg'}
</p>
<p style={{ fontSize: '13px', color: '#6b7280', fontFamily: 'monospace' }}>
{Number(leg.quantity).toLocaleString()} {leg.instrument_id}
</p>
{displayTxHash && (
<p style={{ fontSize: '11px', color: '#9ca3af', fontFamily: 'monospace', marginTop: '2px' }}>
TX: {displayTxHash.slice(0, 10)}...{displayTxHash.slice(-8)}
</p>
)}
</div>
<div>
{isDeposited && (
<span style={{
padding: '4px 10px', borderRadius: '9999px', fontSize: '12px',
fontWeight: 600, backgroundColor: '#dcfce7', color: '#166534',
}}>
Deposited
</span>
)}
{!isDeposited && status === 'idle' && (
<button
onClick={deposit}
style={{
padding: '6px 14px', backgroundColor: '#2DD4A8', color: '#fff',
border: 'none', borderRadius: '6px', fontWeight: 600, cursor: 'pointer', fontSize: '13px',
}}
>
Deposit
</button>
)}
{isProcessing && (
<span style={{ fontSize: '13px', color: '#2563eb' }}>Processing...</span>
)}
{status === 'error' && (
<div style={{ textAlign: 'right' }}>
<p style={{ fontSize: '12px', color: '#dc2626' }}>{error}</p>
<button
onClick={deposit}
style={{
marginTop: '4px', padding: '4px 10px', backgroundColor: '#fee2e2',
color: '#991b1b', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px',
}}
>
Retry
</button>
</div>
)}
</div>
</div>
);
}
Notification badge
A compact badge showing how many settlements need attention (e.g., awaiting deposits). Useful in navigation bars or sidebars.// settlement-badge.tsx
import { useSettlements } from '@keystoneos/react';
interface SettlementBadgeProps {
state: string;
label: string;
}
export function SettlementBadge({ state, label }: SettlementBadgeProps) {
const { total, isLoading } = useSettlements({ state, limit: 1, offset: 0 });
if (isLoading || total === 0) return null;
return (
<div style={{ display: 'inline-flex', alignItems: 'center', gap: '6px' }}>
<span>{label}</span>
<span style={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
minWidth: '20px', height: '20px', borderRadius: '9999px',
backgroundColor: '#ef4444', color: '#fff',
fontSize: '11px', fontWeight: 700, padding: '0 6px',
}}>
{total}
</span>
</div>
);
}
// Usage in a navigation bar:
function Sidebar() {
return (
<nav>
<a href="/settlements">
<SettlementBadge state="AWAITING_DEPOSITS" label="Awaiting Deposits" />
</a>
<a href="/settlements?state=COMPLIANCE_CHECKING">
<SettlementBadge state="COMPLIANCE_CHECKING" label="Compliance Review" />
</a>
</nav>
);
}
Putting it all together
Here is a complete settlement detail page combining the stepper, timeline, and deposit tracker.// settlement-detail-page.tsx
import { useSettlement, isTerminalState } from '@keystoneos/react';
import { ProgressStepper } from './progress-stepper';
import { EventTimeline } from './event-timeline';
import { DepositTracker } from './deposit-tracker';
interface SettlementDetailPageProps {
settlementId: string;
}
export function SettlementDetailPage({ settlementId }: SettlementDetailPageProps) {
const { settlement, isLoading, error } = useSettlement(settlementId);
if (isLoading) return <div style={{ padding: '24px' }}>Loading settlement...</div>;
if (error) return <div style={{ padding: '24px', color: '#dc2626' }}>Error: {error}</div>;
if (!settlement) return <div style={{ padding: '24px' }}>Settlement not found.</div>;
const isTerminal = isTerminalState(settlement.state);
return (
<div style={{ padding: '24px', maxWidth: '900px' }}>
{/* Header */}
<div style={{ marginBottom: '24px' }}>
<h1 style={{ fontSize: '24px', fontWeight: 700 }}>
{settlement.external_reference ?? `Settlement ${settlement.id.slice(0, 8)}`}
</h1>
<p style={{ fontSize: '14px', color: '#6b7280', marginTop: '4px' }}>
Template: {settlement.template_slug} | Created: {new Date(settlement.created_at).toLocaleString()}
</p>
</div>
{/* Two-column layout */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '32px' }}>
{/* Left column: Progress + Deposits */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<ProgressStepper settlementId={settlementId} />
<DepositTracker settlementId={settlementId} />
{/* Parties summary */}
<div>
<h3 style={{ fontSize: '16px', fontWeight: 700, marginBottom: '8px' }}>Parties</h3>
{settlement.parties.map((party, i) => (
<div key={i} style={{ padding: '8px 0', borderBottom: '1px solid #f3f4f6' }}>
<p style={{ fontSize: '14px', fontWeight: 600 }}>
{party.name ?? party.external_reference}
<span style={{ fontWeight: 400, color: '#6b7280', marginLeft: '8px' }}>
({party.role})
</span>
</p>
<p style={{ fontSize: '12px', fontFamily: 'monospace', color: '#9ca3af' }}>
{party.wallet_address}
</p>
</div>
))}
</div>
</div>
{/* Right column: Event timeline */}
<EventTimeline settlementId={settlementId} />
</div>
</div>
);
}
Auto-refresh behavior
TheuseSettlement hook subscribes to real-time updates automatically:
- While the settlement is in a non-terminal state, it polls for updates
- When a state change is detected, the settlement object updates and all components re-render
- Once the settlement reaches a terminal state (
FINALIZED,ROLLED_BACK,TIMED_OUT), polling stops
isTerminalState(settlement.state) to conditionally render UI - for example, hiding the deposit tracker once the settlement finalizes.
import { isTerminalState } from '@keystoneos/react';
// Stop showing action buttons when settlement is done
if (isTerminalState(settlement.state)) {
return <FinalizedView settlement={settlement} />;
}
Next steps
Settlement Dashboard
Build a list view to monitor all settlements.
Custody Integration
Route deposits through Fireblocks or BitGo.
useSettlement Reference
Full API reference for the useSettlement hook.
State Machine
Understand settlement states and transitions.