Repository Public
1/**2 * « Négociation réelle » of the Accounts view: the venue accounts (one per network), their state as the venue reports it, the3 * dedicated trading key, how to fund the account (explained here; « Déposer » / « Retirer » live in LiveCollateral.tsx),4 * the authorization flow (limits → review: jurisdiction text, what is signed, key and expiry, limits → passkey) and revocation.5 * Mainnet is shown, and blocked with the reason, while the server setting keeps it off: that setting is the owner's, on the6 * server — there is no switch for it here.7 */8import { AgentOnAccount } from './LiveAgentNote'; // v7 perp live9import { startAuthentication } from '@simplewebauthn/browser';10import { useQuery, useQueryClient } from '@tanstack/react-query';11import { useContext, useEffect, useRef, useState } from 'react';12import { I18n, useT, type Key } from '../i18n';13import { Addr, Btn, Card, Field, H, Icon, inputCls, Notice, Skeleton, when } from '../ui';14import { Row, ServerSays, Sheet, Stat, timeOf } from './bits';15import { fmt, usd } from './dec';16import { Bullet, LiveErrNotice, ModeBand, ModeTag, SourceTag } from './LiveBits';17import { defaultPolicyForm, isUnfunded, lapi, liveError, policyFromForm, samePolicy, sha256Hex, toneOf, venueWords, type Collateral, type LiveConfig, type LiveErr, type LiveNetwork, type LiveSummary, type PolicyForm, type Prepared, type Revoked, type VenuePolicy } from './live';18import { CollateralPanel } from './LiveCollateral';19import { HaltBanner } from './LiveOrderPanel';20import { liveKey, refreshLive } from './liveStore';2122const sym = (id: string) => id.slice(id.indexOf(':') + 1);23const NETWORKS: LiveNetwork[] = ['testnet', 'mainnet'];24/** the server's English capability words, in the screen's language; unknown words are shown as the server wrote them */25const CAN: Record<string, Key> = { 'place orders': 'flCan_place', 'cancel orders': 'flCan_cancel', 'change leverage': 'flCan_leverage', withdraw: 'flCannot_withdraw', 'transfer to another address': 'flCannot_transfer' };2627/** how the venue account is funded: the explanation. The buttons (« Déposer », « Retirer ») are in CollateralPanel, LiveCollateral.tsx */28export function FundingBox({ network, collateral }: { network: LiveNetwork; collateral?: Collateral }) {29 const t = useT();30 return <div className="rounded-2xl border border-line p-3.5 text-[13px] leading-snug" data-testid="funding-box">31 <div className="mb-1.5 text-[13.5px] font-semibold">{t('flFundTitle')}</div>32 <ul className="space-y-1.5">33 {(network === 'mainnet' ? (['flFundMain1', 'flFundMain2', 'flFundMain3'] as const) : (['flFundTest1', 'flFundTest2'] as const)).map((k) => <Bullet key={k} icon="chev">{t(k)}</Bullet>)}34 <Bullet icon="key">{t('hlcFundBuilt')}</Bullet>35 </ul>36 {collateral && <p className="mt-2 break-words text-[11.5px] text-mute">{t('fxServerSays')} <span className="font-mono text-[11px]">{collateral.deposits.how}</span></p>}37 {collateral && <p className="mt-1 break-words text-[11.5px] text-mute">{t('flWithdrawalsServer')} <span className="font-mono text-[11px]">{collateral.withdrawals.how}</span></p>}38 </div>;39}4041function PolicyRows({ p }: { p: VenuePolicy }) {42 const t = useT(); const { locale } = useContext(I18n);43 return <div data-testid="policy-rows">44 <Row label={t('flLimInstruments')} strong>{p.allowedInstruments.map(sym).join(', ')}</Row>45 <Row label={t('flLim_maxLeverage')} strong>{p.maxLeverage}×</Row>46 <Row label={t('flLim_maxOrderNotional')} strong>{usd(p.maxOrderNotional, locale)}</Row>47 <Row label={t('flLim_maxPositionNotional')}>{usd(p.maxPositionNotional, locale)}</Row>48 <Row label={t('flLim_maxGrossExposure')}>{usd(p.maxGrossExposure, locale)}</Row>49 <Row label={t('flLim_maxOrdersPerDay')}>{p.maxOrdersPerDay}</Row>50 <Row label={t('flLim_bandBps')}>{p.maxPriceDeviationBps} {t('fxBps')} ({fmt(String(p.maxPriceDeviationBps / 100), { locale, trim: true })} %)</Row>51 </div>;52}5354type Step = 'form' | 'preparing' | 'review' | 'passkey' | 'sending' | 'refused';55function AuthorizeSheet({ account, config, collateral, onClose }: { account: LiveSummary; config: LiveConfig; collateral?: Collateral; onClose: () => void }) {56 const t = useT(); const { locale } = useContext(I18n); const qc = useQueryClient();57 const pinned = config.pinnedAssets[account.network], b = config.bounds, tone = toneOf(account);58 const [f, setF] = useState<PolicyForm>(() => defaultPolicyForm(account.network, pinned));59 const [step, setStep] = useState<Step>('form'), [prep, setPrep] = useState<Prepared | null>(null), [read, setRead] = useState(false), [err, setErr] = useState<LiveErr | null>(null), [integrity, setIntegrity] = useState<string | null>(null);60 const lock = useRef(false);61 const once = async (fn: () => Promise<void>) => { if (lock.current) return; lock.current = true; try { await fn(); } finally { lock.current = false; } };62 const parsed = policyFromForm(f, b, pinned);63 const busy = step === 'preparing' || step === 'passkey' || step === 'sending';64 const field = (k: Exclude<keyof PolicyForm, 'instruments'>, unit: string, hintVars: Record<string, string | number> = {}) => <Field label={t(`flLim_${k}` as Key)} hint={t(`flLimHint_${k}` as Key, hintVars)}>65 <div className="flex items-center gap-2"><input className={inputCls} inputMode="decimal" autoComplete="off" aria-label={t(`flLim_${k}` as Key)} value={f[k]} onChange={(e) => { setF({ ...f, [k]: e.target.value.replace(/[^0-9.,]/g, '') }); setErr(null); }} /><span className="w-10 shrink-0 text-[12.5px] text-mute">{unit}</span></div></Field>;6667 const prepare = () => once(async () => {68 if (!parsed.ok) return;69 setStep('preparing'); setErr(null); setRead(false); setIntegrity(null);70 try {71 const p = await lapi.prepare(account.id, { policy: parsed.policy, days: parsed.days, locale });72 // what is displayed must be what the passkey commits to: the text's own hash, the hash inside the authorization, the limits asked for73 const textHash = await sha256Hex(p.acknowledgement.text);74 const bad = textHash !== p.acknowledgement.sha256 || p.authorization.ackSha256 !== p.acknowledgement.sha256 ? 'ack' : !samePolicy(p.authorization.policy, parsed.policy) ? 'policy'75 : p.authorization.agentAddress.toLowerCase() !== account.agentAddress.toLowerCase() || p.authorization.account.toLowerCase() !== account.account.toLowerCase() || p.authorization.network !== account.network ? 'account' : null;76 setIntegrity(bad); setPrep(p); setStep('review');77 } catch (e) { setErr(liveError(e)); setStep('form'); }78 });79 const approve = () => once(async () => {80 if (!prep || !read || integrity) return;81 setStep('passkey'); setErr(null);82 let assertion: unknown;83 try { assertion = await startAuthentication({ optionsJSON: prep.options }); }84 catch (e) { setErr(liveError(e)); setStep('review'); return; } // nothing was sent: the prepared authorization is still the one on screen85 setStep('sending');86 try { await lapi.approveAuthorization(account.id, assertion); await refreshLive(qc, account.id); onClose(); }87 catch (e) { const le = liveError(e); setErr(le); setStep(le.status === 0 ? 'review' : 'refused'); void refreshLive(qc, account.id); } // an answered refusal consumed the prepared authorization88 });89 const unfunded = err?.code === 'venue_rejected' && isUnfunded(err.detail);9091 return <Sheet wide title={t('flAuthTitle', { network: account.network })} onClose={() => { if (!busy) onClose(); }}>92 <div className="space-y-3" data-testid="authorize-sheet" data-step={step}>93 <ModeBand tone={tone} label={account.label} />94 {(step === 'form' || step === 'preparing') && <>95 <p className="text-[13px] leading-snug text-mute">{t('flAuthIntro')}</p>96 <div data-testid="auth-instruments"><div className="mb-1.5 text-[13px] text-mute">{t('flLimInstruments')}</div>97 <div className="flex flex-wrap gap-2">{pinned.map((p) => { const on = f.instruments.includes(p.instrument); return <button key={p.instrument} type="button" role="checkbox" aria-checked={on} onClick={() => setF({ ...f, instruments: on ? f.instruments.filter((x) => x !== p.instrument) : [...f.instruments, p.instrument] })}98 className={`inline-flex min-h-11 items-center gap-2 rounded-full border px-4 text-[14px] font-medium ${on ? 'border-primary bg-primary text-onprimary' : 'border-line'}`}>{sym(p.instrument)}<span className={`text-[11.5px] ${on ? 'opacity-80' : 'text-mute'}`}>≤ {p.maxLeverage}×</span></button>; })}</div>99 <p className="mt-1.5 text-[12px] leading-snug text-mute">{t('flLimHint_instruments', { date: config.pinnedAssets.pinnedAt })}</p></div>100 <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">101 {field('maxLeverage', '×', { max: b.maxLeverage })}{field('maxOrderNotional', 'USDC')}{field('maxPositionNotional', 'USDC')}{field('maxGrossExposure', 'USDC')}102 {field('maxOrdersPerDay', t('fxPerDay'), { max: b.maxOrdersPerDay })}{field('bandBps', t('fxBps'), { min: b.minPriceBandBps, max: b.maxPriceBandBps })}{field('days', t('flDaysUnit'), { max: b.maxAuthorizationDays })}103 </div>104 {account.network === 'testnet' && <p className="text-[12px] leading-snug text-mute">{t('flBandTestnetNote')}</p>}105 {!parsed.ok && <Notice tone="warn"><div data-testid="policy-problem">{t(`flPol_${parsed.problem}` as Key, { maxLev: b.maxLeverage, maxOrders: b.maxOrdersPerDay, minBand: b.minPriceBandBps, maxBand: b.maxPriceBandBps, maxDays: b.maxAuthorizationDays, maxInst: b.maxInstruments })}</div></Notice>}106 {err && <LiveErrNotice err={err} />}107 <Btn full busy={step === 'preparing'} disabled={!parsed.ok} onClick={() => void prepare()}>{t('flAuthContinue')}</Btn>108 </>}109110 {prep && (step === 'review' || step === 'passkey' || step === 'sending') && <>111 <section aria-label={t('flAckTitle')}><h3 className="mb-1.5 text-[14px] font-semibold">1. {t('flAckTitle')}</h3>112 <div tabIndex={0} className="max-h-[38dvh] overflow-y-auto whitespace-pre-wrap rounded-2xl border border-line bg-elev/50 p-3.5 text-[13px] leading-relaxed" data-testid="ack-text">{prep.acknowledgement.text}</div>113 <p className="mt-1 break-all font-mono text-[10.5px] text-mute">SHA-256 {prep.acknowledgement.sha256} · {prep.acknowledgement.version}</p>114 <label className="mt-2 flex min-h-11 cursor-pointer items-center gap-2.5 text-[14px] font-medium"><input type="checkbox" checked={read} disabled={busy} onChange={(e) => setRead(e.target.checked)} className="h-5 w-5 shrink-0 accent-[var(--blue)]" data-testid="ack-check" /><span>{t('flAckCheck')}</span></label>115 <p className="text-[12px] leading-snug text-mute">{t('flAckSigned')}</p></section>116 <section><h3 className="mb-1.5 text-[14px] font-semibold">2. {t('flSignedTitle')}</h3>117 <ul className="space-y-2 text-[13px] leading-snug" data-testid="what-is-signed">118 <Bullet icon="key">{t('flSignedMain')}<span className="mt-0.5 block break-words font-mono text-[11px] text-mute">{prep.whatIsSigned.byMainKey}</span></Bullet>119 <Bullet icon="shield">{t('flSignedPasskey')}<span className="mt-0.5 block break-words font-mono text-[11px] text-mute">{prep.whatIsSigned.byPasskey}</span></Bullet></ul></section>120 <section><h3 className="mb-1.5 text-[14px] font-semibold">3. {t('flKeyTitle')}</h3>121 <div className="rounded-2xl bg-elev/70 p-3"><Addr value={prep.agent.address} />122 <div className="mt-1 text-[12.5px] text-mute">{t('flKeyExpiry', { date: when(new Date(prep.agent.validUntil), locale) })} · {prep.agent.wireName}</div></div>123 <div className="mt-2 grid grid-cols-1 gap-2 text-[13px] leading-snug sm:grid-cols-2" data-testid="agent-can">124 <div><div className="mb-1 text-[12px] font-semibold text-emerald">{t('flCan')}</div><ul className="space-y-1">{prep.agent.can.map((c) => <Bullet key={c} icon="check" tone="text-emerald">{CAN[c] ? t(CAN[c]!) : c}</Bullet>)}</ul></div>125 <div><div className="mb-1 text-[12px] font-semibold text-danger">{t('flCannot')}</div><ul className="space-y-1">{prep.agent.cannot.map((c) => <Bullet key={c} icon="x" tone="text-danger">{CAN[c] ? t(CAN[c]!) : c}</Bullet>)}</ul></div></div>126 <p className="mt-1.5 text-[12px] leading-snug text-mute">{t('flKeyNote')}<span className="mt-0.5 block break-words font-mono text-[11px]">{prep.agent.note}</span></p></section>127 <section><h3 className="mb-1.5 text-[14px] font-semibold">4. {t('flLimitsTitle')}</h3>128 <div className="rounded-2xl border border-line p-3"><PolicyRows p={prep.authorization.policy} /><Row label={t('flAuthExpires')}>{when(new Date(prep.authorization.expiresAt * 1000), locale)}</Row></div>129 <p className="mt-1.5 text-[12px] leading-snug text-mute">{t('flLimitsWho')}</p>130 <p className="mt-1 break-all font-mono text-[10.5px] text-mute">{t('fingerprint')} SHA-256 {prep.hash}</p></section>131 {integrity && <Notice tone="danger"><div data-testid="integrity">{t(`flIntegrity_${integrity}` as Key)}</div></Notice>}132 {step === 'passkey' && <Notice tone="info">{t('flPasskeyWaiting')}</Notice>}133 {err && <LiveErrNotice err={err} tone={err.key === 'passkey_cancelled' ? 'warn' : 'danger'} />}134 <Btn full kind={tone === 'mainnet' ? 'danger' : 'primary'} busy={step !== 'review'} disabled={!read || !!integrity} onClick={() => void approve()} data-testid="auth-approve"><Icon n="key" className="h-4 w-4" />{t(tone === 'mainnet' ? 'flAuthApproveReal' : 'flAuthApproveTestnet')}</Btn>135 {!read && <p className="text-center text-[12px] text-mute">{t('flAckNeeded')}</p>}136 <Btn full kind="ghost" disabled={busy} onClick={() => { setStep('form'); setErr(null); }}>{t('flBackToLimits')}</Btn>137 </>}138139 {step === 'refused' && err && <div className="space-y-3" data-testid="auth-refused">140 {err.code === 'venue_rejected' ? <Notice tone="danger"><b className="font-semibold">{t('flVenueRefusedTitle')}</b> {t('flVenueRefused')}141 <span className="mt-2 block text-[12px] text-mute">{t('flVenueSays')}</span>142 <span className="block break-words rounded-lg bg-surface/70 px-2 py-1.5 font-mono text-[12px] text-ink" data-testid="venue-words">{venueWords(err.detail)}</span></Notice> : <LiveErrNotice err={err} />}143 {unfunded && <><Notice tone="info">{t('flUnfundedNote')}</Notice><FundingBox network={account.network} collateral={collateral} /></>}144 <Btn full kind="soft" onClick={() => { setStep('form'); setErr(null); setPrep(null); }}>{t('flAuthPrepareAgain')}</Btn>145 <Btn full kind="ghost" onClick={onClose}>{t('fxClose')}</Btn></div>}146 </div>147 </Sheet>;148}149150function RevokeSheet({ account, onClose }: { account: LiveSummary; onClose: () => void }) {151 const t = useT(); const qc = useQueryClient(); const { locale } = useContext(I18n);152 // how many orders rest on the venue right now (the venue's own list); unknown stays unknown153 const d = useQuery({ queryKey: liveKey(account.id), queryFn: () => lapi.account(account.id), retry: 0 });154 const openOrders = d.data && !d.data.venueState.error ? d.data.venueState.openOrders.length : null;155 const [cancelFirst, setCancelFirst] = useState(true), [busy, setBusy] = useState(false), [err, setErr] = useState<LiveErr | null>(null), [done, setDone] = useState<Revoked | null>(null);156 const lock = useRef(false);157 const revoke = async () => {158 if (lock.current) return; lock.current = true; setBusy(true); setErr(null);159 try { setDone(await lapi.revoke(account.id, cancelFirst, locale)); } catch (e) { setErr(liveError(e)); } finally { lock.current = false; setBusy(false); void refreshLive(qc, account.id); }160 };161 return <Sheet title={t('flRevokeTitle')} onClose={() => { if (!busy) onClose(); }}><div className="space-y-3" data-testid="revoke-sheet">162 <ModeBand tone={toneOf(account)} label={account.label} />163 {!done && <>164 <Notice tone="warn">{t('flRevokeBody')}</Notice>165 <label className="flex min-h-11 cursor-pointer items-start gap-2.5 text-[13.5px]"><input type="checkbox" checked={cancelFirst} onChange={(e) => setCancelFirst(e.target.checked)} className="mt-0.5 h-5 w-5 shrink-0 accent-[var(--blue)]" data-testid="revoke-cancel-first" />166 <span>{t('flRevokeCancelFirst')}{openOrders !== null && <span className="block text-[12px] text-mute">{t('flRevokeOpenNow', { n: openOrders })}</span>}</span></label>167 {!cancelFirst && (openOrders ?? 0) > 0 && <Notice tone="danger">{t('flRevokeLeaves', { n: openOrders ?? 0 })}</Notice>}168 {err && <LiveErrNotice err={err} />}169 <Btn full kind="danger" busy={busy} onClick={() => void revoke()}><Icon n="stop" className="h-4 w-4" />{t('flRevokeConfirm')}</Btn>170 <Btn full kind="ghost" disabled={busy} onClick={onClose}>{t('cancel')}</Btn></>}171 {done && <div className="space-y-3" data-testid="revoke-done">172 <Notice tone="ok">{t('flRevoked')}</Notice>173 {done.cancelled && <p className="text-[13px] leading-snug">{t('flCancelAllDone', { requested: done.cancelled.requested, accepted: done.cancelled.accepted, remaining: done.cancelled.remaining === null ? '?' : done.cancelled.remaining.length })}</p>}174 {done.warning && <Notice tone="danger"><div data-testid="revoke-warning"><b className="font-semibold">{t('flRevokeWarnTitle')}</b> {t('flRevokeWarn')}<span className="mt-1 block break-words font-mono text-[11.5px] text-mute">{done.warning}</span></div></Notice>}175 <p className="text-[12.5px] leading-snug text-mute">{t('flRevokeAgent')}<span className="mt-0.5 block break-words font-mono text-[11px]">{done.venueAgent}</span></p>176 <Btn full kind="soft" onClick={onClose}>{t('fxClose')}</Btn></div>}177 </div></Sheet>;178}179180function LiveAccountCard({ a, config, selected, onSelect }: { a: LiveSummary; config: LiveConfig; selected: boolean; onSelect: (id: string) => void }) {181 const t = useT(); const { locale } = useContext(I18n);182 const tone = toneOf(a), off = a.network === 'mainnet' && !a.mainnetEnabled, auth = a.authorization, live = a.mode === 'MANUAL_LIVE';183 const col = useQuery({ queryKey: ['deriv', 'live', 'collateral', a.id], queryFn: () => lapi.collateral(a.id, locale), refetchInterval: 30_000, retry: 0 });184 const [sheet, setSheet] = useState<'authorize' | 'revoke' | null>(null);185 const c = col.data;186 return <li className={`rounded-2xl border p-4 ${selected ? 'border-ink' : 'border-line'}`} data-testid={`live-account-${a.network}`} data-mode={a.mode}>187 <ModeBand tone={tone} className="mb-3" />188 <div className="flex flex-wrap items-center gap-2"><span className="text-[15.5px] font-semibold">{a.label}</span>189 <span className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11.5px] font-semibold ${live && !auth?.expired ? 'bg-emerald/10 text-emerald' : 'bg-elev text-mute'}`} data-testid="live-mode"><Icon n={live ? 'key' : 'lock'} className="h-3 w-3" />{t(live ? (auth?.expired ? 'flModeExpired' : 'flModeLive') : 'flModeReadOnly')}</span>190 {selected && <span className="inline-flex items-center gap-1 text-[12px] font-semibold text-ink"><Icon n="check" className="h-3.5 w-3.5" />{t('fxSelected')}</span>}</div>191192 {off && <div className="mt-3"><Notice tone="warn"><div data-testid="mainnet-off"><b className="font-semibold">{t('flMainnetOffTitle')}</b> {t('flMainnetOff')}</div></Notice></div>}193 <div className="mt-3" data-testid="venue-state">194 {col.isLoading ? <Skeleton h="h-12" /> : !c || c.error || col.isError ? <Notice tone="danger">{t('flUnreadable')} <span className="break-words font-mono text-[11.5px] text-mute">{c?.error ?? (col.error ? liveError(col.error).detail || liveError(col.error).code : '')}</span></Notice> : <>195 <div className="grid grid-cols-2 gap-3 sm:grid-cols-4"><Stat label={t('fxEquity')}>{usd(c.equity, locale)}</Stat><Stat label={t('fxCollateral')}>{usd(c.collateral, locale)}</Stat><Stat label={t('fxFreeCollateral')}>{usd(c.freeCollateral, locale)}</Stat><Stat label={t('fxWithdrawable')}>{usd(c.withdrawable, locale)}</Stat></div>196 <p className="mt-1.5 flex items-center gap-1.5 text-[11.5px] text-mute"><SourceTag />{t('flReadAt', { time: timeOf(c.ts, locale) })} · {c.asset}</p></>}197 </div>198 <HaltBanner account={a} />199 {/* v7 perp live: an account an agent trades says so */}200 {a.mandate && a.mandate.phase !== 'ended' && <div className="mt-3"><AgentOnAccount account={a} /></div>}201202 <div className="mt-3 grid grid-cols-1 gap-3 lg:grid-cols-2">203 <div className="rounded-2xl bg-elev/70 p-3"><div className="mb-1 text-[12px] text-mute">{t('flAccountAddr')}</div><Addr value={a.account} /><p className="mt-1 text-[12px] leading-snug text-mute">{t('flAccountAddrNote')}</p></div>204 <div className="rounded-2xl bg-elev/70 p-3" data-testid="trading-key"><div className="mb-1 text-[12px] text-mute">{t('flTradingKey')}</div><Addr value={a.agentAddress} />205 <p className="mt-1 text-[12px] leading-snug text-mute">{t('flTradingKeyNote')}</p></div>206 </div>207208 {live && auth && <div className="mt-3 rounded-2xl border border-line p-3" data-testid="live-authorization"><div className="mb-1 text-[13px] font-semibold">{t('flAuthActive')}</div>209 <PolicyRows p={auth.policy} /><Row label={t('flAuthExpires')}>{when(new Date(auth.expiresAt), locale)}</Row>210 <p className="mt-1 text-[12px] leading-snug text-mute">{t('flLimitsWho')}</p></div>}211 {!live && auth?.revokedAt && <p className="mt-3 text-[12.5px] text-mute">{t('flRevokedAt', { date: when(new Date(auth.revokedAt), locale) })}</p>}212213 <div className="mt-3"><FundingBox network={a.network} collateral={c} /></div>214 <div className="mt-3"><CollateralPanel account={a} /></div>215216 <div className="mt-3 flex flex-wrap gap-2">217 {!selected && <Btn kind="soft" onClick={() => onSelect(a.id)}>{t('fxUse')}</Btn>}218 {!live && <Btn disabled={off} onClick={() => setSheet('authorize')} data-testid="authorize"><Icon n="key" className="h-4 w-4" />{t('flAuthorize')}</Btn>}219 {(live || a.pendingAuthorization) && <Btn kind="danger" onClick={() => setSheet('revoke')} data-testid="revoke"><Icon n="stop" className="h-4 w-4" />{t('flRevoke')}</Btn>}220 </div>221 {off && !live && <p className="mt-2 text-[12px] leading-snug text-mute">{t('flMainnetBlocked')}</p>}222 {sheet === 'authorize' && <AuthorizeSheet account={a} config={config} collateral={c} onClose={() => setSheet(null)} />}223 {sheet === 'revoke' && <RevokeSheet account={a} onClose={() => setSheet(null)} />}224 </li>;225}226227export function LiveSection({ selected, onSelect }: { selected: string | null; onSelect: (id: string) => void }) {228 const t = useT(); const qc = useQueryClient();229 const { locale } = useContext(I18n);230 const config = useQuery({ queryKey: ['deriv', 'live', 'config', locale], queryFn: () => lapi.config(locale), staleTime: 10_000, refetchInterval: 30_000, retry: 0 }); // v7: the server's sentences in the screen's language231 const accounts = useQuery({ queryKey: ['deriv', 'live', 'accounts'], queryFn: lapi.accounts, refetchInterval: 15_000, retry: 0 });232 const [busy, setBusy] = useState(''), [err, setErr] = useState<LiveErr | null>(null);233 const lock = useRef(false);234 useEffect(() => { setErr(null); }, [accounts.data?.length]);235 const cfg = config.data, list = accounts.data ?? [];236 const anyLive = list.some((a) => a.mode === 'MANUAL_LIVE' && !a.authorization?.expired);237 const create = async (network: LiveNetwork) => {238 if (lock.current) return; lock.current = true; setBusy(network); setErr(null);239 try { const a = await lapi.create(network, t(network === 'testnet' ? 'flLabelTestnet' : 'flLabelMainnet')); await refreshLive(qc); onSelect(a.id); }240 catch (e) { setErr(liveError(e)); } finally { lock.current = false; setBusy(''); }241 };242 return <Card><div data-testid="live-card">243 <H right={<span className={`inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[12px] font-semibold ${anyLive ? 'bg-emerald/10 text-emerald' : 'bg-elev text-mute'}`} data-testid="live-status"><Icon n={anyLive ? 'key' : 'lock'} className="h-3.5 w-3.5" />{t(anyLive ? 'flLiveOn' : 'fxLiveOff')}</span>}>{t('fxLive')}</H>244 <p className="text-[13.5px] leading-snug">{t('flIntro')}</p>245 <ul className="mt-3 space-y-2 text-[13.5px] leading-snug">{(['fxLiveReq1', 'flReq2', 'fxLiveReq3', 'fxLiveReq4'] as const).map((k) => <Bullet key={k} icon="key">{t(k)}</Bullet>)}</ul>246 {config.isError || accounts.isError ? <div className="mt-3"><LiveErrNotice err={liveError(config.error ?? accounts.error)} /></div> : !cfg || accounts.isLoading ? <div className="mt-3"><Skeleton h="h-24" /></div> : <>247 {!cfg.mainnetEnabled && <div className="mt-3"><Notice tone="info"><div data-testid="mainnet-setting">{t('flMainnetSetting')}</div></Notice></div>}248 <ul className="mt-4 space-y-3">{NETWORKS.map((n) => { const a = list.find((x) => x.network === n);249 if (a) return <LiveAccountCard key={n} a={a} config={cfg} selected={a.id === selected} onSelect={onSelect} />;250 const off = n === 'mainnet' && !cfg.mainnetEnabled;251 return <li key={n} className="rounded-2xl border border-dashed border-line p-4" data-testid={`live-create-${n}`}>252 <div className="flex flex-wrap items-center gap-2"><ModeTag tone={n === 'mainnet' ? 'mainnet' : 'testnet'} /><span className="text-[14.5px] font-semibold">{t(n === 'testnet' ? 'flCreateTestnetTitle' : 'flCreateMainnetTitle')}</span></div>253 <p className="mt-1.5 text-[13px] leading-snug text-mute">{t(n === 'testnet' ? 'flCreateTestnetBody' : 'flCreateMainnetBody')}</p>254 {off && <p className="mt-1.5 text-[12.5px] font-medium leading-snug text-warn" data-testid="mainnet-blocked">{t('flMainnetBlocked')}</p>}255 <Btn className="mt-3" kind={n === 'testnet' ? 'primary' : 'soft'} busy={busy === n} disabled={off || !!busy} onClick={() => void create(n)}><Icon n="plus" className="h-4 w-4" />{t(n === 'testnet' ? 'flCreateTestnet' : 'flCreateMainnet')}</Btn></li>; })}</ul>256 {err && <div className="mt-3"><LiveErrNotice err={err} /></div>}257 {/* >>> v7 perp live: the V6 shape of GET /config — mandates exist; on mainnet they are closed by a SERVER SETTING, said with the server's own words */}258 <p className="mt-3 text-[12.5px] leading-snug text-mute" data-testid="mandate-mode-note">{cfg.mandateLive.available ? t('plMandateModeNote') : t('flMandateNote')} {t('fxLiveNote')}</p>259 {!cfg.mandateLive.available && cfg.mandateLive.reason && <ServerSays mono text={cfg.mandateLive.reason} className="mt-1 break-words text-[11.5px] text-mute" />}260 {cfg.mandateLive.available && cfg.mandateLive.mainnet && !cfg.mandateLive.mainnet.enabled && <div className="mt-2 rounded-2xl border border-dashed border-line bg-elev/60 p-3 text-[12.5px] leading-snug" data-testid="mandate-mainnet-setting">261 <b className="flex items-start gap-1.5 font-semibold"><Icon n="lock" className="mt-0.5 h-3.5 w-3.5 shrink-0" />{t('plLockTitle')}</b><span className="mt-1 block text-mute">{t('plLockBody')}</span>262 <span className="mt-1 block break-words font-mono text-[11px] text-mute">{t('plLockSays')} {cfg.mandateLive.mainnet.reason ?? cfg.reasons?.mandate_mainnet_disabled?.[locale]}</span></div>}263 {/* <<< v7 perp live */}264 </>}</div>265 </Card>;266}267