Not yet released. The packages on this page are not published to npm yet, so the install commands below will not resolve. The documented interface is stable and the code exists; only publication is outstanding. To use KeyStone from a frontend today, call the REST API or the TypeScript SDK, which is published. Contact us if you need early access.
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 confirmed instructions or direct initiation |
| 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 settlement 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) => (
<LegDepositRow
key={leg.leg_index}
settlementId={settlementId}
legIndex={leg.leg_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.