Implement LI.FI intents API integration and enhance composer route handling - #20
Implement LI.FI intents API integration and enhance composer route handling#20Timidan wants to merge 10 commits into
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR integrates LI.FI Intents into the Earn deposit/withdraw and concierge flows, adding order construction, status polling, escrow lifecycle UI, and cross-chain Composer fallback handling.
Changes:
- Adds LI.FI Intents proxy/client utilities, escrow order helpers, nonce/deadline encoding, and status timeline components.
- Extends deposit/withdraw flows with Intent routes, Composer cross-chain bridge+deposit execution, and richer execution event tracking.
- Updates token typing/copy to tolerate missing symbols and clarify risk/fallback messaging.
Reviewed changes
Copilot reviewed 41 out of 41 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
vite.config.ts | Adds dev proxy for LI.FI Intents API. |
vercel.json | Routes production Intents proxy requests. |
api/lifi-intents.ts | Adds Vercel proxy for order.li.fi. |
src/lib/intents/addressBytes.ts | Adds EVM address-to-bytes32 helpers. |
src/lib/intents/contracts.ts | Defines escrow contract constants, ABI, and event decoding. |
src/lib/intents/deadlines.ts | Builds fill/refund deadline plans. |
src/lib/intents/eip7930.ts | Encodes EIP-7930 EVM addresses. |
src/lib/intents/nonce.ts | Generates order nonces. |
src/lib/intents/standardOrder.ts | Builds standard LI.FI escrow orders. |
src/components/integrations/lifi-earn/intentsApi.ts | Adds Intents API client and status helpers. |
src/components/integrations/lifi-earn/useIntentOrderStatus.ts | Adds React Query polling hook for order status. |
src/components/integrations/lifi-earn/IntentStatusTimeline.tsx | Adds escrow/order lifecycle UI. |
src/components/integrations/lifi-earn/IntentBridgeStep.tsx | Adds Intent-based bridge then vault deposit flow. |
src/components/integrations/lifi-earn/WithdrawIntentRouteStep.tsx | Adds Intent-based withdrawal route flow. |
src/components/integrations/lifi-earn/withdrawComposerRoute.ts | Adds Composer withdrawal routing execution. |
src/components/integrations/lifi-earn/crossChainComposerDeposit.ts | Adds Composer cross-chain bridge then deposit executor. |
src/components/integrations/lifi-earn/DepositFlow.tsx | Wires cross-chain source selection, Intents toggle, and execution events. |
src/components/integrations/lifi-earn/earnApi.ts | Refactors Composer quote URL building and improves errors. |
src/components/integrations/lifi-earn/destinationTokenOptions.ts | Adds curated withdrawal destination tokens. |
src/components/integrations/lifi-earn/hooks/useWithdrawQuote.ts | Clarifies redeem-only quote behavior. |
src/components/integrations/lifi-earn/txUtils.ts | Adds shared ERC-20 safe approval helper. |
src/components/integrations/lifi-earn/types.ts | Makes token symbols optional. |
src/components/integrations/lifi-earn/TokenIcon.tsx | Handles missing token symbols in fallback icon. |
src/components/integrations/lifi-earn/VaultList.tsx | Updates high-risk vault warning copy. |
src/components/integrations/lifi-earn/concierge/types.ts | Extends leg status and execution event types. |
src/components/integrations/lifi-earn/concierge/executionMachine.ts | Handles Composer/Intent execution events in queue state. |
src/components/integrations/lifi-earn/concierge/ExecutionQueue.tsx | Renders recoverable failures, Intents statuses, and richer progress. |
src/components/integrations/lifi-earn/concierge/FlowDiagram.tsx | Adds visual states for Intent/refund/deposit statuses. |
src/components/integrations/lifi-earn/concierge/IdleSweepPanel.tsx | Prevents queue rebuild during new in-flight/recoverable states. |
src/components/integrations/lifi-earn/concierge/fallback.ts | Handles missing symbols and improves safety-floor messaging. |
src/components/integrations/lifi-earn/concierge/LlmErrorAlert.tsx | Updates fallback wording. |
src/components/integrations/lifi-earn/concierge/VaultRecommendations.tsx | Handles missing underlying symbols. |
src/components/integrations/lifi-earn/concierge/intent/IntentPanel.tsx | Adds opt-in LI.FI Intents rebalance pipeline. |
src/components/integrations/lifi-earn/concierge/intent/intentLegs.ts | Builds/degrades planned Intent legs. |
src/components/integrations/lifi-earn/concierge/intent/useIntentLegPipeline.ts | Implements quote/open/refund/deposit pipeline for Intent legs. |
src/components/integrations/lifi-earn/concierge/intent/RebalancePlanCard.tsx | Adds UI for multi-leg Intent rebalance execution. |
src/components/integrations/lifi-earn/concierge/intent/hooks/useIntentRecommendation.ts | Handles missing underlying symbols. |
src/components/integrations/lifi-earn/concierge/intent/hooks/useVaultsByIntent.ts | Handles missing underlying symbols. |
.codegraph/.gitignore | Ignores local CodeGraph artifacts. |
Comments suppressed due to low confidence (1)
src/components/integrations/lifi-earn/txUtils.ts:158
- The final approval receipt is also not checked for
status === "reverted", so callers can continue as if the allowance was granted even when the approval transaction failed. Throw on reverted receipts before returning from the helper.
await wagmiWaitForReceipt(wagmiConfig, {
hash,
chainId,
timeout: timeoutMs,
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ) { | ||
| return; | ||
| } | ||
| depositInFlight.current.add(id); |
| // Without PROXY_SECRET we require a known Origin — rejecting missing-Origin | ||
| // requests (curl, server-to-server) keeps the public endpoint scrape-resistant. | ||
| if (!allowedOrigin || !req.headers.origin) { | ||
| return res.status(403).json({ error: "Origin required" }); |
| // Cap retry amount at the originally-bridged amount when known. If the | ||
| // stored value is "0" (the old broken case), fall back to the live | ||
| // balance — risky but only reachable from a recoverable-fail state the | ||
| // user explicitly retried. | ||
| let chosen = liveBalance; |
| await wagmiWaitForReceipt(wagmiConfig, { | ||
| hash: resetHash, | ||
| chainId, | ||
| timeout: timeoutMs, | ||
| }); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:2a8e7bf4f7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await wagmiWaitForReceipt(wagmiConfig, { | ||
| hash, | ||
| chainId, | ||
| timeout: timeoutMs, | ||
| }); |
There was a problem hiding this comment.
Reject reverted approval receipts in safeApproveErc20
wagmiWaitForReceipt does not fail just because a tx reverted, so this helper currently treats reverted approve transactions as success and continues the flow. In that case the next step (open, sendTransaction, etc.) runs with unchanged allowance and fails later with a misleading error path. Please inspect the returned receipt status (for both reset and final approve txs) and throw when it is reverted.
Useful? React with 👍 / 👎.
| )); | ||
| return; | ||
| } | ||
| const delta = postBridgeDestBalance.sub(preBridgeDestBalance); |
There was a problem hiding this comment.
Guard post-pre balance delta from underflow
This subtraction can throw when postBridgeDestBalance < preBridgeDestBalance (for example if the user transfers tokens out while the bridge is settling, or balance shifts for other reasons). Because ethers.BigNumber.sub underflow throws, the code bypasses the intended recoverable delta <= 0 branch and fails the flow unexpectedly. Compare first and clamp to zero before subtracting so the recoverable path is preserved.
Useful? React with 👍 / 👎.
…ute 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.
…lently drop Chains with many underlyings (Base has ~300) blew past the public-RPC per-eth_call gas limit when `aggregate3` fanned out in a single request. viem's multicall returns ALL-failure (no throw) in that case, so every ERC-20 balance silently disappeared from the idle-asset list while native `getBalance` still worked — visible symptom was "ETH on Base shows $4 but my $200 of USDC doesn't appear." Cap each multicall at 50 sub-calls and fan the chunks out in parallel. Per-chunk catch returns failure shape so one bad chunk doesn't kill the rest. Outer withTimeout gets 2x the per-call budget to absorb the serialization cost of multiple round-trips on slow public RPCs.
| // Without PROXY_SECRET we require a known Origin — rejecting missing-Origin | ||
| // requests (curl, server-to-server) keeps the public endpoint scrape-resistant. | ||
| if (!allowedOrigin || !req.headers.origin) { | ||
| return res.status(403).json({ error: "Origin required" }); |
| await wagmiWaitForReceipt(wagmiConfig, { | ||
| hash, | ||
| chainId, | ||
| timeout: timeoutMs, | ||
| }); |
| chainId: number, | ||
| ): Promise<ethers.BigNumber> { | ||
| const provider = chainRpcProvider(chainId); | ||
| if (!provider) return ethers.BigNumber.from(0); |
| await wagmiWaitForReceipt(config, { | ||
| hash, | ||
| chainId: sourceChainId, | ||
| timeout: 120_000, | ||
| }); |
| await wagmiWaitForReceipt(config, { | ||
| hash, | ||
| chainId: sourceChainId, | ||
| timeout: 120_000, | ||
| }); |
| await wagmiWaitForReceipt(config, { | ||
| hash, | ||
| chainId, | ||
| timeout: 120_000, | ||
| }); |
…le scan
The LI.FI Earn /vaults feed occasionally ships non-EVM identifiers in
`underlyingTokens[].address` (seen in the wild on Base: `coingecko:universal-btc`).
viem's multicall ABI-encodes each entry as `address`, and a single
malformed entry corrupts the entire aggregate3 calldata. The RPC
rejects with "invalid hex string", viem maps the whole batch to
all-failure, and every legitimate balance silently disappears from
the idle-asset list (e.g. USDC on Base — native ETH still shows up
because it's a separate getBalance call).
Pre-validate each address against /^0x[a-f0-9]{40}$/i before building
the multicall, so one bad upstream entry no longer takes the whole
chain offline. Also chunks the multicall into batches of 50 to keep
the aggregate3 payload below the tighter eth_call gas budgets some
RPCs enforce.
vite.config.ts: ignore the edb submodule's 15GB Rust target/ dir
(and other generated dirs) from the file watcher — they blow the
Linux inotify per-process instance cap on startup.Cross-chain holdings under "BRIDGE VIA LI.FI INTENT" now check the
live LI.FI route registry before being offered as selectable. Sources
without a routable solver path from origin to the vault's underlying
appear in the group disabled, with an inline "· no Intent route"
suffix. Radix's data-[disabled]:pointer-events-none disqualifies
tooltip-on-disabled, so the reason is shown inline alongside the
balance instead. A middle-dot separator is wrapped in aria-hidden
so screen readers announce the suffix cleanly.
A selection guard useEffect resets to the canonical DIRECT source
(plus clears the amount + sim result) if a previously selected
cross-chain token becomes known-unavailable after routes resolve —
covers the race where the user picks during the loading window and
the registry later reveals the route doesn't exist.
Shares React Query's cache via queryKey: ["lifi-intent-routes"]
(same key IntentPanel uses) so we don't double-fetch routes when
both the concierge and the deposit drawer mount in the same session.
buildRoutesIndex now treats both `undefined` (loading) AND `[]`
(transient empty cache resolution) as optimistic — returns
{isEmpty: true, has: () => true} for both. The earlier strict
semantics disabled every cross-chain source for the ~200ms window
between cache resolution and the populated fetch landing. The
authoritative runtime gate remains IntentBridgeStep's "No quote
available" panel, so optimistic-during-load is safe.| // Without PROXY_SECRET we require a known Origin — rejecting missing-Origin | ||
| // requests (curl, server-to-server) keeps the public endpoint scrape-resistant. | ||
| if (!allowedOrigin || !req.headers.origin) { | ||
| return res.status(403).json({ error: "Origin required" }); |
| await wagmiWaitForReceipt(wagmiConfig, { | ||
| hash: resetHash, | ||
| chainId, | ||
| timeout: timeoutMs, | ||
| }); |
| // Cap retry amount at the originally-bridged amount when known. If the | ||
| // stored value is "0" (the old broken case), fall back to the live | ||
| // balance — risky but only reachable from a recoverable-fail state the | ||
| // user explicitly retried. | ||
| let chosen = liveBalance; | ||
| try { | ||
| const original = ethers.BigNumber.from(destinationAmountRaw ?? "0"); | ||
| if (original.gt(0) && liveBalance.gte(original)) { | ||
| chosen = original; | ||
| } | ||
| } catch { | ||
| /* keep liveBalance */ | ||
| } | ||
| destinationAmountRaw = chosen.toString(); |
| ) { | ||
| return; | ||
| } | ||
| depositInFlight.current.add(id); |
| })) as bigint; | ||
| if (cancelled) return; | ||
| const pre = run.predeliveryBalance ?? 0n; | ||
| const delta = post > pre ? post - pre : post; |
…receipts api/lifi-intents.ts: when PROXY_SECRET is unset, the gate previously required an Origin header on every request and 403'd otherwise. Browsers omit Origin on many same-origin fetches (especially GETs), so production returned 403 on `/routes`, `/orders/status`, and `/chains/supported` for any client whose browser didn't attach Origin. Mirrors the lifi-composer proxy contract: allow missing Origin, reject only present but unapproved origins. src/components/integrations/lifi-earn/txUtils.ts: safeApproveErc20 was not inspecting receipt.status — wagmi's waitForTransactionReceipt resolves on reverted txs too, so a reverted reset-approve or final approve let the caller proceed assuming allowance was granted. The downstream open()/route execution would then fail with a confusing error far from the actual cause. Capture the receipt and throw on status === "reverted" for both legs.
The picker section was named "Bridge via LI.FI Intent" but the actual cross-chain route is decided by the toggle below the picker — either LI.FI Intent (1 signature, solver auction) or LI.FI Composer 2-step (bridge tx + deposit tx). Promising one engine in the source-group label hides the alternative and confuses users when they expect a separate Composer entry. Rename to "Bridge via Composer or LI.FI Intent" so the group label matches what the underlying flow can actually pick.
- IntentBridgeStep / WithdrawIntentRouteStep / useIntentLegPipeline: throw on receipt.status === "reverted" after refund() — wagmiWaitForReceipt resolves on reverted txs too, so a reverted refund was silently treated as success. - useIntentLegPipeline: wrap depositLeg body in outer try/finally so every early return (predeliveryBalance missing, delta zero, balance read fail) releases the in-flight lock instead of permanently blocking deposits. - crossChainComposerDeposit: readBalance now throws on missing provider (a silent BigNumber(0) caused post-pre deltas to include unrelated user funds); delta clamped via gt-guard; resume path requires a known originalBridged amount instead of falling back to entire live balance. - RebalancePlanCard: balance delta clamps to 0n when post <= pre instead of falling back to the entire post balance.
Applied 9 unambiguous fixes from a 10-zone CLAUDE.md audit (~150 total findings). Only items with zero behavior change, zero parallel-work conflict (PR #20 lifi-intents, #21 mezo, #22 cleanup), and clear dead-code or surface-confusion signal were applied; the rest are deferred to the PR description as a backlog. Net: 12 files, -65 lines net. Surface confusion (P1): - structStorageDecoding.ts:484 — collapse identical-branches ternary 'const elementSize = elementType === 'address' ? 1 : 1' → '= 1' - storageSlotCalculator.ts:86 — parseSlotInput dropped redundant hex branch (both branches called BigInt(trimmed)) - edbTraceConverter.ts:55 — same redundant-hex-branch in parseTraceValue Speculative API surface (P2): - SolidityViewer.tsx — removed unused props highlightLine, scrollToLine, theme (sole caller never passes any) - PackingVisualizer.tsx — removed unused rawHex prop + caller's pass - api/{lifi-composer,lifi-earn,edb-proxy}.ts — dropped 'HEAD' from ALLOWED_METHODS (no caller issues HEAD) Dead exports (P3): - scripts/bridge-security.mjs — sanitizeForLogging (zero callers) - scripts/bridge-config.mjs — TRACE_DETAIL_COMPACT_ARTIFACTS env constant (zero readers) - eslint.config.js — ignore globs for nonexistent dirs (current_bundle, test-results, test-*, **/test-*.js, test-app-*)
Seven defects found while reviewing the intents integration before merge. The first three can cost users funds; the rest remove footguns. - Reject non-positive quote outputs. `amount` is a decimal string, so the `!previewAmount` guard let "0" through and built an order offering the whole input for nothing, which a solver can fill by transferring zero. Adds readQuoteOutputAmount() and uses it in all three quote paths. - Bind the order to the signing account. open() collects from msg.sender but delivers and refunds to order.user; the reset effects keyed on the `recipient` prop and not the connected address, so switching accounts after quoting let one account fund an order that pays another. Adds `address` to the reset deps and asserts the two match before signing. - Parse validUntil by shape. It arrives as a numeric Unix timestamp but was typed as a string and fed to Date.parse, which returns NaN for numeric input in either unit — the real expiry was discarded and replaced by an arbitrary now+15m. - Revalidate the fill window immediately before open(). Deadlines were fixed at quote time and never rechecked, and legs open sequentially, so a later leg could escrow into an order no solver would fill. - Record the open() hash before awaiting its receipt, on both the step components and the concierge pipeline. A receipt timeout on a tx that later mined left the escrow invisible, and retry minted a fresh nonce — two live orders, both fillable. Retry now refuses while a hash is held. - Approve the requested amount instead of MaxUint256 in the composer helper, and stop treating an unreadable allowance as zero, which skipped the USDT-style reset and sent a reverting approve. - Document that PROXY_SECRET breaks browser deployments: the web client never sends x-proxy-secret, so setting it 403s every proxied call. Adds unit tests for the amount and deadline parsing. These are the first tracked tests in the repo: .gitignore matched *test*, so every spec was silently dropped. Narrowed to keep test sources under src/.
There was a problem hiding this comment.
🟡 Changes recommended
The new api/lifi-intents.ts proxy has concrete operational issues (unbounded in-memory rate-limit bucket growth and inconsistent CORS headers on error paths) that should be fixed before merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 45/46 changed files
- Comments generated: 2
- Review effort level: Lite
| function rateLimit(req: VercelRequest): boolean { | ||
| const fwd = req.headers["x-forwarded-for"]; | ||
| const ip = (Array.isArray(fwd) ? fwd[0] : fwd ?? req.socket?.remoteAddress ?? "") | ||
| .toString() | ||
| .split(",")[0] | ||
| .trim(); | ||
| if (!ip) return true; // can't identify caller — let it through, upstream will protect | ||
| const now = Date.now(); | ||
| const slot = rateBuckets.get(ip); | ||
| if (!slot || slot.resetAt < now) { | ||
| rateBuckets.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS }); | ||
| return true; | ||
| } | ||
| if (slot.count >= RATE_LIMIT_MAX) return false; | ||
| slot.count += 1; | ||
| return true; | ||
| } |
| const allowedOrigin = getAllowedOrigin(req); | ||
| if (req.method === "OPTIONS") { |
Six defects found while reviewing the intents integration before merge. The first three can cost users funds; the rest remove footguns. - Reject non-positive quote outputs. `amount` is a decimal string, so the `!previewAmount` guard let "0" through and built an order offering the whole input for nothing, which a solver can fill by transferring zero. Adds readQuoteOutputAmount() and uses it in all three quote paths. - Bind the order to the signing account. open() collects from msg.sender but delivers and refunds to order.user; the reset effects keyed on the `recipient` prop and not the connected address, so switching accounts after quoting let one account fund an order that pays another. Adds `address` to the reset deps and asserts the two match before signing. - Parse validUntil by shape. It arrives as a numeric Unix timestamp but was typed as a string and fed to Date.parse, which returns NaN for numeric input in either unit — the real expiry was discarded and replaced by an arbitrary now+15m. - Revalidate the fill window immediately before open(). Deadlines were fixed at quote time and never rechecked, and legs open sequentially, so a later leg could escrow into an order no solver would fill. - Record the open() hash before awaiting its receipt, on both the step components and the concierge pipeline. A receipt timeout on a tx that later mined left the escrow invisible, and retry minted a fresh nonce — two live orders, both fillable. Retry now refuses while a hash is held. - Approve the requested amount instead of MaxUint256 in the composer helper, and stop treating an unreadable allowance as zero, which skipped the USDT-style reset and sent a reverting approve.
Restores .env.example and .gitignore, and untracks the two spec files that came in with them. The PROXY_SECRET warning was already documented on master, and this repo excludes test sources on purpose. Both were mine to begin with and neither belongs in this PR; the .env.example edit was also what made the branch conflict with master. Leaves only the six code fixes.
Integrate the LI.FI intents API to manage order statuses and improve the withdrawal process through composer routes.