-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
139 additions
and
131 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import invariant from "tiny-invariant" | ||
|
||
export const runtime = "edge" | ||
|
||
const TIME_TO_FILL_PATH = "time-to-fill" | ||
|
||
export type TimeToFillResponse = { | ||
data?: { | ||
estimatedMs: number | ||
} | ||
error?: string | ||
} | ||
|
||
interface BotServerResponse { | ||
data: number // milliseconds | ||
} | ||
|
||
export async function GET(request: Request) { | ||
try { | ||
const BOT_SERVER_URL = process.env.BOT_SERVER_URL | ||
invariant(BOT_SERVER_URL, "BOT_SERVER_URL is not set") | ||
const BOT_SERVER_API_KEY = process.env.BOT_SERVER_API_KEY | ||
invariant(BOT_SERVER_API_KEY, "BOT_SERVER_API_KEY is not set") | ||
|
||
const { searchParams } = new URL(request.url) | ||
const amount = searchParams.get("amount") | ||
const baseToken = searchParams.get("baseToken") | ||
|
||
if (!amount || !baseToken) { | ||
return Response.json( | ||
{ | ||
error: "Invalid amount or baseToken parameter", | ||
} satisfies TimeToFillResponse, | ||
{ status: 400 }, | ||
) | ||
} | ||
|
||
const url = new URL(`${BOT_SERVER_URL}/${TIME_TO_FILL_PATH}`) | ||
url.searchParams.set("amount", amount) | ||
url.searchParams.set("baseToken", baseToken) | ||
|
||
const res = await fetch(url, { | ||
headers: { "x-api-key": BOT_SERVER_API_KEY }, | ||
}) | ||
|
||
if (!res.ok) { | ||
throw new Error( | ||
`Bot server responded with status ${res.status}: ${res.statusText}`, | ||
) | ||
} | ||
|
||
const data = (await res.json()) as BotServerResponse | ||
|
||
return Response.json( | ||
{ data: { estimatedMs: data.data } } satisfies TimeToFillResponse, | ||
{ status: 200 }, | ||
) | ||
} catch (error) { | ||
console.error("[TimeToFill API] Error:", { | ||
name: error instanceof Error ? error.name : "Unknown", | ||
message: error instanceof Error ? error.message : "Unknown error", | ||
error, | ||
}) | ||
|
||
return Response.json( | ||
{ | ||
error: | ||
error instanceof Error | ||
? error.message | ||
: "Failed to fetch time to fill", | ||
} satisfies TimeToFillResponse, | ||
{ status: 500 }, | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,127 +1,39 @@ | ||
import { useMemo } from "react" | ||
import { useQuery } from "@tanstack/react-query" | ||
|
||
interface TokenConfig { | ||
allocation: number // Quoter's allocation in USDC | ||
firstFillValue: number // Amount that can be filled instantly | ||
rematchDelayMs: number // Delay between fill attempts | ||
fillLatency: { | ||
first: number // Duration for first fills in ms | ||
normal: number // Duration for normal fills in ms | ||
priority: number // Duration for priority fills in ms | ||
} | ||
hourlyVolumeLimit: number | ||
} | ||
|
||
// Token allocations in USDC | ||
const ALLOCATIONS: Record<string, number> = { | ||
WBTC: 11000, | ||
WETH: 11000, | ||
ARB: 1000, | ||
GMX: 1000, | ||
PENDLE: 3250, | ||
LDO: 1000, | ||
LINK: 1000, | ||
CRV: 1000, | ||
UNI: 1000, | ||
ZRO: 1000, | ||
LPT: 1000, | ||
GRT: 1000, | ||
COMP: 1000, | ||
AAVE: 1000, | ||
XAI: 1000, | ||
RDNT: 1000, | ||
ETHFI: 1000, | ||
} | ||
|
||
// Default configuration | ||
const DEFAULT_CONFIG: Omit<TokenConfig, "allocation"> = { | ||
firstFillValue: 1000, // Default first fill amount | ||
rematchDelayMs: 60_000, // 1 minute between fills | ||
fillLatency: { | ||
first: 1_000, // 1 second | ||
normal: 30_000, // TODO: Verify this | ||
priority: 54_000, // 54 seconds | ||
}, | ||
hourlyVolumeLimit: 10_000, | ||
} | ||
|
||
// Token-specific configurations (override defaults) | ||
const TOKEN_CONFIGS: Partial< | ||
Record< | ||
string, | ||
Partial<Pick<TokenConfig, "firstFillValue" | "hourlyVolumeLimit">> | ||
> | ||
> = { | ||
WETH: { | ||
firstFillValue: 3000, | ||
hourlyVolumeLimit: 200_000, | ||
}, | ||
WBTC: { | ||
firstFillValue: 3000, | ||
hourlyVolumeLimit: 200_000, | ||
}, | ||
PENDLE: { | ||
firstFillValue: 1000, | ||
hourlyVolumeLimit: 50_000, | ||
}, | ||
} | ||
import { TimeToFillResponse } from "@/app/api/stats/time-to-fill/route" | ||
|
||
interface TimeToFillParams { | ||
amount: number // Amount in USDC | ||
baseToken: string // Base token identifier (e.g., "WETH") | ||
includeVolumeLimit?: boolean | ||
amount: string | ||
baseToken: string | ||
} | ||
|
||
export function useTimeToFill({ | ||
amount, | ||
baseToken, | ||
includeVolumeLimit = false, | ||
}: TimeToFillParams): number { | ||
return useMemo(() => { | ||
const allocation = ALLOCATIONS[baseToken] | ||
|
||
const config = { | ||
...DEFAULT_CONFIG, | ||
...(baseToken ? TOKEN_CONFIGS[baseToken] : {}), | ||
allocation, | ||
} | ||
|
||
// If amount is less than or equal to first fill threshold, return first fill duration | ||
if (amount <= config.firstFillValue) { | ||
return config.fillLatency.first | ||
} | ||
|
||
// Calculate remaining amount after first fill | ||
const remainingAmount = amount - config.firstFillValue | ||
|
||
// Determine if this is a priority fill (amount > 2x allocation) | ||
const isPriorityFill = amount > allocation * 2 | ||
|
||
// Calculate fill amount per interval | ||
// For priority fills: use 2x allocation | ||
// For normal fills: use allocation | ||
const fillPerInterval = isPriorityFill ? allocation * 2 : allocation | ||
|
||
// Calculate number of intervals needed | ||
const intervalsNeeded = Math.ceil(remainingAmount / fillPerInterval) | ||
export function useTimeToFill({ amount, baseToken }: TimeToFillParams) { | ||
return useQuery({ | ||
queryKey: ["timeToFill", amount, baseToken], | ||
queryFn: async () => { | ||
const searchParams = new URLSearchParams({ | ||
amount, | ||
baseToken, | ||
}) | ||
|
||
const response = await fetch(`/api/stats/time-to-fill?${searchParams}`) | ||
if (!response.ok) { | ||
const error = await response.json() | ||
throw new Error(error.error || "Failed to fetch time to fill") | ||
} | ||
|
||
// Use appropriate fill duration based on priority | ||
const fillLatency = isPriorityFill | ||
? config.fillLatency.priority | ||
: config.fillLatency.normal | ||
const { data, error } = (await response.json()) as TimeToFillResponse | ||
|
||
const baseDelay = | ||
config.fillLatency.first + | ||
intervalsNeeded * (config.rematchDelayMs + fillLatency) | ||
if (error) { | ||
throw new Error(error) | ||
} | ||
|
||
if (includeVolumeLimit) { | ||
// Conservatively calculate how many full hours are needed based on amount vs hourly limit | ||
const hoursNeeded = Math.ceil(amount / config.hourlyVolumeLimit) | ||
if (hoursNeeded > 1) { | ||
return baseDelay + (hoursNeeded - 1) * 3600 * 1000 | ||
if (!data) { | ||
throw new Error("No data received") | ||
} | ||
} | ||
|
||
return baseDelay | ||
}, [amount, baseToken, includeVolumeLimit]) | ||
return data.estimatedMs | ||
}, | ||
enabled: Boolean(amount && baseToken), | ||
}) | ||
} |