Skip to content

Commit

Permalink
new
Browse files Browse the repository at this point in the history
  • Loading branch information
sehyunc committed Dec 4, 2024
1 parent bc3a2d8 commit 01af68a
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 131 deletions.
75 changes: 75 additions & 0 deletions app/api/stats/time-to-fill/route.ts
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 },
)
}
}
51 changes: 36 additions & 15 deletions app/stats/charts/time-to-fill-card.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React, { useMemo } from "react"
import React, { useMemo, useRef } from "react"

import NumberFlow, { NumberFlowGroup } from "@number-flow/react"
import { Token } from "@renegade-fi/react"
import { useDebounceValue } from "usehooks-ts"

import { TokenSelect } from "@/app/stats/charts/token-select"
import { useTimeToFill } from "@/app/stats/hooks/use-time-to-fill"

import { Slider } from "@/components/animated-slider"

import { useOrderValue } from "@/hooks/use-order-value"
import { usePriceQuery } from "@/hooks/use-price-query"
import { cn } from "@/lib/utils"

interface TimeDisplayValues {
Expand All @@ -23,40 +22,61 @@ export function TimeToFillCard() {
const [selectedToken, setSelectedToken] = React.useState("WETH")
const [isSell, setIsSell] = React.useState(true)

const baseToken = Token.findByTicker(selectedToken)

const { priceInBase, priceInUsd } = useOrderValue({
amount: selectedAmount.toString(),
base: selectedToken,
isQuoteCurrency: true,
isSell,
})
const [debouncedUsdValue] = useDebounceValue(priceInUsd, 500)

const timeToFillMs = useTimeToFill({
amount: Number(priceInUsd),
const { data: timeToFillMs, isLoading } = useTimeToFill({
amount: debouncedUsdValue,
baseToken: selectedToken,
includeVolumeLimit: true,
})

const lastValidValue = useRef<TimeDisplayValues>({
value: 0,
prefix: "",
suffix: "",
})

const displayValues = useMemo<TimeDisplayValues>(() => {
if (isLoading && lastValidValue.current.value !== 0) {
return lastValidValue.current
}

if (!timeToFillMs) {
return {
value: 0,
prefix: "",
suffix: "",
}
}

const timeInMinutes = timeToFillMs / (1000 * 60)
let result: TimeDisplayValues

if (timeInMinutes >= 60) {
const timeInHours = timeInMinutes / 60
const roundedHours = Math.round(timeInHours)
return {
result = {
value: roundedHours,
prefix: "~",
suffix: roundedHours === 1 ? " hour" : " hours",
}
} else {
result = {
value: timeInMinutes < 1 ? 1 : timeInMinutes,
prefix: timeInMinutes < 1 ? "< " : "~",
suffix:
timeInMinutes < 1 || timeInMinutes === 1 ? " minute" : " minutes",
}
}

return {
value: timeInMinutes < 1 ? 1 : timeInMinutes,
prefix: timeInMinutes < 1 ? "< " : "~",
suffix: timeInMinutes < 1 || timeInMinutes === 1 ? " minute" : " minutes",
}
}, [timeToFillMs])
lastValidValue.current = result
return result
}, [timeToFillMs, isLoading])

return (
<NumberFlowGroup>
Expand Down Expand Up @@ -93,6 +113,7 @@ export function TimeToFillCard() {
value={selectedToken}
onChange={setSelectedToken}
/>
{/* TODO: Placeholder */}
<NumberFlow
className="font-serif font-bold"
format={{
Expand Down
144 changes: 28 additions & 116 deletions app/stats/hooks/use-time-to-fill.ts
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),
})
}

0 comments on commit 01af68a

Please sign in to comment.