// instruction-form.tsx
import { useState } from 'react';
import { useSubmitInstruction, useTemplates } from '@keystoneos/react';
import { TemplateSelector } from './template-selector';
import { TradeReferenceInput } from './trade-reference-input';
import { PendingMatchResult } from './pending-match-result';
import { MatchedResult } from './matched-result';
// Client-side map of template slug to the leg types your UI should collect.
// The API template config does not enumerate leg types, so keep this list
// in your own frontend config.
const LEG_TYPES_BY_TEMPLATE: Record<string, string[]> = {
'cross_platform_dvp': ['asset_delivery', 'payment'],
'single_platform_dvp': ['asset_delivery', 'payment'],
};
const LEG_TYPE_LABELS: Record<string, string> = {
asset_delivery: 'Asset Delivery',
payment: 'Payment',
};
function deriveDirection(role: string, legType: string): 'deliver' | 'receive' {
if ((role === 'seller' && legType === 'asset_delivery') ||
(role === 'buyer' && legType === 'payment')) {
return 'deliver';
}
return 'receive';
}
interface LegState {
instrumentId: string;
quantity: string;
chainId: number;
}
interface FormData {
role: 'seller' | 'buyer';
partyReference: string;
partyName: string;
walletAddress: string;
templateSlug: string;
tradeReference: string;
legs: Record<string, LegState>;
}
function buildInitialLegs(legTypes: string[]): Record<string, LegState> {
return Object.fromEntries(
legTypes.map(lt => [lt, { instrumentId: '', quantity: '', chainId: 11155111 }])
);
}
export function InstructionForm() {
const defaultLegTypes = ['asset_delivery', 'payment'];
const [form, setForm] = useState<FormData>({
role: 'seller',
partyReference: '',
partyName: '',
walletAddress: '',
templateSlug: '',
tradeReference: '',
legs: buildInitialLegs(defaultLegTypes),
});
const { submit, result, isSubmitting, error, reset } = useSubmitInstruction();
const legTypes = LEG_TYPES_BY_TEMPLATE[form.templateSlug] ?? defaultLegTypes;
const handleTemplateChange = (slug: string) => {
const newLegTypes = LEG_TYPES_BY_TEMPLATE[slug] ?? defaultLegTypes;
setForm(prev => ({ ...prev, templateSlug: slug, legs: buildInitialLegs(newLegTypes) }));
};
const updateLeg = (legType: string, field: keyof LegState, value: string | number) => {
setForm(prev => ({
...prev,
legs: { ...prev.legs, [legType]: { ...prev.legs[legType], [field]: value } },
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await submit({
role: form.role,
party: {
external_reference: form.partyReference,
name: form.partyName,
wallet_address: form.walletAddress,
},
legs: legTypes.map(lt => ({
leg_type: lt,
instrument_id: form.legs[lt].instrumentId,
quantity: form.legs[lt].quantity,
direction: deriveDirection(form.role, lt),
chain_id: form.legs[lt].chainId,
})),
templateSlug: form.templateSlug,
timeoutAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
tradeReference: form.tradeReference || undefined,
});
};
const handleReset = () => {
reset();
setForm({
role: 'seller',
partyReference: '',
partyName: '',
walletAddress: '',
templateSlug: '',
tradeReference: '',
legs: buildInitialLegs(defaultLegTypes),
});
};
// Show result screens after submission
if (result?.status === 'pending_match') {
return <PendingMatchResult result={result} onReset={handleReset} />;
}
if (result?.status === 'matched') {
return <MatchedResult result={result} />;
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: '600px', display: 'flex', flexDirection: 'column', gap: '20px' }}>
<h2 style={{ fontSize: '20px', fontWeight: 700 }}>Submit Settlement Instruction</h2>
{/* Role selection */}
<div>
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 600, fontSize: '14px' }}>Role</label>
<div style={{ display: 'flex', gap: '12px' }}>
{(['seller', 'buyer'] as const).map(role => (
<label key={role} style={{ display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer' }}>
<input
type="radio"
name="role"
value={role}
checked={form.role === role}
onChange={() => setForm(prev => ({ ...prev, role }))}
/>
<span style={{ textTransform: 'capitalize' }}>{role}</span>
</label>
))}
</div>
</div>
{/* Party details */}
<fieldset style={{ border: '1px solid #e5e7eb', borderRadius: '8px', padding: '16px' }}>
<legend style={{ fontWeight: 600, fontSize: '14px', padding: '0 8px' }}>Party Details</legend>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>Account Reference</label>
<input
type="text"
value={form.partyReference}
onChange={e => setForm(prev => ({ ...prev, partyReference: e.target.value }))}
placeholder="e.g. KSFI-II-4401"
required
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db' }}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>Display Name</label>
<input
type="text"
value={form.partyName}
onChange={e => setForm(prev => ({ ...prev, partyName: e.target.value }))}
placeholder="e.g. Securitize Fund I"
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db' }}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>Wallet Address</label>
<input
type="text"
value={form.walletAddress}
onChange={e => setForm(prev => ({ ...prev, walletAddress: e.target.value }))}
placeholder="e.g. 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18"
required
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db', fontFamily: 'monospace', fontSize: '13px' }}
/>
</div>
</div>
</fieldset>
{/* Legs - template driven */}
<fieldset style={{ border: '1px solid #e5e7eb', borderRadius: '8px', padding: '16px' }}>
<legend style={{ fontWeight: 600, fontSize: '14px', padding: '0 8px' }}>
Settlement Legs
</legend>
<p style={{ fontSize: '12px', color: '#6b7280', marginBottom: '16px' }}>
Each leg has its own token address and chain. Direction is auto-derived from your role.
</p>
{legTypes.map((lt, index) => (
<div key={lt}>
{index > 0 && <hr style={{ border: 'none', borderTop: '1px dashed #e5e7eb', margin: '16px 0' }} />}
<div style={{ marginBottom: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{
padding: '2px 10px',
borderRadius: '9999px',
fontSize: '11px',
fontWeight: 600,
textTransform: 'uppercase',
background: lt === 'asset_delivery' ? 'rgba(22,163,74,0.1)' : 'rgba(37,99,235,0.1)',
color: lt === 'asset_delivery' ? '#16a34a' : '#2563eb',
border: lt === 'asset_delivery' ? '1px solid rgba(22,163,74,0.3)' : '1px solid rgba(37,99,235,0.3)',
}}>
{LEG_TYPE_LABELS[lt] ?? lt}
</span>
<span style={{ fontSize: '12px', color: '#6b7280', textTransform: 'capitalize' }}>
{deriveDirection(form.role, lt) === 'deliver'
? `${form.role} delivers to ${form.role === 'seller' ? 'buyer' : 'seller'}`
: `${form.role === 'seller' ? 'buyer' : 'seller'} delivers to ${form.role}`}
</span>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>Token Address</label>
<input
type="text"
value={form.legs[lt]?.instrumentId ?? ''}
onChange={e => updateLeg(lt, 'instrumentId', e.target.value)}
placeholder="0x..."
required
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db', fontFamily: 'monospace', fontSize: '13px' }}
/>
</div>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>Quantity</label>
<input
type="text"
value={form.legs[lt]?.quantity ?? ''}
onChange={e => updateLeg(lt, 'quantity', e.target.value)}
placeholder="e.g. 12000000"
required
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db', fontFamily: 'monospace' }}
/>
</div>
<div style={{ width: '160px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>Chain ID</label>
<select
value={form.legs[lt]?.chainId ?? 11155111}
onChange={e => updateLeg(lt, 'chainId', Number(e.target.value))}
style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db' }}
>
<option value={1}>Ethereum (1)</option>
<option value={137}>Polygon (137)</option>
<option value={43114}>Avalanche (43114)</option>
<option value={11155111}>Sepolia (11155111)</option>
</select>
</div>
</div>
</div>
</div>
))}
</fieldset>
{/* Template and trade reference */}
<TemplateSelector value={form.templateSlug || undefined} onChange={handleTemplateChange} />
<TradeReferenceInput value={form.tradeReference} onChange={val => setForm(prev => ({ ...prev, tradeReference: val }))} />
{/* Submit */}
{error && (
<div style={{ padding: '12px', backgroundColor: '#fee2e2', color: '#991b1b', borderRadius: '6px', fontSize: '14px' }}>
{error}
</div>
)}
<button
type="submit"
disabled={isSubmitting || !form.templateSlug}
style={{
padding: '12px 24px',
backgroundColor: isSubmitting ? '#9ca3af' : '#2DD4A8',
color: '#fff',
border: 'none',
borderRadius: '8px',
fontWeight: 600,
cursor: isSubmitting ? 'not-allowed' : 'pointer',
}}
>
{isSubmitting ? 'Submitting...' : 'Submit Instruction'}
</button>
</form>
);
}