Skip to content

Commit 161640e

Browse files
committed
feat: implement LI.FI intents API integration and enhance composer route handling
- Added intentsApi.ts for handling intents-related API calls. - Introduced useIntentOrderStatus.ts for managing intent order status with React Query. - Created withdrawComposerRoute.ts to manage the withdrawal process through composer routes. - Enhanced error handling and user feedback during the withdrawal process. - Updated earnApi.ts to separate quote URL building from fetching logic. - Improved type definitions in types.ts and added nullable symbol handling for EarnToken. - Implemented nonce generation for unique order identification in nonce.ts. - Added deadline management for orders in deadlines.ts. - Created standardOrder.ts for building and managing standard order structures. - Updated Vercel and Vite configurations to support new API endpoints.
1 parent 9b78627 commit 161640e

41 files changed

Lines changed: 7557 additions & 226 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.codegraph/.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# CodeGraph data files
2+
# These are local to each machine and should not be committed
3+
4+
# Database
5+
*.db
6+
*.db-wal
7+
*.db-shm
8+
9+
# Cache
10+
cache/
11+
12+
# Logs
13+
*.log
14+
15+
# Hook markers
16+
.dirty

api/lifi-intents.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import type { VercelRequest, VercelResponse } from "@vercel/node";
2+
import * as crypto from "crypto";
3+
4+
export const config = {
5+
api: { bodyParser: false },
6+
maxDuration: 30,
7+
};
8+
9+
const ORDER_BASE = "https://order.li.fi";
10+
11+
// Allowlist keeps solver-only endpoints out of the browser surface; they'd
12+
// 401 upstream, but a clean 404 here is friendlier and saves a round trip.
13+
const ALLOWED_PATHS = new Set([
14+
"quote/request",
15+
"orders/submit",
16+
"orders/status",
17+
"orders",
18+
"routes",
19+
"chains/supported",
20+
]);
21+
22+
const ALLOWED_METHODS = new Set(["GET", "POST", "OPTIONS", "HEAD"]);
23+
const ALLOWED_ORIGINS = new Set(
24+
(process.env.ALLOWED_ORIGINS || "").split(",").filter(Boolean),
25+
);
26+
const PROXY_SECRET = process.env.PROXY_SECRET || "";
27+
28+
// Real /quote/request payloads are ~1-2 KiB; cap at 16 KiB to bound serverless
29+
// CPU on abuse traffic.
30+
const MAX_BODY_BYTES = 16 * 1024;
31+
32+
// Best-effort per-IP rate limit (resets on cold start). Friction, not auth.
33+
const RATE_LIMIT_WINDOW_MS = 60_000;
34+
const RATE_LIMIT_MAX = 120;
35+
const rateBuckets = new Map<string, { count: number; resetAt: number }>();
36+
37+
function rateLimit(req: VercelRequest): boolean {
38+
const fwd = req.headers["x-forwarded-for"];
39+
const ip = (Array.isArray(fwd) ? fwd[0] : fwd ?? req.socket?.remoteAddress ?? "")
40+
.toString()
41+
.split(",")[0]
42+
.trim();
43+
if (!ip) return true; // can't identify caller — let it through, upstream will protect
44+
const now = Date.now();
45+
const slot = rateBuckets.get(ip);
46+
if (!slot || slot.resetAt < now) {
47+
rateBuckets.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
48+
return true;
49+
}
50+
if (slot.count >= RATE_LIMIT_MAX) return false;
51+
slot.count += 1;
52+
return true;
53+
}
54+
55+
function getAllowedOrigin(req: VercelRequest): string | null {
56+
const origin = req.headers.origin;
57+
if (!origin) return null;
58+
if (ALLOWED_ORIGINS.has(origin)) return origin;
59+
if (origin.startsWith("http://localhost:")) return origin;
60+
const host = req.headers.host;
61+
if (host && origin === `https://${host}`) return origin;
62+
return null;
63+
}
64+
65+
function hasValidSecret(req: VercelRequest): boolean {
66+
if (!PROXY_SECRET) return false;
67+
const header = req.headers["x-proxy-secret"];
68+
if (typeof header !== "string") return false;
69+
const a = Buffer.from(header);
70+
const b = Buffer.from(PROXY_SECRET);
71+
if (a.length !== b.length) return false;
72+
return crypto.timingSafeEqual(a, b);
73+
}
74+
75+
class BodyTooLargeError extends Error {
76+
constructor() {
77+
super("body_too_large");
78+
}
79+
}
80+
81+
async function readBody(req: VercelRequest, cap: number): Promise<string> {
82+
const chunks: Buffer[] = [];
83+
let total = 0;
84+
for await (const chunk of req) {
85+
const buf = Buffer.from(chunk);
86+
total += buf.length;
87+
if (total > cap) throw new BodyTooLargeError();
88+
chunks.push(buf);
89+
}
90+
return Buffer.concat(chunks).toString("utf8");
91+
}
92+
93+
export default async function handler(
94+
req: VercelRequest,
95+
res: VercelResponse,
96+
) {
97+
const allowedOrigin = getAllowedOrigin(req);
98+
99+
if (req.method === "OPTIONS") {
100+
if (allowedOrigin) {
101+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
102+
}
103+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
104+
res.setHeader(
105+
"Access-Control-Allow-Headers",
106+
"Content-Type, x-proxy-secret",
107+
);
108+
return res.status(204).end();
109+
}
110+
111+
if (PROXY_SECRET) {
112+
if (!hasValidSecret(req)) {
113+
return res.status(403).json({ error: "Forbidden" });
114+
}
115+
} else {
116+
// Without PROXY_SECRET we require a known Origin — rejecting missing-Origin
117+
// requests (curl, server-to-server) keeps the public endpoint scrape-resistant.
118+
if (!allowedOrigin || !req.headers.origin) {
119+
return res.status(403).json({ error: "Origin required" });
120+
}
121+
}
122+
123+
if (!ALLOWED_METHODS.has(req.method || "")) {
124+
return res.status(405).json({ error: "Method not allowed" });
125+
}
126+
127+
if (!rateLimit(req)) {
128+
return res.status(429).json({ error: "rate_limited" });
129+
}
130+
131+
const pathParam = req.query?.path;
132+
const subPath = (
133+
Array.isArray(pathParam)
134+
? pathParam.join("/")
135+
: typeof pathParam === "string"
136+
? pathParam
137+
: ""
138+
).replace(/^\/+/, "");
139+
140+
if (!ALLOWED_PATHS.has(subPath)) {
141+
return res.status(404).json({ error: "unsupported_intents_path" });
142+
}
143+
144+
const params = new URLSearchParams();
145+
for (const [key, val] of Object.entries(req.query || {})) {
146+
if (key === "path") continue;
147+
if (Array.isArray(val)) {
148+
val.forEach((v) => params.append(key, v));
149+
} else if (typeof val === "string") {
150+
params.append(key, val);
151+
}
152+
}
153+
const qs = params.toString();
154+
155+
const upstream = `${ORDER_BASE}/${subPath}${qs ? `?${qs}` : ""}`;
156+
const method = (req.method || "GET").toUpperCase();
157+
let body: string | undefined;
158+
if (method === "POST") {
159+
try {
160+
body = await readBody(req, MAX_BODY_BYTES);
161+
} catch (err) {
162+
if (err instanceof BodyTooLargeError) {
163+
return res.status(413).json({ error: "body_too_large", maxBytes: MAX_BODY_BYTES });
164+
}
165+
throw err;
166+
}
167+
}
168+
169+
try {
170+
const upstreamRes = await fetch(upstream, {
171+
method,
172+
headers: {
173+
Accept: "application/json",
174+
...(body ? { "Content-Type": "application/json" } : {}),
175+
},
176+
body,
177+
signal: AbortSignal.timeout(25_000),
178+
});
179+
180+
const text = await upstreamRes.text();
181+
182+
if (allowedOrigin) {
183+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
184+
}
185+
res.setHeader("Content-Type", "application/json");
186+
return res.status(upstreamRes.status).send(text);
187+
} catch (err) {
188+
console.error("[lifi-intents] upstream error:", err);
189+
return res.status(502).json({ error: "Upstream request failed" });
190+
}
191+
}

edb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Subproject commit 1ba2fcaca73cee96bf10107b7b0f98ab1ceab1a4
1+
Subproject commit c5b32ca53e7c2146e647830af486b6481aa37548

0 commit comments

Comments
 (0)