Phase 3.1: Add Exchange page UI
New /exchange page for Bitcoin trading: - Buy/Sell direction toggle with visual feedback - Amount slider (€100-€3000 in €20 increments) - Live price display with auto-refresh every 60s - Direction-specific pricing (buy +5%, sell -5%) - Sats amount displayed in BTC format (0.XX XXX XXX) - Date selection with availability indicators - Time slot selection grid - Confirmation panel with trade summary Regenerate frontend types from updated OpenAPI schema.
This commit is contained in:
parent
811fdf2663
commit
361dc8764d
2 changed files with 1379 additions and 49 deletions
805
frontend/app/exchange/page.tsx
Normal file
805
frontend/app/exchange/page.tsx
Normal file
|
|
@ -0,0 +1,805 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { Permission } from "../auth-context";
|
||||
import { api } from "../api";
|
||||
import { Header } from "../components/Header";
|
||||
import { useRequireAuth } from "../hooks/useRequireAuth";
|
||||
import { components } from "../generated/api";
|
||||
import { formatDate, formatTime, getDateRange } from "../utils/date";
|
||||
import { layoutStyles, typographyStyles, bannerStyles, buttonStyles } from "../styles/shared";
|
||||
|
||||
type ExchangePriceResponse = components["schemas"]["ExchangePriceResponse"];
|
||||
type ExchangeResponse = components["schemas"]["ExchangeResponse"];
|
||||
type BookableSlot = components["schemas"]["BookableSlot"];
|
||||
type AvailableSlotsResponse = components["schemas"]["AvailableSlotsResponse"];
|
||||
|
||||
// Constants from shared config (will be fetched from API)
|
||||
const MIN_ADVANCE_DAYS = 1;
|
||||
const MAX_ADVANCE_DAYS = 30;
|
||||
|
||||
type Direction = "buy" | "sell";
|
||||
|
||||
/**
|
||||
* Format EUR amount from cents to display string
|
||||
*/
|
||||
function formatEur(cents: number): string {
|
||||
return `€${(cents / 100).toLocaleString("de-DE")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format satoshi amount with thousand separators
|
||||
* e.g., 476190 -> "0.00 476 190 sats"
|
||||
*/
|
||||
function formatSats(sats: number): string {
|
||||
const btc = sats / 100_000_000;
|
||||
const btcStr = btc.toFixed(8);
|
||||
const [whole, decimal] = btcStr.split(".");
|
||||
|
||||
// Group decimal into chunks of 3 for readability
|
||||
const grouped = decimal.replace(/(.{2})(.{3})(.{3})/, "$1 $2 $3");
|
||||
return `${whole}.${grouped} sats`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format price for display
|
||||
*/
|
||||
function formatPrice(price: number): string {
|
||||
return `€${price.toLocaleString("de-DE", { maximumFractionDigits: 0 })}`;
|
||||
}
|
||||
|
||||
export default function ExchangePage() {
|
||||
const { user, isLoading, isAuthorized } = useRequireAuth({
|
||||
requiredPermission: Permission.BOOK_APPOINTMENT,
|
||||
fallbackRedirect: "/",
|
||||
});
|
||||
|
||||
// Price and config state
|
||||
const [priceData, setPriceData] = useState<ExchangePriceResponse | null>(null);
|
||||
const [isPriceLoading, setIsPriceLoading] = useState(true);
|
||||
const [priceError, setPriceError] = useState<string | null>(null);
|
||||
const [lastPriceUpdate, setLastPriceUpdate] = useState<Date | null>(null);
|
||||
|
||||
// Trade form state
|
||||
const [direction, setDirection] = useState<Direction>("buy");
|
||||
const [eurAmount, setEurAmount] = useState<number>(10000); // €100 in cents
|
||||
|
||||
// Date/slot selection state
|
||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||
const [availableSlots, setAvailableSlots] = useState<BookableSlot[]>([]);
|
||||
const [selectedSlot, setSelectedSlot] = useState<BookableSlot | null>(null);
|
||||
const [isLoadingSlots, setIsLoadingSlots] = useState(false);
|
||||
const [datesWithAvailability, setDatesWithAvailability] = useState<Set<string>>(new Set());
|
||||
const [isLoadingAvailability, setIsLoadingAvailability] = useState(true);
|
||||
|
||||
// UI state
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [isBooking, setIsBooking] = useState(false);
|
||||
|
||||
// Compute dates
|
||||
const dates = useMemo(() => getDateRange(MIN_ADVANCE_DAYS, MAX_ADVANCE_DAYS), []);
|
||||
|
||||
// Config from API
|
||||
const config = priceData?.config;
|
||||
const eurMin = config?.eur_min ?? 100;
|
||||
const eurMax = config?.eur_max ?? 3000;
|
||||
const eurIncrement = config?.eur_increment ?? 20;
|
||||
|
||||
// Compute trade details
|
||||
const price = priceData?.price;
|
||||
const marketPrice = price?.market_price ?? 0;
|
||||
const premiumPercent = price?.premium_percentage ?? 5;
|
||||
|
||||
// Calculate agreed price based on direction
|
||||
const agreedPrice = useMemo(() => {
|
||||
if (!marketPrice) return 0;
|
||||
if (direction === "buy") {
|
||||
return marketPrice * (1 + premiumPercent / 100);
|
||||
} else {
|
||||
return marketPrice * (1 - premiumPercent / 100);
|
||||
}
|
||||
}, [marketPrice, premiumPercent, direction]);
|
||||
|
||||
// Calculate sats amount
|
||||
const satsAmount = useMemo(() => {
|
||||
if (!agreedPrice) return 0;
|
||||
const eurValue = eurAmount / 100;
|
||||
const btcAmount = eurValue / agreedPrice;
|
||||
return Math.floor(btcAmount * 100_000_000);
|
||||
}, [eurAmount, agreedPrice]);
|
||||
|
||||
// Fetch price data
|
||||
const fetchPrice = useCallback(async () => {
|
||||
setIsPriceLoading(true);
|
||||
setPriceError(null);
|
||||
|
||||
try {
|
||||
const data = await api.get<ExchangePriceResponse>("/api/exchange/price");
|
||||
setPriceData(data);
|
||||
setLastPriceUpdate(new Date());
|
||||
|
||||
if (data.error) {
|
||||
setPriceError(data.error);
|
||||
}
|
||||
if (data.price?.is_stale) {
|
||||
setPriceError("Price is stale. Trade booking may be blocked.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch price:", err);
|
||||
setPriceError("Failed to load price data");
|
||||
} finally {
|
||||
setIsPriceLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Auto-refresh price every 60 seconds
|
||||
useEffect(() => {
|
||||
if (!user || !isAuthorized) return;
|
||||
|
||||
fetchPrice();
|
||||
const interval = setInterval(fetchPrice, 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, [user, isAuthorized, fetchPrice]);
|
||||
|
||||
// Fetch slots for a date
|
||||
const fetchSlots = useCallback(async (date: Date) => {
|
||||
setIsLoadingSlots(true);
|
||||
setError(null);
|
||||
setAvailableSlots([]);
|
||||
setSelectedSlot(null);
|
||||
|
||||
try {
|
||||
const dateStr = formatDate(date);
|
||||
const data = await api.get<AvailableSlotsResponse>(`/api/booking/slots?date=${dateStr}`);
|
||||
setAvailableSlots(data.slots);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch slots:", err);
|
||||
setError("Failed to load available slots");
|
||||
} finally {
|
||||
setIsLoadingSlots(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch availability for all dates on mount
|
||||
useEffect(() => {
|
||||
if (!user || !isAuthorized) return;
|
||||
|
||||
const fetchAllAvailability = async () => {
|
||||
setIsLoadingAvailability(true);
|
||||
const availabilitySet = new Set<string>();
|
||||
|
||||
const promises = dates.map(async (date) => {
|
||||
try {
|
||||
const dateStr = formatDate(date);
|
||||
const data = await api.get<AvailableSlotsResponse>(`/api/booking/slots?date=${dateStr}`);
|
||||
if (data.slots.length > 0) {
|
||||
availabilitySet.add(dateStr);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch availability for ${formatDate(date)}:`, err);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
setDatesWithAvailability(availabilitySet);
|
||||
setIsLoadingAvailability(false);
|
||||
};
|
||||
|
||||
fetchAllAvailability();
|
||||
}, [user, isAuthorized, dates]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDate && user && isAuthorized) {
|
||||
fetchSlots(selectedDate);
|
||||
}
|
||||
}, [selectedDate, user, isAuthorized, fetchSlots]);
|
||||
|
||||
const handleDateSelect = (date: Date) => {
|
||||
const dateStr = formatDate(date);
|
||||
if (datesWithAvailability.has(dateStr)) {
|
||||
setSelectedDate(date);
|
||||
setSuccessMessage(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSlotSelect = (slot: BookableSlot) => {
|
||||
setSelectedSlot(slot);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleBook = async () => {
|
||||
if (!selectedSlot) return;
|
||||
|
||||
setIsBooking(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const exchange = await api.post<ExchangeResponse>("/api/exchange", {
|
||||
slot_start: selectedSlot.start_time,
|
||||
direction,
|
||||
eur_amount: eurAmount,
|
||||
});
|
||||
|
||||
const dirLabel = direction === "buy" ? "Buy" : "Sell";
|
||||
setSuccessMessage(
|
||||
`${dirLabel} trade booked for ${formatTime(exchange.slot_start)}! ` +
|
||||
`${formatEur(exchange.eur_amount)} ↔ ${formatSats(exchange.sats_amount)}`
|
||||
);
|
||||
setSelectedSlot(null);
|
||||
|
||||
// Refresh slots
|
||||
if (selectedDate) {
|
||||
await fetchSlots(selectedDate);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to book trade");
|
||||
} finally {
|
||||
setIsBooking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelSlotSelection = () => {
|
||||
setSelectedSlot(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<main style={layoutStyles.main}>
|
||||
<div style={layoutStyles.loader}>Loading...</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthorized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isPriceStale = priceData?.price?.is_stale ?? false;
|
||||
|
||||
return (
|
||||
<main style={layoutStyles.main}>
|
||||
<Header currentPage="exchange" />
|
||||
<div style={styles.content}>
|
||||
<h1 style={typographyStyles.pageTitle}>Exchange Bitcoin</h1>
|
||||
<p style={typographyStyles.pageSubtitle}>Buy or sell Bitcoin with a 5% premium</p>
|
||||
|
||||
{successMessage && <div style={bannerStyles.successBanner}>{successMessage}</div>}
|
||||
{error && <div style={bannerStyles.errorBanner}>{error}</div>}
|
||||
|
||||
{/* Price Display */}
|
||||
<div style={styles.priceCard}>
|
||||
{isPriceLoading && !priceData ? (
|
||||
<div style={styles.priceLoading}>Loading price...</div>
|
||||
) : priceError && !priceData?.price ? (
|
||||
<div style={styles.priceError}>{priceError}</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={styles.priceRow}>
|
||||
<span style={styles.priceLabel}>Market:</span>
|
||||
<span style={styles.priceValue}>{formatPrice(marketPrice)}</span>
|
||||
<span style={styles.priceDivider}>•</span>
|
||||
<span style={styles.priceLabel}>Our price:</span>
|
||||
<span
|
||||
style={{
|
||||
...styles.priceValue,
|
||||
color: direction === "buy" ? "#f87171" : "#4ade80",
|
||||
}}
|
||||
>
|
||||
{formatPrice(agreedPrice)}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
...styles.premiumBadge,
|
||||
background:
|
||||
direction === "buy" ? "rgba(248, 113, 113, 0.2)" : "rgba(74, 222, 128, 0.2)",
|
||||
color: direction === "buy" ? "#f87171" : "#4ade80",
|
||||
}}
|
||||
>
|
||||
{direction === "buy" ? "+" : "-"}
|
||||
{premiumPercent}%
|
||||
</span>
|
||||
</div>
|
||||
{lastPriceUpdate && (
|
||||
<div style={styles.priceTimestamp}>
|
||||
Updated {lastPriceUpdate.toLocaleTimeString()}
|
||||
{isPriceStale && <span style={styles.staleWarning}> (stale)</span>}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Trade Form */}
|
||||
<div style={styles.tradeCard}>
|
||||
{/* Direction Selector */}
|
||||
<div style={styles.directionRow}>
|
||||
<button
|
||||
onClick={() => setDirection("buy")}
|
||||
style={{
|
||||
...styles.directionBtn,
|
||||
...(direction === "buy" ? styles.directionBtnBuyActive : {}),
|
||||
}}
|
||||
>
|
||||
Buy BTC
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDirection("sell")}
|
||||
style={{
|
||||
...styles.directionBtn,
|
||||
...(direction === "sell" ? styles.directionBtnSellActive : {}),
|
||||
}}
|
||||
>
|
||||
Sell BTC
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Amount Slider */}
|
||||
<div style={styles.amountSection}>
|
||||
<div style={styles.amountHeader}>
|
||||
<span style={styles.amountLabel}>Amount</span>
|
||||
<span style={styles.amountValue}>{formatEur(eurAmount)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={eurMin * 100}
|
||||
max={eurMax * 100}
|
||||
step={eurIncrement * 100}
|
||||
value={eurAmount}
|
||||
onChange={(e) => setEurAmount(Number(e.target.value))}
|
||||
style={styles.slider}
|
||||
/>
|
||||
<div style={styles.amountRange}>
|
||||
<span>{formatEur(eurMin * 100)}</span>
|
||||
<span>{formatEur(eurMax * 100)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trade Summary */}
|
||||
<div style={styles.tradeSummary}>
|
||||
{direction === "buy" ? (
|
||||
<p style={styles.summaryText}>
|
||||
You pay <strong>{formatEur(eurAmount)}</strong>, you receive{" "}
|
||||
<strong style={styles.satsValue}>{formatSats(satsAmount)}</strong>
|
||||
</p>
|
||||
) : (
|
||||
<p style={styles.summaryText}>
|
||||
You send <strong style={styles.satsValue}>{formatSats(satsAmount)}</strong>, you
|
||||
receive <strong>{formatEur(eurAmount)}</strong>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date Selection */}
|
||||
<div style={styles.section}>
|
||||
<h2 style={styles.sectionTitle}>Select a Date</h2>
|
||||
<div style={styles.dateGrid}>
|
||||
{dates.map((date) => {
|
||||
const dateStr = formatDate(date);
|
||||
const isSelected = selectedDate && formatDate(selectedDate) === dateStr;
|
||||
const hasAvailability = datesWithAvailability.has(dateStr);
|
||||
const isDisabled = !hasAvailability || isLoadingAvailability;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={dateStr}
|
||||
onClick={() => handleDateSelect(date)}
|
||||
disabled={isDisabled}
|
||||
style={{
|
||||
...styles.dateButton,
|
||||
...(isSelected ? styles.dateButtonSelected : {}),
|
||||
...(isDisabled ? styles.dateButtonDisabled : {}),
|
||||
}}
|
||||
>
|
||||
<div style={styles.dateWeekday}>
|
||||
{date.toLocaleDateString("en-US", { weekday: "short" })}
|
||||
</div>
|
||||
<div style={styles.dateDay}>
|
||||
{date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Available Slots */}
|
||||
{selectedDate && (
|
||||
<div style={styles.section}>
|
||||
<h2 style={styles.sectionTitle}>
|
||||
Available Slots for{" "}
|
||||
{selectedDate.toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</h2>
|
||||
|
||||
{isLoadingSlots ? (
|
||||
<div style={styles.emptyState}>Loading slots...</div>
|
||||
) : availableSlots.length === 0 ? (
|
||||
<div style={styles.emptyState}>No available slots for this date</div>
|
||||
) : (
|
||||
<div style={styles.slotGrid}>
|
||||
{availableSlots.map((slot) => {
|
||||
const isSelected = selectedSlot?.start_time === slot.start_time;
|
||||
return (
|
||||
<button
|
||||
key={slot.start_time}
|
||||
onClick={() => handleSlotSelect(slot)}
|
||||
style={{
|
||||
...styles.slotButton,
|
||||
...(isSelected ? styles.slotButtonSelected : {}),
|
||||
}}
|
||||
>
|
||||
{formatTime(slot.start_time)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirm Booking */}
|
||||
{selectedSlot && (
|
||||
<div style={styles.confirmCard}>
|
||||
<h3 style={styles.confirmTitle}>Confirm Trade</h3>
|
||||
<div style={styles.confirmDetails}>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Time:</span>
|
||||
<span style={styles.confirmValue}>
|
||||
{formatTime(selectedSlot.start_time)} - {formatTime(selectedSlot.end_time)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Direction:</span>
|
||||
<span
|
||||
style={{
|
||||
...styles.confirmValue,
|
||||
color: direction === "buy" ? "#4ade80" : "#f87171",
|
||||
}}
|
||||
>
|
||||
{direction === "buy" ? "Buy BTC" : "Sell BTC"}
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>EUR:</span>
|
||||
<span style={styles.confirmValue}>{formatEur(eurAmount)}</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>BTC:</span>
|
||||
<span style={{ ...styles.confirmValue, ...styles.satsValue }}>
|
||||
{formatSats(satsAmount)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Rate:</span>
|
||||
<span style={styles.confirmValue}>{formatPrice(agreedPrice)}/BTC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.buttonRow}>
|
||||
<button
|
||||
onClick={handleBook}
|
||||
disabled={isBooking || isPriceStale}
|
||||
style={{
|
||||
...styles.bookButton,
|
||||
background:
|
||||
direction === "buy"
|
||||
? "linear-gradient(135deg, #4ade80 0%, #22c55e 100%)"
|
||||
: "linear-gradient(135deg, #f87171 0%, #ef4444 100%)",
|
||||
...(isBooking || isPriceStale ? buttonStyles.buttonDisabled : {}),
|
||||
}}
|
||||
>
|
||||
{isBooking
|
||||
? "Booking..."
|
||||
: isPriceStale
|
||||
? "Price Stale"
|
||||
: `Confirm ${direction === "buy" ? "Buy" : "Sell"}`}
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelSlotSelection}
|
||||
disabled={isBooking}
|
||||
style={styles.cancelButton}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// Page-specific styles
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: "2rem",
|
||||
maxWidth: "900px",
|
||||
margin: "0 auto",
|
||||
width: "100%",
|
||||
},
|
||||
priceCard: {
|
||||
background: "rgba(255, 255, 255, 0.03)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
borderRadius: "12px",
|
||||
padding: "1rem 1.5rem",
|
||||
marginBottom: "1.5rem",
|
||||
},
|
||||
priceRow: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.75rem",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
priceLabel: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
fontSize: "0.9rem",
|
||||
},
|
||||
priceValue: {
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
color: "#fff",
|
||||
fontSize: "1.1rem",
|
||||
fontWeight: 500,
|
||||
},
|
||||
priceDivider: {
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
margin: "0 0.25rem",
|
||||
},
|
||||
premiumBadge: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 600,
|
||||
padding: "0.2rem 0.5rem",
|
||||
borderRadius: "4px",
|
||||
marginLeft: "0.25rem",
|
||||
},
|
||||
priceTimestamp: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "0.75rem",
|
||||
color: "rgba(255, 255, 255, 0.4)",
|
||||
marginTop: "0.5rem",
|
||||
},
|
||||
staleWarning: {
|
||||
color: "#f87171",
|
||||
fontWeight: 600,
|
||||
},
|
||||
priceLoading: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
textAlign: "center" as const,
|
||||
},
|
||||
priceError: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "#f87171",
|
||||
textAlign: "center" as const,
|
||||
},
|
||||
tradeCard: {
|
||||
background: "rgba(255, 255, 255, 0.03)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
borderRadius: "12px",
|
||||
padding: "1.5rem",
|
||||
marginBottom: "2rem",
|
||||
},
|
||||
directionRow: {
|
||||
display: "flex",
|
||||
gap: "0.5rem",
|
||||
marginBottom: "1.5rem",
|
||||
},
|
||||
directionBtn: {
|
||||
flex: 1,
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "1rem",
|
||||
fontWeight: 600,
|
||||
padding: "0.875rem",
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
borderRadius: "8px",
|
||||
color: "rgba(255, 255, 255, 0.6)",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
directionBtnBuyActive: {
|
||||
background: "rgba(74, 222, 128, 0.15)",
|
||||
border: "1px solid #4ade80",
|
||||
color: "#4ade80",
|
||||
},
|
||||
directionBtnSellActive: {
|
||||
background: "rgba(248, 113, 113, 0.15)",
|
||||
border: "1px solid #f87171",
|
||||
color: "#f87171",
|
||||
},
|
||||
amountSection: {
|
||||
marginBottom: "1.5rem",
|
||||
},
|
||||
amountHeader: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "0.75rem",
|
||||
},
|
||||
amountLabel: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
fontSize: "0.9rem",
|
||||
},
|
||||
amountValue: {
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
color: "#fff",
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: 600,
|
||||
},
|
||||
slider: {
|
||||
width: "100%",
|
||||
height: "8px",
|
||||
appearance: "none" as const,
|
||||
background: "rgba(255, 255, 255, 0.1)",
|
||||
borderRadius: "4px",
|
||||
outline: "none",
|
||||
cursor: "pointer",
|
||||
},
|
||||
amountRange: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginTop: "0.5rem",
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "0.75rem",
|
||||
color: "rgba(255, 255, 255, 0.4)",
|
||||
},
|
||||
tradeSummary: {
|
||||
background: "rgba(255, 255, 255, 0.02)",
|
||||
borderRadius: "8px",
|
||||
padding: "1rem",
|
||||
textAlign: "center" as const,
|
||||
},
|
||||
summaryText: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.8)",
|
||||
fontSize: "0.95rem",
|
||||
margin: 0,
|
||||
},
|
||||
satsValue: {
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
color: "#f7931a", // Bitcoin orange
|
||||
},
|
||||
section: {
|
||||
marginBottom: "2rem",
|
||||
},
|
||||
sectionTitle: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "1.1rem",
|
||||
fontWeight: 500,
|
||||
color: "#fff",
|
||||
marginBottom: "1rem",
|
||||
},
|
||||
dateGrid: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "0.5rem",
|
||||
},
|
||||
dateButton: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
padding: "0.75rem 1rem",
|
||||
background: "rgba(255, 255, 255, 0.03)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
borderRadius: "10px",
|
||||
cursor: "pointer",
|
||||
minWidth: "90px",
|
||||
textAlign: "center" as const,
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
dateButtonSelected: {
|
||||
background: "rgba(167, 139, 250, 0.15)",
|
||||
border: "1px solid #a78bfa",
|
||||
},
|
||||
dateButtonDisabled: {
|
||||
opacity: 0.4,
|
||||
cursor: "not-allowed",
|
||||
background: "rgba(255, 255, 255, 0.01)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.04)",
|
||||
},
|
||||
dateWeekday: {
|
||||
color: "#fff",
|
||||
fontWeight: 500,
|
||||
fontSize: "0.875rem",
|
||||
marginBottom: "0.25rem",
|
||||
},
|
||||
dateDay: {
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
fontSize: "0.8rem",
|
||||
},
|
||||
slotGrid: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "0.5rem",
|
||||
},
|
||||
slotButton: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
padding: "0.6rem 1.25rem",
|
||||
background: "rgba(255, 255, 255, 0.03)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
borderRadius: "8px",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
slotButtonSelected: {
|
||||
background: "rgba(167, 139, 250, 0.15)",
|
||||
border: "1px solid #a78bfa",
|
||||
},
|
||||
emptyState: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.4)",
|
||||
padding: "1rem 0",
|
||||
},
|
||||
confirmCard: {
|
||||
background: "rgba(255, 255, 255, 0.03)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
borderRadius: "12px",
|
||||
padding: "1.5rem",
|
||||
maxWidth: "400px",
|
||||
},
|
||||
confirmTitle: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "1.1rem",
|
||||
fontWeight: 500,
|
||||
color: "#fff",
|
||||
marginBottom: "1rem",
|
||||
},
|
||||
confirmDetails: {
|
||||
marginBottom: "1.5rem",
|
||||
},
|
||||
confirmRow: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
padding: "0.5rem 0",
|
||||
borderBottom: "1px solid rgba(255, 255, 255, 0.05)",
|
||||
},
|
||||
confirmLabel: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
fontSize: "0.875rem",
|
||||
},
|
||||
confirmValue: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "#fff",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: 500,
|
||||
},
|
||||
buttonRow: {
|
||||
display: "flex",
|
||||
gap: "0.75rem",
|
||||
},
|
||||
bookButton: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
flex: 1,
|
||||
padding: "0.875rem",
|
||||
border: "none",
|
||||
borderRadius: "8px",
|
||||
color: "#fff",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
cancelButton: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
padding: "0.875rem 1.25rem",
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
borderRadius: "8px",
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
};
|
||||
|
|
@ -124,35 +124,6 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/exchange/price": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* Get Exchange Price
|
||||
* @description Get the current BTC/EUR price for trading.
|
||||
*
|
||||
* Returns the latest price from the database. If no price exists or the price
|
||||
* is stale, attempts to fetch a fresh price from Bitfinex.
|
||||
*
|
||||
* The response includes:
|
||||
* - market_price: The raw price from the exchange
|
||||
* - agreed_price: The price with admin premium applied
|
||||
* - is_stale: Whether the price is older than 5 minutes
|
||||
* - config: Trading configuration (min/max EUR, increment)
|
||||
*/
|
||||
get: operations["get_exchange_price_api_exchange_price_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/profile": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -465,10 +436,257 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/exchange/price": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* Get Exchange Price
|
||||
* @description Get the current BTC/EUR price for trading.
|
||||
*
|
||||
* Returns the latest price from the database. If no price exists or the price
|
||||
* is stale, attempts to fetch a fresh price from Bitfinex.
|
||||
*
|
||||
* The response includes:
|
||||
* - market_price: The raw price from the exchange
|
||||
* - agreed_price: The price with admin premium applied
|
||||
* - is_stale: Whether the price is older than 5 minutes
|
||||
* - config: Trading configuration (min/max EUR, increment)
|
||||
*/
|
||||
get: operations["get_exchange_price_api_exchange_price_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/exchange": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Create Exchange
|
||||
* @description Create a new exchange trade booking.
|
||||
*
|
||||
* Validates:
|
||||
* - Slot is on a valid date and time boundary
|
||||
* - Slot is within admin availability
|
||||
* - Slot is not already booked
|
||||
* - Price is not stale
|
||||
* - EUR amount is within configured limits
|
||||
*/
|
||||
post: operations["create_exchange_api_exchange_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/trades": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* Get My Trades
|
||||
* @description Get the current user's exchanges, sorted by date (newest first).
|
||||
*/
|
||||
get: operations["get_my_trades_api_trades_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/trades/{exchange_id}/cancel": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Cancel My Trade
|
||||
* @description Cancel one of the current user's exchanges.
|
||||
*/
|
||||
post: operations["cancel_my_trade_api_trades__exchange_id__cancel_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/trades/upcoming": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* Get Upcoming Trades
|
||||
* @description Get all upcoming booked trades, sorted by slot time ascending.
|
||||
*/
|
||||
get: operations["get_upcoming_trades_api_admin_trades_upcoming_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/trades/past": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* Get Past Trades
|
||||
* @description Get past trades with optional filters.
|
||||
*
|
||||
* Filters:
|
||||
* - status: Filter by exchange status
|
||||
* - start_date, end_date: Filter by slot_start date range
|
||||
* - user_search: Search by user email (partial match)
|
||||
*/
|
||||
get: operations["get_past_trades_api_admin_trades_past_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/trades/{exchange_id}/complete": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Complete Trade
|
||||
* @description Mark a trade as completed. Only possible after slot time has passed.
|
||||
*/
|
||||
post: operations["complete_trade_api_admin_trades__exchange_id__complete_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/trades/{exchange_id}/no-show": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Mark No Show
|
||||
* @description Mark a trade as no-show. Only possible after slot time has passed.
|
||||
*/
|
||||
post: operations["mark_no_show_api_admin_trades__exchange_id__no_show_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/trades/{exchange_id}/cancel": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Admin Cancel Trade
|
||||
* @description Cancel any trade (admin only).
|
||||
*/
|
||||
post: operations["admin_cancel_trade_api_admin_trades__exchange_id__cancel_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
/**
|
||||
* AdminExchangeResponse
|
||||
* @description Response model for admin exchange view (includes user contact).
|
||||
*/
|
||||
AdminExchangeResponse: {
|
||||
/** Id */
|
||||
id: number;
|
||||
/** User Id */
|
||||
user_id: number;
|
||||
/** User Email */
|
||||
user_email: string;
|
||||
user_contact: components["schemas"]["ExchangeUserContact"];
|
||||
/**
|
||||
* Slot Start
|
||||
* Format: date-time
|
||||
*/
|
||||
slot_start: string;
|
||||
/**
|
||||
* Slot End
|
||||
* Format: date-time
|
||||
*/
|
||||
slot_end: string;
|
||||
/** Direction */
|
||||
direction: string;
|
||||
/** Eur Amount */
|
||||
eur_amount: number;
|
||||
/** Sats Amount */
|
||||
sats_amount: number;
|
||||
/** Market Price Eur */
|
||||
market_price_eur: number;
|
||||
/** Agreed Price Eur */
|
||||
agreed_price_eur: number;
|
||||
/** Premium Percentage */
|
||||
premium_percentage: number;
|
||||
/** Status */
|
||||
status: string;
|
||||
/**
|
||||
* Created At
|
||||
* Format: date-time
|
||||
*/
|
||||
created_at: string;
|
||||
/** Cancelled At */
|
||||
cancelled_at: string | null;
|
||||
/** Completed At */
|
||||
completed_at: string | null;
|
||||
};
|
||||
/**
|
||||
* AdminUserResponse
|
||||
* @description Minimal user info for admin dropdowns.
|
||||
|
|
@ -627,6 +845,82 @@ export interface components {
|
|||
/** Error */
|
||||
error?: string | null;
|
||||
};
|
||||
/**
|
||||
* ExchangeRequest
|
||||
* @description Request to create an exchange trade.
|
||||
*/
|
||||
ExchangeRequest: {
|
||||
/**
|
||||
* Slot Start
|
||||
* Format: date-time
|
||||
*/
|
||||
slot_start: string;
|
||||
/** Direction */
|
||||
direction: string;
|
||||
/** Eur Amount */
|
||||
eur_amount: number;
|
||||
};
|
||||
/**
|
||||
* ExchangeResponse
|
||||
* @description Response model for an exchange trade.
|
||||
*/
|
||||
ExchangeResponse: {
|
||||
/** Id */
|
||||
id: number;
|
||||
/** User Id */
|
||||
user_id: number;
|
||||
/** User Email */
|
||||
user_email: string;
|
||||
/**
|
||||
* Slot Start
|
||||
* Format: date-time
|
||||
*/
|
||||
slot_start: string;
|
||||
/**
|
||||
* Slot End
|
||||
* Format: date-time
|
||||
*/
|
||||
slot_end: string;
|
||||
/** Direction */
|
||||
direction: string;
|
||||
/** Eur Amount */
|
||||
eur_amount: number;
|
||||
/** Sats Amount */
|
||||
sats_amount: number;
|
||||
/** Market Price Eur */
|
||||
market_price_eur: number;
|
||||
/** Agreed Price Eur */
|
||||
agreed_price_eur: number;
|
||||
/** Premium Percentage */
|
||||
premium_percentage: number;
|
||||
/** Status */
|
||||
status: string;
|
||||
/**
|
||||
* Created At
|
||||
* Format: date-time
|
||||
*/
|
||||
created_at: string;
|
||||
/** Cancelled At */
|
||||
cancelled_at: string | null;
|
||||
/** Completed At */
|
||||
completed_at: string | null;
|
||||
};
|
||||
/**
|
||||
* ExchangeUserContact
|
||||
* @description User contact info for admin view.
|
||||
*/
|
||||
ExchangeUserContact: {
|
||||
/** Email */
|
||||
email: string;
|
||||
/** Contact Email */
|
||||
contact_email: string | null;
|
||||
/** Telegram */
|
||||
telegram: string | null;
|
||||
/** Signal */
|
||||
signal: string | null;
|
||||
/** Nostr Npub */
|
||||
nostr_npub: string | null;
|
||||
};
|
||||
/** HTTPValidationError */
|
||||
HTTPValidationError: {
|
||||
/** Detail */
|
||||
|
|
@ -1050,26 +1344,6 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
get_exchange_price_api_exchange_price_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ExchangePriceResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
get_profile_api_profile_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -1593,4 +1867,255 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
get_exchange_price_api_exchange_price_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ExchangePriceResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
create_exchange_api_exchange_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ExchangeRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ExchangeResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
get_my_trades_api_trades_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ExchangeResponse"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
cancel_my_trade_api_trades__exchange_id__cancel_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
exchange_id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ExchangeResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
get_upcoming_trades_api_admin_trades_upcoming_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AdminExchangeResponse"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
get_past_trades_api_admin_trades_past_get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
status?: string | null;
|
||||
start_date?: string | null;
|
||||
end_date?: string | null;
|
||||
user_search?: string | null;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AdminExchangeResponse"][];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
complete_trade_api_admin_trades__exchange_id__complete_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
exchange_id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AdminExchangeResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
mark_no_show_api_admin_trades__exchange_id__no_show_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
exchange_id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AdminExchangeResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
admin_cancel_trade_api_admin_trades__exchange_id__cancel_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
exchange_id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AdminExchangeResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue