Skip to content

Commit c8107b6

Browse files
author
Codex
committed
Add funding spot USDT recovery
1 parent 78d5032 commit c8107b6

3 files changed

Lines changed: 546 additions & 4 deletions

File tree

frontend/src/lib/api/robson.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,35 @@ export type FundingSagaSummary = {
193193
created_at: string;
194194
};
195195

196+
export type FundingRecoverSpotUsdtRequest = {
197+
asset?: "USDT";
198+
amount?: string;
199+
dry_run?: boolean;
200+
execute?: boolean;
201+
confirm?: string;
202+
correlation_id?: string;
203+
};
204+
205+
export type FundingRecoverSpotUsdtResponse = {
206+
correlation_id: string;
207+
asset: string;
208+
amount: string;
209+
from: string;
210+
to: string;
211+
transfer_type: string;
212+
spot_usdt_before: string;
213+
futures_usdt_wallet_before: string;
214+
futures_usdt_available_before: string;
215+
spot_usdt_after_expected: string;
216+
futures_usdt_wallet_after_expected: string;
217+
spot_usdt_after_actual?: string | null;
218+
futures_usdt_wallet_after_actual?: string | null;
219+
futures_usdt_available_after_actual?: string | null;
220+
transfer_id?: string | null;
221+
dry_run: boolean;
222+
idempotent_skip: boolean;
223+
};
224+
196225
type EventSourceLike = {
197226
onmessage: ((this: EventSource, ev: MessageEvent) => unknown) | null;
198227
onerror: ((this: EventSource, ev: Event) => unknown) | null;
@@ -488,4 +517,13 @@ export const robsonApi = {
488517
getFundingSaga: (id: string) => apiFetch<FundingSaga>(`/funding/${id}`),
489518

490519
listFunding: () => apiFetch<FundingSagaSummary[]>("/funding"),
520+
521+
recoverSpotUsdtToFutures: (body: FundingRecoverSpotUsdtRequest) =>
522+
apiFetch<FundingRecoverSpotUsdtResponse>(
523+
"/funding/recover-spot-usdt-to-futures",
524+
{
525+
method: "POST",
526+
body: JSON.stringify(body),
527+
},
528+
),
491529
};

frontend/src/routes/(authed)/funding/+page.svelte

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import {
77
robsonApi,
88
type FundingQuote,
9+
type FundingRecoverSpotUsdtResponse,
910
type FundingSagaSummary,
1011
type ApiError,
1112
} from '$api/robson';
@@ -25,9 +26,20 @@
2526
let submitting = $state(false);
2627
let tick = $state(Date.now());
2728
let tickTimer: ReturnType<typeof setInterval> | null = null;
29+
let recoveryPreview = $state<FundingRecoverSpotUsdtResponse | null>(null);
30+
let recoveryResult = $state<FundingRecoverSpotUsdtResponse | null>(null);
31+
let recoveryError = $state<string | null>(null);
32+
let recoveryConfirmInput = $state('');
33+
let recoverySubmitting = $state(false);
2834
2935
let keyword = $derived($_('funding.confirmKeyword') ?? 'CONVERTER E MOVER');
3036
let canConfirm = $derived(confirmInput === keyword && !submitting);
37+
const recoveryConfirmPhrase = 'TRANSFER_SPOT_USDT_TO_FUTURES';
38+
let recoveryCanExecute = $derived(
39+
Boolean(recoveryPreview) &&
40+
recoveryConfirmInput === recoveryConfirmPhrase &&
41+
!recoverySubmitting,
42+
);
3143
3244
let expiresInMs = $derived.by(() => {
3345
if (!quote) return 0;
@@ -131,6 +143,54 @@
131143
return $_(`funding.state.${state}`) ?? state;
132144
}
133145
146+
function fmtRecovery(v: string | null | undefined): string {
147+
if (!v) return '';
148+
return `${Number(v).toFixed(8)} USDT`;
149+
}
150+
151+
async function previewSpotRecovery() {
152+
recoveryError = null;
153+
recoveryResult = null;
154+
recoveryConfirmInput = '';
155+
recoverySubmitting = true;
156+
try {
157+
recoveryPreview = await robsonApi.recoverSpotUsdtToFutures({
158+
asset: 'USDT',
159+
dry_run: true,
160+
});
161+
} catch (e) {
162+
recoveryPreview = null;
163+
recoveryError =
164+
e instanceof Error ? e.message : 'Recovery preview failed';
165+
} finally {
166+
recoverySubmitting = false;
167+
}
168+
}
169+
170+
async function executeSpotRecovery() {
171+
if (!recoveryPreview || !recoveryCanExecute) return;
172+
recoveryError = null;
173+
recoverySubmitting = true;
174+
try {
175+
recoveryResult = await robsonApi.recoverSpotUsdtToFutures({
176+
asset: 'USDT',
177+
amount: recoveryPreview.amount,
178+
execute: true,
179+
dry_run: false,
180+
confirm: recoveryConfirmPhrase,
181+
correlation_id: recoveryPreview.correlation_id,
182+
});
183+
recoveryPreview = recoveryResult;
184+
recoveryConfirmInput = '';
185+
void loadRecent();
186+
} catch (e) {
187+
recoveryError =
188+
e instanceof Error ? e.message : 'Recovery execution failed';
189+
} finally {
190+
recoverySubmitting = false;
191+
}
192+
}
193+
134194
$effect(() => {
135195
void loadRecent();
136196
return () => {
@@ -283,6 +343,122 @@
283343
</Stack>
284344
</Card>
285345

346+
<Card padding={6}>
347+
<Stack gap={4}>
348+
<div class="eyebrow">RECOVERY</div>
349+
<h2>Transferir USDT livre da Spot para Futures</h2>
350+
<p class="recovery-copy">
351+
Recupera funding parcialmente executado sem nova conversão, sem compra e
352+
sem trade. O preview é obrigatório antes da execução.
353+
</p>
354+
355+
<Row gap={3} justify="start">
356+
<button
357+
class="btn-primary"
358+
disabled={recoverySubmitting}
359+
onclick={previewSpotRecovery}
360+
>
361+
{#if recoverySubmitting}
362+
Consultando...
363+
{:else}
364+
Preview Spot → Futures
365+
{/if}
366+
</button>
367+
</Row>
368+
369+
{#if recoveryError}
370+
<p class="err-text">{recoveryError}</p>
371+
{/if}
372+
373+
{#if recoveryPreview}
374+
<div class="quote-block">
375+
<Stack gap={3}>
376+
<Row gap={4} justify="between" align="baseline">
377+
<span class="eyebrow">Spot USDT antes</span>
378+
<span class="mono"
379+
>{fmtRecovery(recoveryPreview.spot_usdt_before)}</span
380+
>
381+
</Row>
382+
<Row gap={4} justify="between" align="baseline">
383+
<span class="eyebrow">Futures wallet antes</span>
384+
<span class="mono"
385+
>{fmtRecovery(recoveryPreview.futures_usdt_wallet_before)}</span
386+
>
387+
</Row>
388+
<Row gap={4} justify="between" align="baseline">
389+
<span class="eyebrow">Amount proposto</span>
390+
<span class="mono">{fmtRecovery(recoveryPreview.amount)}</span>
391+
</Row>
392+
<Row gap={4} justify="between" align="baseline">
393+
<span class="eyebrow">Spot esperado depois</span>
394+
<span class="mono"
395+
>{fmtRecovery(recoveryPreview.spot_usdt_after_expected)}</span
396+
>
397+
</Row>
398+
<Row gap={4} justify="between" align="baseline">
399+
<span class="eyebrow">Futures esperado depois</span>
400+
<span class="mono"
401+
>{fmtRecovery(
402+
recoveryPreview.futures_usdt_wallet_after_expected,
403+
)}</span
404+
>
405+
</Row>
406+
<Row gap={4} justify="between" align="baseline">
407+
<span class="eyebrow">Correlation id</span>
408+
<span class="mono dim">{recoveryPreview.correlation_id}</span>
409+
</Row>
410+
{#if recoveryResult}
411+
<Row gap={4} justify="between" align="baseline">
412+
<span class="eyebrow">Transfer id</span>
413+
<span class="mono">{recoveryResult.transfer_id ?? ''}</span>
414+
</Row>
415+
<Row gap={4} justify="between" align="baseline">
416+
<span class="eyebrow">Spot atual</span>
417+
<span class="mono"
418+
>{fmtRecovery(recoveryResult.spot_usdt_after_actual)}</span
419+
>
420+
</Row>
421+
<Row gap={4} justify="between" align="baseline">
422+
<span class="eyebrow">Futures atual</span>
423+
<span class="mono"
424+
>{fmtRecovery(
425+
recoveryResult.futures_usdt_wallet_after_actual,
426+
)}</span
427+
>
428+
</Row>
429+
{/if}
430+
</Stack>
431+
</div>
432+
433+
<Stack gap={2}>
434+
<label class="eyebrow" for="recovery-confirm-input">
435+
Digite exatamente: {recoveryConfirmPhrase}
436+
</label>
437+
<input
438+
id="recovery-confirm-input"
439+
type="text"
440+
bind:value={recoveryConfirmInput}
441+
autocomplete="off"
442+
spellcheck="false"
443+
class="confirm-input"
444+
disabled={recoverySubmitting}
445+
/>
446+
</Stack>
447+
448+
<Row gap={3} justify="end">
449+
<button
450+
class="btn-confirm"
451+
class:ready={recoveryCanExecute}
452+
disabled={!recoveryCanExecute}
453+
onclick={executeSpotRecovery}
454+
>
455+
Transferir USDT Spot → Futures
456+
</button>
457+
</Row>
458+
{/if}
459+
</Stack>
460+
</Card>
461+
286462
{#if recent.length > 0}
287463
<section>
288464
<Stack gap={4}>
@@ -328,6 +504,11 @@
328504
font-weight: 300;
329505
letter-spacing: var(--track-tight);
330506
}
507+
h2 {
508+
font-size: var(--text-xl);
509+
font-weight: 400;
510+
letter-spacing: var(--track-tight);
511+
}
331512
.eyebrow {
332513
font-family: var(--font-mono);
333514
font-size: var(--text-xs);
@@ -342,6 +523,11 @@
342523
border: 1px solid var(--border);
343524
border-radius: var(--radius-sm);
344525
}
526+
.recovery-copy {
527+
color: var(--fg-2);
528+
font-size: var(--text-sm);
529+
line-height: 1.5;
530+
}
345531
.items {
346532
display: flex;
347533
flex-direction: column;

0 commit comments

Comments
 (0)